fix(ai): one session-layer retry owner for provider failures - #2045
Open
snimu wants to merge 4 commits into
Open
fix(ai): one session-layer retry owner for provider failures#2045snimu wants to merge 4 commits into
snimu wants to merge 4 commits into
Conversation
Providers now make exactly one attempt per request and report structured failures; the agent session auto-retry loop is the only retry mechanism: - OpenAI/Azure/Anthropic SDK clients are constructed with maxRetries: 0, Bedrock with maxAttempts: 1 (Google sets no retryOptions and Mistral already used retries: none). Their internal uncapped Retry-After sleeps were invisible to the UI and ignored retry.provider.maxRetryDelayMs. - The Codex hand-rolled retry loop (which also retried deterministic 4xx) is deleted; HTTP errors throw a structured CodexApiError instead. - openai-completions and codex now record provider_stream_failure diagnostics like every other provider; codex parses nested streaming error payloads (usage_limit_reached with plan/resets_at) into the friendly usage-limit message. - StreamFailureInfo carries retryAfterMs extracted from Retry-After / retry-after-ms headers or usage-limit reset info; the session retry loop waits at least that long, capped by retry.provider.maxRetryDelayMs (fails fast with an informative error beyond the cap, 0 disables). - Structured invalid_request/refusal failures are no longer retried once before being treated as permanent; auth keeps its single-retry hedge. - The dead maxRetries/maxRetryDelayMs stream-option plumbing and the retry.provider.maxRetries setting are removed.
…ssion loop Review follow-up: side questions and the one-shot completeSimple consumers (compaction summaries, branch summarization, refinement, auto-refine review) never enter the session auto-retry loop, so setting SDK maxRetries to 0 left them with zero retries anywhere. New core/provider-retry.ts holds the single policy definition (structured failure kind readers, permanent-kind rule, Retry-After-aware delay with the maxRetryDelayMs cap) plus completeWithProviderRetry for one-shot calls; the AgentSession loop now delegates to the same policy functions. Side questions retry with the session's settings (drop failed assistant turn, re-run); the utility completions retry with the default policy. Intentionally not wrapped: the daemon agent-status summarizer - it is a periodic best-effort call that naturally re-attempts on the next cycle.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 36c1426. Configure here.
…ancelled Review follow-up: completeWithProviderRetry swallowed an abort during the retry sleep and returned the previous provider error, so cancelling compaction/branch-summary/refinement mid-backoff surfaced as a failure instead of an abort.
…letions Review follow-up: compaction summaries, branch summarization, and refinement calls used the default retry policy, ignoring configured retry.enabled/maxRetries/baseDelayMs/maxRetryDelayMs. The AgentSession call sites now pass providerRetryPolicy(settingsManager) down, matching side questions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Makes the agent session's auto-retry loop the single owner of provider retries (Linear: RES-1271; discussion evidence: #1474, #1405, #1534, #1981-retry-half, #1553(3)).
Why
Retry policy lived in N+1 places that drifted independently:
Retry-After: 153971sleeps ~42h per attempt, invisibly (no UI event, client timeout does not cover inter-retry sleep) ([Bug] `-p` batch mode hangs on a 429 with a long reset; unknown settings keys are silently accepted #1474, [Bug] OAuth subscription exhaustion can leave the TUI on Waiting instead of surfacing the quota error #1405).retry.provider.maxRetryDelayMswas documented, read, and threaded through agent/proxy/stream options — but no provider or SDK ever consumed it (dead config, [Bug] `-p` batch mode hangs on a 429 with a long reset; unknown settings keys are silently accepted #1474)."usage limit"message substring (Codex Responses: CRLF event streams parse as an empty successful turn, the final event is dropped, and non-retryable 4xx are retried #1553(3)).invalid_requestfailures were retried once anyway (_retryAttempt > 0hedge), so a poisoned session failed two requests per prompt forever (Deterministic 400 invalid_request_error from an orphaned tool_use_id is classified retryable and never repaired: session re-sends the identical rejected request for 6+ hours #1534, [Bug] Gemini API fails with 400: Requests ending with a model turn are not supported #1981).{ type: "error", status_code, error: { type: "usage_limit_reached", plan_type, resets_at } }) bypassed the friendly usage-limit formatter and structured classification entirely ([Bug] OAuth subscription exhaustion can leave the TUI on Waiting instead of surfacing the quota error #1405(2)).What
Mechanism (one owner):
maxRetries: 0, BedrockmaxAttempts: 1. Google sets noretryOptions(SDK only retries when configured) and Mistral already usedretries: { strategy: "none" }— verified, unchanged.CodexApiError(code, status, retryAfterMs).StreamFailureInfogainsretryAfterMs, extracted fromRetry-After/retry-after-msheaders (seconds, ms, or HTTP-date) or Codexresets_at.max(backoff, retryAfterMs), capped byretry.provider.maxRetryDelayMs(default 60s,0disables): a longer server-requested wait fails immediately with an informative error — exactly the documented-but-dead semantics.openai-completionsandcodexnow recordprovider_stream_failurediagnostics like every other provider; codex parses nested error payloads into the friendly usage-limit message.invalid_request/refusalfailures are permanent at attempt 0 (no more one pointless retry);authkeeps its single-retry hedge so a transient auth hiccup does not immediately mark auth stale.Deletion:
isRetryableError+sleephelper.maxRetries/maxRetryDelayMsplumbing throughStreamOptions,simple-options,Agent,streamProxy,sdk.ts,side-question, and theretry.provider.maxRetriessetting (its only effect was re-enabling invisible SDK-internal retries).Provider-local retry/backoff inventory (before → after)
maxRetries: 0maxRetries: 0maxRetries: 0maxRetriesoverride plumbingmaxAttempts: 1retryOptionsset — never set)strategy: "none"(unchanged)_handleRetryableError(visible auto_retry events)Network-level failures previously retried inside providers are covered by the session loop, which retries any non-permanent error stop with visible
auto_retry_start/endevents.Size
Net src LOC: +170/−142 across packages/ai, packages/agent, packages/coding-agent (mechanism ≈ +150 in Retry-After extraction/cap + structured codex errors; deletion ≈ −140 of SDK retry plumbing and the codex loop). Tests: 7 new pins, 1 reworked (the "retries structured permanent failures once" pin now asserts no retry for invalid_request/refusal).
Validation
Linear: RES-1271
Note
Medium Risk
Changes retry semantics on every LLM provider path and removes SDK-level recovery; regressions would show up as extra failures or wrong backoff, but behavior is heavily pinned in new tests.
Overview
Provider retries are no longer handled inside SDKs or Codex’s old loop — each provider call is a single attempt that surfaces structured
provider_stream_failurediagnostics (includingretryAfterMsfromRetry-Afterheaders or Codex usage-limitresets_at).The session auto-retry loop (and a new shared
provider-retrymodule) becomes the only place that backs off and retries: delay ismax(exponential backoff, provider retryAfterMs), capped byretry.provider.maxRetryDelayMs; longer waits fail fast with a clear error.invalid_request/refusalno longer get a retry;authstill gets one hedge retry.Removed
maxRetries/maxRetryDelayMsfrom stream options,Agent, proxy serialization, andretry.provider.maxRetries. Side questions, compaction, branch summarization, and refinement now usecompleteWithProviderRetryso they don’t silently become single-shot after SDK retry removal.Reviewed by Cursor Bugbot for commit 0d2a6f0. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Consolidate provider retries into the session layer and cap retry delays
AgentSessionand the newcompleteWithProviderRetrywrapper.completeWithProviderRetryandProviderRetryPolicyto apply shared retry settings to standalone completions: compaction, refinement, branch summaries, and side questions.Retry-Afterheader parsing toStreamFailureInfoandCodexApiError, including extraction of nested usage-limit details from Codex SSE error events.maxRetryDelayMscap.maxRetriesandmaxRetryDelayMsfields removed fromStreamOptionsin types.ts,AgentOptionsin agent.ts, andProxySerializableStreamOptionsin proxy.ts; theretry.provider.maxRetriessetting is also removed fromProviderRetrySettings.Macroscope summarized 0d2a6f0.