fix(status): ignore malformed persisted model fields

This commit is contained in:
Vincent Koc
2026-05-03 12:27:57 -07:00
parent edb7e00721
commit a0e0bf5848
5 changed files with 96 additions and 34 deletions

View File

@@ -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.

View File

@@ -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",
});
});
});
});

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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,