feat(store): the store learns to count — envelope burn, scoped forecast, cost - #25
Merged
Merged
Conversation
…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>
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
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_atis not a window key. On 2026-08-17 an account'sseven_day.utilizationwent100.0 → 0.0and stayed there, sampled by two independent writers, withseven_day.resets_atunchanged — 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_endcomparedresets_atas a raw string; the server jitters it (06:00:00.515434vs06: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_scannow partitions byuser.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_walknow 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
usagerecord now carries asessionblock, andforecast.cachegains acostobject pricing a 7d point in dollars — the join no single source can make, since the quota API reports percent and never dollars (limit_dollarsis 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.mdgains 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), andmodelis the logging session's, not the spender's.Testing
413 tests (396 baseline + 17), shellcheck clean. Also deflaked three
format_reset_relativefixtures that raced the clock — the fixture readsdate, the function reads it again, and one tick flips2h30mto2h29m. 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