Skip to content

feat(store): the store learns to count — envelope burn, scoped forecast, cost - #25

Merged
lroolle merged 1 commit into
mainfrom
feat/usage-store-learns-to-count
Aug 19, 2026
Merged

feat(store): the store learns to count — envelope burn, scoped forecast, cost#25
lroolle merged 1 commit into
mainfrom
feat/usage-store-learns-to-count

Conversation

@lroolle

@lroolle lroolle commented Aug 19, 2026

Copy link
Copy Markdown
Member

What

The usage store recorded enough to forecast with; the code reading it counted wrong.

Burn was inflated ~3x. The weekday profile summed raw positive deltas of seven_day.utilization, so every stale reading — an idle session reporting numbers it last saw — was refunded and then re-earned. Measured on a real 23 MiB log: 146 points of "burn" for a week that moved 50. Burn is now the rise of a monotone envelope.

resets_at is not a window key. On 2026-08-17 an account's seven_day.utilization went 100.0 → 0.0 and stayed there, sampled by two independent writers, with seven_day.resets_at unchanged — the weekly counter can reset out of band. A newer key is certainly a new window; an unchanged one proves nothing. The envelope re-baselines only on a drop that is both sustained (≥2 consecutive samples) and deep (≥15 points). The failure mode is a bounded under-count, which costs a missed warning where the over-count cost a false alarm every render.

26% of the log was noise. session_start/session_end compared resets_at as a raw string; the server jitters it (06:00:00.515434 vs 06:00:00.087190), so nearly every fetch wrote a marker pair — 24,747 markers against 25,004 real samples, eaten straight out of the 32 MiB rotation cap.

Two more: an expired window is now never logged as a sample (an idle session, or a fixture piped in by hand, reported a window whose reset was already behind us); and week_scan now partitions by user.uuid, which the state-dir contract has required since v2 — the default dir predates account scoping and a real one holds a dozen uuids.

New

The scoped cap gets the learned forecast. fb caps ~Mon 14:00, 2d before reset — where linear pace is structurally silent, because 45% with four days left is a calm straight line. A week whose Tuesday burns 39%/day and whose Sunday burns 6%/day is not a line. _profile_walk now serves both the account 7d and the scoped cap so they cannot disagree about physics, and the scoped one only speaks for the scope its profile was built from (scoped_name).

The store records what a percentage costs. Claude Code hands us cost, tokens, context size, effort and CLI version on stdin every render; they were read for the badges and discarded. Each usage record now carries a session block, and forecast.cache gains a cost object pricing a 7d point in dollars — the join no single source can make, since the quota API reports percent and never dollars (limit_dollars is null on subscription) while transcripts report dollars and never percent. The denominator is paired: only points observed by a sample that also carried a dollar figure, or the months of dollar-free history price a week at pennies.

Docs

docs/api/state-dir.md gains a Reading the quota series section: the four properties above, plus the two things the series cannot tell you — gaps are not idleness (samples exist only while a statusline renders), and model is the logging session's, not the spender's.

Testing

413 tests (396 baseline + 17), shellcheck clean. Also deflaked three format_reset_relative fixtures that raced the clock — the fixture reads date, the function reads it again, and one tick flips 2h30m to 2h29m. Reproduced once in ten full runs.

No change to rows 2 and 3: v0.28.0's notice engine and layout are untouched.

🤖 Generated with Claude Code

…st, cost

The weekday profile summed raw positive deltas of seven_day.utilization,
so every stale reading was refunded and then re-earned: 146 points of
"burn" against a week that actually moved 50, measured on a real 23 MiB
log. Burn is now the rise of a monotone envelope.

Telling a stale reading from a real reset cannot be done with resets_at:
on 2026-08-17 an account went 100.0 -> 0.0 and stayed there, sampled by
two independent writers, with seven_day.resets_at unchanged. The envelope
re-baselines only on a drop that is both sustained (>= 2 samples) and
deep (>= 15 points) — a bounded under-count beats a false alarm on every
render.

Also fixed: an expired window is never logged as a sample; the
session_start/session_end boundary test compared resets_at as a raw
string and wrote a marker pair on nearly every fetch (24,747 markers
against 25,004 real samples, 26% of the log, eaten out of the rotation
cap); week_scan did not partition by user.uuid as the state-dir contract
has required since v2.

New: the model-scoped weekly cap gets the learned forecast, so it can say
"fb caps ~Mon 14:00, 2d before reset" where linear pace is structurally
silent. One walker serves both the account 7d and the scoped cap, and the
scoped one only speaks for the scope its profile was built from.

New: each usage record carries a session block (cost, tokens, context,
effort, CLI version — all handed to us on stdin and previously discarded),
and forecast.cache prices a 7d point in dollars. The quota API reports
percent and never dollars; the transcripts report dollars and never
percent. The denominator is paired, or a week prices at pennies.

Three test fixtures raced the clock (fixture reads date, function reads it
again, one tick flips 2h30m to 2h29m) — +30s of slack.

413 tests, shellcheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lroolle
lroolle merged commit 7947539 into main Aug 19, 2026
1 check passed
@lroolle
lroolle deleted the feat/usage-store-learns-to-count branch August 19, 2026 14:41
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