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.
This commit is contained in:
kiranvk2011 2026-04-03 12:12:54 +00:00 committed by Peter Steinberger
parent 251e086eac
commit e8f6ceedd4
2 changed files with 52 additions and 0 deletions

View File

@ -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<string, unknown>) => void) => {
const store: Record<string, typeof sessionEntry> = { 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();

View File

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