fix(quota): use resets_at, not updated_at, as the freshness signal - #151
Merged
Conversation
updated_at records when a statusline render WROTE an entry, not when it
OBSERVED the reading. A session idle for days still re-renders its
statusline and rewrites its ancient reading with a brand-new updated_at,
so it always looks fresh by that test alone — production evidence: eight
sessions, all written within 30 seconds, several holding readings from
5-hour windows that had rolled over days earlier. The derived flat
five_hour landed at 67% from a window that expired three days prior,
instead of the correct 50% from the live window.
resolveRateLimits now treats resets_at as the real freshness signal, per
window (five_hour/seven_day) independently:
1. drop entries whose resets_at is missing or already <= now (expired
window, meaningless reading)
2. among survivors, keep only entries whose resets_at equals the max
surviving resets_at (excludes a previous-window reading taken just
before rollover, whose resets_at is still-future but earlier)
3. among those same-window entries, take the max used_percentage
(usage only rises within one window, so the highest reading is the
most recently observed truth)
updated_at/staleMs stays as a secondary guard (bounds map growth, drops
vanished sessions) but is no longer the primary test. resets_at is parsed
defensively — persisted entries are string epoch-seconds, but the schema
and parser now also tolerate number epoch-millis.
Also updates escalate-model.test.ts's writeRateLimitsCache fixture to
include resets_at (previously omitted) — resolveRateLimits now needs it
to recognize a fixture window as live at all.
No schema/shape change to the flat five_hour/seven_day/updated_at fields
Programa's ClaudeQuotaSnapshotParser reads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
PR #150 session-scoped
~/.claude/tmp/rate-limits.jsonand took the maxused_percentageacross entries fresher thanCACHE_STALE_MS(10 min). Thatfixed cross-workspace clobber, but the freshness signal it used —
updated_at— is wrong: it records when a statusline render wrote anentry, not when it observed the reading. A session idle for days still
re-renders its statusline on every keystroke elsewhere and rewrites its
ancient reading with a brand-new
updated_at, so it always looks fresh.Live evidence from a real machine, 8 sessions all "written" within 30 seconds:
The derived flat
five_hourwas landing at 67%, taken from a windowthat had already expired on Aug 3 — the correct current-window answer is
50%.
The fix
resolveRateLimits(src/lib/quota.ts) now usesresets_at(already in thepayload, already persisted) as the real freshness signal, per window
independently:
resets_atis missing or already<= now(expired window)resets_atequals the max survivingresets_at(excludes a previous-window reading taken just before rollover)used_percentage(usage only rises within one window)updated_at/CACHE_STALE_MSstays as a secondary guard (bounds map growth,drops vanished sessions) but is no longer the primary freshness test.
resets_atis parsed defensively — persisted entries are stringepoch-seconds, the parser also tolerates number epoch-millis.
The visible statusline chip is unaffected — it reads the live per-render
rate_limitspayload straight from Claude Code, not this cache; only thecached/derived value consumed by
quota-steer.ts,escalate-model.ts, andPrograma's sidebar (
ClaudeQuotaSnapshotParser, unchanged flat schema) was wrong.Test plan
bun test— 1391 pass, 0 fail (1373 baseline + 18 new cases intests/quota.test.ts, covering: the exact production regression above, mixed-window exclusion, same-window max-wins, all-expired → null, and string-seconds/number-millis parsing)tests/escalate-model.test.tsfixture updated to includeresets_at(previously omitted) — the new algorithm needs it to recognize a fixture window as live at allbun run typecheckcleanbun run lintcleantests/output-style-preserve.test.tsverified passing in isolation against this commit (3/3) — unrelated failures reported against this suite come from other uncommitted work in the tree, not this change