refactor(model-fallback): fully encapsulate session state in factory closure

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-18 02:35:46 +09:00
parent e2f5c0d361
commit 5e4102566c
23 changed files with 271 additions and 123 deletions
+77 -40
View File
@@ -5,6 +5,7 @@ import {
createModelFallbackStateController,
type ModelFallbackStateController,
} from "./fallback-state-controller"
import type { ModelFallbackControllerAccessor } from "./controller-accessor"
type FallbackToast = (input: {
title: string
@@ -28,26 +29,45 @@ export type ModelFallbackState = {
pending: boolean
}
const modelFallbackControllerRef: { current?: ModelFallbackStateController } = {}
type ModelFallbackControllerWithState = Pick<
ModelFallbackStateController,
| "lastToastKey"
| "setSessionFallbackChain"
| "clearSessionFallbackChain"
| "setPendingModelFallback"
| "getNextFallback"
| "clearPendingModelFallback"
| "hasPendingModelFallback"
| "getFallbackState"
| "reset"
>
function getOrCreateModelFallbackController(): ModelFallbackStateController {
if (!modelFallbackControllerRef.current) {
createModelFallbackHook()
}
const controller = modelFallbackControllerRef.current
if (!controller) {
throw new Error("Model fallback controller should be initialized")
}
return controller
export type ModelFallbackHook = ModelFallbackControllerWithState & {
"chat.message": (
input: ChatMessageInput,
output: ChatMessageHandlerOutput,
) => Promise<void>
}
export function setSessionFallbackChain(sessionID: string, fallbackChain: FallbackEntry[] | undefined): void {
getOrCreateModelFallbackController().setSessionFallbackChain(sessionID, fallbackChain)
type ModelFallbackHookArgs = {
toast?: FallbackToast
onApplied?: FallbackCallback
controllerAccessor?: ModelFallbackControllerAccessor
}
export function clearSessionFallbackChain(sessionID: string): void {
getOrCreateModelFallbackController().clearSessionFallbackChain(sessionID)
export function setSessionFallbackChain(
controller: Pick<ModelFallbackStateController, "setSessionFallbackChain">,
sessionID: string,
fallbackChain: FallbackEntry[] | undefined,
): void {
controller.setSessionFallbackChain(sessionID, fallbackChain)
}
export function clearSessionFallbackChain(
controller: Pick<ModelFallbackStateController, "clearSessionFallbackChain">,
sessionID: string,
): void {
controller.clearSessionFallbackChain(sessionID)
}
/**
@@ -55,12 +75,13 @@ export function clearSessionFallbackChain(sessionID: string): void {
* Called when a model error is detected in session.error handler.
*/
export function setPendingModelFallback(
controller: Pick<ModelFallbackStateController, "setPendingModelFallback">,
sessionID: string,
agentName: string,
currentProviderID: string,
currentModelID: string,
): boolean {
return getOrCreateModelFallbackController().setPendingModelFallback(
return controller.setPendingModelFallback(
sessionID,
agentName,
currentProviderID,
@@ -73,54 +94,71 @@ export function setPendingModelFallback(
* Increments attemptCount each time called.
*/
export function getNextFallback(
controller: Pick<ModelFallbackStateController, "getNextFallback">,
sessionID: string,
): { providerID: string; modelID: string; variant?: string } | null {
return getOrCreateModelFallbackController().getNextFallback(sessionID)
return controller.getNextFallback(sessionID)
}
/**
* Clears the pending fallback for a session.
* Called after fallback is successfully applied.
*/
export function clearPendingModelFallback(sessionID: string): void {
getOrCreateModelFallbackController().clearPendingModelFallback(sessionID)
export function clearPendingModelFallback(
controller: Pick<ModelFallbackStateController, "clearPendingModelFallback">,
sessionID: string,
): void {
controller.clearPendingModelFallback(sessionID)
}
/**
* Checks if there's a pending fallback for a session.
*/
export function hasPendingModelFallback(sessionID: string): boolean {
return getOrCreateModelFallbackController().hasPendingModelFallback(sessionID)
export function hasPendingModelFallback(
controller: Pick<ModelFallbackStateController, "hasPendingModelFallback">,
sessionID: string,
): boolean {
return controller.hasPendingModelFallback(sessionID)
}
/**
* Gets the current fallback state for a session (for debugging).
*/
export function getFallbackState(sessionID: string): ModelFallbackState | undefined {
return getOrCreateModelFallbackController().getFallbackState(sessionID)
export function getFallbackState(
controller: Pick<ModelFallbackStateController, "getFallbackState">,
sessionID: string,
): ModelFallbackState | undefined {
return controller.getFallbackState(sessionID)
}
/**
* Creates a chat.message hook that applies model fallbacks when pending.
*/
export function createModelFallbackHook(args?: { toast?: FallbackToast; onApplied?: FallbackCallback }) {
if (!modelFallbackControllerRef.current) {
const pendingModelFallbacks = new Map<string, ModelFallbackState>()
const lastToastKey = new Map<string, string>()
const sessionFallbackChains = new Map<string, FallbackEntry[]>()
export function createModelFallbackHook(args?: ModelFallbackHookArgs): ModelFallbackHook {
const pendingModelFallbacks = new Map<string, ModelFallbackState>()
const lastToastKey = new Map<string, string>()
const sessionFallbackChains = new Map<string, FallbackEntry[]>()
const controller = createModelFallbackStateController({
pendingModelFallbacks,
lastToastKey,
sessionFallbackChains,
})
modelFallbackControllerRef.current = createModelFallbackStateController({
pendingModelFallbacks,
lastToastKey,
sessionFallbackChains,
})
}
args?.controllerAccessor?.register(controller)
const controller = getOrCreateModelFallbackController()
const toast = args?.toast
const onApplied = args?.onApplied
return {
lastToastKey: controller.lastToastKey,
setSessionFallbackChain: controller.setSessionFallbackChain,
clearSessionFallbackChain: controller.clearSessionFallbackChain,
setPendingModelFallback: controller.setPendingModelFallback,
getNextFallback: controller.getNextFallback,
clearPendingModelFallback: controller.clearPendingModelFallback,
hasPendingModelFallback: controller.hasPendingModelFallback,
getFallbackState: controller.getFallbackState,
reset: controller.reset,
"chat.message": async (
input: ChatMessageInput,
output: ChatMessageHandlerOutput,
@@ -128,7 +166,7 @@ export function createModelFallbackHook(args?: { toast?: FallbackToast; onApplie
const { sessionID } = input
if (!sessionID) return
const fallback = getNextFallback(sessionID)
const fallback = getNextFallback(controller, sessionID)
if (!fallback) return
await applyFallbackToChatMessage({
@@ -144,9 +182,8 @@ export function createModelFallbackHook(args?: { toast?: FallbackToast; onApplie
}
/**
* Resets all module-global state for testing.
* Clears pending fallbacks, toast keys, and session chains.
* Resets hook-owned state for testing.
*/
export function _resetForTesting(): void {
getOrCreateModelFallbackController().reset()
export function _resetForTesting(controller?: Pick<ModelFallbackStateController, "reset">): void {
controller?.reset()
}