0.9.21 - 2026-08-24
Added
-
Copilot AI Credit plans.
codeburn plan set copilot-pro(1,500 credits / $15 equivalent, not the $10 sticker),copilot-pro-plus(7,000),copilot-max(20,000), andcustom --credits N --provider copilot. Spend issum(finite nanoAiu) / 1e9. Token-pricedcostUSDdoes not fill the credits bar. Claude / cursor / grok / custom-USD plans stay oncostUSD. (#943) -
codeburn models --unpriced. The dashboard warns about models that price at $0 and points atcodeburn model-alias, but the list itself was hard to get out of the TUI. This filters the plain-stdoutmodelsreport to exactly those rows, reusingfindUnpricedModelsso local, free, aliased and price-overridden models are treated the same way the warning treats them, and defaulting that mode's min-cost to 0 so $0 rows are not pre-filtered away. Thanks @kocaemre. (#969) -
optimizespots the same long block pasted at the start of many sessions. The newrecurring-contextdetector groups sessions by their opening block - normalized for whitespace and ANSI, hashed over the first 2 KB - and reports a block of at least 1.5 KB that opens 5 or more sessions, with the top three by tokens, their session counts and the project each is confined to. It is a habit, not an apply-able fix: CodeBurn will not move your own text intoCLAUDE.mdfor you, so the finding asks Claude to give the block a permanent home (aCLAUDE.mdrule, or a file read on demand) and hand back a one-line pointer to open sessions with instead. Savings count the repeats only, never the first paste, and are markedestimated: provider usage is counted per API call, where the pasted block is mixed in with the system prompt, tool schemas andCLAUDE.md, so the block is sized from its own bytes. Injected system reminders and slash-command wrappers are not pastes and are skipped, and neither is a prompt a program wrote - an SDK session or a subagent task - read from the entry's flags, which survive the parser's large-line path. The opening block comes from the session scan that already runs, so nothing extra is read from disk. -
Applied fixes get re-measured on every
optimizerun, and told plainly whether they worked. Aftercodeburn optimize --apply, every still-applied fix comes back in anApplied fixessection on subsequentcodeburn optimizeruns, carrying the verdictact reportalready computes from the same reconciliation:worked(at least 70% of its window-scaled estimate realized),partial(something, but under that),no-effect(no measured reduction, printed with the exactcodeburn act undo <id>that puts it back), ormeasuringfor anything younger than the 3-day measurement window. The numbers are measured - provider-counted usage over the post-apply window - not re-estimated.--applynow says when the re-measure will happen,--format jsongainsappliedFixes[](add-only), and the same section appears in the dashboard TUI and the desktop app. Newcodeburn optimize --auto-revertundoes the fixes that measured no reduction at all through the same code path ascodeburn act undo; it never touchespartialor still-measuring fixes, and never auto-reverts aCLAUDE.mdrule (it prints the undo command instead), matching the--yesguardrail. -
Optimize findings say what to do with them and where their number came from. Every finding now carries a class and a basis, and every surface groups by it:
Fix now (apply-able)for findingscodeburn optimize --applycan write itself,Habitsfor the behavioural ones,FYIfor informational ones whose cost may be justified. A finding only counts as apply-able when a plan can actually be built for that instance, so anmcp-deferral-offcaused by Vertex policy or a shell-profile override is grouped as a habit rather than promising a fix that does not exist. Alongside it, each finding is markedmeasured(summed from provider-counted usage) orestimated(a schema-size or recovery-fraction model), with the split reported in the header asN measured · M estimatedin place of the blanket "Estimates only." footer. Sessions whose cost the provider never reported are kept out of thecost-outlierspeer comparison, and a provider that only ever estimates gets the finding markedestimatedrather than dropped.--format jsongainsclassandbasisper finding plussummary.measuredSavingsUSD(existing fields unchanged), and the newdocs/optimize.mdcovers what is scanned, exactly what--applymay write, and how to read the health grade. -
CODEBURN_CACHE_SCOPE=allforces a full session-cache read. A ranged query reads only the month shards that can contribute a turn to it, which is a real behaviour change on a warm cache; this is the escape hatch for the case where a number looks wrong and you want to know whether the scoped read is why. Set it and every load ignores its scope and reads every shard, one-shot runs and the residentcodeburn servealike. It is a read policy, not an input to any cache fingerprint: setting or unsetting it re-parses nothing and invalidates nothing. -
The interactive dashboard opens on Today, and falls back to 7 days when today is empty. Only the CLI TUI ever defaulted to 7 days; the desktop app and the web dashboard already opened on Today and needed the fallback half alone. The TUI takes the auto path only when
-pcame from the default rather than from you, with no--dayand no--from/--to, and only on a TTY - so piped runs,--format json/csv/markdown,report,sessionsandmodelsstay on week, byte for byte. The probe costs nothing: it is the today slice of the parse the dashboard already runs, and it counts sessions rather than projects, because a project can survive the slice on a subagent anchor with no sessions behind it. The period strip shows what actually opened, keys 1-6 and every explicit selection win over the fallback, and the app's persisteddefaultPeriodpreference always wins. On a cold cache an empty today costs one extra repaint - the first paint's file floor is Today's, so the 7-day view is re-painted once on the wider floor from the cache pass one just wrote, about 2.3s on a 16.7k-file corpus and only when the cache is empty and the day unused. (#1113)
Added (OrcaRouter)
- OrcaRouter sessions now price and label like the model they route to. OrcaRouter is a gateway that exposes route ids (
orcarouter/auto,orcarouter/fusion, …) plus plain upstream ids (deepseek/deepseek-v4-pro), and reports the upstream id that actually ran in the completion response'smodelfield. Theorcarouter/prefix is now a routing wrapper likecmd//antigravity/, so a routed spelling prices at its upstream LiteLLM row instead of $0;orcarouter/autostays unpriced (the smart route lands on a Qwen/Llama flash model - a Sonnet alias would overprice it) and the fusion routes alias to their currentopenai/gpt-oss-120btarget (live completionmodelfield, 2026-08); and an unknown vendor nested inside the route still fails closed. Daily cache v29 so warm days re-derive the new prices.
Added (Windows)
codeburn menubarinstalls and launches the tray app on Windows. The same command that installs the macOS menubar now does the Windows one, through the same pinned-release path: it resolveswindows-v<cliVersion>, falls back to a scan of the newestwindows-v*release carrying both assets when that tag has none, downloads the.msiwith the same retry and backoff, and verifies its sha256 before anything executes it - a mismatch aborts without ever handing the file to the installer. It then runsmsiexecout of%SystemRoot%\System32(never a bare name, so nothing dropped next to the CLI can impersonate it) with/i <msi> /passive /norestart, treats exit 3010 as installed-pending-restart and 1602 as a cancelled install rather than failures, and launches the exe named by the product's Uninstall registry key. An already-installed matching version skips the download and just launches;--forcereinstalls.- A menubar app for Windows.
windows/is a Tauri 2 tray app - Rust binary, React popover - that puts today's spend in the notification area and mirrors the macOS menubar screen for screen: agent tabs, period switcher, Trend, Forecast, Pulse, Stats and Plan insights, activity and model breakdowns, optimize findings, CSV/JSON export, launch at login, currency, and theme. Windows has no menubar title, so the number lives in a second tray icon rendered from the system font at the panel's native icon size (Settings can turn it off; the tooltip always carries it). It reads everything through the CLI like the macOS and GNOME clients do, and gates on codeburn 0.9.9 or newer - the first release acceptingstatus --format menubar-json --no-optimize- showing a setup screen with the install command until it finds one. Refresh follows popover visibility the way the macOS app does: 60 s with optimize findings while open, 2 minutes for today's total while closed, and immediately on open when what you are looking at has gone stale. The Claude quota view never spends Claude's single-use refresh token; on a 401 it re-reads Claude Code's own credential file for a token it has already rotated, matching the macOS client. Ships as an unsigned.msifrom thewindows-v*tag, whichcodeburn menubarnow installs for you. The same crate still builds and runs a tray on Linux, but that stays experimental and unreleased -gnome/is the supported Linux surface.
Added (CLI)
- Copilot input/cache tokens are read per request from
~/.copilot/session-store.db. Previously, codeburn relied onsession.shutdownrollups from the Copilot CLI and GitHub Copilot desktop app. Those rollups are written only after a clean shutdown, stamp all usage on the shutdown day, and reset their counters at in-session compaction - so a crash could lose an entire session's input/cache usage, and even cleanly-closed long sessions were silently truncated. On one machine with long history, reading the per-request rows recovered about 35% of actual Copilot spend. Covered sessions now use per-request tokens with their real timestamps, counted exactly once against existing rollups and never added as extra calls or turns. Pre-store CLI sessions continue using the unchanged rollup path, and a locked or unreadable store defers only its own re-read instead of prematurely sealing daily history. Copilot reasoning tokens are also no longer double-billed: they are a subset of output already priced through the per-turn calls. This triggers a one-time re-parse, with the daily cache bumped to v26 to re-derive finalized days. Because that reconciliation keeps changing while a session is live,codeburn sync pushholds a Copilot session back until it has been quiet for 24 hours and then sends it once, final - the sent-ledger is append-once, so a value sent mid-reconciliation could never be corrected at the receiver (#988). Sync also pins each Copilot session to whichever of the two shapes it was first synced in - thesession.shutdownrollup, or per-request rows plus a residual - because a usage span cannot be retracted and sending the same tokens in the other shape would double them at the receiver permanently. That holds in both directions: a session synced before this release keeps its rollup and never sends rows, and a session synced as rows never sends the rollup that starts serving again once the 90-day age-out prunes them.--dry-runreports the frozen count, andcodeburn sync reset --confirmre-pushes everything under the new breakdown for anyone who can clear the receiver too. (#946) - DeepSeek Harness (
dsh) is now a supported provider. Reads DeepSeek's open-source agent harness from~/.dsh/sessions(DSH_HOMErelocates the root), both the default zstd logs and the uncompressedsession.jsonlvariant. A.zstdlog is a concatenation of independent zstd frames, one per write batch, so it is decoded frame by frame behind a structural frame scan and a torn trailing frame from a crashed writer is ignored rather than failing the file (needs Node 22.15+ forzlibzstd; below that dsh is skipped with a notice instead of counted as $0). One call per(turn, step), with the step's finalassistant/messageusage superseding the streamedassistant/chunksample of the same call rather than adding to it, the model taken from the message that served the step, and reasoning tokens billed at the output rate. DSH records tokens but no cost, so calls are priced from the shared tables. The events a forked session replays from its parent are skipped, since codeburn already counts the parent's own log. The session format is pinned at version 0 upstream with no compatibility implied, so a log stamped with any other version is skipped with a notice instead of read under today's assumptions.
Added (Desktop & Menubar)
- Live quota for Gemini, Copilot and Antigravity in the desktop app. The app's quota module knew claude and codex only; it now carries three more providers, all read from local credentials the tools already wrote - no cookies, no logins, no credential writes anywhere. Gemini goes through Code Assist (
loadCodeAssist/retrieveUserQuota) using the Gemini CLI's own on-disk OAuth material, refreshing a token only when the CLI's documented env overrides are set and keeping the refreshed token in memory; a retired consumer tier degrades to a terminal failure carrying migration guidance rather than a wrong number. Copilot reads the editor-plugin token from~/.config/github-copilot/hosts.json→apps.jsonand callscopilot_internal/user, re-reading once on a 401 - an internal API that may drift, so every failure degrades to an honest connection state. Antigravity probes the local language server over loopback only (RetrieveUserQuotaSummary, falling back toGetUserStatus), discovering it throughps/lsofwith regex-validated pids and relaxing TLS strictly for 127.0.0.1 self-signed certificates. Settings gains a per-provider toggle, defaulting on for detected providers; a disabled provider's fetcher never runs, so it reads no credential and makes no request. Provider display names come from a map rather than hardcoded pairs, sign-in guidance is honest per provider, and error sanitization now redacts Googleya29.and GitHubgh*_token shapes. Kimi live quota already exists in the macOS menubar and was audited rather than duplicated. (#1114) - The macOS menubar reaches quota parity with the desktop app, and gets a System Settings-style Settings window. The three providers above are ported into the menubar read-only from the app's Electron services, on the same load-state machine Kimi, Claude and Codex already use: Gemini through Code Assist and
~/.gemini/oauth_creds.json(in-memory refresh only, handling Google's CLI-OAuth tier retirement), Copilot throughcopilot_internal/userand the editor plugin token in~/.config/github-copilot(preferring the github.com host), and Antigravity throughps+lsofdiscovery and Connect-RPC probes over loopback HTTPS with the self-signed-certificate trust override scoped strictly to 127.0.0.1. Settings is rebuilt as a System Settings-style window modeled on CodexBar (MIT, steipete/CodexBar): a fixed 260pt sidebar with native search, SF Symbol gradient chips, official provider marks (LobeHub icon set) with connected dots and an N-on counter, and a grouped About pane whose Updates row is wired to the existing update checker. The window is resizable and fullscreen-capable, titles itself after the visible pane, and recenters when it would be restored off-screen. Alongside it: an animated flame-gradient wordmark in the popover header at parity with the site, respecting Reduce Motion; the website's binary0101flame as the app icon, the full-colour flame in the About hero and a solid silhouette in the About sidebar row; one tagline everywhere; and em-dashes removed from UI copy.package-app.shnow ships the SwiftPM resource bundle inside the.app- without itBundle.moduleaborts at the first icon render, which the release workflow would have shipped. (#1133)
Changed
-
Report, sessions, overview, compare, export, report JSON and menubar
localModelSavingsnow usebillableOutputTokensfor displayed output. Cost already did. Exclusive providers (Grok and the rest) were under-reporting output by exactly their reasoning tokens; optimize added reasoning on top of output for the inclusive set{claude, codex, copilot}and double-counted it. Displayed aggregates now bill per call while the provider is known, joined onto an existingmodelBreakdownkey (parser short name first, then the raw id) so a leftover raw-id bucket cannot mint a $0Opus 4.8Unpriced orphan. Daily cache v28 re-derives finalized days soreportmatches the live parse. Thanks @saulcanina. (#1115) -
The desktop app's cold start paints as soon as the selected period is readable, instead of after the whole history. The resident
codeburn servechild now does for its JSON clients what the TUI got in #1107: on a cold cache it answers the firststatus --format menubar-jsonfrom the files whose mtime allows them to hold data the requested period can show (the same floor and 48h clock-skew margin), then indexes the rest behind the answer. The payload says so in-band through a new optionalhydration: { complete, indexedFiles, totalFiles }block, and the desktop app and web dashboard renderindexing history · N/M files · totals below cover what is indexed so faruntil it reportscomplete: true. On a 21k-file corpus the app's first payload lands in 7.3s instead of 31.3s, fully indexed at 36.2s. Only the resident serve process ever emitshydration, and only it may answer partially - its clients poll, so they converge. Every one-shot output is unchanged and always a full parse:--format json/csv/markdown,report,sessions,models, MCP, the Swift menubar and GNOME clients, and the desktop app's own spawn fallback, none of which have a second poll to converge with. Absence ofhydrationtherefore always means complete, which is also what an older CLI's payload means. It is notstale(#1100) and never sets it: a first paint is fresh but partial, where a stale payload is complete but old. Nothing is stamped complete until the fill has actually parsed everything, so a process killed mid-fill comes back cold and finishes the job. (#1110) -
The discovery sweep issues its metadata syscalls concurrently. Every dated command re-walks and re-stats every provider tree before it can decide what is already cached, and that sweep was strictly serial: one
readdir, onestat, onestate.jsonread at a time, per provider, one provider after another. On a 21k-file / 9-provider corpus it owned most of a warm run's wall clock while the machine sat idle waiting on the kernel. Provider discovery now runs across providers at once, and the four walks that dominate it (claude, codex, kimicode, grok) plus the Claude project-dir walk and both fingerprint passes fan out through a shared bounded-concurrency helper. Order is unchanged everywhere - each level is re-concatenated in registry/readdirorder before anything reconciles against the cache - so what is discovered, in what sequence, is byte-identical to the serial walk. Two smaller cuts ride along: the Claude walk reads directory entries with their types so a plain file no longer costs a wastedsubagents/probe, and the Codex result cache (a single file that can reach hundreds of MB) now shares one in-flight load between concurrent readers instead of letting each one re-read and re-parse it. Warmcodeburn todayon that corpus: 5.30s to 2.99s; a cold parse 34.7s to 31.1s. (#1104) -
A kill mid-way through a non-Claude provider phase no longer restarts that whole phase.
scanProjectDirs(Claude) has long taken a throttledsaveProgresscallback so a killed cold parse resumes from a warm cache;parseProviderSources(every other provider - codex, cursor, gemini, and the rest) did not, and only persisted at the whole-provider boundary. On a large single-provider corpus (a multi-GB codex history is the common case) an app-timeout SIGKILL, crash, or force-quit during that phase discarded everything parsed since the last provider finished, forcing the entire phase to re-parse from zero on the next run.parseProviderSourcesnow takes the same callback, invoked once per source right after that source's cache entry lands (mirroringscanProjectDirs' placement, outside the per-file try/catch), on the same file-count/wall-clock throttle. A file only ever gets a fingerprint once it has fully parsed, so a mid-file kill can never leave a half-parsed file's entry looking complete on resume. -
Routed model ids price as the model they wrap, and an unknown vendor prefix no longer prices by blind stripping. Token-plan and gateway spellings of the same model (
omniroute:,cp/,cline-pass/,cline-free/,cmd/,antigravity/,orcarouter/) are peeled and the remaining id is priced, so a Cline Pass or OmniRoute session shows a~estimate instead of $0. In exchange,provider/modelis no longer treated as authority on its own: the leading segment is stripped only when it is a namespace the bundled pricing catalog itself uses (anthropic/,openai/,google/,x-ai/,qwen/,moonshotai/,nousresearch/,xiaomi/,z-ai/, and every other vendor prefix in the LiteLLM snapshot), one of the routing wrappers above, or one of the client-side spellingskimi/,mimo/,zhipu/,litellm_proxy/andopenai_like/. Anything else stays unpriced and is reported as unpriced rather than inheriting the price of a same-named cloud row, and local-runner prefixes (ollama/,lmstudio/,hosted_vllm/,local/) are excluded on purpose so an unlisted local tag can never invent cloud spend. A user price override for the bare id wins over the catalog row a routed spelling would otherwise hit. -
SQLite providers now survive read-only database parents. A read-only SQLite open is not read-only on disk: on a WAL database SQLite must create
<db>-shmand<db>-walin the database's own directory, so a source on read-only media, under restrictive permissions, or inside a Flatpak/snap confinement failed withattempt to write a readonly database(orunable to open database filewhen a-walwas present without its-shm), and both discovery sites swallowed it - the provider read as "not installed" rather than as an error. That covers cursor, cursor-agent, opencode, goose, warp, kilo-code, zerostack and the copilot agent-traces database. The direct open stays the fast path and is byte-identical when it succeeds. When it fails for want of sidecars: a database with no WAL frames to lose is opened in place withimmutable=1, which costs nothing and cannot go stale; a database with a non-empty-walis copied with its-walinto the CodeBurn cache and read there, so its un-checkpointed rows are never silently dropped. The copy costs one database's worth of disk and is taken once per change - it is keyed by the main-plus-WAL fingerprint, published under a fingerprint-stamped name so a refresh never overwrites a copy another process is reading, and superseded copies are evicted once a day has passed without a read, keeping at most one predecessor. If the cache itself cannot be written, the database is skipped with a notice naming it and the reason rather than in silence. The original provider database is never opened writable or modified. -
Grok Build now reads the CLI's own completed-turn usage instead of estimating it. Usage comes from the
turn_completed.usagerecords Grok CLI already writes intoupdates.jsonl(inputTokens,outputTokens,cachedReadTokens,cacheCreationTokens,reasoningTokens), deduplicated byprompt_idand emitted as one session-level call from the top-level totals. The previous parser reconstructed an estimate from the running_meta.totalTokenscontext counter, so existing Grok totals will change materially on upgrade - on one real 568-session corpus cache-read went from 150K to 96.3M tokens, total tokens from 20.0M to 113.9M, and cost from $36.98 to $56.79. Cache read and cache creation are subsets of input and reasoning is a subset of output, so reasoning is clamped to the record's reported output and split back out to match this repo's exclusive-reasoning contract.modelUsageonly selects a priced attribution id; multi-model rate attribution stays out of scope, so one session is priced at one model's rate.costUsdTicksis ignored because its scale is undocumented. Sessions with no usable record - older CLI versions - keep the old context-curve heuristic and stay flagged estimated. In a session that has at least oneturn_completedrecord, turns without one are not counted at all (their tokens are dropped rather than estimated), and the session is marked estimated instead of claiming full provider coverage. Cached Grok sessions re-parse once. The daily cache re-derives once on first run after upgrade: this is a global re-derivation of every day and every provider, since the daily cache has no per-provider invalidation, but it reads the warm session cache rather than re-parsing transcripts, so it costs seconds (~3s on the corpus above), and the superseded cache file is retained on disk as the baseline for days no source can still re-derive. (#998) -
Codex rollouts parse across worker threads too, and the workload gate now takes bytes or files. Codex is the bigger half of a real cold parse - a 4 GB rollout corpus against 1.8 GB of Claude sessions - and it was still decoding one file at a time. A whole-file rollout decode now runs on the same pool, against an empty dedup set, and comes back with the calls, the dedup keys it claimed, and the codex-cache entry it would have written; the parent installs all three in the serial loop's order, so
codex-results.jsonand every payload come out byte-identical to a serial run. Cross-file state stays where it was: a forked rollout replaying its parent's token_count history collides on the parent's keys and is re-parsed in-process, and no worker ever touches the cache module's per-directory state. Files the Codex cache can serve exactly or resume into from a byte offset never reach a worker - they read a few KB and the resume state belongs to the parent. The workload gate is now pending BYTES alone (200 MB), not file count: 250 pending files holding under a megabyte between them spawned threads that made the run ~5% slower, while a few hundred huge rollouts were being turned away. The count takesmax(pendingFiles / 50, pendingBytes / 200 MB), and the per-thread memory budget is derived per parse asclamp(256 MB, 2 × average pending file + 128 MB, 1 GB)rather than a flat 256 MB - a 260 MB rollout peaks near 430 MB in its worker and scales linearly with the pool, so the flat figure over-subscribed exactly the workload this adds. The decision is per provider, and at most one pool is alive at a time. -
A large cold Claude parse now runs across worker threads. Reading, decoding and line-parsing a session JSONL is per-file work that never touches anything shared, so it moves onto
worker_threads; each worker ships its parsed turns back as a JSON string and the parent installs them in the exact order the serial loop would. Everything with cross-file state - the streaming-message dedup, canonical project paths, spawn links, PR correlation, progress saves - stays on the main thread, and a file whose message ids were already claimed by an earlier file (or whose worker failed) is simply re-parsed in-process, so the session cache and every payload are identical either way. On a 6 GB corpus a coldstatusdrops from 27.5s to 14.8s with peak RSS up 2.27 GB → 2.52 GB. Threads only engage for a genuinely large cold parse: never with under 200 MB behind the pending whole-file re-parses, 2 or fewer cores, or under 4 GB of available memory - so warm and incremental runs are untouched and spawn nothing. Otherwise the count ismin(cores - 1, min(0.25 × available, 2 GB) / 256 MB, pendingFiles / 50), where available isprocess.availableMemory()(cgroup-aware in containers) rather than free memory, which on macOS reports free pages and would switch the feature on and off between runs.CODEBURN_PARSE_WORKERS=0forces the serial parse andCODEBURN_PARSE_WORKERS=Nforces N (capped at the core count), both bypassing every gate;CODEBURN_VERBOSE=1prints the resolved count and why. -
A warm launch rewrites only the month that changed, and a ranged query reads only the months it can report on. Per-provider shards still meant one appended session republished that provider's entire history - 95 MB for Claude on a 6 GB corpus. Each provider's shard is now split again by the UTC month of the cached session's FIRST turn, a bucket that never moves as a session grows, so an append rewrites one month. Every shard records the newest month it holds, which lets
--period today/weekskip the shards that cannot contribute a turn to the range; the skipped months stay on disk untouched across the save, and providers whose cache is the only surviving record (durable) or whose parse fingerprint moved are always read in full. Remaining shards are read concurrently. Existing v8 and v7 caches are re-laid-out losslessly on first load and the old layout removed once the new one is published: nothing re-parses. -
A warm launch rewrites only the provider that changed. The session cache was a single blob, so any provider appending a few KB republished the whole thing - 147 MB of stringify + fsync on a 6 GB corpus, ~18% of a warm run. It is now a version-suffixed directory holding one shard per provider plus a small envelope, written per provider and published by a single envelope rename. An existing v7 cache is re-laid-out losslessly on first load and the old file removed once the new layout is on disk: nothing re-parses. One unreadable shard now costs that provider a re-parse instead of discarding every provider's history, and partial saves during a cold parse are triggered every 2000 files rather than every 5 seconds, so a slow cold parse no longer rewrites the growing cache on a wall clock.
-
An appended Codex rollout parses only its tail. Rollout files are append-only and the active ones run to hundreds of MB, but the Codex result cache keyed on mtime + size alone, so any growth re-read the file from byte 0. Each entry now records a restart point at the last task boundary - byte offset plus the state the single-pass decode carries across it - and a grown file with the same inode resumes there, producing output identical to a full re-parse. An entry without a usable restart point simply re-parses in full once and gains one.
-
A date-ranged report classifies only the turns it keeps. Every cached turn went through the turn classifier - category, retries, edit detection, and a full reconstruction of its API calls - before the date slice discarded most of them, so a week view paid to classify all of history to keep a few percent of it. The keep/drop decision is now taken on the raw cached turn and only the survivors are classified, still from their complete call list, with the branch and pull-request carries still walking the full ordered turn list. Output is byte-identical.
-
One rule for every cache file.
CODEBURN_CACHE_DIRwhen set, otherwise~/.cache/codeburn.XDG_CACHE_HOMEis no longer consulted; the sync ledger, the only file that ever honored it, is merged into the canonical location on first read and the legacy copy is retired, so nothing is re-uploaded after the move. (#972) -
A cold interactive launch paints the dated view first and indexes the rest behind it. An empty cache parsed all ~21k files before painting a dashboard that shows 7 days. On a cold interactive TTY launch with a dated default view, the first paint now parses only the files that can hold in-range data -
fp.mtimeMs >= rangeStart - 48h, the clock-skew margin - which is safe because a session log's last event is never later than its mtime, so an older file provably cannot move the dated view; a file that already has a cache entry is never deferred, and network sources always load. The deferred files are parsed by the existing background reload without blocking keys, writing the per-file cache exactly as a full cold parse would and refreshing the panels when it lands, with anindexing history · N/M files · totals below cover what is indexed so farbanner until it converges. Nothing is marked seen without being parsed: a first paint that deferred anything cannot stamp the cache complete, so a run killed mid-fill re-enters cold and converges, and the floored parse's memo key carries a:paint<floor>suffix so it can never be served to an unfloored request. The path is gated onisTTYwith no--dayand no--from/--to, so one-shot JSON/CSV/markdown,report/sessions/modelsand every serve, menubar and app payload never enter it and never return partial data. On a real 21k-file corpus with an empty cache, time to first paint drops from 36.3s to 9.9s, and full indexing finishes at 47.5s instead of 36.1s - about eleven extra background seconds, paid while you are already looking at data. Two deliberate limits: a period switch during the fill queues behind it rather than slicing partially, and the plan-usage bar can read low until the fill lands, which the banner covers. (#1109) -
The dashboard parses once per launch instead of up to three times. Rendering the dashboard issued three
parseAllSessionscalls - the scan, the plan usages and the durable overview - whose ranges differed only in their ends, about a second apart, so the exact-key memo never hit and each one paid a full parse. The dashboard now declares its widest range up front, and any request inside it that is a pure narrowing - same start, an end no later, the same month-shard scope - is served by slicing that single parse.monthgoes from 2 parses to 1 andtodayfrom 3 to 2; the plan window starts on the 1st while the scan starts at midnight, and two different starts cannot be merged under the rule that makes this provably lossless, because a wider parse reads files a narrower one never sees and its dedup seeding can drop an in-range turn the narrower parse keeps. Alongside it the cross-provider PR correlation stops re-filtering the session list per child and the launch list per candidate, using a one-shot agent index and a windowed scan over a sorted array instead; the match sets are identical. On a frozen corpus, warmtodayin the TUI goes 5.12s to 3.81s andmonth5.46s to 3.41s, with every checked warm and cold output byte-identical. (#1108)
Changed (Linux packaging)
- The snap asks for the log directories it reads, not each tool's whole home. The first Snap Store submission declared a
personal-filesread of every AI tool's root -$HOME/.claude,$HOME/.codex,$HOME/.cursorand the rest - and that interface is recursive, so it granted read of every credential file those roots hold. Each entry now names the subdirectory the provider actually opens (.claude/projects,.codex/sessions,.cline/data,.vibe/logs/session,.dsh/sessions,.kiro/sessions,.quickwork/{profiles.json,sessions,metrics},.config/Claude/local-agent-mode-sessions,.config/Open Design/{runs,data/runs,namespaces}), two are single files (.forge/.forge.db,.zcode/cli/db/db.sqlite), and the editor entries name only the extension folders holding transcripts instead of the editor's whole configuration. Five providers that were missing entirely and would have shown no data are declared - opencode, crush, goose, kilo, kimi-code - and four roots stay roots only because the file the provider opens sits directly in them (.config/github-copilot,.local/share/{opencode,crush,kilo}). One credential file is now requested openly rather than implicitly:.claude/.credentials.json, read-only, for the live plan gauge. Codex's equivalent would need write access to the Codex CLI's ownauth.jsonto rotate the token, so neither it nor a Codex root is declared and the Codex live gauge is disabled under$SNAP; Codex usage and cost are unaffected, they come from the session rollouts. Two consequences inside the snap:.lingtaiis dropped, because its per-agent log directory needs a wildcard the interface has no form for, andoptimize,context-budgetandactno longer see the user-scope~/.claude/settings.json,agents/,skills/andcommands/- project-scope copies still work through thehomeplug. Nothing outside the snap changes.
Fixed (Desktop & Menubar)
-
The menubar stops killing its own cold cache rebuild. On a large corpus after the 0.9.20 cache-version bump, the menubar never completed a single fetch: every child was killed at exactly 45 seconds, and each kill left the
session-refresh.lockbehind for the next one to wait out. Four things were wrong and all four are fixed. The 45-second cap was a TOTAL-runtime kill, so it is now the same no-output watchdog the desktop app got in #1096 - spawns and the residentcodeburn servealike setCODEBURN_PROGRESS=1, the window restarts on every byte of stdout or stderr (the CLI heartbeats every 10 seconds while parsing, and now also while WAITING on the refresh lock, which was the silent stretch that mattered), and only a genuinely mute child is killed, with the same 10-minute cold floor until the first payload lands and the same 15-minute absolute ceiling behind it. Kills are SIGTERM first and SIGKILL only after a 5-second grace, and the refresh lock now arms the same signal cleanup the hydration lock has, so a killed holder unlinks its own lock instead of leaving one. A lock that was abandoned anyway is recovered immediately rather than after 90 seconds: a waiter takes over a lock whose recorded holder pid is gone, or whose heartbeat has frozen, and the waiter's own budget is now derived from the stale window so it can never again expire before the gate it is waiting for opens - a live holder, whose heartbeat keeps the mtime fresh and whose pid answers, is never taken from. The resident child's fixed 60-second warm request cap becomes a silence window that each progress frame restarts, and a spent restart budget is a five-minute cooldown rather than leaving the resident dead for the rest of the app run. Serve orphans get three new backstops: the app closes its end of a retired child's stdin (dropping the handle was not enough - the pipe stayed alive inside the Process), reaps every child it started synchronously at quit before the async shutdown can be skipped, and records the child's pid and command line so a serve orphaned by a crash is reaped on the next launch. On the CLI side a serve child's final exit no longer runs through theprocess.exitan in-flight request has monkeypatched, and its post-drain cleanup is bounded like the drain. (#1117) -
A long panel query is no longer killed for being slow. The desktop app capped every read at 45 seconds of TOTAL runtime, so on a slow machine
optimize,yield,models,sessions,spend,audit,act reportandplanwere SIGKILLed mid-parse and the panel painted a red "timed out" that a 60-second poll then reproduced forever. That cap is now a no-output watchdog: the window restarts on every byte the child writes, so only a genuinely silent child times out, with a 15-minute absolute ceiling still catching a livelocked one. Silence now means stopped rather than slow, because the parse itself heartbeats: every read spawn setsCODEBURN_PROGRESS=1, and under it a running parse emits a keepalive line every 10 seconds - the stretch that mattered was a cold parse's inter-provider cache save, measured at 31.6 seconds of total silence on a large corpus, which the old scan-progress stream did not cover at all. A served request heartbeats for its whole duration, parse and the aggregation and serialization after it alike; a one-shot spawn heartbeats through the parse, and the roughly 8-second tail that follows it stays well inside the window on its own. Residentcodeburn serverequests reset their window on each frame of their own response. Alongside it: the cold-cache floor now covers EVERY read while the first hydration is still running, not just the overview, so a section that starts polling the moment the app is ready is not killed waiting behind that parse; a read that times out while the hydration is still going keeps the indexing splash instead of painting an error panel, bounded so that an install which can never hydrate still surfaces a real error once the cold window has elapsed rather than sitting behind the splash forever; and a read killed for timing out, or a resident child replaced by a settings mutation, is sent SIGTERM first and SIGKILL only after a 5-second grace, so the child can unlink its own cache refresh lock rather than leave it for the next parse's stale-pid takeover (quit stays a hard kill, its flush budget being shorter than the grace). A hydration that genuinely needs more than 15 minutes is still ended by the absolute ceiling, but it no longer starts from nothing next time: the partial cache saved along the way means successive polls converge instead of each repeating the whole scan. The app also records the resident child's pid and full command line, and reaps a serve orphaned by a previous crash on the next launch - on Windows too, where there is nopsand no stdin-close recourse after a crash - signalling only when that pid still runs that exact command. Separately,codeburn servedrains an in-flight request before exiting on stdin close, bounded at 45 seconds so a wedged request cannot turn the child into the orphan the drain prevents. -
The menubar's copies of your Claude and Codex credentials move out of Application Support and into the login Keychain. Connecting a provider used to leave the copied OAuth material in
~/Library/Application Support/CodeBurn/*-credentials.v1.json, written world-readable (0644) because macOS ignores.completeFileProtectionoutside iOS. The copy now lives in a CodeBurn-owned login-Keychain item, and the first read after upgrading migrates the old file: it is reopened withO_NOFOLLOW, refused if it is a symlink or not owned by you, repaired to 0600 before a single secret byte is read, written to the Keychain, read back and compared, and only then unlinked - a failed or unverified write leaves the (now 0600) file in place so a retry can still find it, and the next read retries the cleanup. Where both a Keychain item and an old file exist, the one that expires later wins before anything is removed, so an item left behind by a much older build cannot displace a fresher token. Claude's entry no longer stores a refresh token at all - the CLI owns that grant and the menubar never spends it - and any refresh token in a historical blob is dropped on read. Disconnect only reports success once the material is actually gone; if the delete fails it says so and leaves the provider connected so you can retry. Keychain reads are non-interactive and are skipped outright while the login Keychain is locked, so a background quota refresh can never raise an unlock panel. (#1037) -
First launch no longer asks to control System Events. The macOS menubar registered its login item by driving System Events over AppleScript, which made macOS put up an Automation consent dialog the first time the app ran. It now registers itself through
SMAppService.mainApp, an in-process call that needs no Automation grant; there is no AppleScript fallback, so a failure logs and leaves the login item unset rather than bringing the prompt back. The samecodeburn.loginItemRegisteredguard still limits this to the first launch, so a login item you removed by hand stays removed. (#1026) -
The resident
codeburn servechild. The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972) -
The desktop app's Pull requests tab says why it is empty. A PR row exists only where a transcript actually contains a pull-request URL, and the default period is Today, so a week of work through providers that never write those URLs left the tab blank with copy that named neither the period nor the reason - indistinguishable from a broken extractor. The empty note now names the selected period, and when the All period does hold rows it says how many and points at the period control. Attribution itself is untouched: no link is inferred, no PR is invented, and the default period is unchanged. (#1098)
Fixed
-
Hermes sessions that keep running after a day is sealed no longer lose later token and cost growth. A sidecar ledger under the CodeBurn cache records last-seen lifetime totals per
(profile, sessionId)and emits observation-time deltas (weight 0) so a sealed day stays put while today's growth still lands. An all-zero reset is visible to the cursor after discovery filtering, so a later 40 is+40rather than a silent shrink. Explicit$0is treated as recorded. Ledger publication failures are retryable and hold the daily watermark. (#916) -
codeburn doctornow probes the five remaining fixed-location providers.codebuff,devin,gemini,kiro, andmistral-vibeimplementprobeRoots()through the same resolvers discovery uses, so a silent zero is distinguishable from a missing install. Codebuff reports all three manicode channels unless a factory orCODEBUFF_DATA_DIRpins one; Devin reportstranscriptsplussessions.db, not the parent; Gemini reports only~/.gemini/tmp; Kiro reports pre-filter candidates (empty CLI/v2 skipped, empty agent/workspace fall back); Mistral Vibe reports the joined sessions dir. Missing defaults still appear. Thanks @therickfactr. (#899) -
Subscription SKUs are classified from real product ids, and a false-positive built-in can be opted out.
codex-auto-reviewconsumes ordinary Codex usage (openai/codex#32224) and is priced as GPT-5.5 on #1056, so treating it as $0 hid real spend - it left the flat-rate list. Warp's product id isauto, not the syntheticwarp.kimi-for-coding-highspeed(the SKU #968 was filed around) is now honestly $0.big-picklewas dropped: it appears under OpenCode, not as a cited ClinePass codename.codeburn model-flat-rate --removenow opts out of a built-in, so a wrong classifier entry can warn again without waiting for a release. The daily-cache config hash now always includes the flat-rate section (even when empty), so the first run after upgrade re-derives every stored day once from the warm session cache. (#968, #1050) -
Codex MCP and skill usage is attributed from every shape Codex records a shell command in.
mcp-cli call <server> <tool>was only recognized when the command arrived asfunction_callarguments (#656). Codex has two other shapes for the same exec: its custom-tool transport records the shell tool as acustom_tool_callwhose payload is aninputprogram rather thanarguments, and its item model repeats a finished command asevent_msg/item_completedcarrying aCommandExecutionitem with an argvcommand. Both reached the Bash counter and neither reached the matcher, so a CLI-wrapped MCP call stayed absent from the MCP breakdown exactly as before the fix. All three shapes now feed one classification pipeline. The same pipeline learns skills: Codex has no skill tool, so loading one is a shell read of the skill'sSKILL.md, and those reads landed entirely under Bash with the Skills dimension empty. A read counts as a skill load only when the command segment starts with a file-reading binary (cat/bat/sed/head/tail/less/more) and the path it reads ends in<name>/SKILL.md; the skill is<name>, the same keypiderives for a native skill read (#588) and the same vocabulary the Claude parser records from theSkilltool. Agrep/rg/lsthat merely mentions aSKILL.mdis a search near the file, not a skill load, and stays plain Bash. This is attribution only - no call, token or cost figure moves, and a command carried by both a response item and an item-model item is attributed once. On a 1,397-rollout corpus: Skills went from empty to 7 skills over 35 turns (55 attributions), Bash was unchanged at 42,170, and cost, calls, tokens, sessions, daily, models and projects came back identical. Cached Codex sessions re-parse once (CODEX_CACHE_VERSION13 → 14 and the codex parse version both move; without them the fix is invisible on a warm cache). Thanks @chr-evensen. (#478) -
gpt-5.6-codexandgpt-5.6-codex-maxnow have their own pricing rows. Neither id is in LiteLLM yet, and both were missing from the bundled snapshot - flagged during #1075 verification on a real corpus (285 sessions, 5,446 calls).getModelCostsalready resolved both through thegpt-5.6prefix fallback, so live pricing was already correct once a session priced fresh; every prior Codex-suffixed id LiteLLM does carry bills identically to its bare-model sibling of the same generation (gpt-5-codex==gpt-5,gpt-5.1-codex==gpt-5.1-codex-max==gpt-5.1,gpt-5.2-codex==gpt-5.2,gpt-5.3-codex==gpt-5.3), which is the evidence both new rows mirror rather than inventing a rate. The gap that does not self-heal is the daily cache: it has no per-provider invalidation, so a day finalized while either id had no billable rate keeps that $0 forever. RaisingMIN_SUPPORTED_VERSION(v23 -> v24) forces the one-time re-derivation, a lossless no-op for days already correct. (#1077) -
Mixed-version installs no longer thrash the Codex / Cursor / Antigravity result caches. Daily and session caches already own a version-suffixed file so an old desktop binary and a newer CLI cannot clobber each other. The three per-provider result caches still used one unsuffixed filename with an internal version field, so a v10 and a v11 binary rewrote the same
codex-results.json(and the Cursor / Antigravity siblings) on every run and each re-parsed its whole corpus. They now write*-results.v<n>.jsonthe same way the daily cache does. The unsuffixed file is left for older binaries; a matching-version copy is adopted once and never overwritten. (#1082) -
Codex spend no longer counts reasoning tokens twice, and cache writes are priced only where OpenAI actually charges for them. OpenAI bills reasoning tokens as part of
output_tokens, not on top of it - on a 1,396-rollout corpus all 134,316 events carrying a total satisfyinput + output == total- but CodeBurn addedreasoning_output_tokensto output when pricing a Codex call and again in the models, audit and per-model displays. Every Codex number was therefore too high: on that corpus cost by $166.03 (3.5%) and displayed Output tokens by 34.6% ($4,713.12 -> $4,547.09; 22.6M -> 16.8M output tokens). The rawreasoningTokensfigure is unchanged and still reported on its own; only the double-count is gone. Both places that price a Codex call - the parser and the cache-rehydration re-price - now go through one sharedbillableOutputTokenshelper, so a cold run and a warm run can never disagree. Separately, Codex'scache_write_input_tokens(new in codex PR #33454) was never read and cache-creation tokens were hardcoded to 0; they are now carved out of the uncached-input bucket and clamped so they can never exceed it. That carve-out happens only on models whose pricing source publishes a real cache-write rate - gpt-5.6 and its terra/sol/luna variants charge 1.25x input for a cache write, everything before it charges nothing extra - because CodeBurn fabricates a 1.25x rate when a source omits one, and charging that would have invented a surcharge on gpt-5.5, gpt-5.4, gpt-5.3-codex and gpt-5. On models without an explicit rate the tokens stay in the plain input bucket and the price is unchanged to the cent. The field is new enough that today's impact is $0 on that corpus. Codex sessions re-parse once and the daily cache re-derives once off the warm session cache (a global re-derivation of every day and every provider, since it has no per-provider invalidation); no other provider's numbers move. Days whose Codex transcripts have since aged out are held by the same never-lose guard #1040 relies on: a re-derivation that finds fewer calls than the settled baseline keeps the older, pre-fix (double-counted) total rather than truncating it, so those days do not pick up the repricing until their sources are re-derived with equal or greater evidence. Long-context pricing tiers from the same report are tracked separately in #1076 and the missinggpt-5.6-codexsnapshot rows in #1077. Thanks @chr-evensen. (#1075) -
Codex Tok/s no longer counts reasoning tokens twice or credits harness startup as model time. Two distortions in the same metric, found and fixed together because they share the same cache-invalidation and test surface. (1) #1075 fixed the reasoning-token double-count for cost, but
activeGeneratedTokens/taskGeneratedTokensin the Codex parser andgeneratedTokensin thecodex-tpslive-throughput reader still summedoutputTokens + reasoningTokens; both now go through the samebillableOutputTokens('codex', …)helper #1075 introduced, so the numerator can never drift from the billed one. (2) Codex firestask_startedbefore it assembles the request, so the gap up to the first request-context event (turn_context,world_state,event_msg/user_message, or aresponse_item/message) was pure CLI/harness startup counted as active model time - the active window now starts at that first event instead, which matters most for one-shotcodex execsessions that pay the gap on every task. The duplicated tool-interval clip/merge/cap logic inproviders/codex.tsandcodex-throughput.tsis now one function (mergeToolIntervals, exported fromcodex-throughput.ts), which also closes a live trap wheretask_complete's duration only parsed a plain number and silently dropped the{secs,nanos}/string formsmcp_tool_call_endalready tolerated. (A third suspected distortion - fork-replay dedup dropping a token_count event's tokens from the numerator without shrinking the window to match - was investigated and retracted: the earlierprevCumulativeTotalguard already discards a repeated running total before dedup is ever reached, so a real Codex writer never produces a partial drop; the dedup site now carries a comment recording this so the trip isn't repeated.) Display only, no cost or token-count impact - verified byte-identical on the same real corpus. Combined effect on a real Codex corpus (original bug -> all fixes): GPT-5.5 37.8 -> 28.2 tok/s (-25.5%), Codex Auto Review 23.3 -> 20.0 (-14.2%), GPT-5.6 Sol 43.2 -> 33.9 (-21.4%), GPT-5.6 Luna 53.5 -> 49.0 (-8.5%), GPT-5.4 68.0 -> 43.6 (-35.9%), GPT-5.4 Mini 54.9 -> 55.6 (+1.1%, the harness-startup correction outweighing the reasoning-count correction for this model on this corpus).activeGeneratedTokens/activeDurationMs/toolWaitMsare stored verbatim in both the Codex result cache and the session cache rather than re-derived on read, so none of this self-heals: Codex sessions re-parse once (one cache-version bump covers both fixes, since they touch the same fields). The dashboard's per-model column stays labelledTok/s- a wider label had zero room at the standard three-column layout, verified by breaking a real width-budget test - but the legend beneath it now reads "Effective Tok/s: generated tokens ÷ time the agent spent waiting on the model, tool execution excluded. Includes prefill, request assembly and reasoning. Not comparable to vendor decode-speed figures." (#1079, #1088) -
Codex calls attributed from session metadata no longer carry a stale model. The Buffer fast path scanned
session_metafor the first"model"string anywhere in the payload, so a nestedbase_instructions.provenance.modelwas read as if it werepayload.model- and since the model is last-writer-wins state, that wrong value was credited to every call before the rollout's firstturn_contextand to every call after any mid-filesession_meta(29 of 1380 rollouts on one real corpus carry a latesession_meta, and 57 record usage before anyturn_context). Direct payload fields are now read depth-aware, which is what the non-fastJSON.parsepath always did. Codex sessions re-parse once (~9s on a 4 GB rollout corpus) and the daily cache re-derives once off the warm session cache, a global re-derivation of every day and every provider since it has no per-provider invalidation; it moves per-model attribution, and clears any rollup an earlier parse change had left stale. Days whose transcripts have partly aged out are held by the never-lose guard: on a real 110-day cache no day lost value and none disappeared - 100 days came back identical and 9 grok days rose by $19.80 in total. Thanks @timdp. (#1040) -
Codex
session_metacwd / session id / originator follow the same depth-1 window asmodel. #1040 fixed nestedprovenance.model; the compact Buffer path still took the firstcwd,session_id,originator,name,forked_from_idormodel_provideranywhere in the payload, so adynamic_tools[].name(or any same-named nested key) could steal the top-level field. Those strings now use the existing payload-depth-1 scan. Function-callnameon other event types is unchanged. Codex sessions re-parse once. (#1045) -
Plan rows for sticker-price presets read as a budget instead of live provider quota. There is no Grok quota endpoint, so a SuperGrok row was parsed API-equivalent spend divided by the plan's sticker price on a monthly reset - but the TUI labelled that math "plan" and "reset", which next to a client showing xAI's real weekly window read as CodeBurn being wrong. The bars and the arithmetic are unchanged; the words are not. Both the dashboard and the desktop app now say the number is an API-equivalent monthly budget and not a live provider window, in the same wording on both surfaces, and for every preset rather than as a SuperGrok special case. The window is anniversary-based (
plan.resetDay, settable withcodeburn plan set --reset-day), so it is called a budget reset rather than a calendar one. The row was also shortened to fit 80 columns: at that width the percentage and the projected month were being truncated away, including on custom plans, whose label carries the provider. -
MiMo sessions price from the LiteLLM Xiaomi rows, and MiMo v2 Flash no longer crashes the display path. Hermes / Xiaomi token-plan sessions store the bare id (
mimo-v2.5-pro,mimo-v2.5) while LiteLLM namespaces its row (xiaomi/…), so those models reported $0. They now alias to the existing snapshot rows - no invented rate, andkimi-k3still has none - which means a session Hermes left costless is priced from the shared tables and carries the estimated marker, exactly asmimo-v2-flashalready did. The same change fixes a pre-existing crash that this alias did not introduce: the shippedmimo-v2-flash -> xiaomi/mimo-v2-flashalias already cycled through display-name resolution - strip the namespace, alias it back, take the leaf, repeat - sogetShortModelNameblew the stack on any real MiMo v2 Flash session and took every surface that names a model down with it, themodelstable included. Display-name resolution is now cycle-safe, and themimo-v2-flashandmimo-v2.5rows are named rather than shown as raw slugs. -
A date-ranged run no longer republishes the month shards it never read. A scoped load leaves an out-of-range month on disk, so the files it holds have no visible cache entry and the reconcile re-parses them - re-deriving the entry the shard already stores. That re-parse marked the unloaded month dirty, and the save merged and republished it under a fresh nonce name on every single run, byte-identical content and all, so a repeated
codeburn status --format jsonchurned old months (on a real corpus: claude/2026-03, cursor/2026-02 and warp/2026-03 renamed every run) and left the retired shards for the sweeper. A merge into an unloaded month that neither adds, changes nor removes an entry now keeps the published shard, so unchanged months keep their names and their bytes. (#1032) -
modelsandauditno longer show two identicalGrok 4.5rows.grok-4.5-build- the Grok Build harness's variant id - fell into thegrok-4.5display entry by prefix, and since rows bucket by model id, not display name, the two came out as visually identical rows with different numbers. The variant now shows asGrok 4.5 (build). Display only: no id is rewritten and no cost moves. (#1029) -
An upgrade no longer loses history for days whose transcripts have only PARTLY aged out. The never-lose contract carried a cached (day, provider) slice forward only when the re-derivation found NOTHING for it, but transcripts expire per FILE rather than per day: on a day whose sources are mostly gone, a handful of turns from surviving later files still bucket onto it, so the fresh slice came back non-empty but truncated and REPLACED the full cached one. On a real cache upgrading from the last shipped daily-cache version, 2026-07-16 fell from $1,685.17 / 12,530 calls to $385.44 / 560 calls, and 13 days lost $2,765.75, 19,209 calls and 520 sessions in total. A fresh slice now replaces a settled baseline slice only when it carries at least as many CALLS - the same or more evidence; fewer calls means the source set demonstrably lost data, and the baseline is kept whole. The comparison is on calls alone: cost and tokens are re-priced accounting on the same evidence, which is exactly what a legitimate re-derivation changes (the Grok accounting fix keeps its per-day calls and is unaffected), and session counts drift down by a few on days whose sources are entirely intact. Days inside a 7-day settle window stay authoritative - their session files are still on disk, so a shrink there is a real change rather than expiry. The trade-off is deliberate and matches the direction this cache has always chosen: a future fix that legitimately REDUCES calls on a settled day keeps the older, higher value until that day is re-derived at an equal or greater call count. The timezone-change re-derive gets the exact form of the same rule - what the fresh parse can no longer explain under the old bucketing is added on top of the fresh slice instead of being dropped - and the cross-file adoption union is unchanged, where the newer schema still wins per (day, provider).
-
The session chart legend now leads with a visible session disambiguator and title instead of the project path. Every series in a monorepo shared the same project prefix, so the only thing separating them was a truncated hex fragment - and per-application cost attribution is the main reason to open that chart.
SessionSummary.titleis already parsed and already rendered in the Context tab; the legend now puts the short session id first, prefers the title, and falls back to the previous project-based label when a session never produced one. Titles come from transcripts, so they are stripped of ANSI and control characters and capped before they reach either the legend or the tooltip. (#997) -
Context-bloat detection now counts reasoning tokens as generated output.
detectContextBloatdivided context bytotalOutputTokensalone, but reasoning is stored beside output rather than inside it, so for every reasoning-bearing provider the detector saw a fraction of the tokens actually generated and invented findings - a session whose real ratio was 20:1, under the 25:1 threshold, was reported as 133:1 and "high impact". It now uses the sameoutput + reasoningsum the reports use, which corrects grok, codex, kiro, hermes, qwen and cursor-agent alike. -
The unpriced-models warning in the dashboard is now readable at every terminal width. It lived in a fixed-width panel with an inline model list and a fix command, so it clipped mid-name at 80 columns and clipped earlier at 200, where the three-column layout narrows each panel - neither the affected models nor a runnable command survived. The panel line is now a pointer,
! N unpriced: codeburn models --unpriced(shortened to! N: codeburn models --unpricedbelow 45 columns of panel), and the model list moves to that command's plain output, which is full width, copyable, and lists every model rather than the first two. The command's hint no longer reads as an unconditional instruction to alias: a subscription or flat-rate model is correctly $0, and mapping it onto another model's per-token rate would invent spend that was never billed. Provider-supplied model IDs are now stripped of terminal control characters in every human-readable report rather than only on the unpriced path, and--unpricedshows raw IDs instead of friendly names becausemodel-aliaskeys on the raw ID. (#969) -
codeburn models --unpriced --top Nreturned nothing for a--top Nsmaller than the number of priced models.--topis applied insideaggregateModels, before the unpriced filter, on rows sorted cost-first - and unpriced rows are $0 on both, so they sorted last and the slice removed exactly the rows the flag exists to show. A user with unpriced models was told they had none. The slice now runs after the filter - and after ranking, because unpriced rows tie at $0 on both keys, so slicing them in aggregate order kept whichever models happened to appear earliest in the transcript rather than the largest. The order now matches the one the unpriced-models warning shows. (#969) -
Old durable sources remain visible while they still exist. The 90-day session-cache age-out now applies only after a durable source disappears from discovery, so an unchanged older Copilot source keeps reporting usage and reuses its persisted fingerprint instead of being reparsed and immediately discarded. (#987) On long-lived machines this makes previously dropped history reappear, so lifetime totals can jump once after upgrading.
-
optimizeno longer treats subagent transcripts as your sessions. Claude Code writes each subagent's transcript to its ownsubagents/agent-*.jsonlfile withisSidechain: trueon every entry, and optimize counted each one as a user-started session. That inflated the session count in the header and fed the session-level detectors a population that fails their tests by construction: a sidechain is handed a large context and returns a short answer (context-heavy), and it never commits or opens a PR because its parent does (low-worth). Excluded from sidechains now: the header session count, thelow-worth-sessions,context-bloat,cost-outliersandcapability-reliabilitydetectors, the coaching notes, the file-churn table, the median time-to-first-edit, the worst one-shot category, and the model-default recommendation - plusduplicate-reads, because a subagent starts on a fresh context and re-reading what its parent read is a necessary read, not a repeat. Everything else keeps the full population:build-folder-readsandread-edit-ratiostill count calls made inside a sidechain, since readingnode_modulesor editing without reading is the same waste whoever does it and theCLAUDE.mdrule they suggest binds subagents too, and so do the MCP, cache-bloat, ghost-command and configuration-overhead findings. Classification is sticky across the whole file, so calls that appear before the first marked entry are reclassified too, andisSidechainnow survives the compact parser's 32 KB large-line path and warm-cache range rebuilds. Nothing is deleted from spend: sidechain tokens, calls and cost stay in every total and instatus, and the optimize result cache keys on sidechain identity so a run cannot be served a pre-fix result. Absent markers still read as user-started, so no cache re-parse is needed. (#974) -
optimizeno longer offersclaude mcp removefor claude.ai connectors, and its MCP schema-cost estimate is per session. Aclaude_ai_*namespace that no readable local MCP config claims is a claude.ai connector, managed through/mcpor claude.ai Settings rather than as a local MCP server (a local server that carries the prefix keeps its removal command and gains a same-name connector note); low-coverage findings now render them as a manual follow-up and build--applyplans only for exact local server names found in readable MCP config, so mixed findings remove only the local subset and the "apply-able" subtotal counts only that subset. The same change replaces the old global schema-cost cap with per-session, per-server proportional attribution - a more accurate model that lowersmcp-low-coverageestimates for everyone, connectors or not (on a large corpus roughly by half). (#975, #991) -
Bash command splitting was quadratic on long whitespace-heavy commands. The separator regex retried its leading
\s*from every offset; matching the separator alone and widening over whitespace by hand makes cold parse ~24% and warm ~40% faster on large corpora, output unchanged. -
Cold parse no longer retains full message bodies through cached previews.
flatSliceskipped its Buffer round-trip for strings already within the bound, but provider adapters pre-truncate user-message previews with.slice(0, 500)before the cache-site call - those pre-sliced views are still V8 SlicedStrings pinning their large parent, so the retention that OOM'd cold parses of large histories survived. The round-trip now always runs. -
Kiro sessions carry the real
projectPath(CLI meta.cwd, v2workspacePaths[0], workspace sessions'workspaceDirectory), so git-repo attribution can resolve them; previously they were attribution-blind. Bumps the kiro parse version, so the first run after upgrade re-parses kiro history once, and kiro sessions in linked git worktrees now group under the main repo. -
A ranged parse can no longer write a day outside the range it was asked for. A turn that straddles local midnight keeps its original anchor through range slicing, so the day aggregator could emit a residue day just outside the parsed window - cost, calls and tokens all zero, category counts only - and both ranged call sites, the re-derive and the gap fill, wrote it into durable history. The merge guards below defuse the overwrite, but the residue should never reach the cache at all, so both ranged sites now filter to the days actually in range. The aggregator and the slicer are untouched, so whole-corpus callers are unaffected. (#1131)
-
The daily cache's gap-fill path can no longer overwrite a good day with a degraded parse, and a day left holding only midnight residue heals itself.
ensureCacheHydrated's gap fill wrote days into durable history through a blind overwrite with no completeness gate, so a parse degraded by the refresh lock - the menubar refreshing concurrently is the ordinary way this happens - could permanently replace a settled day with its undercount; the re-derive path had completeness and partial-survival guards, the gap path had neither. A gap merge is now completeness-gated: a complete parse wins per (date, provider) through the same guarded merge the re-derive uses, while a partial parse can only fill gaps and never shrinks the baseline, and apendingRederiveis no longer silently dropped. A day holding only a straddling turn's category counts, with cost, calls and tokens at zero, now pulls the watermark back so the next launch re-derives it - bounded to the settle window and never the oldest cached day.codeburn doctorreports cache health read-only, listing residue-only dates and failed or empty session-cache entries, so the next report of this shape is one command rather than archaeology. There is no cache-version bump: every affected cache heals on its next launch. (#1129) -
codeburn syncno longer puts a filesystem path or a raw output count on the wire. Every synced span carriedai.projectas the slugified absolute path (-Users-you-Projects-thing), contradicting bothdocs/sync/README.md("paths stay local") and the compatibility contract's "safe project basename"; the wire name is now the leaf directory name, derived at the single choke point after the parse, andcodeburn yield's local output is untouched. Two directories with the same leaf collapse to one wire identity - that is what the contract's project identity means - andgit.repodisambiguates them. Separately,ai.output_tokenswas the raw output field, so exclusive-reasoning providers were under-reported on the wire by exactly their reasoning volume (measured on one corpus: an opencode session short by 37,848 tokens, its summed reasoning); it now goes through the samebillableOutputTokenshelper the display layer took, leaving the inclusive providers byte-identical. Cost is unaffected, being priced before export. Neither field is part of a span id, so nothing is re-sent and nothing duplicates at the receiver: usage span ids and commit attribution keys carry no project, and session attribution re-emits once per session under an upsert keyed on (org, trace). (#1126) -
Outbound project metadata now requires proven provenance instead of failing open. The basename mapper above still accepted whatever the cache held, so a Claude path slug, a generated session title, a home root, a relative path, a provider's own storage directory, a credential-shaped basename or a provider-owned container path could all become an outbound project label. The privacy decision moves to the OTLP boundary and demands explicit provenance: a usage span's
ai.projectcomes only from a provider-recorded absolute cwd, attribution resolves repositories only from that same trusted cwd and derives itsai.projectfrom the normalizedgit.repo, and a legacy cached cwd with no provenance fails closed. A relative cwd can never be resolved against the directory that happens to be runningsync; Hermes prompt-derived paths, including the legacy WindowsCurrent working directory:messages, stay local grouping labels and never become outbound provenance - a deliberate privacy correction; Goose propagates its ownsessions.working_dirfield while/sessions/<container>paths are rejected centrally; and provider, model, tool and span-name identifiers are sanitized at serialization, with real routed identifiers still supported.ai.projectremains optional, so a receiver must accept an unattributed span; where a trace carries both labels and they differ, the attribution repository basename is authoritative and the provisional usage basename must not be counted as a second project. (#1128) -
Attribution spans no longer carry the session id in cleartext.
codeburn sync push --attributioncopiedai.session_idonto session and commit spans even thoughderiveTraceId(sessionId)already keys both usage and attribution spans, and usage spans never carried the field; it is dropped from both, and the docs now match the wire. This is hygiene rather than a leak - the id existed on the wire only as hash input before attribution shipped - but a receiver that keyed session rows onai.session_idmust key ontraceId, the same id usage spans already carry. The join to usage is unchanged, and the local sent-ledger keys still use the session id. (#1073) -
codeburn doctornames Buzz as a launcher instead of showing it as a second Codex tree. Buzz sits on top of Codex, so a Buzz usage parser or a walk of~/.buzz/.codexas a second Codex home would count the same billed seat twice - but doctor only knew providers, so a nest holding no usage store of its own read as "nothing found". Buzz is now listed as a launcher billed through Codex, with no session count, as is Grok Bot when~/.grokis the real store, and Codex discovery returns nothing for a home nested under~/.buzzwhile a distinct primary Codex home exists. A sole Codex home that does live under.buzzis still counted. Spend stays on the Codex row; no Buzz parser and no rates were added. (#1099) -
codeburn modelsresolves raw ids the wayreportdoes, and stops splitting one SKU across two rows. The report bucketed by raw id after asking each provider for a label, so a provider whose local table missed fell back to the raw id (gpt-5.6-sol,accounts/fireworks/models/kimi-k2p6) - and a later merge keyed on the display string, which summed genuinely distinct SKUs whose labels happen to collide into a single row. Provider-first resolution stays and local labels still win (the Cursor estimated suffix, provider overrides); a provider that merely echoes the raw id now falls through to the global short-name table, whichcursor-agentandvercel-gatewayalso consult after their own transform. Rows merge on provider plus alias-resolved canonical id, so a path-form id merges with its bare slug and display names go back to being cosmetic: two SKUs that only share a label stay two rows. The first-seen raw id is kept, somodels --format jsonis not rewritten to a lexically smallest spelling, and a Codex credit row mixing rated and unrated buckets partial-sums the rated ones and reportscreditsIncompleterather than a confident total.auditstill buckets by raw id, because it recomputes rates per id. (#1053) -
A resumed Copilot CLI session's stampless shutdown legs no longer collapse onto one day. Each leg of a resumed session appends its own
session.shutdownrollup, and a journal whose legs carrysessionStartTimebut no per-leg timestamp fell back to that start time - identical across every leg - so all of them billed onto one date and, with that stamp in the dedup key, onto one row. The timestamp fallback is now the event's own stamp, then the last event seen, and only then the session start, so a stampless leg lands on its last stamped event; the occurrence keys (copilot:<sid>:shutdown:<model>:<n>) are unchanged, so nothing already synced is re-sent under a new name. (#1054) -
codeburn optimize --provider <x>no longer tells you to ask Claude. Provider filtering already skipped the Claude-only detectors, but the cross-provider ones -retry-heavy-capabilities,low-worth-sessions,context-heavy-sessions,cost-outliers,mcp-project-scope- baked "Ask Claude" andCLAUDE.mdinto their fix labels, and the CLI and TUI destination headers said Claude whatever the run was scoped to, with the JSON report carrying the same copy through. One remediation table now drives all of them: an unscoped run,--provider alland--provider claudekeep the shipped Claude andCLAUDE.mdcopy;codexsays Codex andAGENTS.md, the only other instruction file CodeBurn already names; every other provider gets its display name and a generic "project instructions" rather than an invented filename. Apply plans still writeCLAUDE.mdonly for Claude-only findings, and the paste-destination values are unchanged. (#1049) -
Resizing the terminal or zooming its font produces one settled reflow instead of a burst of intermediate widths. CodeBurn re-rendered on every
resizeevent fromprocess.stdoutwith the livecolumns/rows, and debouncing that listener alone cannot stop Ink, which runs its own resize handler. During a burst the dashboard now sees a frozen stdout facade and a private resize emitter, and on settle it re-renders once and lets Ink see a single resize - no write interceptor, which was the earlier shape's trap: swallowing frames still advanced Ink's last-output state, so a settle producing the same string wrote nothing and lost a mid-burst update forever. One transient old-width frame during the burst is accepted. Static and non-interactive output and the periodic refresh are unchanged. (#1038)
Contributors
Thanks to everyone who shipped 0.9.21: @AndrewDongminYoo, @Enclavet, @MiloMMIN, @avs-io, @erzz, @kelchm, @kocaemre, @ozymandiashh, @timdp, and @iamtoruk.
New Contributors
- @AndrewDongminYoo made their first contribution in #1003
- @kocaemre made their first contribution in #985
- @MiloMMIN made their first contribution in #1001
- @timdp made their first contribution in #1040
- @erzz made their first contribution in #1125
Full Changelog: v0.9.20...v0.9.21