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>
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
import type { HookDeps } from "./types"
|
|
import { HOOK_NAME } from "./constants"
|
|
import { log } from "../../shared/logger"
|
|
import { createFallbackState } from "./fallback-state"
|
|
|
|
export function createChatMessageHandler(deps: HookDeps) {
|
|
const { config, sessionStates, sessionLastAccess } = deps
|
|
|
|
return async (
|
|
input: { sessionID: string; agent?: string; model?: { providerID: string; modelID: string } },
|
|
output: { message: { model?: { providerID: string; modelID: string } }; parts?: Array<{ type: string; text?: string }> }
|
|
) => {
|
|
if (!config.enabled) return
|
|
|
|
const { sessionID } = input
|
|
let state = sessionStates.get(sessionID)
|
|
|
|
if (!state) return
|
|
|
|
sessionLastAccess.set(sessionID, Date.now())
|
|
|
|
const requestedModel = input.model
|
|
? `${input.model.providerID}/${input.model.modelID}`
|
|
: undefined
|
|
|
|
if (requestedModel && requestedModel !== state.currentModel) {
|
|
if (state.pendingFallbackModel && state.pendingFallbackModel === requestedModel) {
|
|
state.pendingFallbackModel = undefined
|
|
return
|
|
}
|
|
|
|
log(`[${HOOK_NAME}] Detected manual model change, resetting fallback state`, {
|
|
sessionID,
|
|
from: state.currentModel,
|
|
to: requestedModel,
|
|
})
|
|
state = createFallbackState(requestedModel)
|
|
sessionStates.set(sessionID, state)
|
|
return
|
|
}
|
|
|
|
if (state.currentModel === state.originalModel) return
|
|
|
|
const activeModel = state.currentModel
|
|
|
|
log(`[${HOOK_NAME}] Applying fallback model override`, {
|
|
sessionID,
|
|
from: input.model,
|
|
to: activeModel,
|
|
})
|
|
|
|
if (output.message && activeModel) {
|
|
const parts = activeModel.split("/")
|
|
if (parts.length >= 2) {
|
|
output.message.model = {
|
|
providerID: parts[0],
|
|
modelID: parts.slice(1).join("/"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|