From e8f6ceedd4e7c2a58f8543aac1d414400c97bbf8 Mon Sep 17 00:00:00 2001 From: kiranvk2011 Date: Fri, 3 Apr 2026 12:12:54 +0000 Subject: [PATCH] fix: clear stale liveModelSwitchPending flag when model already matches When the liveModelSwitchPending flag is set but the current model already matches the persisted selection (e.g. the switch was applied as an override and the current attempt is already using the new model), the flag is now consumed eagerly via a fire-and-forget clearLiveModelSwitchPending() call. Without this, the stale flag could persist across fallback iterations and later cause a spurious LiveSessionModelSwitchError when the model rotates to a fallback candidate that differs from the persisted selection. Also expands JSDoc on shouldSwitchToLiveModel to document the stale-flag clearing and deferral semantics. --- src/agents/live-model-switch.test.ts | 33 ++++++++++++++++++++++++++++ src/agents/live-model-switch.ts | 19 ++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/agents/live-model-switch.test.ts b/src/agents/live-model-switch.test.ts index 81414782c99..a75361652a5 100644 --- a/src/agents/live-model-switch.test.ts +++ b/src/agents/live-model-switch.test.ts @@ -350,6 +350,39 @@ describe("live model switch", () => { expect(result).toBeUndefined(); }); + it("clears the stale liveModelSwitchPending flag when models already match", async () => { + const sessionEntry = { + liveModelSwitchPending: true, + providerOverride: "anthropic", + modelOverride: "claude-opus-4-6", + }; + state.loadSessionStoreMock.mockReturnValue({ main: sessionEntry }); + state.updateSessionStoreMock.mockImplementation( + async (_path: string, updater: (store: Record) => void) => { + const store: Record = { main: sessionEntry }; + updater(store); + }, + ); + + const { shouldSwitchToLiveModel } = await loadModule(); + + const result = shouldSwitchToLiveModel({ + cfg: { session: { store: "/tmp/custom-store.json" } }, + sessionKey: "main", + agentId: "reply", + defaultProvider: "anthropic", + defaultModel: "claude-opus-4-6", + currentProvider: "anthropic", + currentModel: "claude-opus-4-6", + }); + + expect(result).toBeUndefined(); + // Give the fire-and-forget clearLiveModelSwitchPending a tick to resolve + await new Promise((r) => setTimeout(r, 10)); + expect(state.updateSessionStoreMock).toHaveBeenCalledTimes(1); + expect(sessionEntry).not.toHaveProperty("liveModelSwitchPending"); + }); + it("returns undefined when sessionKey is missing", async () => { const { shouldSwitchToLiveModel } = await loadModule(); diff --git a/src/agents/live-model-switch.ts b/src/agents/live-model-switch.ts index 13a338b9ca2..3b9a63a4763 100644 --- a/src/agents/live-model-switch.ts +++ b/src/agents/live-model-switch.ts @@ -119,6 +119,17 @@ export function shouldTrackPersistedLiveSessionModelSelection( * `liveModelSwitchPending` flag is `true` AND the persisted selection differs * from the currently running model; otherwise returns `undefined`. * + * When the flag is set but the current model already matches the persisted + * selection (e.g. the switch was applied as an override and the current + * attempt is already using the new model), the flag is consumed (cleared) + * eagerly to prevent it from persisting as stale state. + * + * **Deferral semantics:** The caller in `run.ts` only acts on the returned + * selection when `canRestartForLiveSwitch` is `true`. If the run cannot + * restart (e.g. a tool call is in progress), the flag intentionally remains + * set so the switch fires on the next clean retry opportunity — even if that + * falls into a subsequent user turn. + * * This replaces the previous approach that used an in-memory map * (`consumeEmbeddedRunModelSwitch`) which could not distinguish between * user-initiated `/model` switches and system-initiated fallback rotations. @@ -164,6 +175,14 @@ export function shouldSwitchToLiveModel(params: { persisted, ) ) { + // Current model already matches the persisted selection — the switch has + // effectively been applied. Clear the stale flag so subsequent fallback + // iterations don't re-evaluate it. + void clearLiveModelSwitchPending({ + cfg, + sessionKey, + agentId: params.agentId, + }); return undefined; } return persisted ?? undefined;