Skip to content

menubar: honor Retry-After on Claude quota 429s, back off failed refreshes#702

Merged
iamtoruk merged 2 commits into
getagentseal:mainfrom
avs-io:fix/701-quota-rate-limit
Jul 16, 2026
Merged

menubar: honor Retry-After on Claude quota 429s, back off failed refreshes#702
iamtoruk merged 2 commits into
getagentseal:mainfrom
avs-io:fix/701-quota-rate-limit

Conversation

@avs-io

@avs-io avs-io commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The Claude Plan section can get stuck in a perpetual "rate-limited, retrying" loop (#701, reproduced on a real user machine — persisted blockedUntil in defaults — while CodexBar polls the identical endpoint on the same account without issue). Three compounding causes:

  1. 429 handling reads retry_after from the JSON body only; the standard Retry-After response header is never read, so recovery nearly always falls back to a flat 300s and re-probes into a still-open server window, overwriting (not escalating) the block.
  2. The refresh cadence anchor advances only on success while the app ticks every 30s, so any transient failure escalates polling from the configured cadence to one request per 30s until a success — the burst that earns the 429 in the first place.
  3. The popover-open refresh floor is an OR across providers, and the Settings / Plan-tab retry buttons bypass the AppDelegate single-flight, so interactive refreshes can stack with timer fetches.

Fix

  1. Retry-After header first (delta-seconds and HTTP-date forms, POSIX locale, UTC), then the body retry_after, then the 300s default — and the persisted block is now max(existing, new) so repeated 429s escalate instead of resetting. Both parsing and escalation are extracted as pure static functions.
  2. Attempt-anchored cadence with backoff: the anchor advances when an attempt starts; failures schedule the next attempt via exponential backoff (30s → 60s → …) with bounded jitter, capped at the configured cadence. One quick retry after a transient blip is preserved; sustained failure converges to steady-state cadence instead of 4x hotter. Rate-limited periods make no network requests at all — the existing persisted-block gate is consulted before every fetch, including forced ones.
  3. Per-provider popover floor: opening the popover only force-fetches the provider actually past the 30s floor.
  4. Single-flight routing: the Settings "Refresh Now" and Plan-tab Retry buttons route through the AppDelegate path (same as the context menu) instead of calling store.refreshSubscription() directly, so interactive refreshes can't run concurrently with timer fetches. Retry therefore triggers the same full refresh as "Refresh Now" — heavier than a bare quota fetch, but one consistent code path.

Tests

New mac/Tests/CodeBurnMenubarTests/ClaudeRateLimitTests.swift: header delta-seconds (takes precedence over body), HTTP-date parsing against an injected clock, malformed-header → body fallback → 300s default, block escalation never shrinks, and backoff growth/cap/jitter bounds for the extracted pure function.

Verification

swiftc -parse clean on all five touched files; git diff --check clean. swift build/swift test cannot run on the development machine (SwiftPM manifest linking broken machine-wide, fails on unmodified main too) — CI/maintainer verification is the build gate, as with #698.

Fixes #701

@avs-io
avs-io marked this pull request as ready for review July 16, 2026 20:20
@avs-io
avs-io requested a review from iamtoruk July 16, 2026 20:20
@avs-io

avs-io commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Verification upgrade: a standalone Swift 6.3.3 toolchain is now set up on the dev machine, so swift build runs — it caught an operator-precedence bug in the claudeDue fallback branch (?? binding tighter than >=), fixed in the latest push. Build is clean; XCTest still needs Xcode, so the new unit tests remain CI/maintainer-verified.

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified locally: swift build green, the full test run passes including the new XCTest rate-limit suite alongside the Swift Testing suites, and both core behaviors are mutation-pinned (removing the Retry-After header precedence fails two tests, reverting block escalation to overwrite fails one). The three-cause diagnosis matches the code and the #701 repro. Not driven against a live rate-limited account, which the unit pins and the reporter's persisted-defaults evidence cover as well as possible.

@iamtoruk
iamtoruk merged commit 4689b5a into getagentseal:main Jul 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude quota polling ignores the Retry-After header and retries failures at 30s, causing sustained 429s on /api/oauth/usage

2 participants