diff --git a/CHANGELOG.md b/CHANGELOG.md index 094bcd182e4..ec5474bd7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ Docs: https://docs.openclaw.ai - Plugins/tools: cold-load selected plugin tool registries when the active registry only has partial tool coverage, so wildcard-expanded allowlists no longer hide installed plugin tools from `tools.effective`. Fixes #76780. Thanks @lilesjtu. - Plugins/tools: compare cached and runtime plugin tool name conflicts with normalized core tool names, so case variants of core tools are blocked instead of leaking duplicate tool registrations. Thanks @vincentkoc. - Plugins/OpenRouter: advertise DeepSeek V4 thinking levels, including `xhigh` and `max`, through the runtime and lightweight provider policy surfaces so `/think` validation no longer rejects OpenRouter-routed DeepSeek V4 models. Fixes #74788. Thanks @vincentkoc. -- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Thanks @vincentkoc. +- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Fixes #76206. Thanks @vincentkoc. - CLI/config: remove only the targeted array element for `openclaw config unset array[index]` instead of replaying the unset during config write and deleting the shifted next element. Fixes #76290. Thanks @SymbolStar and @vincentkoc. - Plugins/voice-call: treat abnormal local Gateway close code 1006 as a standalone CLI fallback case, so `voicecall smoke` and related commands can still run the provider check path when the Gateway socket closes before returning a response. - Agents/tools: stop treating `tools.deny: ["write"]` as an implicit `apply_patch` deny; operators who want to block patch writes should deny `apply_patch` or `group:fs` explicitly. Fixes #76749. (#76795) Thanks @Nek-12 and @hclsys. diff --git a/src/agents/model-selection-display.test.ts b/src/agents/model-selection-display.test.ts index 47dcfd8b3c2..6671a5c4526 100644 --- a/src/agents/model-selection-display.test.ts +++ b/src/agents/model-selection-display.test.ts @@ -32,6 +32,18 @@ describe("model-selection-display", () => { }), ).toBe("anthropic/claude-sonnet-4-6"); }); + + it("ignores malformed persisted model values instead of throwing", () => { + expect( + resolveModelDisplayRef({ + runtimeProvider: { provider: "openai" }, + runtimeModel: false, + overrideProvider: ["anthropic"], + overrideModel: 123, + fallbackModel: " openai/gpt-5.5 ", + }), + ).toBe("openai/gpt-5.5"); + }); }); describe("resolveModelDisplayName", () => { @@ -100,5 +112,21 @@ describe("model-selection-display", () => { model: "gpt-5.4", }); }); + + it("ignores malformed persisted session model values", () => { + expect( + resolveSessionInfoModelSelection({ + currentProvider: { provider: "openai" }, + currentModel: false, + defaultProvider: "anthropic", + defaultModel: "claude-sonnet-4-6", + entryProvider: ["openrouter"], + entryModel: 123, + }), + ).toEqual({ + modelProvider: "anthropic", + model: "claude-sonnet-4-6", + }); + }); }); }); diff --git a/src/agents/model-selection-display.ts b/src/agents/model-selection-display.ts index c6a8a6d29b9..3c13e3dc948 100644 --- a/src/agents/model-selection-display.ts +++ b/src/agents/model-selection-display.ts @@ -1,14 +1,16 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + type ModelDisplaySelectionParams = { - runtimeProvider?: string | null; - runtimeModel?: string | null; - overrideProvider?: string | null; - overrideModel?: string | null; - fallbackModel?: string | null; + runtimeProvider?: unknown; + runtimeModel?: unknown; + overrideProvider?: unknown; + overrideModel?: unknown; + fallbackModel?: unknown; }; export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): string | undefined { - const runtimeModel = params.runtimeModel?.trim(); - const runtimeProvider = params.runtimeProvider?.trim(); + const runtimeModel = normalizeOptionalString(params.runtimeModel); + const runtimeProvider = normalizeOptionalString(params.runtimeProvider); if (runtimeModel) { if (runtimeModel.includes("/")) { return runtimeModel; @@ -22,8 +24,8 @@ export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): str return runtimeProvider; } - const overrideModel = params.overrideModel?.trim(); - const overrideProvider = params.overrideProvider?.trim(); + const overrideModel = normalizeOptionalString(params.overrideModel); + const overrideProvider = normalizeOptionalString(params.overrideProvider); if (overrideModel) { if (overrideModel.includes("/")) { return overrideModel; @@ -37,7 +39,7 @@ export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): str return overrideProvider; } - const fallbackModel = params.fallbackModel?.trim(); + const fallbackModel = normalizeOptionalString(params.fallbackModel); return fallbackModel || undefined; } @@ -54,33 +56,39 @@ export function resolveModelDisplayName(params: ModelDisplaySelectionParams): st } type SessionInfoModelSelectionParams = { - currentProvider?: string | null; - currentModel?: string | null; - defaultProvider?: string | null; - defaultModel?: string | null; - entryProvider?: string | null; - entryModel?: string | null; - overrideProvider?: string | null; - overrideModel?: string | null; + currentProvider?: unknown; + currentModel?: unknown; + defaultProvider?: unknown; + defaultModel?: unknown; + entryProvider?: unknown; + entryModel?: unknown; + overrideProvider?: unknown; + overrideModel?: unknown; }; export function resolveSessionInfoModelSelection(params: SessionInfoModelSelectionParams): { modelProvider?: string; model?: string; } { - const fallbackProvider = params.currentProvider ?? params.defaultProvider ?? undefined; - const fallbackModel = params.currentModel ?? params.defaultModel ?? undefined; + const fallbackProvider = + normalizeOptionalString(params.currentProvider) ?? + normalizeOptionalString(params.defaultProvider) ?? + undefined; + const fallbackModel = + normalizeOptionalString(params.currentModel) ?? + normalizeOptionalString(params.defaultModel) ?? + undefined; if (params.entryProvider !== undefined || params.entryModel !== undefined) { return { - modelProvider: params.entryProvider ?? fallbackProvider, - model: params.entryModel ?? fallbackModel, + modelProvider: normalizeOptionalString(params.entryProvider) ?? fallbackProvider, + model: normalizeOptionalString(params.entryModel) ?? fallbackModel, }; } - const overrideModel = params.overrideModel?.trim(); + const overrideModel = normalizeOptionalString(params.overrideModel); if (overrideModel) { - const overrideProvider = params.overrideProvider?.trim(); + const overrideProvider = normalizeOptionalString(params.overrideProvider); return { modelProvider: overrideProvider || fallbackProvider, model: overrideModel, diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index c51c7bf0890..58db9f310cf 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -389,6 +389,18 @@ describe("model-selection", () => { model: "kimi-code", }); }); + + it("ignores malformed persisted model fields and tolerates a missing default provider", () => { + expect( + resolvePersistedModelRef({ + defaultProvider: undefined, + runtimeProvider: { provider: "openai" }, + runtimeModel: false, + overrideProvider: ["anthropic"], + overrideModel: 123, + }), + ).toBeNull(); + }); }); describe("resolvePersistedOverrideModelRef", () => { @@ -416,6 +428,16 @@ describe("model-selection", () => { model: "kimi-code", }); }); + + it("ignores malformed persisted override fields", () => { + expect( + resolvePersistedOverrideModelRef({ + defaultProvider: undefined, + overrideProvider: ["anthropic"], + overrideModel: 123, + }), + ).toBeNull(); + }); }); describe("resolvePersistedSelectedModelRef", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 58e6abcb7ea..aecba082ad1 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -81,13 +81,17 @@ export { }; export { isCliProvider } from "./model-selection-cli.js"; +function normalizePersistedDefaultProvider(value: unknown): string { + return normalizeOptionalString(value) ?? DEFAULT_PROVIDER; +} + export function resolvePersistedOverrideModelRef(params: { - defaultProvider: string; + defaultProvider?: unknown; overrideProvider?: unknown; overrideModel?: unknown; allowPluginNormalization?: boolean; }): ModelRef | null { - const defaultProvider = params.defaultProvider.trim(); + const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider); const overrideProvider = normalizeOptionalString(params.overrideProvider); const overrideModel = normalizeOptionalString(params.overrideModel); if (!overrideModel) { @@ -109,14 +113,14 @@ export function resolvePersistedOverrideModelRef(params: { * Use this when callers intentionally want the last executed model identity. */ export function resolvePersistedModelRef(params: { - defaultProvider: string; + defaultProvider?: unknown; runtimeProvider?: unknown; runtimeModel?: unknown; overrideProvider?: unknown; overrideModel?: unknown; allowPluginNormalization?: boolean; }): ModelRef | null { - const defaultProvider = params.defaultProvider.trim(); + const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider); const runtimeProvider = normalizeOptionalString(params.runtimeProvider); const runtimeModel = normalizeOptionalString(params.runtimeModel); if (runtimeModel) { @@ -146,7 +150,7 @@ export function resolvePersistedModelRef(params: { * overrides before falling back to runtime identity. */ export function resolvePersistedSelectedModelRef(params: { - defaultProvider: string; + defaultProvider?: unknown; runtimeProvider?: unknown; runtimeModel?: unknown; overrideProvider?: unknown; @@ -171,11 +175,11 @@ export function resolvePersistedSelectedModelRef(params: { } export function normalizeStoredOverrideModel(params: { - providerOverride?: string | null; - modelOverride?: string | null; + providerOverride?: unknown; + modelOverride?: unknown; }): { providerOverride?: string; modelOverride?: string } { - const providerOverride = params.providerOverride?.trim(); - const modelOverride = params.modelOverride?.trim(); + const providerOverride = normalizeOptionalString(params.providerOverride); + const modelOverride = normalizeOptionalString(params.modelOverride); if (!providerOverride || !modelOverride) { return { providerOverride,