diff --git a/src/commands/doctor/shared/codex-route-warnings.test.ts b/src/commands/doctor/shared/codex-route-warnings.test.ts index 7ee4235265b..167b44496f8 100644 --- a/src/commands/doctor/shared/codex-route-warnings.test.ts +++ b/src/commands/doctor/shared/codex-route-warnings.test.ts @@ -3776,6 +3776,80 @@ describe("collectCodexRouteWarnings", () => { expect(store.main.agentRuntimeOverride).toBeUndefined(); }); + it("repairs providerless auto Codex session overrides", () => { + const store: Record = { + main: { + sessionId: "s1", + updatedAt: 1, + modelProvider: "ollama", + model: "gpt-5.5", + modelOverride: "gpt-5.5", + modelOverrideSource: "auto", + authProfileOverride: "openai-codex:default", + authProfileOverrideSource: "auto", + contextTokens: 64_000, + contextBudgetStatus: { + schemaVersion: 1, + source: "pre-prompt-estimate", + updatedAt: 1, + provider: "ollama", + model: "gpt-5.5", + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 1_000, + contextTokenBudget: 64_000, + promptBudgetBeforeReserve: 62_000, + reserveTokens: 2_000, + effectiveReserveTokens: 2_000, + remainingPromptBudgetTokens: 61_000, + overflowTokens: 0, + toolResultReducibleChars: 0, + messageCount: 1, + unwindowedMessageCount: 1, + }, + }, + }; + + const result = repairCodexSessionStoreRoutes({ + store, + now: 123, + }); + + expect(result).toEqual({ changed: true, sessionKeys: ["main"] }); + expect(store.main.updatedAt).toBe(123); + expect(store.main.providerOverride).toBe("openai"); + expect(store.main.modelOverride).toBe("gpt-5.5"); + expect(store.main.modelOverrideSource).toBe("auto"); + expect(store.main.authProfileOverride).toBe("openai-codex:default"); + expect(store.main.authProfileOverrideSource).toBe("auto"); + expect(store.main.modelProvider).toBeUndefined(); + expect(store.main.model).toBeUndefined(); + expect(store.main.contextTokens).toBeUndefined(); + expect(store.main.contextBudgetStatus).toBeUndefined(); + }); + + it("preserves legacy providerless overrides with Codex auth pins", () => { + const store: Record = { + main: { + sessionId: "s1", + updatedAt: 1, + modelOverride: "gpt-5.5", + authProfileOverride: "openai-codex:default", + authProfileOverrideSource: "auto", + }, + }; + + const result = repairCodexSessionStoreRoutes({ + store, + now: 123, + }); + + expect(result).toEqual({ changed: false, sessionKeys: [] }); + expect(store.main.updatedAt).toBe(1); + expect(store.main.providerOverride).toBeUndefined(); + expect(store.main.modelOverride).toBe("gpt-5.5"); + }); + it("preserves canonical OpenAI sessions that are explicitly pinned to OpenClaw", () => { const store: Record = { main: { diff --git a/src/commands/doctor/shared/codex-route-warnings.ts b/src/commands/doctor/shared/codex-route-warnings.ts index 2550d9ddd6c..3d72c57da8d 100644 --- a/src/commands/doctor/shared/codex-route-warnings.ts +++ b/src/commands/doctor/shared/codex-route-warnings.ts @@ -91,6 +91,15 @@ function isOpenAICodexModelRef(model: string | undefined): model is string { return normalizeString(model)?.startsWith("openai-codex/") === true; } +function isOpenAICodexAuthProfileRef(profile: unknown): boolean { + return normalizeString(profile)?.startsWith("openai-codex:") === true; +} + +function isProviderlessModelRef(model: unknown): model is string { + const normalized = normalizeString(model); + return Boolean(normalized && !normalized.includes("/")); +} + function toCanonicalOpenAIModelRef(model: string): string | undefined { if (!isOpenAICodexModelRef(model)) { return undefined; @@ -2918,6 +2927,31 @@ function clearStaleSessionRuntimePins(entry: SessionEntry): boolean { return changed; } +function repairProviderlessCodexSessionOverride(entry: SessionEntry): boolean { + if ( + !isProviderlessModelRef(entry.modelOverride) || + !isOpenAICodexAuthProfileRef(entry.authProfileOverride) || + entry.authProfileOverrideSource !== "auto" || + entry.modelOverrideSource !== "auto" || + normalizeString(entry.providerOverride) + ) { + return false; + } + + entry.providerOverride = "openai"; + if (entry.model !== undefined || entry.modelProvider !== undefined) { + delete entry.model; + delete entry.modelProvider; + } + if (entry.contextTokens !== undefined) { + delete entry.contextTokens; + } + if (entry.contextBudgetStatus !== undefined) { + delete entry.contextBudgetStatus; + } + return true; +} + export function repairCodexSessionStoreRoutes(params: { store: Record; now?: number; @@ -2938,7 +2972,9 @@ export function repairCodexSessionStoreRoutes(params: { providerKey: "providerOverride", modelKey: "modelOverride", }); - const changedModelRoute = changedRuntimeModelRoute || changedOverrideModelRoute; + const changedProviderlessOverride = repairProviderlessCodexSessionOverride(entry); + const changedModelRoute = + changedRuntimeModelRoute || changedOverrideModelRoute || changedProviderlessOverride; const changedFallbackNotice = clearStaleCodexFallbackNotice(entry); const changedRuntimePins = changedModelRoute || changedFallbackNotice ? clearStaleSessionRuntimePins(entry) : false; @@ -2964,6 +3000,11 @@ function scanCodexSessionStoreRoutes(store: Record): strin normalizeString(entry.providerOverride) === "openai-codex" || isOpenAICodexModelRef(entry.model) || isOpenAICodexModelRef(entry.modelOverride) || + (isProviderlessModelRef(entry.modelOverride) && + isOpenAICodexAuthProfileRef(entry.authProfileOverride) && + entry.authProfileOverrideSource === "auto" && + entry.modelOverrideSource === "auto" && + !normalizeString(entry.providerOverride)) || isOpenAICodexModelRef(entry.fallbackNoticeSelectedModel) || isOpenAICodexModelRef(entry.fallbackNoticeActiveModel); return hasLegacyRoute ? [sessionKey] : [];