diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 38dac41b058..968dee0259e 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -367,6 +367,16 @@ export namespace SessionProcessor { const retry = SessionRetry.retryable(error) if (retry !== undefined) { attempt++ + if (attempt > SessionRetry.RETRY_MAX_ATTEMPTS) { + log.error("max retries exceeded", { attempt, sessionID: input.sessionID }) + input.assistantMessage.error = error + Bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error, + }) + SessionStatus.set(input.sessionID, { type: "idle" }) + break + } const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) SessionStatus.set(input.sessionID, { type: "retry", diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 6d057f539f8..b06a768b3ff 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -6,7 +6,9 @@ export namespace SessionRetry { export const RETRY_INITIAL_DELAY = 2000 export const RETRY_BACKOFF_FACTOR = 2 export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds + export const RETRY_MAX_DELAY_WITH_HEADERS = 60_000 // 60 seconds — cap for when response headers are present but missing retry-after export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout + export const RETRY_MAX_ATTEMPTS = 10 // maximum retry attempts before giving up export async function sleep(ms: number, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { @@ -51,7 +53,7 @@ export namespace SessionRetry { } } - return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1) + return Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_WITH_HEADERS) } }