summaryrefslogtreecommitdiff
path: root/internal/executor/ratelimit.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-08 06:32:14 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-08 06:32:14 +0000
commit076c0faa0ae63278b3120cd6622e64ba1e36e36b (patch)
tree2dfe67328f1e6c795cc956d268aa6d84dc9ef93d /internal/executor/ratelimit.go
parentcad057fd64fbf44f953bc2784f70ce344f3389cf (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.go14
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 {