diff --git a/src/plugin/event.model-fallback.test.ts b/src/plugin/event.model-fallback.test.ts index 5c5870bb9..a711ef08b 100644 --- a/src/plugin/event.model-fallback.test.ts +++ b/src/plugin/event.model-fallback.test.ts @@ -1,5 +1,5 @@ -declare const require: (name: string) => any -const { afterEach, describe, expect, spyOn, test } = require("bun:test") +/// +import { afterEach, describe, expect, spyOn, test } from "bun:test" import { createEventHandler } from "./event" import { createChatMessageHandler } from "./chat-message" @@ -8,6 +8,17 @@ import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/mod import * as connectedProvidersCache from "../shared/connected-providers-cache" import { unsafeTestValue } from "../../test-support/unsafe-test-value" +type EventInput = { event: { type: string; properties?: unknown } } +type EventHandlerInput = Parameters>[0] +type ChatMessageOutput = { + message: Record + parts: Array<{ type: string; text?: string }> +} + +function asEventHandlerInput(input: EventInput): EventHandlerInput { + return unsafeTestValue(input) +} + let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined @@ -17,25 +28,40 @@ function setupConnectedProviderCacheMocks(): void { } describe("createEventHandler - model fallback", () => { - const createHandler = (args?: { hooks?: any; pluginConfig?: any }) => { + const createHandler = (args?: { + hooks?: any + pluginConfig?: any + promptAsync?: (input: { path: { id: string } }) => Promise + }) => { setupConnectedProviderCacheMocks() const abortCalls: string[] = [] const promptCalls: string[] = [] + const promptAsyncCalls: string[] = [] - const handler = createEventHandler({ + const sessionClient = { + abort: async ({ path }: { path: { id: string } }) => { + abortCalls.push(path.id) + return {} + }, + prompt: async ({ path }: { path: { id: string } }) => { + promptCalls.push(path.id) + return {} + }, + ...(args?.promptAsync + ? { + promptAsync: async (input: { path: { id: string } }) => { + promptAsyncCalls.push(input.path.id) + return args.promptAsync?.(input) + }, + } + : {}), + } + + const eventHandler = createEventHandler({ ctx: unsafeTestValue({ directory: "/tmp", client: { - session: { - abort: async ({ path }: { path: { id: string } }) => { - abortCalls.push(path.id) - return {} - }, - prompt: async ({ path }: { path: { id: string } }) => { - promptCalls.push(path.id) - return {} - }, - }, + session: sessionClient, }, }), pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})), @@ -54,8 +80,9 @@ describe("createEventHandler - model fallback", () => { }), hooks: args?.hooks ?? (unsafeTestValue({})), }) + const handler = (input: EventInput): Promise => eventHandler(asEventHandlerInput(input)) - return { handler, abortCalls, promptCalls } + return { handler, abortCalls, promptCalls, promptAsyncCalls } } afterEach(() => { @@ -139,6 +166,207 @@ describe("createEventHandler - model fallback", () => { expect(promptCalls).toEqual([sessionID]) }) + test("does not dispatch duplicate fallback continuations when error events overlap", async () => { + //#given + const sessionID = "ses_model_fallback_concurrent_events" + setMainSession(sessionID) + let releasePromptAsync: (() => void) | undefined + const promptAsyncBlocked = new Promise((resolve) => { + releasePromptAsync = resolve + }) + let firstPromptAsyncStartedResolve: (() => void) | undefined + const firstPromptAsyncStarted = new Promise((resolve) => { + firstPromptAsyncStartedResolve = resolve + }) + let pendingFallbackArms = 0 + const modelFallback = unsafeTestValue({ + setSessionFallbackChain: () => {}, + setPendingModelFallback: () => { + pendingFallbackArms += 1 + return true + }, + }) + const { handler, abortCalls, promptAsyncCalls } = createHandler({ + hooks: { modelFallback }, + promptAsync: async () => { + if (promptAsyncCalls.length === 1) { + firstPromptAsyncStartedResolve?.() + } + await promptAsyncBlocked + return {} + }, + }) + + const assistantError = { + name: "APIError", + data: { + message: + "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", + isRetryable: true, + }, + } + + //#when + const messageUpdated = handler({ + event: { + type: "message.updated", + properties: { + info: { + id: "msg_err_concurrent_1", + sessionID, + role: "assistant", + error: assistantError, + modelID: "claude-opus-4-7-thinking", + providerID: "anthropic", + agent: "Sisyphus - Ultraworker", + }, + }, + }, + }) + await firstPromptAsyncStarted + const sessionError = handler({ + event: { + type: "session.error", + properties: { + sessionID, + providerID: "anthropic", + modelID: "claude-opus-4-7-thinking", + error: assistantError, + }, + }, + }) + + releasePromptAsync?.() + await Promise.all([messageUpdated, sessionError]) + + //#then + expect(pendingFallbackArms).toBe(1) + expect(promptAsyncCalls).toEqual([sessionID]) + expect(abortCalls).toEqual([sessionID]) + }) + + test("does not dispatch duplicate fallback continuations when session.error omits provider after dispatch", async () => { + //#given + const sessionID = "ses_model_fallback_providerless_duplicate" + setMainSession(sessionID) + let pendingFallbackArms = 0 + const modelFallback = unsafeTestValue({ + setSessionFallbackChain: () => {}, + setPendingModelFallback: () => { + pendingFallbackArms += 1 + return true + }, + }) + const { handler, abortCalls, promptAsyncCalls } = createHandler({ + hooks: { modelFallback }, + promptAsync: async () => ({}), + }) + + const assistantError = { + name: "APIError", + data: { + message: + "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", + isRetryable: true, + }, + } + + await handler({ + event: { + type: "message.updated", + properties: { + info: { + id: "msg_err_providerless_duplicate_1", + sessionID, + role: "assistant", + error: assistantError, + modelID: "claude-opus-4-7-thinking", + providerID: "anthropic", + agent: "Sisyphus - Ultraworker", + }, + }, + }, + }) + + //#when - same failed model arrives without provider metadata after first dispatch resolved + await handler({ + event: { + type: "session.error", + properties: { + sessionID, + error: assistantError, + }, + }, + }) + + //#then + expect(pendingFallbackArms).toBe(1) + expect(promptAsyncCalls).toEqual([sessionID]) + expect(abortCalls).toEqual([sessionID]) + }) + + test("does not collapse fallback continuations for different providers with the same model id", async () => { + //#given + const sessionID = "ses_model_fallback_same_model_different_provider" + setMainSession(sessionID) + let pendingFallbackArms = 0 + const modelFallback = unsafeTestValue({ + setSessionFallbackChain: () => {}, + setPendingModelFallback: () => { + pendingFallbackArms += 1 + return true + }, + }) + const { handler, abortCalls, promptAsyncCalls } = createHandler({ + hooks: { modelFallback }, + promptAsync: async () => ({}), + }) + + const assistantError = { + name: "APIError", + data: { + message: + "Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}", + isRetryable: true, + }, + } + + await handler({ + event: { + type: "message.updated", + properties: { + info: { + id: "msg_err_same_model_provider_1", + sessionID, + role: "assistant", + error: assistantError, + modelID: "claude-opus-4-7-thinking", + providerID: "anthropic", + agent: "Sisyphus - Ultraworker", + }, + }, + }, + }) + + //#when - a distinct provider reports the same normalized model id before idle cleanup + await handler({ + event: { + type: "session.error", + properties: { + sessionID, + providerID: "quotio", + modelID: "claude-opus-4-7-thinking", + error: assistantError, + }, + }, + }) + + //#then + expect(pendingFallbackArms).toBe(2) + expect(promptAsyncCalls).toEqual([sessionID, sessionID]) + expect(abortCalls).toEqual([sessionID, sessionID]) + }) + test("triggers retry prompt on session.status retry events and applies fallback", async () => { //#given const sessionID = "ses_status_retry_fallback" @@ -208,7 +436,7 @@ describe("createEventHandler - model fallback", () => { }, }) - const output = { message: {}, parts: [] as Array<{ type: string; text?: string }> } + const output: ChatMessageOutput = { message: {}, parts: [] } await chatMessageHandler( { sessionID, @@ -289,7 +517,7 @@ describe("createEventHandler - model fallback", () => { expect(promptCalls).toEqual([sessionID]) }) - test("does not re-arm fallback when a duplicate error reports the same failed model after fallback was applied", async () => { + test("does not leave stale pending fallback when a providerless duplicate arrives after fallback was applied", async () => { //#given const sessionID = "ses_model_fallback_duplicate_surface" setMainSession(sessionID) @@ -344,7 +572,7 @@ describe("createEventHandler - model fallback", () => { }, }) - const output = { message: {}, parts: [] as Array<{ type: string; text?: string }> } + const output: ChatMessageOutput = { message: {}, parts: [] } await chatMessageHandler( { sessionID, @@ -354,14 +582,12 @@ describe("createEventHandler - model fallback", () => { output, ) - //#when - same failed model arrives again through another OpenCode event surface + //#when - same failed model arrives again without provider metadata after fallback was applied await handler({ event: { type: "session.error", properties: { sessionID, - providerID: "anthropic", - modelID: "claude-opus-4-7-thinking", error: { name: "UnknownError", data: { @@ -375,9 +601,21 @@ describe("createEventHandler - model fallback", () => { }, }) + const staleOutput: ChatMessageOutput = { message: {}, parts: [] } + await chatMessageHandler( + { + sessionID, + agent: "sisyphus", + model: { providerID: "opencode-go", modelID: "kimi-k2.6" }, + }, + staleOutput, + ) + //#then expect(abortCalls).toEqual([sessionID]) expect(promptCalls).toEqual([sessionID]) + expect(modelFallback.hasPendingModelFallback(sessionID)).toBe(false) + expect(staleOutput.message["model"]).toBeUndefined() }) test("does not trigger model-fallback from session.status when runtime_fallback is enabled", async () => { @@ -509,7 +747,7 @@ describe("createEventHandler - model fallback", () => { }, }) - const output = { message: {}, parts: [] as Array<{ type: string; text?: string }> } + const output: ChatMessageOutput = { message: {}, parts: [] } await chatMessageHandler( { sessionID, @@ -603,7 +841,7 @@ describe("createEventHandler - model fallback", () => { }) const triggerRetryCycle = async (providerID: string, modelID: string) => { - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.error", properties: { @@ -621,9 +859,9 @@ describe("createEventHandler - model fallback", () => { }, }, }, - }) + })) - const output = { message: {}, parts: [] as Array<{ type: string; text?: string }> } + const output: ChatMessageOutput = { message: {}, parts: [] } await chatMessageHandler( { sessionID, diff --git a/src/plugin/event.ts b/src/plugin/event.ts index b1c44a3eb..976c732ab 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -54,6 +54,24 @@ type FirstMessageVariantGate = { clear: (sessionID: string) => void; }; +type FallbackContinuationDedupeKeys = { + modelKey?: string; + providerModelKey?: string; +}; + +type FallbackContinuationDedupeState = { + modelKeys: Set; + providerModelKeys: Set; + providerlessModelKeys: Set; +}; + +type FallbackContinuationContext = { + agentName?: string; + providerID?: string; + dedupeProviderID?: string; + modelID?: string; +}; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -219,6 +237,8 @@ export function createEventHandler(args: { const lastHandledModelErrorMessageID = new Map(); const lastHandledRetryStatusKey = new Map(); const lastKnownModelBySession = new Map(); + const modelFallbackContinuationsInFlight = new Set(); + const lastDispatchedModelFallbackContinuationKeys = new Map(); const resolveFallbackProviderID = (sessionID: string, providerHint?: string): string => { const normalizedProviderHint = providerHint?.trim(); @@ -359,55 +379,147 @@ export function createEventHandler(args: { return true; }; + const getFallbackContinuationKeys = (fallbackContext?: FallbackContinuationContext): FallbackContinuationDedupeKeys => { + const agentKey = fallbackContext?.agentName + ? getAgentConfigKey(fallbackContext.agentName).trim().toLowerCase() + : ""; + const providerID = fallbackContext?.dedupeProviderID?.trim().toLowerCase() ?? ""; + const modelID = fallbackContext?.modelID?.trim().toLowerCase() ?? ""; + + if (!agentKey || !modelID) { + return {}; + } + + return { + modelKey: `${agentKey}:${modelID}`, + ...(providerID ? { providerModelKey: `${agentKey}:${providerID}:${modelID}` } : {}), + }; + }; + + const getFallbackContinuationDedupeState = (sessionID: string): FallbackContinuationDedupeState => { + const existingState = lastDispatchedModelFallbackContinuationKeys.get(sessionID); + if (existingState) { + return existingState; + } + + const state = { + modelKeys: new Set(), + providerModelKeys: new Set(), + providerlessModelKeys: new Set(), + }; + lastDispatchedModelFallbackContinuationKeys.set(sessionID, state); + return state; + }; + + const wasFallbackContinuationAlreadyDispatched = ( + state: FallbackContinuationDedupeState | undefined, + keys: FallbackContinuationDedupeKeys, + ): boolean => { + if (!state || !keys.modelKey) { + return false; + } + + if (!keys.providerModelKey) { + return state.modelKeys.has(keys.modelKey); + } + + return state.providerModelKeys.has(keys.providerModelKey) || state.providerlessModelKeys.has(keys.modelKey); + }; + + const shouldSkipFallbackContinuation = ( + sessionID: string, + source: string, + fallbackContext?: FallbackContinuationContext, + ): boolean => { + const fallbackKeys = getFallbackContinuationKeys(fallbackContext); + + if (modelFallbackContinuationsInFlight.has(sessionID)) { + log("[event] model-fallback continuation skipped because one is already in flight", { sessionID, source }); + return true; + } + + const lastDispatchedKeys = lastDispatchedModelFallbackContinuationKeys.get(sessionID); + if (wasFallbackContinuationAlreadyDispatched(lastDispatchedKeys, fallbackKeys)) { + log("[event] model-fallback continuation skipped because matching fallback was already dispatched", { + sessionID, + source, + }); + return true; + } + + return false; + }; + const autoContinueAfterFallback = async ( sessionID: string, source: string, - fallbackContext?: { - agentName?: string; - providerID?: string; - modelID?: string; - }, + fallbackContext?: FallbackContinuationContext, ): Promise => { - await pluginContext.client.session.abort({ path: { id: sessionID } }).catch((error) => { - log("[event] model-fallback abort failed", { sessionID, source, error }); - }); + const fallbackKeys = getFallbackContinuationKeys(fallbackContext); - const launchAgent = fallbackContext?.agentName - ? resolveRegisteredAgentName(fallbackContext.agentName) - : undefined; - const launchModel = fallbackContext?.providerID && fallbackContext?.modelID - ? { providerID: fallbackContext.providerID, modelID: fallbackContext.modelID } - : undefined; - - const agentConfigKey = fallbackContext?.agentName - ? getAgentConfigKey(fallbackContext.agentName) - : undefined; - const agentSettings = agentConfigKey - ? pluginConfig.agents?.[agentConfigKey as keyof NonNullable] - : undefined; - const launchVariant = (agentSettings as { variant?: string } | undefined)?.variant; - - const promptBody = { - path: { id: sessionID }, - body: { - ...(launchAgent ? { agent: launchAgent } : {}), - ...(launchModel ? { model: launchModel } : {}), - ...(launchVariant ? { variant: launchVariant } : {}), - parts: [createInternalAgentContinuationTextPart("continue")], - }, - query: { directory: pluginContext.directory }, - }; - - if (typeof pluginContext.client.session.promptAsync === "function") { - await pluginContext.client.session.promptAsync(promptBody).catch((error) => { - log("[event] model-fallback promptAsync failed", { sessionID, source, error }); - }); + if (shouldSkipFallbackContinuation(sessionID, source, fallbackContext)) { return; } - await pluginContext.client.session.prompt(promptBody).catch((error) => { - log("[event] model-fallback prompt failed", { sessionID, source, error }); - }); + modelFallbackContinuationsInFlight.add(sessionID); + let dispatched = false; + try { + await pluginContext.client.session.abort({ path: { id: sessionID } }).catch((error) => { + log("[event] model-fallback abort failed", { sessionID, source, error }); + }); + + const launchAgent = fallbackContext?.agentName + ? resolveRegisteredAgentName(fallbackContext.agentName) + : undefined; + const launchModel = fallbackContext?.providerID && fallbackContext?.modelID + ? { providerID: fallbackContext.providerID, modelID: fallbackContext.modelID } + : undefined; + + const agentConfigKey = fallbackContext?.agentName + ? getAgentConfigKey(fallbackContext.agentName) + : undefined; + const agentSettings = agentConfigKey + ? pluginConfig.agents?.[agentConfigKey as keyof NonNullable] + : undefined; + const launchVariant = (agentSettings as { variant?: string } | undefined)?.variant; + + const promptBody = { + path: { id: sessionID }, + body: { + ...(launchAgent ? { agent: launchAgent } : {}), + ...(launchModel ? { model: launchModel } : {}), + ...(launchVariant ? { variant: launchVariant } : {}), + parts: [createInternalAgentContinuationTextPart("continue")], + }, + query: { directory: pluginContext.directory }, + }; + + if (typeof pluginContext.client.session.promptAsync === "function") { + await pluginContext.client.session.promptAsync(promptBody).then(() => { + dispatched = true; + }).catch((error) => { + log("[event] model-fallback promptAsync failed", { sessionID, source, error }); + }); + return; + } + + await pluginContext.client.session.prompt(promptBody).then(() => { + dispatched = true; + }).catch((error) => { + log("[event] model-fallback prompt failed", { sessionID, source, error }); + }); + } finally { + if (dispatched && fallbackKeys.modelKey) { + const dispatchedKeys = getFallbackContinuationDedupeState(sessionID); + dispatchedKeys.modelKeys.add(fallbackKeys.modelKey); + if (fallbackKeys.providerModelKey) { + dispatchedKeys.providerModelKeys.add(fallbackKeys.providerModelKey); + } else { + dispatchedKeys.providerlessModelKeys.add(fallbackKeys.modelKey); + } + } + modelFallbackContinuationsInFlight.delete(sessionID); + } }; return async (input): Promise => { @@ -526,6 +638,8 @@ export function createEventHandler(args: { lastHandledModelErrorMessageID.delete(sessionID); lastHandledRetryStatusKey.delete(sessionID); lastKnownModelBySession.delete(sessionID); + modelFallbackContinuationsInFlight.delete(sessionID); + lastDispatchedModelFallbackContinuationKeys.delete(sessionID); if (modelFallback) { clearPendingModelFallback(modelFallback, sessionID); clearSessionFallbackChain(modelFallback, sessionID); @@ -643,29 +757,30 @@ export function createEventHandler(args: { } if (agentName) { - const currentProvider = resolveFallbackProviderID( - sessionID, - info?.providerID as string | undefined, - ); + const providerHint = info?.providerID as string | undefined; + const currentProvider = resolveFallbackProviderID(sessionID, providerHint); const rawModel = (info?.modelID as string | undefined) ?? "claude-opus-4-7"; const currentModel = normalizeFallbackModelID(rawModel); - applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); + const fallbackContext = { + agentName, + providerID: currentProvider, + dedupeProviderID: providerHint, + modelID: currentModel, + }; + const shouldAutoContinue = shouldAutoRetrySession(sessionID) && + !hooks.stopContinuationGuard?.isStopped(sessionID); - const setFallback = modelFallback - ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) - : false; + if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "message.updated", fallbackContext)) { + applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); - if ( - setFallback && - shouldAutoRetrySession(sessionID) && - !hooks.stopContinuationGuard?.isStopped(sessionID) - ) { - lastHandledModelErrorMessageID.set(sessionID, assistantMessageID); - await autoContinueAfterFallback(sessionID, "message.updated", { - agentName, - providerID: currentProvider, - modelID: currentModel, - }); + const setFallback = modelFallback + ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) + : false; + + if (setFallback && shouldAutoContinue) { + lastHandledModelErrorMessageID.set(sessionID, assistantMessageID); + await autoContinueAfterFallback(sessionID, "message.updated", fallbackContext); + } } } } @@ -684,6 +799,7 @@ export function createEventHandler(args: { // (non-retry idle) so future failures with the same key can trigger fallback again. if (sessionID && status?.type === "idle") { lastHandledRetryStatusKey.delete(sessionID); + lastDispatchedModelFallbackContinuationKeys.delete(sessionID); } if (sessionID && status?.type === "retry" && isModelFallbackEnabled && !isRuntimeFallbackEnabled) { @@ -718,22 +834,25 @@ export function createEventHandler(args: { const currentProvider = resolveFallbackProviderID(sessionID, parsed.providerID); let currentModel = parsed.modelID ?? lastKnown?.modelID ?? "claude-opus-4-7"; currentModel = normalizeFallbackModelID(currentModel); - applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); + const fallbackContext = { + agentName, + providerID: currentProvider, + dedupeProviderID: parsed.providerID, + modelID: currentModel, + }; + const shouldAutoContinue = shouldAutoRetrySession(sessionID) && + !hooks.stopContinuationGuard?.isStopped(sessionID); - const setFallback = modelFallback - ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) - : false; + if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "session.status", fallbackContext)) { + applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); - if ( - setFallback && - shouldAutoRetrySession(sessionID) && - !hooks.stopContinuationGuard?.isStopped(sessionID) - ) { - await autoContinueAfterFallback(sessionID, "session.status", { - agentName, - providerID: currentProvider, - modelID: currentModel, - }); + const setFallback = modelFallback + ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) + : false; + + if (setFallback && shouldAutoContinue) { + await autoContinueAfterFallback(sessionID, "session.status", fallbackContext); + } } } } @@ -804,28 +923,29 @@ export function createEventHandler(args: { if (agentName) { const parsed = extractProviderModelFromErrorMessage(errorMessage); - const currentProvider = resolveFallbackProviderID( - sessionID, - (props?.providerID as string | undefined) || parsed.providerID, - ); + const providerHint = (props?.providerID as string | undefined) || parsed.providerID; + const currentProvider = resolveFallbackProviderID(sessionID, providerHint); let currentModel = (props?.modelID as string) || parsed.modelID || "claude-opus-4-7"; currentModel = normalizeFallbackModelID(currentModel); - applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); + const fallbackContext = { + agentName, + providerID: currentProvider, + dedupeProviderID: providerHint, + modelID: currentModel, + }; + const shouldAutoContinue = shouldAutoRetrySession(sessionID) && + !hooks.stopContinuationGuard?.isStopped(sessionID); - const setFallback = modelFallback - ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) - : false; + if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "session.error", fallbackContext)) { + applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig); - if ( - setFallback && - shouldAutoRetrySession(sessionID) && - !hooks.stopContinuationGuard?.isStopped(sessionID) - ) { - await autoContinueAfterFallback(sessionID, "session.error", { - agentName, - providerID: currentProvider, - modelID: currentModel, - }); + const setFallback = modelFallback + ? setPendingModelFallback(modelFallback, sessionID, agentName, currentProvider, currentModel) + : false; + + if (setFallback && shouldAutoContinue) { + await autoContinueAfterFallback(sessionID, "session.error", fallbackContext); + } } } }