feat(store): a directory is where a sample landed, not who it belongs to - #32
Merged
Conversation
The forecast messages were bad because a co-writer was poisoning the model behind them. `forecast.cache` sits in `~/.claude/statusline/`, a published shared store, and it is the one derived file more than one tool wants to write. ccpace rebuilt the five fields it knew, wrote them flat over the file, and did it with an accounting this script abandoned two releases ago — burn as the sum of raw positive deltas, which credits every stale dip and then credits the re-climb. Downstream, everything did the right thing and the result was still wrong. The published profile had 149%/day in Thursday; the walk's corrupt-profile guard judged it impossible and went silent. The exchange rate, the price and the scoped forecast lost their inputs and said "still learning". The budget line dropped to linear pace: ~92% became ~61% on a week nothing had changed about. Nothing lied — the account simply had a sound profile ten minutes earlier and no way back to it, because the rebuild gate asked only whether the file was fresh, and it was, having been overwritten seconds ago. So the cache carries `schema`, the version of the MODEL rather than of the file, and freshness is necessary instead of sufficient: a cache whose schema is missing or lower is rebuilt on sight. The contract for co-writers is written down — stamp the schema you implement, and either emit the full key set or merge into what is there. Silence is a defence against a bad model; it is not a substitute for knowing whose model you are reading. `heading` was neither a direction nor a destination. The budget line carried two futures with no grammar to tell them apart: `even 6.2%/win` is a RATION — spend that per window and the pool lands exactly on 100 — and the other number is a PREDICTION of where your own pattern takes you. It reads `lands ~91%` now, in the budget line and in report's week-in-progress alike, and row 2 states the landing rather than the ration: of the three clauses the long form carries it is the only one not already on screen, and it answers what a calm week actually asks — not "how do I ration this" but "am I going to strand it". The strips are one typeface again. The baseline was `ˍ` (U+02CD MODIFIER LETTER LOW MACRON) while every bar above it came from Block Elements; terminals resolve those through different faces, so the zero line sat at a different height and advance width than the bars beside it and the seam showed on every row that held both. It is `▁` now, the shortest bar of the same run. Burn starts one rung up, at `▂`; `▅` and above keep their thresholds, so a fully burned window reads the height it always did. `LEDGER_BASE_GLYPH` overrides it. 441 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aggregating readers (forecast, week strip, report) read every usage store under the data root — top level plus accounts/*/ — and partition by user.uuid, instead of the one directory they were standing in. The same account had 301 days at the root and 28 in its tagged dir; a fresh DEVA_AUTH_TAG rendered 'still learning' for two weeks over ten months of history one level up. forecast.cache gains a corpus stamp (uuid, files, samples, dropped_no_uuid, oldest): schema versions the model and cannot say what it was run over. uuid-less rows stay dropped but are counted. Each session block now carries session.project (basename only). 447 tests. Hourly rebuild over 31 MiB / 5 files: 1.2 s. Co-Authored-By: Claude Fable 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.
A directory is where a sample landed, not who it belongs to. The
same account reaches
~/.claude/statusline/when an untagged statuslinefetched and
accounts/<tag>/when a deva-tagged container did, andevery aggregating reader — the forecast, the ledger strip,
report—read only the directory it was standing in. Measured on one machine:
301 days of history at the root, 28 in the tagged dir, one uuid. Nothing
was visibly wrong, because 28 clears the 14-day floor. The failure sits
one step ahead: every new
DEVA_AUTH_TAGstarts an empty directory foran account with ten months of samples one level up, and renders "still
learning" for two weeks. The readers now take the union — root plus
every
accounts/*/— and partition byuser.uuid, which is the rulethe state-dir contract already stated and ccpace already followed. The
union is safe for this data because every quantity is envelope-based:
the same window seen from two containers takes a max, never a sum.
week.cachekeys on every store's mtime:size so a sample landinganywhere invalidates it. Cost: 1.2 s per hourly rebuild over 31 MiB.
forecast.cachesays which samples it stands on.schemaversionsthe model and cannot: statusline and ccpace agree on envelope burn, read
different stores, both pass the gate, and
days_history— the numberthat decides whether a forecast speaks at all — depended on which binary
rendered last. A
corpusstamp (uuid,files,samples,dropped_no_uuid,oldest) makes that visible in onejq. It isinformative, not a gate; ccpace v0.3.1 writes the same one.
A dropped row is counted. 94 rows in that store carry no
user.uuid(the field is younger than the log) and everyuuid-partitioned reader discarded them in silence. They stay dropped —
thirteen carry an email that would identify them, and guessing identity
on a log that already interleaves accounts is how a 9000%/day burn rate
gets manufactured — but the count is in the stamp. Loss is acceptable;
silent loss is not.
session.project. Eachusageline's session block now carriesthe basename of the project directory. The quota log knows percent, the
transcript knows tokens, and neither knew which repo the week went to;
this is the one dimension a breakdown cannot recover later. Basename
only, never the path.
The ledger is one typeface again. The baseline — a cell that ran and
cost nothing — was
ˍ(U+02CD MODIFIER LETTER LOW MACRON). Every barabove it comes from Block Elements, and terminals resolve the two through
different faces: the zero line sat at a different height and a different
advance width than the bars beside it, and the seam showed on every row
that held both. It is
▁now, the shortest bar of the same run, so thewhole ladder is one block. Burn therefore starts one rung up, at
▂;▅and everything above it keep their old thresholds, so a fully burnedwindow still reads the same height it always did.
LEDGER_BASE_GLYPHoverrides it.
A shared cache needs a version, not just a timestamp.
forecast.cachelives in
~/.claude/statusline/, which is a published shared store, andit is the one derived file more than one tool wants to write. A
co-writer that counted burn as the sum of raw positive deltas — the
accounting this script abandoned two releases ago, because a stale dip
gets refunded and then re-earned — published a profile with
149.11into Thursday and dropped
pct_per_window,scoped_*andcoston theway past.
Everything downstream did the right thing and the result was still
wrong. The walk's corrupt-profile guard saw 149%/day, judged it
impossible and went silent. The exchange rate, the price and the scoped
forecast lost their inputs and said "still learning". The budget line
fell back to linear pace. Nothing lied; the account simply had a sound
profile ten minutes earlier and no way back to it, because the rebuild
gate asked only whether the file was fresh — and it was, having been
overwritten seconds ago. Silence is a defence against a bad model. It is
not a substitute for knowing whose model you are reading.
So the cache carries
schemanow, the version of the MODEL rather thanof the file, and freshness is necessary instead of sufficient: a cache
whose schema is missing or lower is rebuilt on sight. The contract for
co-writers is written down (
docs/api/state-dir.md) and it cuts bothways — stamp the schema you actually implement, and either write the
full key set or merge into what is there. Rebuilding the fields you know
and dropping the rest is a truncating write.
headingwas neither. The budget line carried two futures with nogrammar to tell them apart:
even 6.2%/winis a RATION — spend that perwindow and the pool lands exactly on 100 — and the other number is a
PREDICTION of where your own pattern takes you. A direction is not a
destination, and the reader was left working out which of the two was
the forecast. It reads
lands ~91%now, in the budget line and inreport's week-in-progress alike.Row 2 states the landing rather than the ration. Of the three clauses
the long form carries it is the only one not already on screen: the
count is drawn on the strip beside it as
...▯(✕9)and the ration issurplus ÷ that count, but where the week ends up is nowhere else. It is
also the clause that answers what a calm week actually asks — not "how
do I ration this" but "am I going to strand it". With no projection yet
(a young week, a cold profile) there is no landing to state and the row
falls back to the ration.
🤖 Generated with Claude Code