Skip to content

feat(agents): wait for the usage window to reset instead of aborting on rate limits - #179

Open
jackpolloway wants to merge 1 commit into
kunchenguid:mainfrom
jackpolloway:feat/rate-limit-wait
Open

feat(agents): wait for the usage window to reset instead of aborting on rate limits#179
jackpolloway wants to merge 1 commit into
kunchenguid:mainfrom
jackpolloway:feat/rate-limit-wait

Conversation

@jackpolloway

Copy link
Copy Markdown

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_event with the exact resetsAt timestamp, 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.ts parses rate_limit_event lines from the stream-json output. A rejected rate limit followed by a failure now throws a typed RateLimitAgentError carrying the provider-reported reset time as a Date. A later allowed event clears the flag so unrelated failures are not misclassified.
  • The orchestrator handles RateLimitAgentError as a new iteration outcome: roll back the attempt, do not count it toward failCount / maxConsecutiveFailures / --max-iterations, wait until resetsAt + 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 stale resetsAt cannot spin the loop. Ctrl+C interrupts the wait exactly like existing backoff.
  • Exit-error details now include the CLI's synthetic result message, so failures read "claude exited with code 1: You've hit your org's monthly spend limit" instead of "claude exited with code 1: ".

The renderer needs no changes: the wait reuses the existing waiting status, and lastAgentError shows "claude usage limit reached until ".

Tests

  • 3 new claude.test.ts cases: rejected rate limit before non-zero exit, rate-limited error result with exit 0, and recovery (allowed after rejected) not misclassifying a later failure; plus one pinning the synthetic-result-message detail.
  • 2 new orchestrator.test.ts cases: waits until resetsAt + buffer and retries the same iteration with zero failures counted, and the bounded fallback when no reset time is reported.
  • Full suite: 667 passing; lint, typecheck, and prettier clean.

README "How It Works" and AGENTS.md updated to document the new failure-handling rule.

🤖 Generated with Claude Code

…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.
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.

1 participant