feat(quota): Claude Code quota + per-session cost in the header and statusline #80
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/quota"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Surfaces Claude Code quota (5-hour / 7-day rate-limit windows) and per-session cost/context in two places:
Historyicon for reset times, warn (>75%) / danger (>90%) bands.◆ feat/quota | 5h 31% | 7d 8% | ctx 24% | $0.42inside each session.$0.42 · 12k ctx).Where the data comes from (not what you'd expect)
GET /api/oauth/usage— the obvious endpoint — returns429unconditionally, even for unauthenticated requests, so it is not account-scoped and cannot be used (see claude-code#31021). Instead, Claude Code reads theanthropic-ratelimit-unified-*headers off its own/v1/messagesresponses and hands them to thestatusLinecommand on stdin. So the worker's statusline script is the source: it renders the terminal line and reports the numbers to Nexus. No extra API call, no token handling, and it can't be rate-limited.Architecture
statusLineis seeded into the fleet-sharedsettings.jsononce, flag-guarded (statusline_seeded) exactly like the agent-instructions seeder, so operator deletion is honoured.The invariant this feature is built around: unknown ≠ zero
A session that has spent nothing and a session we know nothing about are different facts, and the payload signals "unknown" three different ways that disagree — so the code never infers unknown from a falsy value:
rate_limits— an absent key until the session's first inference callcontext_window.used_percentage—null, while its siblingtotal_input_tokensis0for the same stateAll three render as
—, never0%/$0.00.Testing
worker/nexus-statusline.test.sh, 20 cases): render for every state, the em-dash unknown states, the per-session throttle stamp (3 sessions → 3 POSTs), the POSTed wire body preservingnull, the jq-absent fallback, and always-exit-0.offsetHeight): the badge switches line↔circles correctly, the circle switch does not grow the header, warn/danger colours and the dashed unknown state render as designed.753 vitest + 20 shell tests, lint + typecheck clean.Notable during review
Built task-by-task with per-task verification, then a 4-lens review pass (tests / errors / types / comments). Verification caught several issues that a green suite missed — most notably a self-referential container query that made the single-line badge dead code at every width (fixed to a viewport
@mediaat 900px), and the statusline POST body's unknown-as-null guarantee being untested at the one layer that mattered. Seedocs/superpowers/specs/2026-07-16-quota-statusline-design.md(design + verified findings) anddocs/superpowers/plans/2026-07-17-quota-statusline.md(implementation plan) for the full record.Docs
AGENTS.md gains fact #19 documenting the above, and a correction to the Tests section (the SQLite test helper is
openTestDb/sqliteAvailableinnexus/src/lib/server/testing/db.ts, not the previously-statedmakeTestDb).Related
--muted-2text fails WCAG) — deliberately out of scope here; this feature uses--mutedmatching existing convention.Quota rides on anthropic-ratelimit-unified-* headers of ordinary inference responses, and Claude Code already forwards it to the statusline as rate_limits.{five_hour,seven_day}. No API call, no token, no rate limit — /api/oauth/usage (claude-code#31021) is not needed. Approach A: in-memory store, no migration. Account-global quota in a header badge (single line -> 20px circles under 640px, header height unchanged); per-session cost on session rows.Captured a real statusline payload by temporarily pointing statusLine at a dumper. The three flagged assumptions held (resets_at = epoch seconds, NEXUS_SESSION_ID inherits, cadence ~1.6/s peak confirms the 30s throttle). Four things it corrected: - settings.json is LIVE-RELOADED; 4 already-running sessions picked the hook up within seconds. No restart needed -- and a bad statusLine reaches the whole fleet instantly, so the flag-guard matters more, not less. - /tmp is shared by all N sessions in a worker (4 distinct NEXUS_SESSION_IDs observed writing the same files). A single stamp would starve every session but the first; the stamp is now keyed by NEXUS_SESSION_ID, with a test. - session_name is claude-code's auto conversation title ('Check API quota endpoint for Claude Code'), not the Nexus name. Derive from cwd instead. - used_percentage carries float noise (28.999999999999996 observed). zod max(100) would 400 the report exactly when quota is exhausted -- clamp. Also fixed a test that could never fail: grep -q has no lookahead, so the 'auto title absent' assertion needed a real run_not helper.Two PR-review findings (each flagged by two independent reviewers): 1. setAccountQuota replaced the whole window set, so a partial rate_limits report (one window present, the other absent — structurally possible since claude-code's builder makes each window independently conditional) silently dropped the omitted window from the badge until re-supplied. Now merges per-window: { ...accountQuota, ...windows, updatedAt: now }. Test added (mutation-confirmed: fails against the old replace-semantics). 2. The 75/90 band thresholds were hardcoded in three places (format.ts, QuotaBadge.svelte, the shell script); the Svelte copy was the one NOT unit-tested and could drift from the server. Extracted WARN_ABOVE/ DANGER_ABOVE into $lib/quota-bands.ts (client-importable, unlike $lib/server) and imported into both TS sites. Shell stays separate (bash) but is pinned by its own test.