From d84da290e328f1e923a111a90c280f147dcaafac Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 11 Mar 2026 17:53:41 +0900 Subject: [PATCH] Route runtime fallback session.status events Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/runtime-fallback/event-handler.ts | 92 ++------------------- 1 file changed, 5 insertions(+), 87 deletions(-) diff --git a/src/hooks/runtime-fallback/event-handler.ts b/src/hooks/runtime-fallback/event-handler.ts index c631c8cd5..d39d9e97a 100644 --- a/src/hooks/runtime-fallback/event-handler.ts +++ b/src/hooks/runtime-fallback/event-handler.ts @@ -2,15 +2,15 @@ import type { HookDeps } from "./types" import type { AutoRetryHelpers } from "./auto-retry" import { HOOK_NAME } from "./constants" import { log } from "../../shared/logger" -import { extractStatusCode, extractErrorName, classifyErrorType, isRetryableError, extractAutoRetrySignal } from "./error-classifier" +import { extractStatusCode, extractErrorName, classifyErrorType, isRetryableError } from "./error-classifier" import { createFallbackState, prepareFallback } from "./fallback-state" import { getFallbackModelsForSession } from "./fallback-models" import { SessionCategoryRegistry } from "../../shared/session-category-registry" -import { normalizeRetryStatusMessage, extractRetryAttempt } from "../../shared/retry-status-utils" +import { createSessionStatusHandler } from "./session-status-handler" export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { const { config, pluginConfig, sessionStates, sessionLastAccess, sessionRetryInFlight, sessionAwaitingFallbackResult, sessionFallbackTimeouts } = deps - const sessionStatusRetryKeys = new Map() + const sessionStatusHandler = createSessionStatusHandler(deps, helpers) const handleSessionCreated = (props: Record | undefined) => { const sessionInfo = props?.info as { id?: string; model?: string } | undefined @@ -35,7 +35,7 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { sessionRetryInFlight.delete(sessionID) sessionAwaitingFallbackResult.delete(sessionID) helpers.clearSessionFallbackTimeout(sessionID) - sessionStatusRetryKeys.delete(sessionID) + sessionStatusHandler.clearRetryKey(sessionID) SessionCategoryRegistry.remove(sessionID) } } @@ -185,88 +185,6 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { } } - const handleSessionStatus = async (props: Record | undefined) => { - const sessionID = props?.sessionID as string | undefined - const status = props?.status as { type?: string; message?: string; attempt?: number } | undefined - const agent = props?.agent as string | undefined - const model = props?.model as string | undefined - - if (!sessionID || status?.type !== "retry") return - - const retryMessage = typeof status.message === "string" ? status.message : "" - const retrySignal = extractAutoRetrySignal({ status: retryMessage, message: retryMessage }) - if (!retrySignal) return - - const retryKey = `${extractRetryAttempt(status.attempt, retryMessage)}:${normalizeRetryStatusMessage(retryMessage)}` - if (sessionStatusRetryKeys.get(sessionID) === retryKey) { - return - } - sessionStatusRetryKeys.set(sessionID, retryKey) - - if (sessionRetryInFlight.has(sessionID)) { - log(`[${HOOK_NAME}] session.status retry skipped — retry already in flight`, { sessionID }) - return - } - - const resolvedAgent = await helpers.resolveAgentForSessionFromContext(sessionID, agent) - const fallbackModels = getFallbackModelsForSession(sessionID, resolvedAgent, pluginConfig) - if (fallbackModels.length === 0) return - - let state = sessionStates.get(sessionID) - if (!state) { - const detectedAgent = resolvedAgent - const agentConfig = detectedAgent - ? pluginConfig?.agents?.[detectedAgent as keyof typeof pluginConfig.agents] - : undefined - const inferredModel = model || (agentConfig?.model as string | undefined) - if (!inferredModel) { - log(`[${HOOK_NAME}] session.status retry missing model info, cannot fallback`, { sessionID }) - return - } - state = createFallbackState(inferredModel) - sessionStates.set(sessionID, state) - } - sessionLastAccess.set(sessionID, Date.now()) - - if (state.pendingFallbackModel) { - log(`[${HOOK_NAME}] session.status retry skipped (pending fallback in progress)`, { - sessionID, - pendingFallbackModel: state.pendingFallbackModel, - }) - return - } - - log(`[${HOOK_NAME}] Detected provider auto-retry signal in session.status`, { - sessionID, - model: state.currentModel, - retryAttempt: status.attempt, - }) - - await helpers.abortSessionRequest(sessionID, "session.status.retry-signal") - - const result = prepareFallback(sessionID, state, fallbackModels, config) - if (result.success && config.notify_on_fallback) { - await deps.ctx.client.tui - .showToast({ - body: { - title: "Model Fallback", - message: `Switching to ${result.newModel?.split("/").pop() || result.newModel} for next request`, - variant: "warning", - duration: 5000, - }, - }) - .catch(() => {}) - } - - if (result.success && result.newModel) { - await helpers.autoRetryWithFallback(sessionID, result.newModel, resolvedAgent, "session.status") - } - - if (!result.success) { - log(`[${HOOK_NAME}] Fallback preparation failed`, { sessionID, error: result.error }) - } - } - return async ({ event }: { event: { type: string; properties?: unknown } }) => { if (!config.enabled) return @@ -276,7 +194,7 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { if (event.type === "session.deleted") { handleSessionDeleted(props); return } if (event.type === "session.stop") { await handleSessionStop(props); return } if (event.type === "session.idle") { handleSessionIdle(props); return } - if (event.type === "session.status") { await handleSessionStatus(props); return } + if (event.type === "session.status") { await sessionStatusHandler.handleSessionStatus(props); return } if (event.type === "session.error") { await handleSessionError(props); return } } }