fix(runtime-fallback): 9 critical bug fixes for auto-retry, agent preservation, and model override

Bug fixes:
1. extractStatusCode: handle nested data.statusCode (Anthropic error structure)
2. Error regex: relax credit.*balance.*too.*low pattern for multi-char gaps
3. Zod schema: bump max_fallback_attempts from 10 to 20 (config rejected silently)
4. getFallbackModelsForSession: fallback to sisyphus/any agent when session.error lacks agent
5. Model detection: derive model from agent config when session.error lacks model info
6. Auto-retry: resend last user message with fallback model via promptAsync
7. Persistent fallback: override model on every chat.message (not just pendingFallbackModel)
8. Manual model change: detect UI model changes and reset fallback state
9. Agent preservation: include agent in promptAsync body to prevent defaulting to sisyphus

Additional:
- Add sessionRetryInFlight guard to prevent double-retries
- Add resolveAgentForSession with 3-tier resolution (event → session memory → session ID)
- Add normalizeAgentName for display names like "Prometheus (Planner)" → "prometheus"
- Add resolveAgentForSessionFromContext to fetch agent from session messages
- Move AGENT_NAMES and agentPattern to module scope for reuse
- Register runtime-fallback hooks in event.ts and chat-message.ts
- Remove diagnostic debug logging from isRetryableError
- Add 400 to default retry_on_errors and credit/balance patterns to RETRYABLE_ERROR_PATTERNS
This commit is contained in:
Youngbin Kim
2026-02-11 16:59:26 -05:00
committed by YeonGyu-Kim
parent 708b9ce9ff
commit fbafb8cf67
4 changed files with 380 additions and 46 deletions
+59 -3
View File
@@ -23,7 +23,12 @@ describe("runtime-fallback", () => {
logSpy?.mockRestore()
})
function createMockPluginInput() {
function createMockPluginInput(overrides?: {
session?: {
messages?: (args: unknown) => Promise<unknown>
promptAsync?: (args: unknown) => Promise<unknown>
}
}) {
return {
client: {
tui: {
@@ -35,6 +40,10 @@ describe("runtime-fallback", () => {
})
},
},
session: {
messages: overrides?.session?.messages ?? (async () => ({ data: [] })),
promptAsync: overrides?.session?.promptAsync ?? (async () => ({})),
},
},
directory: "/test/dir",
} as any
@@ -174,7 +183,10 @@ describe("runtime-fallback", () => {
})
test("should log when no fallback models configured", async () => {
const hook = createRuntimeFallbackHook(createMockPluginInput(), { config: createMockConfig() })
const hook = createRuntimeFallbackHook(createMockPluginInput(), {
config: createMockConfig(),
pluginConfig: {},
})
const sessionID = "test-session-no-fallbacks"
await hook.event({
@@ -487,7 +499,7 @@ describe("runtime-fallback", () => {
const output = { message: {}, parts: [] }
await hook["chat.message"]?.(
{ sessionID, model: { providerID: "anthropic", modelID: "claude-opus-4-5" } },
{ sessionID },
output
)
@@ -588,6 +600,50 @@ describe("runtime-fallback", () => {
expect(fallbackLog).toBeDefined()
expect(fallbackLog?.data).toMatchObject({ to: "openai/gpt-5.2" })
})
test("should preserve resolved agent during auto-retry", async () => {
const promptCalls: Array<Record<string, unknown>> = []
const hook = createRuntimeFallbackHook(
createMockPluginInput({
session: {
messages: async () => ({
data: [
{
info: { role: "user" },
parts: [{ type: "text", text: "test" }],
},
],
}),
promptAsync: async (args: unknown) => {
promptCalls.push(args as Record<string, unknown>)
return {}
},
},
}),
{
config: createMockConfig({ notify_on_fallback: false }),
pluginConfig: createMockPluginConfigWithAgentFallback("prometheus", ["github-copilot/claude-opus-4.6"]),
},
)
const sessionID = "test-preserve-agent-on-retry"
await hook.event({
event: {
type: "session.error",
properties: {
sessionID,
model: "anthropic/claude-opus-4-6",
error: { statusCode: 503, message: "Service unavailable" },
agent: "prometheus",
},
},
})
expect(promptCalls.length).toBe(1)
const callBody = promptCalls[0]?.body as Record<string, unknown>
expect(callBody?.agent).toBe("prometheus")
expect(callBody?.model).toEqual({ providerID: "github-copilot", modelID: "claude-opus-4.6" })
})
})
describe("cooldown mechanism", () => {