2026-03-12 01:43:17 +09:00
|
|
|
import type { HookDeps } from "./types"
|
|
|
|
|
import type { AutoRetryHelpers } from "./auto-retry"
|
|
|
|
|
import { HOOK_NAME } from "./constants"
|
|
|
|
|
import { log } from "../../shared/logger"
|
|
|
|
|
import { extractAutoRetrySignal } from "./error-classifier"
|
|
|
|
|
import { createFallbackState } from "./fallback-state"
|
|
|
|
|
import { getFallbackModelsForSession } from "./fallback-models"
|
|
|
|
|
import { normalizeRetryStatusMessage, extractRetryAttempt } from "../../shared/retry-status-utils"
|
|
|
|
|
import { resolveFallbackBootstrapModel } from "./fallback-bootstrap-model"
|
|
|
|
|
import { dispatchFallbackRetry } from "./fallback-retry-dispatcher"
|
|
|
|
|
|
|
|
|
|
export function createSessionStatusHandler(
|
|
|
|
|
deps: HookDeps,
|
|
|
|
|
helpers: AutoRetryHelpers,
|
|
|
|
|
sessionStatusRetryKeys: Map<string, string>,
|
|
|
|
|
) {
|
|
|
|
|
const {
|
|
|
|
|
pluginConfig,
|
|
|
|
|
sessionStates,
|
|
|
|
|
sessionLastAccess,
|
|
|
|
|
sessionRetryInFlight,
|
|
|
|
|
} = deps
|
|
|
|
|
|
|
|
|
|
return async (props: Record<string, unknown> | 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
|
2026-03-14 13:47:32 +09:00
|
|
|
const timeoutEnabled = deps.config.timeout_seconds > 0
|
2026-03-12 01:43:17 +09:00
|
|
|
|
|
|
|
|
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)) {
|
2026-03-14 13:47:32 +09:00
|
|
|
if (timeoutEnabled) {
|
|
|
|
|
log(`[${HOOK_NAME}] Overriding in-flight retry due to provider auto-retry signal`, {
|
|
|
|
|
sessionID,
|
|
|
|
|
model,
|
|
|
|
|
})
|
|
|
|
|
await helpers.abortSessionRequest(sessionID, "session.status.retry-signal")
|
|
|
|
|
sessionRetryInFlight.delete(sessionID)
|
|
|
|
|
} else {
|
|
|
|
|
log(`[${HOOK_NAME}] session.status retry skipped — retry already in flight`, { sessionID })
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-12 01:43:17 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedAgent = await helpers.resolveAgentForSessionFromContext(sessionID, agent)
|
|
|
|
|
const fallbackModels = getFallbackModelsForSession(sessionID, resolvedAgent, pluginConfig)
|
2026-03-12 11:31:58 +09:00
|
|
|
if (fallbackModels.length === 0) {
|
|
|
|
|
if (!sessionStates.has(sessionID)) {
|
|
|
|
|
sessionStatusRetryKeys.delete(sessionID)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-12 01:43:17 +09:00
|
|
|
|
|
|
|
|
let state = sessionStates.get(sessionID)
|
|
|
|
|
if (!state) {
|
|
|
|
|
const initialModel = resolveFallbackBootstrapModel({
|
|
|
|
|
sessionID,
|
|
|
|
|
source: "session.status",
|
|
|
|
|
eventModel: model,
|
|
|
|
|
resolvedAgent,
|
|
|
|
|
pluginConfig,
|
|
|
|
|
})
|
|
|
|
|
if (!initialModel) {
|
2026-03-12 11:31:58 +09:00
|
|
|
sessionStatusRetryKeys.delete(sessionID)
|
2026-03-12 01:43:17 +09:00
|
|
|
log(`[${HOOK_NAME}] session.status retry missing model info, cannot fallback`, { sessionID })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state = createFallbackState(initialModel)
|
|
|
|
|
sessionStates.set(sessionID, state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sessionLastAccess.set(sessionID, Date.now())
|
|
|
|
|
|
|
|
|
|
if (state.pendingFallbackModel) {
|
2026-03-14 13:47:32 +09:00
|
|
|
if (timeoutEnabled) {
|
|
|
|
|
log(`[${HOOK_NAME}] Clearing pending fallback due to provider auto-retry signal`, {
|
|
|
|
|
sessionID,
|
|
|
|
|
pendingFallbackModel: state.pendingFallbackModel,
|
|
|
|
|
})
|
|
|
|
|
state.pendingFallbackModel = undefined
|
|
|
|
|
} else {
|
|
|
|
|
log(`[${HOOK_NAME}] session.status retry skipped (pending fallback in progress)`, {
|
|
|
|
|
sessionID,
|
|
|
|
|
pendingFallbackModel: state.pendingFallbackModel,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-12 01:43:17 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
await dispatchFallbackRetry(deps, helpers, {
|
|
|
|
|
sessionID,
|
|
|
|
|
state,
|
|
|
|
|
fallbackModels,
|
|
|
|
|
resolvedAgent,
|
|
|
|
|
source: "session.status",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|