feat(agents): wait for the usage window to reset instead of aborting on rate limits - #179
Open
jackpolloway wants to merge 1 commit into
Open
feat(agents): wait for the usage window to reset instead of aborting on rate limits#179jackpolloway wants to merge 1 commit into
jackpolloway wants to merge 1 commit into
Conversation
…on rate limits When Claude rejects requests because a usage window is exhausted (e.g. the subscription 5-hour window), the claude CLI exits with an empty stderr and the run previously burned the consecutive-failure budget in ~3 minutes of generic backoff, aborting hours before the window reset. The claude agent now parses the rate_limit_event lines from the stream-json output and throws a typed RateLimitAgentError carrying the provider-reported reset time. The orchestrator rolls the attempt back without counting it as a failure, waits until the reset time plus a 60s buffer (bounded escalating fallback when the reset time is missing or stale), and retries the same iteration number, so overnight runs resume on their own. Also include the CLI's synthetic result message (e.g. the spend-limit text) in exit-error details, since stderr is empty for API-level failures.
This was referenced Aug 9, 2026
Closed
Closed
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.
Problem
On a Claude subscription, an overnight run dies long before the usage window resets.
When the 5-hour usage window is exhausted, the claude CLI fails with an empty stderr while the actual rejection (a
rate_limit_eventwith the exactresetsAttimestamp, plus a synthetic result message like "You've hit your org's monthly spend limit") is only on stdout.gnhf treats this like any transient error: 60s + 120s backoff, then the 3-consecutive-failures abort - about 3 minutes of tolerance for an outage that lasts hours.
A real run last night did 13 good iterations in ~1h50m, then aborted at the limit while the window reset was ~4 hours away.
Change
claude.tsparsesrate_limit_eventlines from the stream-json output. A rejected rate limit followed by a failure now throws a typedRateLimitAgentErrorcarrying the provider-reported reset time as aDate. A laterallowedevent clears the flag so unrelated failures are not misclassified.RateLimitAgentErroras a new iteration outcome: roll back the attempt, do not count it towardfailCount/maxConsecutiveFailures/--max-iterations, wait untilresetsAt+ 60s buffer, then retry the same iteration number. When the reset time is missing or already past, it falls back to a bounded escalating wait (60s doubling, capped at 30min) so a staleresetsAtcannot spin the loop. Ctrl+C interrupts the wait exactly like existing backoff.The renderer needs no changes: the wait reuses the existing
waitingstatus, andlastAgentErrorshows "claude usage limit reached until ".Tests
claude.test.tscases: rejected rate limit before non-zero exit, rate-limited error result with exit 0, and recovery (allowedafterrejected) not misclassifying a later failure; plus one pinning the synthetic-result-message detail.orchestrator.test.tscases: waits untilresetsAt+ buffer and retries the same iteration with zero failures counted, and the bounded fallback when no reset time is reported.README "How It Works" and AGENTS.md updated to document the new failure-handling rule.
🤖 Generated with Claude Code