From 11d942f3a2c2bf81daf984f3cfeeeb5e630cd5b8 Mon Sep 17 00:00:00 2001 From: MoerAI Date: Mon, 16 Mar 2026 10:35:31 +0900 Subject: [PATCH] fix(runtime-fallback): detect Gemini quota errors in session.status retry events When Gemini returns a quota exhausted error, OpenCode auto-retries and fires session.status with type='retry'. The extractAutoRetrySignal function requires BOTH 'retrying in' text AND a quota pattern to match, but some providers (like Gemini) include only the error text in the retry message without the 'retrying in' phrase. Since status.type='retry' already confirms this is a retry event, the fix adds a fallback check: if extractAutoRetrySignal fails, check the message directly against RETRYABLE_ERROR_PATTERNS. This ensures quota errors like 'exhausted your capacity' trigger the fallback chain even when the retry message format differs from expected. Fixes #2454 --- src/hooks/runtime-fallback/session-status-handler.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/hooks/runtime-fallback/session-status-handler.ts b/src/hooks/runtime-fallback/session-status-handler.ts index 2e15d41d5..92ccfab80 100644 --- a/src/hooks/runtime-fallback/session-status-handler.ts +++ b/src/hooks/runtime-fallback/session-status-handler.ts @@ -1,6 +1,6 @@ import type { HookDeps } from "./types" import type { AutoRetryHelpers } from "./auto-retry" -import { HOOK_NAME } from "./constants" +import { HOOK_NAME, RETRYABLE_ERROR_PATTERNS } from "./constants" import { log } from "../../shared/logger" import { extractAutoRetrySignal } from "./error-classifier" import { createFallbackState } from "./fallback-state" @@ -32,7 +32,14 @@ export function createSessionStatusHandler( const retryMessage = typeof status.message === "string" ? status.message : "" const retrySignal = extractAutoRetrySignal({ status: retryMessage, message: retryMessage }) - if (!retrySignal) return + if (!retrySignal) { + // Fallback: status.type is already "retry", so check the message against + // retryable error patterns directly. This handles providers like Gemini whose + // retry status message may not contain "retrying in" text alongside the error. + const messageLower = retryMessage.toLowerCase() + const matchesRetryablePattern = RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(messageLower)) + if (!matchesRetryablePattern) return + } const retryKey = `${extractRetryAttempt(status.attempt, retryMessage)}:${normalizeRetryStatusMessage(retryMessage)}` if (sessionStatusRetryKeys.get(sessionID) === retryKey) {