fix(daemon): send claude-code User-Agent to fix persistent statusline quota 429#290
Conversation
… quota 429 The Anthropic OAuth usage endpoint (/api/oauth/usage) requires a `User-Agent: claude-code/<version>` header. Without it, requests land in an aggressively rate-limited bucket and return persistent 429s, surfacing as `🚦 err:api:429` in the Claude Code statusline. - Send `User-Agent` (+ Content-Type) on the usage request; flow the real Claude Code version (data.Version) CLI -> daemon via CCInfoRequest, with a fallback. - Add 429-aware backoff: honor Retry-After, otherwise back off 30m, so the daemon stops poking the throttled bucket. - Stop wiping the rate-limit cache on idle (stopTimer) so the last-good usage and the TTL/backoff survive idle cycles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (78.91%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request updates the daemon to receive and forward the Claude Code version from the statusline client, using it to set the User-Agent header for Anthropic usage API requests to avoid aggressive rate limiting. It also introduces a 429 backoff mechanism that respects the Retry-After header, along with preserving the rate-limit cache across idle cycles. Feedback on the changes suggests capping the parsed Retry-After duration to a maximum limit (e.g., 24 hours) to prevent potential integer overflow or excessively long backoff periods.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func parseRetryAfter(v string) time.Duration { | ||
| v = strings.TrimSpace(v) | ||
| if v == "" { | ||
| return 0 | ||
| } | ||
| if secs, err := strconv.Atoi(v); err == nil && secs > 0 { | ||
| return time.Duration(secs) * time.Second | ||
| } | ||
| return 0 | ||
| } |
There was a problem hiding this comment.
To prevent potential integer overflow and excessively long backoff periods, it is highly recommended to cap the parsed Retry-After value to a reasonable maximum duration (e.g., 24 hours). If a buggy or malicious server returns an extremely large value, it could cause time.Duration overflow or result in the daemon backing off indefinitely.
| func parseRetryAfter(v string) time.Duration { | |
| v = strings.TrimSpace(v) | |
| if v == "" { | |
| return 0 | |
| } | |
| if secs, err := strconv.Atoi(v); err == nil && secs > 0 { | |
| return time.Duration(secs) * time.Second | |
| } | |
| return 0 | |
| } | |
| func parseRetryAfter(v string) time.Duration { | |
| v = strings.TrimSpace(v) | |
| if v == "" { | |
| return 0 | |
| } | |
| if secs, err := strconv.Atoi(v); err == nil && secs > 0 { | |
| const maxSeconds = 24 * 3600 | |
| if secs > maxSeconds { | |
| secs = maxSeconds | |
| } | |
| return time.Duration(secs) * time.Second | |
| } | |
| return 0 | |
| } |
Problem
Running
shelltime statuslinealways shows🚦 err:api:429in the quota section.The error is not from
commands/cc_statusline.go— that only renders theQuotaErrorthe daemon returns. The daemon'sfetchAnthropicUsagecallshttps://api.anthropic.com/api/oauth/usagesending onlyAuthorization+anthropic-beta. That endpoint requires aUser-Agent: claude-code/<version>header — without it, requests fall into an aggressively rate-limited bucket and return persistent429s regardless of actual usage (see anthropics/claude-code#31021).Two secondary issues kept the error sticky:
stopTimer()wiped the rate-limit cache after 3 min idle → immediate re-fetch on next activity, defeating the TTL.Fix
fetchAnthropicUsagenow sendsUser-Agent: claude-code/<version>andContent-Type. The real Claude Code version (already present in the statusline input asdata.Version) is plumbed CLI → daemon viaCCInfoRequest.ClaudeCodeVersion, stored on the timer service, with aclaudeCodeFallbackVersionfallback.anthropicAPIErrorcarries the status + parsedRetry-After; on 429 the cache setsbackoffUntil = max(Retry-After, 30m)and the TTL gate skips while in backoff. Cleared on success.stopTimer()no longer zeroes the rate-limit cache, so last-good usage shows instantly and the TTL/backoff hold across idle cycles.Files
daemon/anthropic_ratelimit.go— headers, version arg, typed error +parseRetryAfter, backoff const, injectable URLdaemon/cc_info_timer.go— version store + Set/Get, 429 backoff infetchRateLimit,stopTimerno longer wipes cachedaemon/socket.go,daemon/client.go,commands/cc_statusline.go— plumb version CLI → daemonTesting
go build ./...,go vet,gofmtall clean.api:429),parseRetryAftertable, version Set/Get,stopTimerpreserves cache.daemon/commandssuites: zero new failures vs. clean tree. Pre-existing failures are environmental (bind: invalid argument— macOS unix-socket path length fromt.TempDir()).To confirm against the live endpoint:
curl/api/oauth/usagewith the OAuth token returns200withUser-Agent: claude-code/2.0.0but429without it.🤖 Generated with Claude Code