0.9.22 - 2026-08-25
Added
codeburn sessions --by-work-unitgroups the sessions report into provider-recorded work units. The new deterministic resolver (src/work-units.ts) folds each session carrying the #1140lineagefield under the orchestration root it names, giving one row per unit with the root's title and project, cost/calls summed over root plus children, and a child count, with the children listed indented beneath and standalone sessions rendered exactly as before. A work unit's id isderiveTraceId(rootSessionId), the exact trace-id derivation sync already uses, so a unit's identity matches the root's existing trace identity. Evidence stays strictly provider-recorded: a session without lineage is its own standalone unit with roleunknown, a child that names an in-range parent folds even when the parent recorded nothing (one-sided evidence), and anything ambiguous fails closed per the boundary spec's MAIN-02 - a parent id outside the parsed window leaves the child ungrouped, cycles and self-references are broken and markedunknown, and duplicate or cross-provider id collisions never fold.--format jsonwith the flag emits an add-only{ sessions, workUnits }envelope:sessionsis today's row array unchanged,workUnitsis the resolver's full partition (standalone sessions included, so counts and totals reconcile). Default output without the flag is byte-identical, no wire/sync or daily-cache behavior changes, and a pinning test asserts the grouped footer total equals the ungrouped total. Implements slice CB-2 of the CodeBurn main/Teams boundary spec (CODEBURN-MAIN-TEAMS-BOUNDARY-v0.1). (#1140)codeburn status --format menubar-jsoncan serve from a disk-persisted snapshot. A background poll whose corpus has not changed since the last computation is answered from a per-query snapshot keyed on a corpus fingerprint (ordered source topology, per-provider env fingerprints, five config hashes, currency, pricing generation, cache and package versions - any mismatch is a miss), eliminating the per-poll re-parse on idle machines. A snapshot is only ever written for a complete, non-degraded parse: the save gate requires the payload's own captured markers (stale !== true, nohydrationblock), so a poll that went read-only under a held refresh lock can never pin its under-reported totals to disk - a regression holds the refresh lock and asserts no snapshot file is written. Serve-side and one-shot outputs are unchanged; the optimization currently engages for--no-optimizepolls (the menubar background refresh). Thanks @dgabehar, hardening @ozymandiashh. (#999)- The menubar status snapshot now re-validates persisted records on load, mirroring the save gate. A snapshot record whose payload carries degradation markers (
stale: trueor ahydrationblock) is treated as a miss and recomputed instead of served, so a record that should never have been written (older build, hand-edited file) can never pin under-reported totals; the save gate from #999 is unchanged. Extending the snapshot to the default optimize path was evaluated on this branch and reverted in post-build review:scanAndDetect's findings depend on mutable state the corpus fingerprint never observes (~/.claudeand project-levelsettings.json,CLAUDE.md, defined skills, agents, commands, MCP config), and re-deriving them on a snapshot hit requires the parsed corpus, which is the full parse the snapshot exists to avoid on exactly the cold processes it serves. The optimize path therefore still never reads or writes the disk snapshot, and one-shot and serve-child outputs are byte-identical to before for both optimize values. (#1135) - The parser captures provider-recorded session lineage instead of discarding it. Claude agent transcripts carry a provider-written parent session reference (and the parent side records the spawned agent id when compaction has not dropped it); Kimi Code subagent sessions live inside their parent session's own directory. Both now populate an optional
lineagefield on the parsed session model -parentSessionId, a root/child role, andevidence: 'provider-recorded'- persisted through the session cache, with claude and kimicode parse versions bumped for the one-time re-derive. Strictly provider-recorded evidence only: no inference from time adjacency or shared projects, and a session with no evidence carries no lineage field at all. Purely additive metadata: a pinning test asserts every report total is byte-identical with the field present or absent. Groundwork for spend attribution across delegated-agent families (a measured 5.1% of sessions carry direct lineage but 76% of 90-day spend). (#1140)
Fixed (macOS)
- CodeBurnMenubar now launches mise-installed CodeBurn correctly from Spotlight. Spotlight gives GUI apps a minimal PATH, so a persisted
mise use -g npm:codeburnlauncher could be found but then fail with exit 127 when its shell wrapper tried to resolvenode. The menubar child environment now includes mise's stable shim directory (including a customMISE_DATA_DIRwhen available), matching the existing Volta/asdf/nvm handling without invoking a shell. (#1124)
Fixed (TUI)
- The interactive dashboard stays responsive during the cold-start background fill, and
Ctrl+Cexits the TUI at any moment after paint. The progressive cold start introduced by #1109 paints the dashboard from recent files, then runs a background fill that parses the deferred history and writes the per-file session cache. On a real 21k-file corpus that fill's cache save publishes one JSON.stringify + fsync per provider shard in a tight loop, and the whole save held the event loop long enough that Ink's stdin reader never ran - aqorCtrl+Cpressed in the post-paint window was swallowed, not buffered, and the user saw a frozen terminal (#1139, #1141).saveCachenow yields to the event loop between every shard write, so the Ink input handler fires between publishes. The yield sits BETWEEN shards, never inside one, so each shard's temp+rename atomicity and the lost-fence cleanup that owns the canonical envelope are preserved; nothing marked seen without being parsed, the kill-safe resume invariant from #1109 is intact, and the floored-parse memo key suffix keeps warm reads byte-identical. The fill still parses every deferred file, so money totals are unchanged. The dashboard'suseInputnow exits onCtrl+Cthrough the same path asq, so a raw 0x03 from the terminal lands as soon as the loop breathes. Pinning tests:tests/save-cache-yields.test.ts(asetImmediateprobe must interleave withsaveCache's shard writes) andtests/dashboard-exit.test.ts(a raw\x03exits the dashboard the same wayqdoes). The #1109 cold-start one-shots - report, sessions, models, json/csv/markdown, the desktop and menubar payloads - are unchanged. - The first
qduring the cold-start background fill renders feedback instead of a silent drain, and a secondqexits the same wayCtrl+Cdoes. With the #1142 input fix in place,qpressed during the post-paint fill landed instantly but the exit path drained the fill first - a deterministic ~16.5s silent wait on a 21k-file corpus (#1143). The fill's indexing signal already flows to the dashboard (the #1109 banner reuses it), so theuseInputhandler now arms a confirmation on the firstqwhile a fill is active and rendersFinishing background index so the next launch starts warm - press q or Ctrl+C again to quit nowin the footer area, with the secondqtaking the abrupt path.Ctrl+Cis unchanged: it always exits through the abrupt path, and because #1109 made that path kill-safe (nothing marked seen without being parsed, resume converges) the two are equivalent. When the fill completes, the confirmation clears itself so a laterqcannot be trapped by a stale flag.qwith no fill active exits immediately as before, with no status-line flicker. Pinning tests added totests/dashboard-exit.test.ts.