Skip to content

feat(store): a directory is where a sample landed, not who it belongs to - #32

Merged
lroolle merged 2 commits into
mainfrom
fix/one-model-one-landing
Aug 27, 2026
Merged

feat(store): a directory is where a sample landed, not who it belongs to#32
lroolle merged 2 commits into
mainfrom
fix/one-model-one-landing

Conversation

@lroolle

@lroolle lroolle commented Aug 27, 2026

Copy link
Copy Markdown
Member

A directory is where a sample landed, not who it belongs to. The
same account reaches ~/.claude/statusline/ when an untagged statusline
fetched and accounts/<tag>/ when a deva-tagged container did, and
every 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_TAG starts an empty directory for
an 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 by user.uuid, which is the rule
the 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.cache keys on every store's mtime:size so a sample landing
anywhere invalidates it. Cost: 1.2 s per hourly rebuild over 31 MiB.

forecast.cache says which samples it stands on. schema versions
the model and cannot: statusline and ccpace agree on envelope burn, read
different stores, both pass the gate, and days_history — the number
that decides whether a forecast speaks at all — depended on which binary
rendered last. A corpus stamp (uuid, files, samples,
dropped_no_uuid, oldest) makes that visible in one jq. It is
informative, 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 every
uuid-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. Each usage line's session block now carries
the 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 bar
above 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 the
whole ladder is one block. Burn therefore starts one rung up, at ;
and everything above it keep their old thresholds, so a fully burned
window still reads the same height it always did. LEDGER_BASE_GLYPH
overrides it.

A shared cache needs a version, not just a timestamp. forecast.cache
lives in ~/.claude/statusline/, which is a published shared store, and
it 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.11
into Thursday and dropped pct_per_window, scoped_* and cost on the
way 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 schema now, 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 (docs/api/state-dir.md) and it cuts both
ways — 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.

heading was neither. 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. 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 in
report'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 is
surplus ÷ 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

lroolle and others added 2 commits August 24, 2026 10:16
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>
@lroolle
lroolle merged commit a818b2d into main Aug 27, 2026
1 check passed
@lroolle
lroolle deleted the fix/one-model-one-landing branch August 27, 2026 08:00
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