usage_projection/usage_breakdown re-aggregated every validated final row on
every display read (~120-150ms of CPU per /api/state poll on a grown ledger)
even though the PR-140 rows memo already made the rows themselves warm. The
renders are now cached inside the memo, keyed by every input that shapes the
output (function, root/task filters, resolved limit, integrity bit,
include_roots), so a warm hit does zero full replays AND zero re-aggregation:
- _LedgerRowsMemo gains a fingerprint-keyed `renders` dict, cleared on refold
and on every non-empty advance (never TTL), plus a `generation` counter: a
render computed outside the lock is published only when the memo object and
generation are unchanged since the rows were read, so a concurrent append
between read and publish returns the stale render to that caller without
caching it (clear-then-publish race guard).
- The non-resumable crash-tail fingerprint (st_ino == -2) is never cached —
every read of a torn tail stays a full replay; _memoized_final_rows now
transports (rows, cacheable, memo, generation).
- TOTAL_BUDGET resolves BEFORE the key is built; the quarantine stat happens
after the row read (which owns quarantine) and joins the key.
- Renders are served and stored as deep copies: callers mutate nested buckets
in place, and the cached object is shared between requests.
- The cache seam stays inside the usage_projection/usage_breakdown bodies, so
the 10+ monkeypatch-by-name pins and every existing call site inherit it.
- usage_projection(include_roots=True) is a new keyword: the default keeps
the full contract; gateway/state.py passes include_roots=False because
/api/state serializes named scalars only, so by_root was built per poll for
zero readers. The slim projection still carries limit_usd and
remaining_known_usd (the two fields budget_remaining consumes); the by_root
loop itself went from O(N x roots) list scans to one-pass dict grouping.
- The memo + render-cache layer moves to ouroboros/_usage_rows_memo.py
(usage_accounting sat at 1565 of the 1600-line module gate; same extraction
precedent as _usage_rows.py). It resolves _locked/_read_records_locked back
through the usage_accounting namespace at call time, so historical
monkeypatch sites keep governing display reads; usage_ledger.py is
untouched and stays cache-ignorant.
- New tests pin: warm hit = zero replays + zero re-aggregation, append and
rotation invalidation, crash-tail never cached, include_roots=False omits
only by_root while /api/state stays field-identical, nested-bucket deep-copy
isolation, and the concurrent-append publish guard.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>