diff --git a/src/config/index.ts b/src/config/index.ts index 213c78d59..a561a2e66 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -32,4 +32,5 @@ export type { SisyphusConfig, SisyphusTasksConfig, RuntimeFallbackConfig, + FallbackModels, } from "./schema" diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index d73fa144d..bd4a0122e 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -522,9 +522,22 @@ describe("runtime-fallback", () => { }) describe("fallback models configuration", () => { + function createMockPluginConfigWithAgentFallback(agentName: string, fallbackModels: string[]): OhMyOpenCodeConfig { + return { + agents: { + [agentName]: { + fallback_models: fallbackModels, + }, + }, + } + } + test("should use agent-level fallback_models", async () => { const input = createMockPluginInput() - const hook = createRuntimeFallbackHook(input, { config: createMockConfig() }) + const hook = createRuntimeFallbackHook(input, { + config: createMockConfig({ notify_on_fallback: false }), + pluginConfig: createMockPluginConfigWithAgentFallback("oracle", ["openai/gpt-5.2", "google/gemini-3-pro"]), + }) const sessionID = "test-agent-fallback" //#given - agent with custom fallback models @@ -543,13 +556,17 @@ describe("runtime-fallback", () => { }, }) - //#then - should use oracle's fallback models - const fallbackLog = logCalls.find((c) => c.msg.includes("No fallback models configured") || c.msg.includes("Fallback triggered")) + //#then - should prepare fallback to openai/gpt-5.2 + const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback")) expect(fallbackLog).toBeDefined() + expect(fallbackLog?.data).toMatchObject({ from: "anthropic/claude-opus-4-5", to: "openai/gpt-5.2" }) }) test("should detect agent from sessionID pattern", async () => { - const hook = createRuntimeFallbackHook(createMockPluginInput(), { config: createMockConfig() }) + const hook = createRuntimeFallbackHook(createMockPluginInput(), { + config: createMockConfig({ notify_on_fallback: false }), + pluginConfig: createMockPluginConfigWithAgentFallback("sisyphus", ["openai/gpt-5.2"]), + }) const sessionID = "sisyphus-session-123" await hook.event({ @@ -566,8 +583,10 @@ describe("runtime-fallback", () => { }, }) - const errorLog = logCalls.find((c) => c.msg.includes("session.error received")) - expect(errorLog?.data).toMatchObject({ sessionID }) + //#then - should detect sisyphus from sessionID and use its fallback + const fallbackLog = logCalls.find((c) => c.msg.includes("Preparing fallback")) + expect(fallbackLog).toBeDefined() + expect(fallbackLog?.data).toMatchObject({ to: "openai/gpt-5.2" }) }) }) diff --git a/src/hooks/runtime-fallback/index.ts b/src/hooks/runtime-fallback/index.ts index 134aaeb13..8c07e67d1 100644 --- a/src/hooks/runtime-fallback/index.ts +++ b/src/hooks/runtime-fallback/index.ts @@ -115,7 +115,26 @@ function getFallbackModelsForSession( if (result) return result } - const sessionAgentMatch = sessionID.match(/\b(sisyphus|oracle|librarian|explore|prometheus|atlas|metis|momus|hephaestus|sisyphus-junior|build|plan|multimodal-looker)\b/i) + const AGENT_NAMES = [ + "sisyphus", + "oracle", + "librarian", + "explore", + "prometheus", + "atlas", + "metis", + "momus", + "hephaestus", + "sisyphus-junior", + "build", + "plan", + "multimodal-looker", + ] + const agentPattern = new RegExp( + `(?:^|[^a-zA-Z0-9_-])(${AGENT_NAMES.map((a) => a.replace(/-/g, "\\-")).join("|")})(?:$|[^a-zA-Z0-9_-])`, + "i", + ) + const sessionAgentMatch = sessionID.match(agentPattern) if (sessionAgentMatch) { const detectedAgent = sessionAgentMatch[1].toLowerCase() const result = tryGetFallbackFromAgent(detectedAgent) diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index a9e450fb2..9618b15ed 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -73,3 +73,13 @@ export function resolveModelWithFallback( variant: resolved.variant, } } + +/** + * Normalizes fallback_models config (which can be string or string[]) to string[] + * Centralized helper to avoid duplicated normalization logic + */ +export function normalizeFallbackModels(models: string | string[] | undefined): string[] | undefined { + if (!models) return undefined + if (typeof models === "string") return [models] + return models +} diff --git a/src/tools/delegate-task/sync-task.ts b/src/tools/delegate-task/sync-task.ts index 13b701c24..75b3f9e40 100644 --- a/src/tools/delegate-task/sync-task.ts +++ b/src/tools/delegate-task/sync-task.ts @@ -150,6 +150,7 @@ session_id: ${sessionID} } finally { if (syncSessionID) { subagentSessions.delete(syncSessionID) + SessionCategoryRegistry.remove(syncSessionID) } } }