diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 06:32:14 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 06:32:14 +0000 |
| commit | 076c0faa0ae63278b3120cd6622e64ba1e36e36b (patch) | |
| tree | 2dfe67328f1e6c795cc956d268aa6d84dc9ef93d /internal/executor/ratelimit.go | |
| parent | cad057fd64fbf44f953bc2784f70ce344f3389cf (diff) | |
fix: detect quota exhaustion from stream; map to BUDGET_EXCEEDED not FAILED
When claude hits the 5-hour usage limit it exits 1. execOnce was
returning the generic "exit status 1" error, hiding the real cause from
the retry loop and the task state machine.
Fix:
- execOnce now surfaces streamErr when it indicates rate limiting or
quota exhaustion, so callers see the actual message.
- New isQuotaExhausted() detects "hit your limit" messages — these are
not retried (retrying a depleted 5h bucket wastes nothing but is
pointless), and map to BUDGET_EXCEEDED in both execute/executeResume.
- isRateLimitError() remains for transient throttling (429/overloaded),
which continues to trigger exponential backoff retries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/ratelimit.go')
| -rw-r--r-- | internal/executor/ratelimit.go | 14 |
1 files changed, 13 insertions, 1 deletions
diff --git a/internal/executor/ratelimit.go b/internal/executor/ratelimit.go index 884da43..deaad18 100644 --- a/internal/executor/ratelimit.go +++ b/internal/executor/ratelimit.go @@ -13,7 +13,8 @@ var retryAfterRe = regexp.MustCompile(`(?i)retry[-_ ]after[:\s]+(\d+)`) const maxBackoffDelay = 5 * time.Minute -// isRateLimitError returns true if err looks like a Claude API rate-limit response. +// isRateLimitError returns true if err looks like a transient Claude API +// rate-limit that is worth retrying (e.g. per-minute/per-request throttle). func isRateLimitError(err error) bool { if err == nil { return false @@ -25,6 +26,17 @@ func isRateLimitError(err error) bool { strings.Contains(msg, "overloaded") } +// isQuotaExhausted returns true if err indicates the 5-hour usage quota is +// fully exhausted. Unlike transient rate limits, these should not be retried. +func isQuotaExhausted(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "hit your limit") || + strings.Contains(msg, "you've hit your limit") +} + // parseRetryAfter extracts a Retry-After duration from an error message. // Returns 0 if no retry-after value is found. func parseRetryAfter(msg string) time.Duration { |
