diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4139665bd2bd..f0d0fcd4c2d9 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -58,7 +58,12 @@ export function delay(attempt: number, error?: SessionV1.APIError) { } } - return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)) + // Headers were present but carried no usable retry-after hint. Apply the + // same 30s cap as the no-headers branch so a flaky 5xx with normal + // response headers (content-type, date, ...) cannot grow the backoff + // past RETRY_MAX_DELAY_NO_HEADERS. Without this, attempt 10 waits ~17min, + // attempt 15 ~9h, attempt 20 ~12d. + return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)) } } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index e53a6c1f18ee..3c78b78f0fb0 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -39,6 +39,19 @@ describe("session.retry.delay", () => { expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000]) }) + test("caps delay at 30 seconds when headers present but lack retry-after", () => { + // Real 5xx responses almost always carry headers (content-type, date, ...). + // The fallback path must still respect RETRY_MAX_DELAY_NO_HEADERS. + const error = apiError({ "content-type": "application/json" }) + const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error)) + expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000]) + }) + + test("caps delay at 30 seconds when retry-after header is unparseable", () => { + const error = apiError({ "retry-after": "not-a-number" }) + expect(SessionRetry.delay(8, error)).toBe(30000) + }) + test("prefers retry-after-ms when shorter than exponential", () => { const error = apiError({ "retry-after-ms": "1500" }) expect(SessionRetry.delay(4, error)).toBe(1500)