From 87a3f727fadaf9e8341aaa1a4c969a4c97577239 Mon Sep 17 00:00:00 2001 From: Eduard Riera Date: Thu, 16 Jul 2026 16:01:49 +0200 Subject: [PATCH] fix(opencode): use exponential backoff when retry-after-ms is zero --- packages/opencode/src/session/retry.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4139665bd2bd..4a96f9991edf 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -27,12 +27,14 @@ 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 = 2_147_483_647 // max 32-bit signed integer for setTimeout +export const RETRY_MAX_ATTEMPTS = 5 function cap(ms: number) { return Math.min(ms, RETRY_MAX_DELAY) } export function delay(attempt: number, error?: SessionV1.APIError) { + const exponentialMs = RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1) if (error) { const headers = error.data.responseHeaders if (headers) { @@ -40,6 +42,7 @@ export function delay(attempt: number, error?: SessionV1.APIError) { if (retryAfterMs) { const parsedMs = Number.parseFloat(retryAfterMs) if (!Number.isNaN(parsedMs)) { + if (parsedMs === 0) return cap(exponentialMs) return cap(parsedMs) } } @@ -58,11 +61,11 @@ export function delay(attempt: number, error?: SessionV1.APIError) { } } - return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)) + return cap(exponentialMs) } } - return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)) + return cap(Math.min(exponentialMs, RETRY_MAX_DELAY_NO_HEADERS)) } export function retryable(error: Err, provider: string) { @@ -183,6 +186,7 @@ export function policy(opts: { const error = opts.parse(meta.input) const retry = retryable(error, opts.provider) if (!retry) return Cause.done(meta.attempt) + if (meta.attempt > RETRY_MAX_ATTEMPTS) return Cause.done(meta.attempt) return Effect.gen(function* () { const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined) const now = yield* Clock.currentTimeMillis