Skip to content

fix(quota): use resets_at, not updated_at, as the freshness signal - #151

Merged
arzafran merged 1 commit into
mainfrom
fix/quota-resets-at-window
Aug 6, 2026
Merged

fix(quota): use resets_at, not updated_at, as the freshness signal#151
arzafran merged 1 commit into
mainfrom
fix/quota-resets-at-window

Conversation

@arzafran

@arzafran arzafran commented Aug 6, 2026

Copy link
Copy Markdown
Member

What this does

PR #150 session-scoped ~/.claude/tmp/rate-limits.json and took the max
used_percentage across entries fresher than CACHE_STALE_MS (10 min). That
fixed cross-workspace clobber, but the freshness signal it used —
updated_at — is wrong: it records when a statusline render wrote an
entry, 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:

session   5h    resets_at        7d    resets_at
6271d111  28%   08-06 13:10 LIVE  75%   08-09 07:00
33276606  50%   08-06 13:10 LIVE  79%   08-09 07:00
9cbb69fa  50%   08-06 13:10 LIVE  79%   08-09 07:00
5005e845  25%   08-06 13:10 LIVE  74%   08-09 07:00
fe277e28   6%   08-03 23:20 DEAD  28%   08-09 07:00
0964fc10  29%   08-04 15:00 DEAD  37%   08-09 07:00
8367e119  67%   08-03 18:20 DEAD  25%   08-09 07:00
071862a0  22%   08-04 15:00 DEAD  35%   08-09 07:00

The derived flat five_hour was landing at 67%, taken from a window
that had already expired on Aug 3 — the correct current-window answer is
50%.

The fix

resolveRateLimits (src/lib/quota.ts) now uses resets_at (already in the
payload, already persisted) as the real freshness signal, per window
independently:

  1. drop entries whose resets_at is missing or already <= now (expired window)
  2. among survivors, keep only the ones whose resets_at equals the max surviving resets_at (excludes a previous-window reading taken just before rollover)
  3. among those same-window entries, take the max used_percentage (usage only rises within one window)

updated_at/CACHE_STALE_MS stays as a secondary guard (bounds map growth,
drops vanished sessions) but is no longer the primary freshness test.
resets_at is parsed defensively — persisted entries are string
epoch-seconds, the parser also tolerates number epoch-millis.

The visible statusline chip is unaffected — it reads the live per-render
rate_limits payload straight from Claude Code, not this cache; only the
cached/derived value consumed by quota-steer.ts, escalate-model.ts, and
Programa's sidebar (ClaudeQuotaSnapshotParser, unchanged flat schema) was wrong.

Test plan

  • bun test — 1391 pass, 0 fail (1373 baseline + 18 new cases in tests/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.ts fixture updated to include resets_at (previously omitted) — the new algorithm needs it to recognize a fixture window as live at all
  • bun run typecheck clean
  • bun run lint clean
  • tests/output-style-preserve.test.ts verified 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

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.
@arzafran
arzafran merged commit c2ffda3 into main Aug 6, 2026
14 of 45 checks passed
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