79fb746a1c
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
225 lines
8.0 KiB
TypeScript
225 lines
8.0 KiB
TypeScript
<<<<<<< HEAD
|
|
import type { HookDeps, RuntimeFallbackTimeout } from "./types"
|
|
||||||| parent of b6f740ed (fix: enable runtime fallback for delegated child sessions (#2357))
|
|
import type { HookDeps } from "./types"
|
|
=======
|
|
import type { HookDeps } from "./types"
|
|
import type { TimerHandle } from "./timer-handle"
|
|
>>>>>>> b6f740ed (fix: enable runtime fallback for delegated child sessions (#2357))
|
|
import { HOOK_NAME } from "./constants"
|
|
import { log } from "../../shared/logger"
|
|
import { normalizeAgentName, resolveAgentForSession } from "./agent-resolver"
|
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
|
import { getFallbackModelsForSession } from "./fallback-models"
|
|
import { prepareFallback } from "./fallback-state"
|
|
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
|
|
import { buildRetryModelPayload } from "./retry-model-payload"
|
|
import { getLastUserRetryParts } from "./last-user-retry-parts"
|
|
import { extractSessionMessages } from "./session-messages"
|
|
|
|
const SESSION_TTL_MS = 30 * 60 * 1000
|
|
|
|
<<<<<<< HEAD
|
|
declare function setTimeout(callback: () => void | Promise<void>, delay?: number): RuntimeFallbackTimeout
|
|
declare function clearTimeout(timeout: RuntimeFallbackTimeout): void
|
|
||||||| parent of b6f740ed (fix: enable runtime fallback for delegated child sessions (#2357))
|
|
declare function setTimeout(callback: () => void | Promise<void>, delay?: number): ReturnType<typeof globalThis.setTimeout>
|
|
declare function clearTimeout(timeout: ReturnType<typeof globalThis.setTimeout>): void
|
|
=======
|
|
declare function setTimeout(callback: () => void | Promise<void>, delay?: number): TimerHandle
|
|
declare function clearTimeout(timeout: TimerHandle): void
|
|
>>>>>>> b6f740ed (fix: enable runtime fallback for delegated child sessions (#2357))
|
|
|
|
export function createAutoRetryHelpers(deps: HookDeps) {
|
|
const { ctx, config, options, sessionStates, sessionLastAccess, sessionRetryInFlight, sessionAwaitingFallbackResult, sessionFallbackTimeouts, pluginConfig, sessionStatusRetryKeys } = deps
|
|
|
|
const abortSessionRequest = async (sessionID: string, source: string): Promise<void> => {
|
|
try {
|
|
await ctx.client.session.abort({ path: { id: sessionID } })
|
|
log(`[${HOOK_NAME}] Aborted in-flight session request (${source})`, { sessionID })
|
|
} catch (error) {
|
|
log(`[${HOOK_NAME}] Failed to abort in-flight session request (${source})`, {
|
|
sessionID,
|
|
error: String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
const clearSessionFallbackTimeout = (sessionID: string) => {
|
|
const timer = sessionFallbackTimeouts.get(sessionID)
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
sessionFallbackTimeouts.delete(sessionID)
|
|
}
|
|
}
|
|
|
|
const scheduleSessionFallbackTimeout = (sessionID: string, resolvedAgent?: string) => {
|
|
clearSessionFallbackTimeout(sessionID)
|
|
|
|
const timeoutMs = options?.session_timeout_ms ?? config.timeout_seconds * 1000
|
|
if (timeoutMs <= 0) return
|
|
|
|
const timer = setTimeout(async () => {
|
|
sessionFallbackTimeouts.delete(sessionID)
|
|
|
|
const state = sessionStates.get(sessionID)
|
|
if (!state) return
|
|
|
|
if (sessionRetryInFlight.has(sessionID)) {
|
|
log(`[${HOOK_NAME}] Overriding in-flight retry due to session timeout`, { sessionID })
|
|
}
|
|
|
|
await abortSessionRequest(sessionID, "session.timeout")
|
|
sessionRetryInFlight.delete(sessionID)
|
|
|
|
if (state.pendingFallbackModel) {
|
|
state.pendingFallbackModel = undefined
|
|
}
|
|
|
|
const fallbackModels = getFallbackModelsForSession(sessionID, resolvedAgent, pluginConfig)
|
|
if (fallbackModels.length === 0) return
|
|
|
|
log(`[${HOOK_NAME}] Session fallback timeout reached`, {
|
|
sessionID,
|
|
timeoutSeconds: config.timeout_seconds,
|
|
currentModel: state.currentModel,
|
|
})
|
|
|
|
const result = prepareFallback(sessionID, state, fallbackModels, config)
|
|
if (result.success && result.newModel) {
|
|
await autoRetryWithFallback(sessionID, result.newModel, resolvedAgent, "session.timeout")
|
|
}
|
|
}, timeoutMs)
|
|
|
|
sessionFallbackTimeouts.set(sessionID, timer)
|
|
}
|
|
|
|
const autoRetryWithFallback = async (
|
|
sessionID: string,
|
|
newModel: string,
|
|
resolvedAgent: string | undefined,
|
|
source: string,
|
|
): Promise<void> => {
|
|
if (sessionRetryInFlight.has(sessionID)) {
|
|
log(`[${HOOK_NAME}] Retry already in flight, skipping (${source})`, { sessionID })
|
|
return
|
|
}
|
|
|
|
const retryModelPayload = buildRetryModelPayload(newModel)
|
|
if (!retryModelPayload) {
|
|
log(`[${HOOK_NAME}] Invalid model format (missing provider prefix): ${newModel}`)
|
|
const state = sessionStates.get(sessionID)
|
|
if (state?.pendingFallbackModel) {
|
|
state.pendingFallbackModel = undefined
|
|
}
|
|
return
|
|
}
|
|
|
|
sessionRetryInFlight.add(sessionID)
|
|
let retryDispatched = false
|
|
try {
|
|
const messagesResp = await ctx.client.session.messages({
|
|
path: { id: sessionID },
|
|
query: { directory: ctx.directory },
|
|
})
|
|
const retryParts = getLastUserRetryParts(messagesResp)
|
|
if (retryParts.length > 0) {
|
|
log(`[${HOOK_NAME}] Auto-retrying with fallback model (${source})`, {
|
|
sessionID,
|
|
model: newModel,
|
|
})
|
|
|
|
const retryAgent = resolvedAgent ?? getSessionAgent(sessionID)
|
|
sessionAwaitingFallbackResult.add(sessionID)
|
|
scheduleSessionFallbackTimeout(sessionID, retryAgent)
|
|
|
|
await ctx.client.session.promptAsync({
|
|
path: { id: sessionID },
|
|
body: {
|
|
...(retryAgent ? { agent: retryAgent } : {}),
|
|
...retryModelPayload,
|
|
parts: retryParts,
|
|
},
|
|
query: { directory: ctx.directory },
|
|
})
|
|
retryDispatched = true
|
|
} else {
|
|
log(`[${HOOK_NAME}] No user message found for auto-retry (${source})`, { sessionID })
|
|
}
|
|
} catch (retryError) {
|
|
log(`[${HOOK_NAME}] Auto-retry failed (${source})`, { sessionID, error: String(retryError) })
|
|
} finally {
|
|
sessionRetryInFlight.delete(sessionID)
|
|
if (!retryDispatched) {
|
|
sessionAwaitingFallbackResult.delete(sessionID)
|
|
clearSessionFallbackTimeout(sessionID)
|
|
const state = sessionStates.get(sessionID)
|
|
if (state?.pendingFallbackModel) {
|
|
state.pendingFallbackModel = undefined
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const resolveAgentForSessionFromContext = async (
|
|
sessionID: string,
|
|
eventAgent?: string,
|
|
): Promise<string | undefined> => {
|
|
const resolved = resolveAgentForSession(sessionID, eventAgent)
|
|
if (resolved) return resolved
|
|
|
|
try {
|
|
const messagesResp = await ctx.client.session.messages({
|
|
path: { id: sessionID },
|
|
query: { directory: ctx.directory },
|
|
})
|
|
const msgs = extractSessionMessages(messagesResp)
|
|
if (!msgs || msgs.length === 0) return undefined
|
|
|
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
const info = msgs[i]?.info
|
|
const infoAgent = typeof info?.agent === "string" ? info.agent : undefined
|
|
const normalized = normalizeAgentName(infoAgent)
|
|
if (normalized) {
|
|
return normalized
|
|
}
|
|
}
|
|
} catch {
|
|
return undefined
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
const cleanupStaleSessions = () => {
|
|
const now = Date.now()
|
|
let cleanedCount = 0
|
|
for (const [sessionID, lastAccess] of sessionLastAccess.entries()) {
|
|
if (now - lastAccess > SESSION_TTL_MS) {
|
|
sessionStates.delete(sessionID)
|
|
sessionLastAccess.delete(sessionID)
|
|
sessionRetryInFlight.delete(sessionID)
|
|
sessionAwaitingFallbackResult.delete(sessionID)
|
|
clearSessionFallbackTimeout(sessionID)
|
|
SessionCategoryRegistry.remove(sessionID)
|
|
sessionStatusRetryKeys.delete(sessionID)
|
|
cleanedCount++
|
|
}
|
|
}
|
|
if (cleanedCount > 0) {
|
|
log(`[${HOOK_NAME}] Cleaned up ${cleanedCount} stale session states`)
|
|
}
|
|
}
|
|
|
|
return {
|
|
abortSessionRequest,
|
|
clearSessionFallbackTimeout,
|
|
scheduleSessionFallbackTimeout,
|
|
autoRetryWithFallback,
|
|
resolveAgentForSessionFromContext,
|
|
cleanupStaleSessions,
|
|
}
|
|
}
|
|
|
|
export type AutoRetryHelpers = ReturnType<typeof createAutoRetryHelpers>
|