fix(heartbeat): give provider_quota_exhausted a retry family (BLO-28924) - #1458
Merged
allyblockcast[bot] merged 1 commit intoAug 21, 2026
Merged
Conversation
`finalizeAgentStatus` treats three error codes as recoverable — parking the agent `idle` instead of `error` and firing the quota-exhausted hook — but the transient retry that actually resumes the work is keyed on the error FAMILY, resolved by `readHeartbeatRunErrorFamily`. `provider_quota_exhausted` was in the first list and absent from the second, so it resolved to `null`, `readTransientRecoveryContractFromRun` returned `null`, and no retry was scheduled. Since #1407 there is no recovery wake for it either: an agent with a heartbeat interval limps to its next timer tick, an interval-less event-driven agent never recovers at all. The code is emitted by the first-party `claude-local` adapter, which also extracts a provider-supplied `retryNotBefore` — the emit side was already complete, only the mapping was missing. It inherits the `provider_quota` family because it shares that contract: a billing/session boundary with an authoritative reset instant, which `clampTransientHorizon` deliberately exempts, so the adapter's floor is honoured verbatim. Extracts the recoverable set to RECOVERABLE_AGENT_STATUS_ERROR_CODES so `finalizeAgentStatus` and the invariant test read the same list, and adding a fourth code without a family arm fails CI rather than silently reopening this. Co-Authored-By: Claude <noreply@anthropic.com>
Author
|
🔗 Paperclip issue: BLO-28924 |
Author
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8b54b81
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The new
provider_quota_exhaustedmapping reuses the existingprovider_quotarecovery contract while preserving persisted-family precedence. - The recoverable-code invariant test directly guards against future drift between agent-status recovery and retry-family mapping.
- The tests cover the adapter reset hint, retry scheduling, persisted metadata precedence, and the non-recoverable inverse case.
Recommended Action
- No Critical or Important issues found. The change is suitable for the next merge decision once CI completes.
13 tasks
18 tasks
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.
Thinking Path
Linked Issues or Issue Description
a3023c93) and deliberately split out, because changing this shared contract would have moved that PR's head and dismissed a clean review during a live incident.What Changed
readHeartbeatRunErrorFamilynow resolvesprovider_quota_exhaustedto theprovider_quotafamily. It shares that contract — a billing/session boundary carrying an authoritative reset instant, not a capacity estimate — so it correctly inheritsprovider_quota's exemption fromclampTransientHorizonand the adapter'sretryNotBeforeis honoured verbatim instead of being clamped to a per-attempt capacity ceiling.||infinalizeAgentStatusinto an exportedRECOVERABLE_AGENT_STATUS_ERROR_CODES.finalizeAgentStatusand the new invariant test now read the same list, so a fourth code added without a family arm fails CI.readHeartbeatRunErrorFamilyandreadTransientRecoveryContractFromRun(previously module-private) so they can be asserted directly, matching the existing convention for pure helpers in this module (shouldScheduleAutomaticRunRetry,shouldUseRepoLessFallbackWorkspaceSource).server/src/__tests__/heartbeat-recoverable-error-family.test.ts.provider_quota_exhaustedas a "legacy/pluggable adapter signal". It is emitted by the first-partyclaude-localadapter (packages/adapters/claude-local/src/server/execute.ts:1364), which also populatesretryNotBefore— so the emit side was already complete and only the family mapping was missing.Verification
Regression proof — the behavioural arm was reverted while keeping the new exports in place, so the test fails on a meaningful assertion rather than an import error:
The 4 that still pass are the controls (persisted-family precedence, the unrelated non-recoverable code, and the other two recoverable codes in the parameterised invariant) — confirming the suite is not simply globally broken.
With the fix applied, 372 tests pass across 8 suites:
Risks
Low risk, and the blast radius was enumerated rather than assumed. Mapping the code to a non-null family changes behaviour at exactly two call sites:
shouldScheduleAutomaticRunRetry(heartbeat.ts:1138) — now returnstrue. This is the fix. The stale-kill evidence gate above it is unaffected: it returns before this line.keepIdleOnFailure(heartbeat.ts:25550) — now computestruefor these runs. This is a no-op:recoverablealready includesprovider_quota_exhausted, and both feed the same? "idle"branch, so the resulting status is unchanged.Not a behavioural change: a persisted
resultJson.errorFamilystill takes precedence over the code fallback (explicitly tested), and unrelated codes stay unmapped (also tested, to guard the inverse). TheRECOVERABLE_AGENT_STATUS_ERROR_CODESrefactor is value-identical to the||chain it replaces, including fornull/undefined(?? ""matches nothing in the set).Low live exposure: a full parked census on 2026-08-19 (n=700,
truncated: false) plus a 16-run stratified sample across all 12 fleet agents found zero runs carrying this code — the fleet runsclaude_k8s/opencode_k8s, notclaude-local. This is a latent correctness gap that bites the first deployment using theclaude-localadapter, not a live outage.Model Used
claude-opus-5[1m](1M context), extended thinking, with tool use and code execution — run as the Paperclip CTO agent on theclaude_k8sadapter.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template