Version 0.9.21 across CLI, desktop app, and Windows menubar; refreshed
LiteLLM pricing snapshot and fallback (catches the 2026-08-24 OpenAI
GPT-5.6 cut and DeepSeek v4 repricing mid-rollout, #1134 tracks
re-tightening); CHANGELOG stamped for 0.9.21; README release links to
0.9.21. Snapshot-pinned model tests updated to current catalog truth.
The #1126 test pinned basename presence; under #1128's fail-closed
provenance the same fixture (no trusted provider-recorded cwd) correctly
omits the field entirely — stronger privacy, same zero-paths guarantee.
The positive basename-on-the-wire case is covered by
sync-project-provenance.
A turn anchored before the range start survives range slicing whole, so
the aggregator can emit a residue day the parse never covered (#1130,
found in #1127's root-cause work). The merge guards already defuse the
overwrite; this stops the residue from reaching durable history at all.
Applied at both ranged call sites (re-derive and gap); the aggregator
and slicer semantics are untouched — whole-corpus callers still
aggregate undated parses freely.
Diagnosed via #1127. Three guards:
- The gap-fill path no longer overwrites cached days wholesale: a
complete parse wins per (date, provider) through the same guarded
merge the re-derive path uses; a partial parse only fills days and
slices the baseline lacks. Previously a parse degraded by the
refresh lock (menubar running) could replace a good day with its
undercount permanently. The gap path also stops dropping
pendingRederive.
- A day whose only content is turn-anchored residue (a midnight-
straddling turn's category counts with zero cost/calls/tokens) is
never sealed: the watermark pulls back so the next launch re-derives
it — self-healing for every broken cache in the wild, no version
bump. Bounded to the settle window and never for the oldest cached
day.
- codeburn doctor gains a read-only Cache health section listing
residue-only dates and failed/empty session-cache entries, so the
next report like #1127 is diagnosable in one command.
Tests: residue-shape pin (day-aggregator), partial-gap cannot shrink a
populated day (fails on the old code), complete-gap parity, watermark
pull-back re-derive.
rolloutFileSessionId sliced 8192 bytes then JSON.parse. Production
session_meta first lines are 22-27 KB, so both trees returned
undefined ids and the overlapping nest source was kept. Read up to
the existing 1 MiB first-line cap and parse payload.session_id from
the complete line. Pin billed-A / nest-A / nest-B at 30 KB on the
exported no-arg singleton.
foldCopilotSupplementaryTurns will not fold a supplementary twin across
a local-day boundary, so the pair can occupy separate turns. Credit
spend now groups at session scope.
filterOverlap required an explicit nest path, so export const codex =
createCodexProvider() never deduped. When the resolved dir is a launcher
nest and ~/.codex is a distinct existing tree, discover both and drop nest
sources whose session id is already billed. Unique nest and billed sessions
stay. Soften the Buzz doctor note to a presence heuristic.
Providers that bill reasoning separately from output (the exclusive set)
were under-reported on the wire by exactly their reasoning volume —
measured on a real pilot push: opencode short by 37,848 tokens, the
summed reasoningTokens of the window. Cost was already correct (priced
locally before export). Route ai.output_tokens through
billableOutputTokens, the same correction the display layer took in
PR #1115. Inclusive providers (claude, codex, copilot) are byte-identical.
Span ids derive from dedup keys that carry no token counts, so nothing
re-sends and nothing duplicates.
Several providers key a project by its sanitized absolute path
("-Users-me-Projects-app"), and sync shipped that verbatim as ai.project
on usage and attribution spans — contradicting docs/sync/README.md's
promise that file paths stay local. Confirmed on real exports.
One choke point after parseAllSessions maps project to the leaf directory
name before either wire path reads it; codeburn yield's local output is
untouched. Never-lose: usage span ids and commit attribution keys carry
no project (unchanged, no re-send); session attribution keys hash the
record, so each already-synced session re-emits once and the receiver
upserts by (org, trace) — replaced, not duplicated. Two directories with
the same leaf name now collapse to one wire project identity; that is
the documented contract, and git.repo still disambiguates.
Parser sessions key modelBreakdown by getShortModelName. Leftover
raw-id buckets (overview fixtures, cached summaries) were minting a
second $0 / 0-call short-name row that findUnpricedModels flagged as
Unpriced and failed CI on #1116.
Spent credits are finite nanoAiu / 1e9. Token-priced costUSD does not
fill the Copilot bar. Thread nanoAiu through cachedCallToApiCall with
no cache-version bump.
Share one discovery-root resolver per provider so doctor reports the exact
paths discoverSessions reads: all three Codebuff CHANNELS, Devin's
transcripts + sessions.db children, Gemini's tmp parent, Kiro's pre-filter
candidates with empty CLI/v2 skipped, and Mistral Vibe's joined sessions dir.
Later observations with costBasis calculated were emitted as measured.
Derive the estimate marker from the stored basis so LiteLLM-priced
growth stays estimated, persist it through the session cache, and
lock the path with a discover/parse/ledger fresh+warm regression.
Per-call billable output was written under raw call.model while
parser buckets use getShortModelName. Display aliases split into a
zero-output named row and a phantom raw-id row. Aggregate-only
sessions fall back to each existing bucket.
Hermes rows are lifetime totals stamped at started_at. After a day is
sealed, later growth was dropped. A sidecar ledger records last-seen
totals per (profile, sessionId) and emits weight-0 observation-time
deltas so sealed days stay put while today's growth still lands.
Closes#916
Compare, export, report JSON, overview Top-model/By-tool/day, and
menubar localModelSavings still summed raw output. Session totals
stay defined on aggregate-only and stub calls. Upgrade verifier
expects daily-cache.v28.json.
The 0.9.20 menubar never completed a fetch on a large corpus: the cache
version bump forced a full rehydration, DataClient's fixed 45s kill ended
it mid-transaction, cache-refresh-lock had no signal cleanup so the dead
holder's lock survived, and DEFAULT_WAIT_MS (30s) < DEFAULT_STALE_MS (90s)
meant no waiter could ever recover it.
A. Swift port of #1096's watchdog. CLIWatchdog holds the constants and the
pure verdict; spawns and the resident serve child set CODEBURN_PROGRESS=1;
the window restarts on any stdout/stderr byte; 45s silence, 10min cold
floor until the first payload, 15min ceiling, SIGTERM then SIGKILL after
5s. ServeConnection's fixed 60s warm cap becomes the same silence window,
re-armed by each progress frame, and a spent death budget is now a
5-minute cooldown instead of disabling the resident for the app run.
B. cache-refresh-lock arms SIGINT/SIGTERM cleanup the way session-cache does
for hydrating.lock, so a SIGTERMed holder unlinks its own lock.
C. Staleness also opens on a dead holder pid, and the waiter budget derives
from staleMs so it can never expire before the gate it waits for. A live
holder - fresh heartbeat, pid answers signal 0 - is never taken from.
parser.ts heartbeats through the lock wait, the one silent stretch left.
D. The app closes its end of a retired child's stdin (dropping the handle
left the pipe alive inside the Process), reaps every serve child
synchronously at quit, and records pid+argv so a crash-orphaned child is
reaped next launch. serve's final exit no longer runs through a
monkeypatched process.exit, and its post-drain cleanup is bounded.
Closes#1117
The upgrade-path gate asserts serve menubar-json == the spawned one-shot
payload; an always-present hydration block on serve broke that on every
platform. Absence already means complete (the rule one-shot consumers live
by), so the explicit complete:true block bought nothing the invariant did
not already guarantee. Consumers were written for the optional field and
need no change.
Each surface holds its OWN serve child: the Swift menubar (mac/), GNOME and
Windows clients spawn `codeburn serve --stdio` too, and their payload
decoders ignore an unknown key — so they would have shown a partial total
with no way to say so. The paint is now opt-in via
CODEBURN_SERVE_PROGRESSIVE, which the Electron app sets on its own serve
child because its renderer shows the indexing banner. Unset means the child
answers only with complete data, so no surface can present partial totals
unlabelled.
Add orcarouter/ as a routing wrapper like cmd/ and antigravity/, alias the
smart and fusion routes to their current upstream rows, and give each route
a branded display name. Unknown vendors nested inside the route still fail
closed.
Cost already used the helper. Report, sessions, overview and optimize
did not, so exclusive providers dropped reasoning and optimize
double-counted it for claude/codex/copilot. Daily cache v28 re-derives.
Closes#1115
A second factory whose path is a distinct alias of the primary home
is complete overlap, not a distinct nest. The primary factory itself
still discovers.
The TTY got the fast cold start in #1109; every JSON surface still waited
for the full parse because a payload has no way to say "partial". Add one.
- `hydration: { complete, indexedFiles, totalFiles }` on the menubar payload,
add-only and emitted ONLY by the resident serve child (which polls, and
therefore converges). Absence keeps meaning "complete", so every one-shot
output is byte-identical and no script can be handed partial data.
- serve: a cold child answers `status --format menubar-json` from the files
the requested period can show (same mtime floor as the TUI) and schedules
the unfloored fill behind it. Partial answers are never memoized; requests
waiting behind the fill are heartbeated by id so the client's no-output
watchdog stays armed.
- `stale` keeps its own meaning: a first paint is fresh but partial, so it
reports through `hydration` and never sets `stale`.
- desktop app and web dash render an honest "indexing history · N/M files"
notice while `complete` is false.
A second createCodexProvider(nest, {primaryDir, launcherRoots})
filters shared rollout-*.jsonl basenames. The production singleton
honors CODEX_HOME and never hides unique nest sessions because
~/.codex exists.
The unset default opened on 7 days everywhere. It now opens on today and
only falls back to 7 days when today holds no sessions yet — the decision
is made from the today-scoped slice of the parse the first paint already
runs, so the probe costs a filter, not a second pass.
Explicit selections are untouched: -p/--period, --day, --from/--to, the
TUI period keys, the app's persisted default period, and every one-shot
(--format json, report/sessions/status, the piped non-TTY render) keep
the 7-day default they had.
Fixes#1111
A cold start parsed the whole corpus before painting anything (~36s on a 21k
corpus) even though the default view only shows the last 7 days. A session log
is append-only, so its last event timestamp is <= its mtime: a file stamped
before the start of the displayed range provably holds nothing that range can
show. On a cold cache the interactive dashboard now parses only the files above
that floor, paints, and indexes the rest from the mounted UI.
- parser: `withColdFirstPaintFloor` scopes a parse to files that can hold
in-range data (mtime >= rangeStart - 48h clock-skew margin), deferring only
files with NO cache entry — nothing that has something to serve is held back,
and network sources (synthetic now-stamped fingerprint) always load.
- A run that deferred anything is a partial hydration: it does not stamp the
session cache complete and reports hydrationComplete false, so the daily
backfill cannot finalize history off it and the next launch resumes cold.
A floored run that deferred NOTHING keeps the normal stamps.
- The floor participates in the parse memo key, so a first-paint result can
never be served to (or burst-reused by) an unfloored request.
- dashboard: cold + TTY + standard dated view only. The background fill is an
ordinary unscoped background reload, so its cache writes and its refreshed
panels are exactly what a full cold parse produces. While it runs, an
"indexing history N/M files" banner states that the totals cover only what is
indexed so far.
One-shot outputs (json/csv/markdown, report, sessions, models, serve/menubar/app
payloads) are non-TTY and never enter this path.
Fixes#1107
renderDashboard issued three parseAllSessions calls (scan range, plan window,
durable headline). They often share a START and differ only in where they end —
end-of-day vs each caller's own `new Date()` — which the exact-key memo cannot
match, so a warm render paid for the discovery sweep, the cache read and the
parse two or three times over.
withSinglePassParse lets a caller declare the widest range it will ask for. A
later request that is a PURE NARROWING of it is served by slicing that parse
instead of running the pipeline again. Pure narrowing is deliberately strict:
same start, an end that is inside, and the same month shard scope. Both extra
conditions are load-bearing, because a parse's file set is a function of its
range:
- a changed file with `mtimeMs < range.start` is skipped without being parsed,
so an earlier start reads files a later start never sees;
- loadCache reads only the shards monthScopeForRange selects;
- either way the extra files seed seenKeys/seenMsgIds BEFORE the range slice,
and a seeded key suppresses the matching in-range turn in a provider parsed
later — usage the narrower parse would have counted.
Holding start and month scope equal makes both parses see an identical file set,
leaving the range slice as the only difference — the same trade burstReuse
already makes for the mirror-image case (same start, later end).
Independently, correlateCrossProviderPrSessions ran two O(n*m) scans: a
`sessions.filter` per subagent child (now a one-shot agentId index) and a full
`launches.filter` per candidate (now a sorted array plus a windowed scan). Both
preserve the exact match set; launch order was never observable because the
matches collapse into a Map keyed by the sorted ref list.
Warm, 21k-file corpus, isolated cache, median of 5 interleaved:
today 5.12s -> 3.81s (1.34x) 3 parses -> 2
month 5.46s -> 3.41s (1.60x) 2 parses -> 1
today --format json 4.11s -> 3.49s (1.18x, correlation hoist only)
vitest.config gains an explicit exclude so a .claude worktree's stale tests/
copy stops being swept into the run.
Refs #1106
Every dated command re-walks and re-stats every provider tree before it can
decide what the cache already covers. That sweep was strictly serial at every
level -- one readdir, one stat, one state.json read at a time, and one provider
after another -- so on a 21k-file / 9-provider corpus it owned most of a warm
run's wall clock with the machine idle waiting on the kernel.
Measured per warm `today` pass on that corpus before this change: 859 ms in
discoverAllSessions (kimicode 467, codex 143, claude 135, grok 77) and 727 ms in
the Claude project walk + fingerprint pass.
- fs-utils: mapWithConcurrency + FS_SCAN_CONCURRENCY, one bounded, order-
preserving helper for the whole sweep.
- providers/index: run provider discovery concurrently, concatenated in
registry order.
- claude/codex/grok/kimicode: walk each level with the level fanned out,
re-concatenated in readdir order before anything reconciles.
- parser: the Claude dir walk and both fingerprint passes (scanProjectDirs and
parseProviderSources) fan out, then reconcile serially in discovery order,
which is what changedFiles ordering and the seenMsgIds pre-seed depend on.
- parser: collectJsonlFiles reads entries with their types, so a plain file no
longer costs a wasted subagents/ probe.
- codex-cache: share one in-flight load between concurrent readers. The memo is
only populated after the read + parse resolves, so concurrent discovery had
every caller re-reading and re-parsing the same (here 59 MB) file.
No reconciliation logic changed: the same fingerprints reach the same cache
comparison in the same order. Warm `today` 5.30s -> 2.99s, cold 34.7s -> 31.1s
(medians of 5 / 2, isolated HOME + cache). today/report -p month/models/sessions
JSON, warm and cold, per-provider and combined, are byte-identical apart from
the run's own `generated` timestamp.
Closes#1104
isSessionHydrationComplete() (parser.ts) reads a module-level global that is
only safe immediately after the parse it describes, with no intervening
awaits. The stale-marker wiring landed in the prior commit read it at the
bottom of buildMenubarPayloadForRange, after several awaits -- including,
on the claude-config-scoped branch, a second parseAllSessions call for the
365-day history block that runs after the parse producing the headline
data. That let the history backfill's hydration outcome silently overwrite
the headline's.
Capture the flag into a local right after each branch's primary parse
resolves (the claude-config-scoped parse, or buildDurablePeriod) and thread
that captured value to buildMenubarPayload instead of re-reading the global
at the end. This also closes a cross-request race: once captured
synchronously, no other in-flight request's parse can flip it under us.
Regression test simulates a complete primary parse followed by an
incomplete bystander parse and asserts the payload reflects the primary
outcome, not the bystander's.
Drafted with minimax/MiniMax-M3 via local gateway.