Skip to content

Commit a0e0bf5

Browse files
committed
fix(status): ignore malformed persisted model fields
1 parent edb7e00 commit a0e0bf5

5 files changed

Lines changed: 96 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- 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.
4646
- 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.
4747
- 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.
48-
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Thanks @vincentkoc.
48+
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Fixes #76206. Thanks @vincentkoc.
4949
- 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.
5050
- 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.
5151
- 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.

src/agents/model-selection-display.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ describe("model-selection-display", () => {
3232
}),
3333
).toBe("anthropic/claude-sonnet-4-6");
3434
});
35+
36+
it("ignores malformed persisted model values instead of throwing", () => {
37+
expect(
38+
resolveModelDisplayRef({
39+
runtimeProvider: { provider: "openai" },
40+
runtimeModel: false,
41+
overrideProvider: ["anthropic"],
42+
overrideModel: 123,
43+
fallbackModel: " openai/gpt-5.5 ",
44+
}),
45+
).toBe("openai/gpt-5.5");
46+
});
3547
});
3648

3749
describe("resolveModelDisplayName", () => {
@@ -100,5 +112,21 @@ describe("model-selection-display", () => {
100112
model: "gpt-5.4",
101113
});
102114
});
115+
116+
it("ignores malformed persisted session model values", () => {
117+
expect(
118+
resolveSessionInfoModelSelection({
119+
currentProvider: { provider: "openai" },
120+
currentModel: false,
121+
defaultProvider: "anthropic",
122+
defaultModel: "claude-sonnet-4-6",
123+
entryProvider: ["openrouter"],
124+
entryModel: 123,
125+
}),
126+
).toEqual({
127+
modelProvider: "anthropic",
128+
model: "claude-sonnet-4-6",
129+
});
130+
});
103131
});
104132
});

src/agents/model-selection-display.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
import { normalizeOptionalString } from "../shared/string-coerce.js";
2+
13
type ModelDisplaySelectionParams = {
2-
runtimeProvider?: string | null;
3-
runtimeModel?: string | null;
4-
overrideProvider?: string | null;
5-
overrideModel?: string | null;
6-
fallbackModel?: string | null;
4+
runtimeProvider?: unknown;
5+
runtimeModel?: unknown;
6+
overrideProvider?: unknown;
7+
overrideModel?: unknown;
8+
fallbackModel?: unknown;
79
};
810

911
export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): string | undefined {
10-
const runtimeModel = params.runtimeModel?.trim();
11-
const runtimeProvider = params.runtimeProvider?.trim();
12+
const runtimeModel = normalizeOptionalString(params.runtimeModel);
13+
const runtimeProvider = normalizeOptionalString(params.runtimeProvider);
1214
if (runtimeModel) {
1315
if (runtimeModel.includes("/")) {
1416
return runtimeModel;
@@ -22,8 +24,8 @@ export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): str
2224
return runtimeProvider;
2325
}
2426

25-
const overrideModel = params.overrideModel?.trim();
26-
const overrideProvider = params.overrideProvider?.trim();
27+
const overrideModel = normalizeOptionalString(params.overrideModel);
28+
const overrideProvider = normalizeOptionalString(params.overrideProvider);
2729
if (overrideModel) {
2830
if (overrideModel.includes("/")) {
2931
return overrideModel;
@@ -37,7 +39,7 @@ export function resolveModelDisplayRef(params: ModelDisplaySelectionParams): str
3739
return overrideProvider;
3840
}
3941

40-
const fallbackModel = params.fallbackModel?.trim();
42+
const fallbackModel = normalizeOptionalString(params.fallbackModel);
4143
return fallbackModel || undefined;
4244
}
4345

@@ -54,33 +56,39 @@ export function resolveModelDisplayName(params: ModelDisplaySelectionParams): st
5456
}
5557

5658
type SessionInfoModelSelectionParams = {
57-
currentProvider?: string | null;
58-
currentModel?: string | null;
59-
defaultProvider?: string | null;
60-
defaultModel?: string | null;
61-
entryProvider?: string | null;
62-
entryModel?: string | null;
63-
overrideProvider?: string | null;
64-
overrideModel?: string | null;
59+
currentProvider?: unknown;
60+
currentModel?: unknown;
61+
defaultProvider?: unknown;
62+
defaultModel?: unknown;
63+
entryProvider?: unknown;
64+
entryModel?: unknown;
65+
overrideProvider?: unknown;
66+
overrideModel?: unknown;
6567
};
6668

