From c598afa521f90b3ff7d203a3ee9cd2a15d2e37e0 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Wed, 4 Mar 2026 20:52:15 +0100 Subject: [PATCH] Address PR review follow-ups for retry status handling --- .../runtime-fallback/error-classifier.test.ts | 14 +++++++++++ .../runtime-fallback/error-classifier.ts | 2 +- src/hooks/runtime-fallback/event-handler.ts | 17 +------------- src/hooks/runtime-fallback/index.test.ts | 3 +++ src/plugin/event.ts | 23 +------------------ src/shared/retry-status-utils.ts | 19 +++++++++++++++ 6 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 src/shared/retry-status-utils.ts diff --git a/src/hooks/runtime-fallback/error-classifier.test.ts b/src/hooks/runtime-fallback/error-classifier.test.ts index 1885e80eb..52c9bb502 100644 --- a/src/hooks/runtime-fallback/error-classifier.test.ts +++ b/src/hooks/runtime-fallback/error-classifier.test.ts @@ -17,6 +17,20 @@ describe("runtime-fallback error classifier", () => { expect(signal).toBeDefined() }) + test("detects single-word cooldown auto-retry status signals", () => { + //#given + const info = { + status: + "All credentials for model claude-opus-4-6 are cooldown [retrying in 7m 56s attempt #1]", + } + + //#when + const signal = extractAutoRetrySignal(info) + + //#then + expect(signal).toBeDefined() + }) + test("treats cooling-down retry messages as retryable", () => { //#given const error = { diff --git a/src/hooks/runtime-fallback/error-classifier.ts b/src/hooks/runtime-fallback/error-classifier.ts index f05493daf..39b6ecdbb 100644 --- a/src/hooks/runtime-fallback/error-classifier.ts +++ b/src/hooks/runtime-fallback/error-classifier.ts @@ -102,7 +102,7 @@ export interface AutoRetrySignal { export const AUTO_RETRY_PATTERNS: Array<(combined: string) => boolean> = [ (combined) => /retrying\s+in/i.test(combined), (combined) => - /(?:too\s+many\s+requests|quota\s*exceeded|quota\s+will\s+reset\s+after|usage\s+limit|rate\s+limit|limit\s+reached|all\s+credentials\s+for\s+model|cool(?:ing)?\s+down|exhausted\s+your\s+capacity)/i.test(combined), + /(?:too\s+many\s+requests|quota\s*exceeded|quota\s+will\s+reset\s+after|usage\s+limit|rate\s+limit|limit\s+reached|all\s+credentials\s+for\s+model|cool(?:ing)?\s*down|exhausted\s+your\s+capacity)/i.test(combined), ] export function extractAutoRetrySignal(info: Record | undefined): AutoRetrySignal | undefined { diff --git a/src/hooks/runtime-fallback/event-handler.ts b/src/hooks/runtime-fallback/event-handler.ts index a6c8b3249..c631c8cd5 100644 --- a/src/hooks/runtime-fallback/event-handler.ts +++ b/src/hooks/runtime-fallback/event-handler.ts @@ -6,6 +6,7 @@ import { extractStatusCode, extractErrorName, classifyErrorType, isRetryableErro import { createFallbackState, prepareFallback } from "./fallback-state" import { getFallbackModelsForSession } from "./fallback-models" import { SessionCategoryRegistry } from "../../shared/session-category-registry" +import { normalizeRetryStatusMessage, extractRetryAttempt } from "../../shared/retry-status-utils" export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { const { config, pluginConfig, sessionStates, sessionLastAccess, sessionRetryInFlight, sessionAwaitingFallbackResult, sessionFallbackTimeouts } = deps @@ -184,22 +185,6 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { } } - const normalizeRetryStatusMessage = (message: string): string => - message - .replace(/\[retrying in [^\]]*attempt\s*#\d+\]/gi, "[retrying]") - .replace(/retrying in\s+[^(]*attempt\s*#\d+/gi, "retrying") - .replace(/\s+/g, " ") - .trim() - .toLowerCase() - - const extractRetryAttempt = (statusAttempt: unknown, message: string): string => { - if (typeof statusAttempt === "number" && Number.isFinite(statusAttempt)) { - return String(statusAttempt) - } - const match = message.match(/attempt\s*#\s*(\d+)/i) - return match?.[1] ?? "?" - } - const handleSessionStatus = async (props: Record | undefined) => { const sessionID = props?.sessionID as string | undefined const status = props?.status as { type?: string; message?: string; attempt?: number } | undefined diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index 45a432e08..73e1586f5 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -515,6 +515,7 @@ describe("runtime-fallback", () => { sessionID, status: { type: "retry", + next: 476, attempt: 1, message: "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 56s attempt #1]", }, @@ -573,6 +574,7 @@ describe("runtime-fallback", () => { sessionID, status: { type: "retry", + next: 476, attempt: 1, message: "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 56s attempt #1]", }, @@ -587,6 +589,7 @@ describe("runtime-fallback", () => { sessionID, status: { type: "retry", + next: 475, attempt: 1, message: "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 55s attempt #1]", }, diff --git a/src/plugin/event.ts b/src/plugin/event.ts index 73fcfb315..0b7c3c2ad 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -22,6 +22,7 @@ import { getAgentConfigKey } from "../shared/agent-display-names"; import { log } from "../shared/logger"; import { shouldRetryError } from "../shared/model-error-classifier"; import { buildFallbackChainFromModels } from "../shared/fallback-chain-from-models"; +import { extractRetryAttempt, normalizeRetryStatusMessage } from "../shared/retry-status-utils"; import { clearSessionModel, setSessionModel } from "../shared/session-model-state"; import { deleteSessionTools } from "../shared/session-tools-store"; import { lspManager } from "../tools"; @@ -47,28 +48,6 @@ function normalizeFallbackModelID(modelID: string): string { .replace(/-high$/i, ""); } -function normalizeRetryStatusMessage(message: string): string { - return message - .replace(/\[retrying in [^\]]*attempt\s*#\d+\]/gi, "[retrying]") - .replace(/retrying in\s+[^(]*attempt\s*#\d+/gi, "retrying") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); -} - -function extractRetryAttempt(statusAttempt: unknown, message: string): string { - if (typeof statusAttempt === "number" && Number.isFinite(statusAttempt)) { - return String(statusAttempt); - } - - const attemptMatch = message.match(/attempt\s*#\s*(\d+)/i); - if (attemptMatch?.[1]) { - return attemptMatch[1]; - } - - return "?"; -} - function extractErrorName(error: unknown): string | undefined { if (isRecord(error) && typeof error.name === "string") return error.name; if (error instanceof Error) return error.name; diff --git a/src/shared/retry-status-utils.ts b/src/shared/retry-status-utils.ts new file mode 100644 index 000000000..3b93ddf02 --- /dev/null +++ b/src/shared/retry-status-utils.ts @@ -0,0 +1,19 @@ +export function normalizeRetryStatusMessage(message: string): string { + return message + .replace(/\[retrying in [^\]]*attempt\s*#\d+\]/gi, "[retrying]") + .replace(/retrying in\s+[^(]*attempt\s*#\d+/gi, "retrying") + .replace(/\s+/g, " ") + .trim() + .toLowerCase() +} + +export function extractRetryAttempt(statusAttempt: unknown, message: string): string { + if (typeof statusAttempt === "number" && Number.isFinite(statusAttempt)) { + return String(statusAttempt) + } + const attemptMatch = message.match(/attempt\s*#\s*(\d+)/i) + if (attemptMatch?.[1]) { + return attemptMatch[1] + } + return "?" +}