diff --git a/CHANGELOG.md b/CHANGELOG.md index 0199c4f5b93..14bb21eb19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai - iMessage: stop leaking inline `[[reply_to:...]]` tags into delivered text by sending `reply_to` as RPC metadata and stripping stray directive tags from outbound messages. (#39512) Thanks @mvanhorn. - CLI/plugins: make routed commands use the same auto-enabled bundled-channel snapshot as gateway startup, so configured bundled channels like Slack load without requiring a prior config rewrite. (#54809) Thanks @neeravmakwana. - CLI/message send: write manual `openclaw message send` deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617. +- Agents/live switch: stop transient cron and subagent model overrides from being misread as persisted live-session switches, so isolated runs no longer fail with `LiveSessionModelSwitchError`. Thanks @vincentkoc. - CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider - Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana. - OpenAI/WebSocket: preserve reasoning replay metadata and tool-call item ids on WebSocket tool turns, and start a fresh response chain when full-context resend is required. (#53856) Thanks @xujingchen1996. diff --git a/src/agents/live-model-switch.test.ts b/src/agents/live-model-switch.test.ts index 42e73404c26..7c4bbbc2224 100644 --- a/src/agents/live-model-switch.test.ts +++ b/src/agents/live-model-switch.test.ts @@ -90,6 +90,34 @@ describe("live model switch", () => { }); }); + it("prefers persisted runtime model fields ahead of session overrides", async () => { + state.loadSessionStoreMock.mockReturnValue({ + main: { + providerOverride: "anthropic", + modelOverride: "claude-opus-4-6", + modelProvider: "anthropic", + model: "claude-sonnet-4-6", + }, + }); + + const { resolveLiveSessionModelSelection } = await loadModule(); + + expect( + resolveLiveSessionModelSelection({ + cfg: { session: { store: "/tmp/custom-store.json" } }, + sessionKey: "main", + agentId: "reply", + defaultProvider: "openai", + defaultModel: "gpt-5.4", + }), + ).toEqual({ + provider: "anthropic", + model: "claude-sonnet-4-6", + authProfileId: undefined, + authProfileIdSource: undefined, + }); + }); + it("queues a live switch only when an active run was aborted", async () => { state.abortEmbeddedPiRunMock.mockReturnValue(true); @@ -126,4 +154,21 @@ describe("live model switch", () => { ), ).toBe(false); }); + + it("does not track persisted live selection when the run started on a transient model override", async () => { + const { shouldTrackPersistedLiveSessionModelSelection } = await loadModule(); + + expect( + shouldTrackPersistedLiveSessionModelSelection( + { + provider: "anthropic", + model: "claude-haiku-4-5", + }, + { + provider: "anthropic", + model: "claude-sonnet-4-6", + }, + ), + ).toBe(false); + }); }); diff --git a/src/agents/live-model-switch.ts b/src/agents/live-model-switch.ts index 0202e83090d..1da62c090c0 100644 --- a/src/agents/live-model-switch.ts +++ b/src/agents/live-model-switch.ts @@ -48,8 +48,10 @@ export function resolveLiveSessionModelSelection(params: { agentId, }); const entry = loadSessionStore(storePath, { skipCache: true })[sessionKey]; - const provider = entry?.providerOverride?.trim() || defaultModelRef.provider; - const model = entry?.modelOverride?.trim() || defaultModelRef.model; + const runtimeProvider = entry?.modelProvider?.trim(); + const runtimeModel = entry?.model?.trim(); + const provider = runtimeProvider || entry?.providerOverride?.trim() || defaultModelRef.provider; + const model = runtimeModel || entry?.modelOverride?.trim() || defaultModelRef.model; const authProfileId = entry?.authProfileOverride?.trim() || undefined; return { provider, @@ -101,3 +103,15 @@ export function hasDifferentLiveSessionModelSelection( next.authProfileIdSource ); } + +export function shouldTrackPersistedLiveSessionModelSelection( + current: { + provider: string; + model: string; + authProfileId?: string; + authProfileIdSource?: string; + }, + persisted: LiveSessionModelSelection | null | undefined, +): boolean { + return !hasDifferentLiveSessionModelSelection(current, persisted); +} diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 5da8752c117..0a29a3f8271 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -28,6 +28,7 @@ import { hasDifferentLiveSessionModelSelection, LiveSessionModelSwitchError, resolveLiveSessionModelSelection, + shouldTrackPersistedLiveSessionModelSelection, consumeLiveSessionModelSwitch, } from "../live-model-switch.js"; import { @@ -242,6 +243,10 @@ export async function runEmbeddedPiAgent( defaultProvider: provider, defaultModel: modelId, }); + const shouldTrackPersistedLiveSelection = shouldTrackPersistedLiveSessionModelSelection( + resolveCurrentLiveSelection(), + resolvePersistedLiveSelection(), + ); const { advanceAuthProfile, initializeAuthProfile, @@ -449,7 +454,9 @@ export async function runEmbeddedPiAgent( }; } runLoopIterations += 1; - const nextSelection = resolvePersistedLiveSelection(); + const nextSelection = shouldTrackPersistedLiveSelection + ? resolvePersistedLiveSelection() + : null; if (hasDifferentLiveSessionModelSelection(resolveCurrentLiveSelection(), nextSelection)) { log.info( `live session model switch detected before attempt for ${params.sessionId}: ${provider}/${modelId} -> ${nextSelection.provider}/${nextSelection.model}`, @@ -605,9 +612,10 @@ export async function runEmbeddedPiAgent( } const failedOrAbortedAttempt = aborted || Boolean(promptError) || Boolean(assistantErrorText) || timedOut; - const persistedSelection = failedOrAbortedAttempt - ? resolvePersistedLiveSelection() - : null; + const persistedSelection = + failedOrAbortedAttempt && shouldTrackPersistedLiveSelection + ? resolvePersistedLiveSelection() + : null; if ( failedOrAbortedAttempt && canRestartForLiveSwitch &&