6769
export function resolveSessionInfoModelSelection(params: SessionInfoModelSelectionParams): {
6870
modelProvider?: string;
6971
model?: string;
7072
} {
71-
const fallbackProvider = params.currentProvider ?? params.defaultProvider ?? undefined;
72-
const fallbackModel = params.currentModel ?? params.defaultModel ?? undefined;
73+
const fallbackProvider =
74+
normalizeOptionalString(params.currentProvider) ??
75+
normalizeOptionalString(params.defaultProvider) ??
76+
undefined;
77+
const fallbackModel =
78+
normalizeOptionalString(params.currentModel) ??
79+
normalizeOptionalString(params.defaultModel) ??
80+
undefined;
7381

7482
if (params.entryProvider !== undefined || params.entryModel !== undefined) {
7583
return {
76-
modelProvider: params.entryProvider ?? fallbackProvider,
77-
model: params.entryModel ?? fallbackModel,
84+
modelProvider: normalizeOptionalString(params.entryProvider) ?? fallbackProvider,
85+
model: normalizeOptionalString(params.entryModel) ?? fallbackModel,
7886
};
7987
}
8088

81-
const overrideModel = params.overrideModel?.trim();
89+
const overrideModel = normalizeOptionalString(params.overrideModel);
8290
if (overrideModel) {
83-
const overrideProvider = params.overrideProvider?.trim();
91+
const overrideProvider = normalizeOptionalString(params.overrideProvider);
8492
return {
8593
modelProvider: overrideProvider || fallbackProvider,
8694
model: overrideModel,

src/agents/model-selection.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,18 @@ describe("model-selection", () => {
389389
model: "kimi-code",
390390
});
391391
});
392+
393+
it("ignores malformed persisted model fields and tolerates a missing default provider", () => {
394+
expect(
395+
resolvePersistedModelRef({
396+
defaultProvider: undefined,
397+
runtimeProvider: { provider: "openai" },
398+
runtimeModel: false,
399+
overrideProvider: ["anthropic"],
400+
overrideModel: 123,
401+
}),
402+
).toBeNull();
403+
});
392404
});
393405

394406
describe("resolvePersistedOverrideModelRef", () => {
@@ -416,6 +428,16 @@ describe("model-selection", () => {
416428
model: "kimi-code",
417429
});
418430
});
431+
432+
it("ignores malformed persisted override fields", () => {
433+
expect(
434+
resolvePersistedOverrideModelRef({
435+
defaultProvider: undefined,
436+
overrideProvider: ["anthropic"],
437+
overrideModel: 123,
438+
}),
439+
).toBeNull();
440+
});
419441
});
420442

421443
describe("resolvePersistedSelectedModelRef", () => {

src/agents/model-selection.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,17 @@ export {
8181
};
8282
export { isCliProvider } from "./model-selection-cli.js";
8383

84+
function normalizePersistedDefaultProvider(value: unknown): string {
85+
return normalizeOptionalString(value) ?? DEFAULT_PROVIDER;
86+
}
87+
8488
export function resolvePersistedOverrideModelRef(params: {
85-
defaultProvider: string;
89+
defaultProvider?: unknown;
8690
overrideProvider?: unknown;
8791
overrideModel?: unknown;
8892
allowPluginNormalization?: boolean;
8993
}): ModelRef | null {
90-
const defaultProvider = params.defaultProvider.trim();
94+
const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider);
9195
const overrideProvider = normalizeOptionalString(params.overrideProvider);
9296
const overrideModel = normalizeOptionalString(params.overrideModel);
9397
if (!overrideModel) {
@@ -109,14 +113,14 @@ export function resolvePersistedOverrideModelRef(params: {
109113
* Use this when callers intentionally want the last executed model identity.
110114
*/
111115
export function resolvePersistedModelRef(params: {
112-
defaultProvider: string;
116+
defaultProvider?: unknown;
113117
runtimeProvider?: unknown;
114118
runtimeModel?: unknown;
115119
overrideProvider?: unknown;
116120
overrideModel?: unknown;
117121
allowPluginNormalization?: boolean;
118122
}): ModelRef | null {
119-
const defaultProvider = params.defaultProvider.trim();
123+
const defaultProvider = normalizePersistedDefaultProvider(params.defaultProvider);
120124
const runtimeProvider = normalizeOptionalString(params.runtimeProvider);
121125
const runtimeModel = normalizeOptionalString(params.runtimeModel);
122126
if (runtimeModel) {
@@ -146,7 +150,7 @@ export function resolvePersistedModelRef(params: {
146150
* overrides before falling back to runtime identity.
147151
*/
148152
export function resolvePersistedSelectedModelRef(params: {
149-
defaultProvider: string;
153+
defaultProvider?: unknown;
150154
runtimeProvider?: unknown;
151155
runtimeModel?: unknown;
152156
overrideProvider?: unknown;
@@ -171,11 +175,11 @@ export function resolvePersistedSelectedModelRef(params: {
171175
}
172176

173177
export function normalizeStoredOverrideModel(params: {
174-
providerOverride?: string | null;
175-
modelOverride?: string | null;
178+
providerOverride?: unknown;
179+
modelOverride?: unknown;
176180
}): { providerOverride?: string; modelOverride?: string } {
177-
const providerOverride = params.providerOverride?.trim();
178-
const modelOverride = params.modelOverride?.trim();
181+
const providerOverride = normalizeOptionalString(params.providerOverride);
182+
const modelOverride = normalizeOptionalString(params.modelOverride);
179183
if (!providerOverride || !modelOverride) {
180184
return {
181185
providerOverride,

0 commit comments

Comments
 (0)