From 96886381a3b547e30ef7edd48d96b640674043bc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:44:55 -0700 Subject: [PATCH] fix(status): tolerate malformed session model refs --- CHANGELOG.md | 1 + src/agents/model-selection.test.ts | 12 ++++++++ src/agents/model-selection.ts | 33 +++++++++++---------- src/commands/status.summary.runtime.test.ts | 14 +++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34720222dc8..7f657f808b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. 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. - 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. - Plugins/release: verify published plugin npm tarballs expose compiled runtime entries after publish, catching TS-only package artifacts before release closeout. Thanks @vincentkoc. diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index af11246986c..c51c7bf0890 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -448,6 +448,18 @@ describe("model-selection", () => { model: "anthropic/claude-haiku-4.5", }); }); + + it("ignores malformed persisted model metadata instead of throwing", () => { + expect( + resolvePersistedSelectedModelRef({ + defaultProvider: "anthropic", + runtimeProvider: { provider: "openai" }, + runtimeModel: false, + overrideProvider: ["openrouter"], + overrideModel: 123, + }), + ).toBeNull(); + }); }); describe("inferUniqueProviderFromConfiguredModels", () => { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index ac258e78120..58e6abcb7ea 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -4,7 +4,10 @@ import { toAgentModelListLike, } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { resolveAgentConfig, resolveAgentEffectiveModelPrimary, @@ -80,13 +83,13 @@ export { isCliProvider } from "./model-selection-cli.js"; export function resolvePersistedOverrideModelRef(params: { defaultProvider: string; - overrideProvider?: string; - overrideModel?: string; + overrideProvider?: unknown; + overrideModel?: unknown; allowPluginNormalization?: boolean; }): ModelRef | null { const defaultProvider = params.defaultProvider.trim(); - const overrideProvider = params.overrideProvider?.trim(); - const overrideModel = params.overrideModel?.trim(); + const overrideProvider = normalizeOptionalString(params.overrideProvider); + const overrideModel = normalizeOptionalString(params.overrideModel); if (!overrideModel) { return null; } @@ -107,15 +110,15 @@ export function resolvePersistedOverrideModelRef(params: { */ export function resolvePersistedModelRef(params: { defaultProvider: string; - runtimeProvider?: string; - runtimeModel?: string; - overrideProvider?: string; - overrideModel?: string; + runtimeProvider?: unknown; + runtimeModel?: unknown; + overrideProvider?: unknown; + overrideModel?: unknown; allowPluginNormalization?: boolean; }): ModelRef | null { const defaultProvider = params.defaultProvider.trim(); - const runtimeProvider = params.runtimeProvider?.trim(); - const runtimeModel = params.runtimeModel?.trim(); + const runtimeProvider = normalizeOptionalString(params.runtimeProvider); + const runtimeModel = normalizeOptionalString(params.runtimeModel); if (runtimeModel) { if (runtimeProvider) { return { provider: runtimeProvider, model: runtimeModel }; @@ -144,10 +147,10 @@ export function resolvePersistedModelRef(params: { */ export function resolvePersistedSelectedModelRef(params: { defaultProvider: string; - runtimeProvider?: string; - runtimeModel?: string; - overrideProvider?: string; - overrideModel?: string; + runtimeProvider?: unknown; + runtimeModel?: unknown; + overrideProvider?: unknown; + overrideModel?: unknown; allowPluginNormalization?: boolean; }): ModelRef | null { const override = resolvePersistedOverrideModelRef({ diff --git a/src/commands/status.summary.runtime.test.ts b/src/commands/status.summary.runtime.test.ts index 6d5d46e2e9f..6c9cf27bbfc 100644 --- a/src/commands/status.summary.runtime.test.ts +++ b/src/commands/status.summary.runtime.test.ts @@ -106,4 +106,18 @@ describe("statusSummaryRuntime.resolveSessionModelRef", () => { model: "gpt-5.4", }); }); + + it("falls back to configured defaults when persisted session model fields are malformed", () => { + expect( + statusSummaryRuntime.resolveSessionModelRef(cfg, { + modelProvider: { provider: "openai" }, + model: false, + providerOverride: ["anthropic"], + modelOverride: 123, + } as never), + ).toEqual({ + provider: "anthropic", + model: "claude-sonnet-4-6", + }); + }); });