df990f1174
Split 1021-line index.ts into 10 focused modules per project conventions. New structure: - error-classifier.ts: error analysis with dynamic status code extraction - agent-resolver.ts: agent detection utilities - fallback-state.ts: state management and cooldown logic - fallback-models.ts: model resolution from config - auto-retry.ts: retry helpers with mutual recursion support - event-handler.ts: session lifecycle events - message-update-handler.ts: message.updated event handling - chat-message-handler.ts: chat message interception - hook.ts: main factory with proper cleanup - types.ts: updated with HookDeps interface - index.ts: 2-line barrel re-export Embedded fixes: - Fix setInterval leak with .unref() - Replace require() with ESM import - Add log warning on invalid model format - Update sessionLastAccess on normal traffic - Make extractStatusCode dynamic from config - Remove unused SessionErrorInfo type All 61 tests pass without modification. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import type { FallbackState, FallbackResult } from "./types"
|
|
import { HOOK_NAME } from "./constants"
|
|
import { log } from "../../shared/logger"
|
|
import type { RuntimeFallbackConfig } from "../../config"
|
|
|
|
export function createFallbackState(originalModel: string): FallbackState {
|
|
return {
|
|
originalModel,
|
|
currentModel: originalModel,
|
|
fallbackIndex: -1,
|
|
failedModels: new Map<string, number>(),
|
|
attemptCount: 0,
|
|
pendingFallbackModel: undefined,
|
|
}
|
|
}
|
|
|
|
export function isModelInCooldown(model: string, state: FallbackState, cooldownSeconds: number): boolean {
|
|
const failedAt = state.failedModels.get(model)
|
|
if (failedAt === undefined) return false
|
|
const cooldownMs = cooldownSeconds * 1000
|
|
return Date.now() - failedAt < cooldownMs
|
|
}
|
|
|
|
export function findNextAvailableFallback(
|
|
state: FallbackState,
|
|
fallbackModels: string[],
|
|
cooldownSeconds: number
|
|
): string | undefined {
|
|
for (let i = state.fallbackIndex + 1; i < fallbackModels.length; i++) {
|
|
const candidate = fallbackModels[i]
|
|
if (!isModelInCooldown(candidate, state, cooldownSeconds)) {
|
|
return candidate
|
|
}
|
|
log(`[${HOOK_NAME}] Skipping fallback model in cooldown`, { model: candidate, index: i })
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function prepareFallback(
|
|
sessionID: string,
|
|
state: FallbackState,
|
|
fallbackModels: string[],
|
|
config: Required<RuntimeFallbackConfig>
|
|
): FallbackResult {
|
|
if (state.attemptCount >= config.max_fallback_attempts) {
|
|
log(`[${HOOK_NAME}] Max fallback attempts reached`, { sessionID, attempts: state.attemptCount })
|
|
return { success: false, error: "Max fallback attempts reached", maxAttemptsReached: true }
|
|
}
|
|
|
|
const nextModel = findNextAvailableFallback(state, fallbackModels, config.cooldown_seconds)
|
|
|
|
if (!nextModel) {
|
|
log(`[${HOOK_NAME}] No available fallback models`, { sessionID })
|
|
return { success: false, error: "No available fallback models (all in cooldown or exhausted)" }
|
|
}
|
|
|
|
log(`[${HOOK_NAME}] Preparing fallback`, {
|
|
sessionID,
|
|
from: state.currentModel,
|
|
to: nextModel,
|
|
attempt: state.attemptCount + 1,
|
|
})
|
|
|
|
const failedModel = state.currentModel
|
|
const now = Date.now()
|
|
|
|
state.fallbackIndex = fallbackModels.indexOf(nextModel)
|
|
state.failedModels.set(failedModel, now)
|
|
state.attemptCount++
|
|
state.currentModel = nextModel
|
|
state.pendingFallbackModel = nextModel
|
|
|
|
return { success: true, newModel: nextModel }
|
|
}
|