81b68d828c
Move the pending fallback, toast, and session-chain maps behind a shared controller initialized from the hook factory. This preserves the existing singleton semantics because exported helpers and hook instances still resolve the same lazily initialized controller while hook.ts stays under the 200-line limit. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
136 lines
4.4 KiB
TypeScript
136 lines
4.4 KiB
TypeScript
import type { FallbackEntry } from "../../shared/model-requirements"
|
|
import { getAgentConfigKey } from "../../shared/agent-display-names"
|
|
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
|
import { log } from "../../shared/logger"
|
|
import { getNextReachableFallback } from "./next-fallback"
|
|
|
|
type ModelFallbackStateLike = {
|
|
providerID: string
|
|
modelID: string
|
|
fallbackChain: FallbackEntry[]
|
|
attemptCount: number
|
|
pending: boolean
|
|
}
|
|
|
|
export type ModelFallbackStateController = {
|
|
lastToastKey: Map<string, string>
|
|
setSessionFallbackChain: (sessionID: string, fallbackChain: FallbackEntry[] | undefined) => void
|
|
clearSessionFallbackChain: (sessionID: string) => void
|
|
setPendingModelFallback: (
|
|
sessionID: string,
|
|
agentName: string,
|
|
currentProviderID: string,
|
|
currentModelID: string,
|
|
) => boolean
|
|
getNextFallback: (sessionID: string) => ReturnType<typeof getNextReachableFallback>
|
|
clearPendingModelFallback: (sessionID: string) => void
|
|
hasPendingModelFallback: (sessionID: string) => boolean
|
|
getFallbackState: (sessionID: string) => ModelFallbackStateLike | undefined
|
|
reset: () => void
|
|
}
|
|
|
|
export function createModelFallbackStateController(input: {
|
|
pendingModelFallbacks: Map<string, ModelFallbackStateLike>
|
|
lastToastKey: Map<string, string>
|
|
sessionFallbackChains: Map<string, FallbackEntry[]>
|
|
}): ModelFallbackStateController {
|
|
const { pendingModelFallbacks, lastToastKey, sessionFallbackChains } = input
|
|
|
|
function setSessionFallbackChain(sessionID: string, fallbackChain: FallbackEntry[] | undefined): void {
|
|
if (!sessionID) return
|
|
sessionFallbackChains.set(sessionID, fallbackChain?.length ? fallbackChain : [])
|
|
}
|
|
|
|
function clearSessionFallbackChain(sessionID: string): void {
|
|
sessionFallbackChains.delete(sessionID)
|
|
}
|
|
|
|
function setPendingModelFallback(
|
|
sessionID: string,
|
|
agentName: string,
|
|
currentProviderID: string,
|
|
currentModelID: string,
|
|
): boolean {
|
|
const agentKey = getAgentConfigKey(agentName)
|
|
const requirements = AGENT_MODEL_REQUIREMENTS[agentKey]
|
|
const fallbackChain = sessionFallbackChains.has(sessionID)
|
|
? sessionFallbackChains.get(sessionID)
|
|
: requirements?.fallbackChain
|
|
|
|
if (!fallbackChain?.length) {
|
|
log("[model-fallback] No fallback chain for agent: " + agentName + " (key: " + agentKey + ")")
|
|
return false
|
|
}
|
|
|
|
const existing = pendingModelFallbacks.get(sessionID)
|
|
if (existing) {
|
|
if (existing.pending) {
|
|
log("[model-fallback] Pending fallback already armed for session: " + sessionID)
|
|
return false
|
|
}
|
|
existing.providerID = currentProviderID
|
|
existing.modelID = currentModelID
|
|
existing.pending = true
|
|
if (existing.attemptCount >= existing.fallbackChain.length) {
|
|
log("[model-fallback] Fallback chain exhausted for session: " + sessionID)
|
|
return false
|
|
}
|
|
log("[model-fallback] Re-armed pending fallback for session: " + sessionID)
|
|
return true
|
|
}
|
|
|
|
pendingModelFallbacks.set(sessionID, {
|
|
providerID: currentProviderID,
|
|
modelID: currentModelID,
|
|
fallbackChain,
|
|
attemptCount: 0,
|
|
pending: true,
|
|
})
|
|
log("[model-fallback] Set pending fallback for session: " + sessionID + ", agent: " + agentName)
|
|
return true
|
|
}
|
|
|
|
function getNextFallback(sessionID: string): ReturnType<typeof getNextReachableFallback> {
|
|
const state = pendingModelFallbacks.get(sessionID)
|
|
if (!state?.pending) return null
|
|
|
|
const fallback = getNextReachableFallback(sessionID, state)
|
|
if (fallback) return fallback
|
|
|
|
log("[model-fallback] No more fallbacks for session: " + sessionID)
|
|
pendingModelFallbacks.delete(sessionID)
|
|
return null
|
|
}
|
|
|
|
function clearPendingModelFallback(sessionID: string): void {
|
|
pendingModelFallbacks.delete(sessionID)
|
|
lastToastKey.delete(sessionID)
|
|
}
|
|
|
|
function hasPendingModelFallback(sessionID: string): boolean {
|
|
return pendingModelFallbacks.get(sessionID)?.pending === true
|
|
}
|
|
|
|
function getFallbackState(sessionID: string): ModelFallbackStateLike | undefined {
|
|
return pendingModelFallbacks.get(sessionID)
|
|
}
|
|
|
|
function reset(): void {
|
|
pendingModelFallbacks.clear()
|
|
lastToastKey.clear()
|
|
sessionFallbackChains.clear()
|
|
}
|
|
|
|
return {
|
|
lastToastKey,
|
|
setSessionFallbackChain,
|
|
clearSessionFallbackChain,
|
|
setPendingModelFallback,
|
|
getNextFallback,
|
|
clearPendingModelFallback,
|
|
hasPendingModelFallback,
|
|
getFallbackState,
|
|
reset,
|
|
}
|
|
}
|