Skip to content

fix(daemon): send claude-code User-Agent to fix persistent statusline quota 429#290

Merged
AnnatarHe merged 1 commit into
mainfrom
fix/statusline-quota-429-user-agent
Jun 21, 2026
Merged

fix(daemon): send claude-code User-Agent to fix persistent statusline quota 429#290
AnnatarHe merged 1 commit into
mainfrom
fix/statusline-quota-429-user-agent

Conversation

@AnnatarHe

Copy link
Copy Markdown
Contributor

Problem

Running shelltime statusline always shows 🚦 err:api:429 in the quota section.

The error is not from commands/cc_statusline.go — that only renders the QuotaError the daemon returns. The daemon's fetchAnthropicUsage calls https://api.anthropic.com/api/oauth/usage sending only Authorization + anthropic-beta. That endpoint requires a User-Agent: claude-code/<version> header — without it, requests fall into an aggressively rate-limited bucket and return persistent 429s regardless of actual usage (see anthropics/claude-code#31021).

Two secondary issues kept the error sticky:

  • No extra backoff on 429 → re-poked the throttled bucket every 10 min (the TTL).
  • stopTimer() wiped the rate-limit cache after 3 min idle → immediate re-fetch on next activity, defeating the TTL.

Fix

  1. User-Agent (primary fix): fetchAnthropicUsage now sends User-Agent: claude-code/<version> and Content-Type. The real Claude Code version (already present in the statusline input as data.Version) is plumbed CLI → daemon via CCInfoRequest.ClaudeCodeVersion, stored on the timer service, with a claudeCodeFallbackVersion fallback.
  2. 429-aware backoff: a typed anthropicAPIError carries the status + parsed Retry-After; on 429 the cache sets backoffUntil = max(Retry-After, 30m) and the TTL gate skips while in backoff. Cleared on success.
  3. Cache preserved across idle: 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 URL
  • daemon/cc_info_timer.go — version store + Set/Get, 429 backoff in fetchRateLimit, stopTimer no longer wipes cache
  • daemon/socket.go, daemon/client.go, commands/cc_statusline.go — plumb version CLI → daemon

Testing

  • go build ./..., go vet, gofmt all clean.
  • Added tests (pass): User-Agent set + fallback, 429 → typed error/Retry-After (still shortens to api:429), parseRetryAfter table, version Set/Get, stopTimer preserves cache.
  • Full daemon/commands suites: zero new failures vs. clean tree. Pre-existing failures are environmental (bind: invalid argument — macOS unix-socket path length from t.TempDir()).

To confirm against the live endpoint: curl /api/oauth/usage with the OAuth token returns 200 with User-Agent: claude-code/2.0.0 but 429 without it.

🤖 Generated with Claude Code

… 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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
daemon/cc_info_timer.go 52.38% 9 Missing and 1 partial ⚠️
daemon/socket.go 0.00% 1 Missing and 1 partial ⚠️

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

Flag Coverage Δ
unittests 78.91% <75.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
commands/cc_statusline.go 96.64% <100.00%> (ø)
daemon/anthropic_ratelimit.go 73.84% <100.00%> (+39.84%) ⬆️
daemon/client.go 85.71% <100.00%> (+0.17%) ⬆️
daemon/socket.go 80.76% <0.00%> (-1.05%) ⬇️
daemon/cc_info_timer.go 87.46% <52.38%> (-1.65%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AnnatarHe AnnatarHe merged commit d159f34 into main Jun 21, 2026
3 of 4 checks passed
@AnnatarHe AnnatarHe deleted the fix/statusline-quota-429-user-agent branch June 21, 2026 16:09

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +187 to +196
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}

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