You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, external call resilience is inconsistent:
anthropic_client.mjs: single raw fetch, zero retry, zero timeout — any transient failure fails the entire run immediately.
groq_client.mjs: retries on HTTP 429 only (good pattern), but HTTP 5xx throws immediately with no retry, and there is no per-call timeout.
ghFetch in auto_fix_pr.mjs: no retry, no timeout — a single network error on any GitHub API call (diff fetch, label fetch, comment post) fails the entire run.
The fix is a single shared retryWithBackoff utility consumed by all three call sites.
If the response includes a Retry-After header (or body hint), use that wait time instead of computed backoff (preserving groq_client.mjs existing behaviour).
Integrate into anthropic_client.mjs (wrap the existing fetch call).
Extend groq_client.mjs to pass 5xx through retryWithBackoff (the existing 429 loop can be simplified to use the same utility).
Apply to ghFetch in auto_fix_pr.mjs.
Out:
Changes to retry budgets for non-external operations.
Global pipeline performance optimisation.
Persistent cross-run state.
🧪 Acceptance criteria
Functional
HTTP 500, 502, 503, 504 responses are retried up to maxAttempts - 1 times before throwing.
HTTP 429 responses are retried using Retry-After wait time when present, or computed backoff otherwise.
HTTP 400, 401, 403, 404, 422 responses throw immediately without any retry attempt.
Network-level errors (connection reset, DNS failure) are treated as retryable and follow the same backoff schedule.
Edge cases
After maxAttempts (default: 4) total attempts, the last error is thrown — no further retries.
A single attempt that exceeds timeoutMs (default: 15 000 ms) is aborted and counted as a retryable failure.
Jitter ensures no two concurrent retries produce identical wait times.
A 429 with Retry-After: 60 waits exactly 60 seconds before the next attempt (regardless of backoff schedule).
Tests
Unit: retryWithBackoff retries exactly 3 times on 503 before throwing (4 total attempts).
Unit: retryWithBackoff throws immediately on 401 (1 total attempt, no wait).
Unit: backoff sequence for attempts 1–3 is approximately [200, 400, 800] ms before jitter.
Unit: timeoutMs triggers an AbortError that is counted as a retryable attempt.
Unit: Retry-After: 2 causes a ~2 000 ms wait on 429.
Regression: llm_client.mjs provider fallback is NOT triggered when anthropic_client.mjs throws on 401 (permanent error must propagate, not be absorbed).
⚙️ Constraints
No new runtime dependencies (use setTimeout + AbortController, both available in Node 20).
The utility must be synchronously importable (no top-level await).
All retry attempts must emit a structured log line via logger.mjs ({ msg: 'retry', attempt, statusCode, waitMs }).
Keep API minimal: retryWithBackoff(fn, options?) where fn is an async function returning Response.
🎯 Goal
Add a shared retry utility with bounded exponential backoff + jitter to absorb transient external failures without destabilizing a run.
📍 Context
scripts/lib/anthropic_client.mjs,scripts/lib/groq_client.mjs,scripts/auto_fix_pr.mjs(ghFetch)🚀 Description
Currently, external call resilience is inconsistent:
anthropic_client.mjs: single rawfetch, zero retry, zero timeout — any transient failure fails the entire run immediately.groq_client.mjs: retries on HTTP 429 only (good pattern), but HTTP 5xx throws immediately with no retry, and there is no per-call timeout.ghFetchinauto_fix_pr.mjs: no retry, no timeout — a single network error on any GitHub API call (diff fetch, label fetch, comment post) fails the entire run.The fix is a single shared
retryWithBackoffutility consumed by all three call sites.🧩 Scope
In:
scripts/lib/retry.mjsexportingretryWithBackoff(fn, options).maxAttempts: 4baseDelayMs: 200maxDelayMs: 8000timeoutMs: 15000 (per individual attempt)jitter: true (±20% random spread on each delay)429,500,502,503,504.400,401,403,404,422— throw immediately.Retry-Afterheader (or body hint), use that wait time instead of computed backoff (preservinggroq_client.mjsexisting behaviour).anthropic_client.mjs(wrap the existingfetchcall).groq_client.mjsto pass 5xx throughretryWithBackoff(the existing 429 loop can be simplified to use the same utility).ghFetchinauto_fix_pr.mjs.Out:
🧪 Acceptance criteria
Functional
maxAttempts - 1times before throwing.Retry-Afterwait time when present, or computed backoff otherwise.Edge cases
maxAttempts(default: 4) total attempts, the last error is thrown — no further retries.timeoutMs(default: 15 000 ms) is aborted and counted as a retryable failure.Retry-After: 60waits exactly 60 seconds before the next attempt (regardless of backoff schedule).Tests
retryWithBackoffretries exactly 3 times on 503 before throwing (4 total attempts).retryWithBackoffthrows immediately on 401 (1 total attempt, no wait).[200, 400, 800]ms before jitter.timeoutMstriggers anAbortErrorthat is counted as a retryable attempt.Retry-After: 2causes a ~2 000 ms wait on 429.llm_client.mjsprovider fallback is NOT triggered whenanthropic_client.mjsthrows on 401 (permanent error must propagate, not be absorbed).⚙️ Constraints
setTimeout+AbortController, both available in Node 20).logger.mjs({ msg: 'retry', attempt, statusCode, waitMs }).retryWithBackoff(fn, options?)wherefnis an async function returningResponse.