codeburn/CHANGELOG.md
Resham Joshi b2be8eba56
feat: mark estimated dollars the same way in every view (#1657)
* Mark estimated costs with the same ~ in every view

The ~ marker and legend that report already used now appear wherever a
dollar figure is mostly a guess: the models and sessions CLI output, the
desktop Models, Overview, Sessions and Settings provider rows, the macOS
menu bar model rows and provider tabs, and the Windows tray.

One rule decides the marker everywhere: the estimated portion is at least
1% of the figure. report, overview and the MCP tables switch to it from
"any estimated cent", so a metered Codex row holding a few text-estimated
turns stays unmarked.

JSON is additive: estimatedCostUSD and isEstimated on models rows,
estimatedCost and isEstimated on sessions rows, estimatedCostUSD on
menubar providerDetails rows. Totals are unchanged.

* Skip the ~ marker on figures that print as zero

A sub-cent row that is fully estimated printed as "~$0.00": nothing visible
to qualify. The shared helper now also requires the amount, as the surface
prints it in the active currency, to show a non-zero digit. CLI, desktop,
macOS and Windows helpers all apply it.
2026-10-06 22:27:13 +02:00

340 KiB
Raw Blame History

Changelog

Unreleased

Added

  • Estimated dollars carry the same ~ in every view. models (table, markdown, JSON), sessions (table, browser, JSON), the desktop Models, Overview, Sessions and Settings provider rows, the macOS menu bar (model rows and provider tabs) and the Windows tray now mark a figure with ~ when at least 1% of it is priced from estimates, the marker and legend report already used. report, overview and the MCP tables move to the same 1% rule, so a metered Codex row holding a few text-estimated turns is no longer marked. JSON gains estimatedCostUSD and isEstimated on each models row, estimatedCost and isEstimated on each sessions row, and estimatedCostUSD on each providerDetails row of status --format menubar-json. Totals are unchanged.
  • Overview marks Cursor dollars that are local estimates, not the bill. When a period still holds local Cursor or Cursor Agent usage that no dashboard import covers, codeburn overview prints one line naming how much of the total is priced from the IDE's local files (includes $X of Cursor priced from local files, not Cursor's bill) and points at codeburn import cursor <file.csv>, instead of letting the estimate read as the dashboard bill — on the account that reported #1545 the local figure was about 2% of what Cursor actually charged. Imported events and days an import covers never trigger the line, since covered local calls are dropped at serve time and the imported rows carry Cursor's own billed amounts. (#1545)
  • Amp support. CodeBurn reads Sourcegraph Amp's local thread mirror at ~/.local/share/amp/threads/ (AMP_DATA_DIR honored, comma-separated): one JSON document per thread with a per-request usage ledger (tokens.input/output, cache tokens joined from the billed message through toMessageId) and a per-message usage fallback for older threads. Every call is priced from the underlying model's token rates and marked estimated; Amp's own credits field is not converted; thread files carry no cwd, so sessions group under the provider name. Reported in #1572.
  • codeburn import cursor <file.csv> replaces the local Cursor estimates with Cursor's own usage export. The local database has no per-request token counts and none of the cache reads Cursor re-sends, so on one real month CodeBurn saw about 7M Cursor tokens where the dashboard showed 644M. Export CSV at cursor.com/dashboard/usage and import it: for the time the export covers, its events replace the local Cursor, Cursor Agent and (when the export holds Grok Bot events) Grok Bot estimates, and outside it the estimates stay. Dollar amounts in the export are kept as billed; included usage is priced at API rates like every other local call. Pass --from/--to with the range you exported, re-import overlapping exports without double counting, and --remove to go back. Stored locally, nothing is uploaded. Reported in #1545.

Removed

  • Roo Code support. Roo Code was discontinued in May 2026 (its repository is archived).

Fixed

  • Crush: the session cost Crush records now reaches reports instead of a much smaller re-priced figure. Crush accumulates each session's cost in sessions.cost, but its prompt_tokens and completion_tokens columns are not session totals; they hold recent usage. Crush was not on the reported-cost pass-through, so the session cache dropped the recorded cost and every read, cold and warm, re-priced the session from those counters. On real data the reported figure was roughly a twelfth of what Crush recorded for the same sessions. A session with a positive recorded cost now keeps it through the cache, the same way Warp, OpenClaw and Pi do; a session that recorded zero keeps the existing behavior and is still priced from its tokens. Crush sessions re-parse once and settled days re-derive (daily cache v49).
  • DeepSeek v4 and the gpt-5.6 Codex SKUs price at their repriced rates again. The 0.9.21 snapshot had caught the 2026-08-24 repricings mid-transition and relaxed two pricing invariants. LiteLLM has since synced DeepSeek v4 to the official peak rates (pro $1.32/$3.96 per million, flash $0.30/$1.20, off-peak half), so the stale hand-pinned entries are gone and bare and deepseek/-prefixed rows agree again. The same refresh exposed a bundler ordering defect: a reseller's openrouter/deepseek/deepseek-v4-pro row could claim the deepseek/deepseek-v4-pro slot ahead of the official row (whichever came first in the upstream JSON), quietly pricing the namespaced id at the resale rate, ~40% under official peak. A prefixed row may now fill only a slot that is not itself an upstream entry, so the official row always wins and the fill no longer depends on key order. The same fix reprices zai/glm-4.6 from the vercel_ai_gateway resale row ($0.45/$1.8 per million) to the official row ($0.6/$2.2), and moves mistral/codestral-embed and mistral/mistral-embed to the fallback table at unchanged rates. The gpt-5.6-codex and gpt-5.6-codex-max entries, still absent from LiteLLM, now mirror the repriced gpt-5.6 base row verbatim including its >272k tier, restoring the codex-equals-base assertion at $4/$20 per million. DeepSeek v4 sessions re-price on read; no re-parse needed. (#1134)
  • Codex long-context calls now use the published higher rates at their prompt-token threshold. The bundled and live pricing readers retain LiteLLM's context tiers, and Codex applies them per call; below-threshold calls and other providers retain base pricing, and explicit price overrides still win. Audit totals use the same per-call calculation. Pricing and daily-cache versions advance so warm history re-derives with the corrected rates. The regenerated bundle preserves the snapshot refresh's base-rate guards and exact-key carry-forward for previously priced models. (#1076)
  • Bundled pricing is refreshed: 5,896 primary entries (298 added, 158 repriced by upstream, 364 no-longer-published ids moved to the fallback table, which grows from 212 to 543). grok-4.6 base input moves to $1.25/M; claude-opus-4 is pinned at Anthropic list price since LiteLLM dropped it.
  • OMP: side calls outside the chat transcript now count. OMP logs find, judge, TTSR checks and cache warming as model_usage entries, not assistant messages, and the parser read only messages, so that spend was missing. On one real week that was 605 find decisions on TypeSafe Jev and a $0.048 cache warm. Each side call now counts at its reported cost, with its purpose in the tools column as omp:find, omp:cache-warm and so on, and a zero reported cost is priced from tokens. A side call never duplicates an assistant turn. OMP sessions re-parse once and settled days re-derive (daily cache v41).
  • Text selection works again in the terminal dashboard. Mouse tracking, added in 0.9.20 so the wheel scrolls the view, makes the terminal send mouse events to the application, and in most terminals that stops click and drag from selecting text, so copying a number out of the dashboard needed Shift held down. Tracking is now off by default and m turns it on and off while the dashboard runs.
  • Cursor Enterprise and team seats connect in Plan & Usage instead of showing an unrecognized quota response. Their usage-summary carries no individual meter, only a team on-demand meter, which now fills the bar under the Enterprise plan label. On other plans it is listed beside the monthly window. In the macOS app a rate limit, outage or unrecognized response no longer adds sign-in guidance, which is only shown when the session is missing or rejected. Reported in #1546, thanks @paulodearaujo.
  • Windows: the Claude quota shows for Claude Code installed only inside WSL. The tray's Plan panel and codeburn quota read the credential from the Windows profile only, so a WSL-only login showed nothing. Both now also read ~/.claude/.credentials.json in each running distro's home over \\wsl$, never waking a stopped distro and never writing to it; when both sides are logged in, the credential that expires last wins. A slow or unreachable share times out instead of holding up the poll. CODEBURN_WSL=off skips it. (#1061)
  • VS Code Copilot: OTel conversations land on their workspace instead of copilot-chat. Copilot Chat's OTel spans name the conversation but carry no repository attribute CodeBurn could read, so every conversation from agent-traces.db went to copilot-chat. A conversation is now matched to the workspace whose chatSessions folder holds its session id, and named from that workspace's workspace.json, including multi-root and remote/WSL workspaces. Only the project split moves; cost, tokens and calls are unchanged. Copilot re-parses once and settled days re-derive (daily cache v38); calls already pruned from agent-traces.db keep copilot-chat. Reported in #1529, thanks @jDegut.
  • OpenCode: Vertex turns and turns CodeBurn cannot price no longer show $0. OpenCode 2 records a Vertex model as {"id":"claude-sonnet-5@default","providerID":"google-vertex-anthropic"}. Per-message turns already price on the bare id; the session-level rollup still used provider/model and now uses the bare id too. Turns through google-vertex and google-vertex-anthropic land on their own row, like "Sonnet 5 (Vertex)". A turn whose model CodeBurn cannot price used the cost OpenCode recorded only until it reached the session cache, which re-priced it from tokens to $0 on every read; that recorded cost now survives the cache, and a price CodeBurn learns later still wins. Applies to Kilo Code too. OpenCode and Kilo Code sessions re-parse once and settled days re-derive. Reported in #1547, thanks @bulwahn.
  • Cursor Agent reads CLI sessions that exist only in ~/.cursor/chats/*/*/store.db. The provider read exported transcripts only, so a session with no transcript was never counted. A store is read when its session has no transcript; a session with both is read from the transcript alone, so nothing counts twice. Store turns follow the transcript rules (every assistant message is a turn, the prompt is billed once, tool inputs count as output), take their time from Cursor's per-prompt <timestamp> stamp, and are priced at the same Cursor (auto) rate as transcripts, so a session keeps one price whether it is read from its store or from the transcript Cursor writes when it ends. Messages Cursor summarized away on a long session are still read from the older conversation roots the store keeps. The daily cache moves to v39 so settled days re-derive. Reported in #986, thanks @kelchm.
  • Cursor Agent counts every assistant message instead of one turn per user message. Agentic loops emit dozens of assistant messages per user message and the parser kept only the first (on one real corpus: 247 of 4,946 messages, ~4% of assistant text). Each message now carries the last user message forward, tool_use inputs join the output text, and input tokens use the full user text, billed once per user message, while the display stays truncated at 500 chars. The parse version bump re-parses cached sessions, and the daily cache moves to v36 so settled days re-derive.
  • Windows Store installs: no update offer the Store cannot deliver, and codeburn menubar opens the Store copy. The desktop app read GitHub releases for its update banner, so a Store copy was told about a version still waiting on Store certification and sent to a Store page that did not have it. Store installs now skip the GitHub check and the About update button, like the tray app already did; the Store updates them. codeburn menubar spawned the tray exe inside WindowsApps, which Windows refuses from outside the package (spawn EPERM); it now opens the Store app by its app id, and the desktop app starts the tray. Reported in #1520, thanks @Brandin.
  • Menu bar apps 0.9.14 to 0.9.24 load Today again with a 0.9.25 CLI. Homebrew and npm update the CLI on their own, and 0.9.25 left avgCostPerSession out of a project row whose session count is a lower bound, a key those apps require, so the popover showed "Couldn't load Today". The payload always carries it again, with sessionCountBasis still telling 0.9.25 and later not to show a partial average. A new test checks the payload against the keys every released menu bar app requires. Reported in #1541, thanks @AleksandrLiadov.
  • Update in the menu bar app restarts it into the new version. The app runs codeburn menubar --force as its own child, and pkill/pgrep skip their own ancestors unless told otherwise, so the running app was never stopped: the bundle was replaced and open re-activated the old copy. The CLI now finds the app among its ancestors, swaps the bundle, exits, and a detached helper stops the old copy and opens the new one. Run from a terminal, it stops the app first as before. Because the fix is in the CLI, older menu bar apps update correctly once the CLI is current. Reported in #1541, thanks @AleksandrLiadov.
  • OpenCode 2.x: upgraded history is no longer lost and v2 turns price like 1.x ones. OpenCode 2 copies 1.x sessions into new tables on first start, but the copy drops compaction summary and task-tool turns, skips messages it cannot decode, and can stop partway. CodeBurn read only the copy for any migrated session, so on one real upgraded database more than half the sessions and three quarters of the all-time OpenCode cost disappeared. A session present in both generations now reads its legacy turns first and adds only the turns written after the upgrade, so pre-upgrade figures match 1.x exactly. v2 turns also carried a provider/model id that failed pricing for ids like opencode/claude-haiku-4-5 or github-copilot/gpt-5 so they showed as $0 in reports; they now use the bare id 1.x used. OpenCode sessions re-parse once. Users on CodeBurn 0.9.24 or older see no OpenCode 2 usage at all and need 0.9.25 or later. Reported by a user.
  • The session cache is stored by day, so a today view reads only today. Each provider keeps an index of every cached session and one file per day of activity. A narrow range reads the index, the duplicate-check keys and only the sessions it reports on, and saving an appended session rewrites one day instead of a month. Older Claude transcripts are no longer re-parsed on every narrow query. The old cache converts once on first run, a session at a time. On a heavy corpus overview -p today drops from about 3.1 s and 690 MB to 0.8 s and 560 MB; on a normal one from 0.54 s to 0.41 s. Figures are unchanged.
  • overview, status and the menu bar load only the part of the session cache a date range can use. Cached sessions outside the range stay on disk and are held in memory as small stubs that still take part in duplicate checks and clean-up, so figures are unchanged. Shards are written one session per line, so a month past 512 MB is saved instead of silently re-parsed on every run, and Codex project names come from a small index instead of the whole results cache. On a heavy corpus overview -p today drops from about 3.4 GB to 1.2 GB and runs under a 1 GB heap where it used to crash; on a normal one it drops from about 970 MB to 280 MB and gets faster. Closes #1504.
  • An expired Claude Code login is recovered from the Keychain, and codeburn quota and the desktop app say so when it cannot be. Both providers read the OAuth credential from ~/.claude/.credentials.json and fall back to the macOS Keychain, which is where Claude Code keeps it on a normal Mac. After a 401 the retry re-read the file whatever the credential had come from, so a Keychain user got Temporarily unavailable. until Claude Code was run again, and the desktop Plans card kept showing the last percentage it had fetched. A 401 now re-reads the store the credential came from, which picks up a login Claude Code has since renewed. When nothing newer is there and the credential's expiresAt has passed, the row reports a terminal failure with a line telling you to run Claude Code once. The desktop app still reads the Keychain only on Check now or a forced refresh, and a background poll keeps that reconnect prompt instead of falling back to Check now. A 401 on a credential still within its life stays transient with its backoff. (#1516, thanks @ozymandiashh)
  • Export daily rows now bucket each call on its own day, like the history and the summary, and no longer drop a turn that has no timestamp.
  • Cache-write tokens on a non-Anthropic model with no published write price are billed at the input rate, not an invented 1.25x. Daily cache moves to v35 so settled days re-derive.
  • An OpenCode or Kilo Code message with output but no recorded usage, and a Copilot transcript call with no token count, are marked estimated instead of reading as a measured $0.
  • Export records keep each call's unrounded cost, so they sum to the summary, and gain estimated and unpriced columns at the end.

0.9.25 - 2026-09-21

Added

  • Bundled pricing is refreshed, and the refresh can no longer drop a model's pricing or re-price a model it still covers. 5,942 primary entries against the 0.9.24 bundle (1,215 added, 182 repriced by upstream's own data; among the additions, the Grok Build family makes grok-4.6-build priceable so the Grok parser's authoritative-model rule now prefers the real modelUsage id), with the fallback moving from 203 to 212 entries. Three bundler rules make that true. Coverage carry is exact-key only: the runtime resolver (getModelCosts) looks the queried id up verbatim, peels segments and strips variant suffixes, but never adds a vendor prefix, so a ~x-ai/grok-latest primary does not answer a bare grok-latest query, and the first version of this carry, which also accepted a vendor-prefixed form, dropped exactly that way: 96 previously-priced fallback ids resolved to null while their model kept only a prefixed key. The completeness guard in the prefixed pass is strictly slot-filling: a richer upstream row may fill cache-write/cache-read slots an entry lacks, but only when its input and output rates are identical to the entry already present (every filled slot must also survive verbatim). Completeness alone was swapping in a different row's rates (grok-3 $3/$15 became $1.25/$2.50, mistral-large-latest $8/$24 became $0.50/$1.50; 43 input/output and 34 cache rates moved that way), which a refresh has no authority to do, and a sparser alias still cannot displace the publisher's entry (a nebius/MiniMaxAI/MiniMax-M3 row without cache-read rates can never replace the MiniMax entry that carries them). And what a refresh would otherwise drop stays priced: the previous fallback's entries and the previous primary rows the new upstream data dropped or renamed (this cycle, twelve ids: the gpt-image-2 family, the Bedrock marengo embeds, the friendliai llama-3.1 rows) carry forward verbatim into the fallback tier. Verified by resolving every id from either bundle through the real resolver: nothing that priced on 0.9.24 prices as null now, and no rate changed except where upstream repriced the row itself. (#1483) Thanks @ozymandiashh.
  • The desktop app and every reporting command can be scoped to the projects you choose. sessions, models, compare, audit, optimize, yield and spend now declare --project and --exclude and apply the filter at the one point they all funnel through. An absolute-path pattern is anchored to a path-segment boundary instead of matched as a substring, and a leading ~ expands. The desktop app gains a Projects pane that writes the same filter, with a search box and cost order: it lists projects for the whole history rather than the visible period, so a dormant exclude does not read as an orphan, it fails closed when the filter file cannot be read instead of quietly reporting unfiltered totals, it re-reads the filter when it changes outside the window, and it slices the 365-day history the menu bar reads so both agree. compare scopes its self-correction scan to the filtered projects, and web warns when a rooted pattern names no project. Closes #1276. (#1285, #1302, #1307) Thanks @mathieustan.
  • Every aggregate in the desktop app opens the sessions behind it. A day bar, a model, a task category, a project and a pull request row each open a filtered session list with a detail drawer and navigation history, separating what a session contributed to the selection from its full cost. The CLI carries the same identities: sessions --format json --contributions reports per-session contribution segments, and the JSON report and the menubar payload gain the optional identity fields the app keys on. Each fetch is scoped by the Projects filter, so a drill-through never lists sessions the headline above it is not counting. Closes #1338. (#1316, #1350) Thanks @ozymandiashh.
  • Spend has a By branch lens: what each branch and worktree of a project cost. codeburn spend --format branch-json reports project-by-branch attribution and the desktop app draws it with a project picker. The lens is scoped by the Projects filter like every other fetch, so a project the filter hides no longer shows its branches. (#1319) Thanks @ozymandiashh.
  • Compare periods puts two date ranges side by side and says what moved. codeburn compare-periods runs a deterministic B-minus-A diff engine, and the desktop app adds a Compare > Periods screen on it: a lead sentence that states the change in plain words, three tiles with their change, both ranges day by day as slim paired bars with a two-row tooltip, and the five biggest movers, with the totals, the normalized view and the coverage notes folded below. Every row drills through to the sessions behind it, carrying the clicked side's range and the contribution's project and model filters, so the destination is the population the row describes. (#1323, #1440, #1446) Thanks @ozymandiashh.
  • Compare > Cohorts compares models over an explicit observation population. codeburn compare --format cohort-json with --from, --to, --category and --project-id defines one observation as an edit turn whose behavioural calls carry exactly one model; that model owns the turn, and only its own calls' cost is attributed. A turn that mixes models goes to a shared exclusion ledger with its combined cost, shown on screen rather than dropped, and a sample row opens the session it came from. (#1324) Thanks @ozymandiashh.
  • The desktop app and the macOS menu bar run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the menu bar follows the same choice; Settings > General > Language in the menu bar overrides it for CodeBurn alone by writing AppleLanguages into the app's own preferences domain, which is the key System Settings writes too, so the two surfaces are one setting rather than two. Every user-facing string in the renderer goes through a local lookup table, 1,249 keys per language with no framework and no network, and the menu bar reads a Localizable.strings catalog per locale, 626 keys each, whose key is the English copy, so an untranslated string degrades to correct English rather than a visible identifier. A completeness gate fails the build when a language is missing a key, when a value is blank, when the format specifiers disagree or when a multi-argument key is not spelled positionally, and a scanner reads the menu bar sources themselves and fails when a user-facing literal never reaches the catalog at all. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. Desktop English output is byte-identical; in the menu bar three display-only date formatters now follow the locale, so EEE MMM d reads Sat, Sep 12 in en_US and Sat 12 Sep in en_GB. Strings the codeburn CLI itself produces stay English until the CLI is localized, which is separate. Closes #1219, #1335. (#1330, #1493) Thanks @ozymandiashh.
  • The desktop app's Plugins page has a Menu bar card for the macOS menubar app. On macOS it shows one of three states: nothing installed and an Install button; installed, with its version and Open and Uninstall; running, with a green dot, the version, Open, Quit, Uninstall and a Capacity Dock switch. Install runs the same codeburn menubar the command line does, so the release is pinned to the desktop's own version, the checksum and the bundle identity are verified before anything is moved, an older copy is quit before it is replaced and exactly one ends up running, and it runs the CLI the desktop carries, so a person who only ever downloaded the .dmg can install the menu bar without npm install -g codeburn first. Before the install the desktop writes a small launcher for that bundled CLI and records it where the menubar already looks for a codeburn before PATH (~/Library/Application Support/CodeBurn/codeburn-cli-path.v1), which is what leaves the installed menubar something to run; codeburn menubar now keeps an existing runnable record rather than refusing when nothing is on PATH. A menubar installed by hand or by the CLI is found in ~/Applications, /Applications, or wherever Spotlight knows its bundle identifier, so the card describes what is on the machine rather than only what it put there. Install names what it is doing while it runs (Downloading, Verifying, Installing, Starting), read from the CLI's own narration rather than a second progress protocol, because eight megabytes and a launch take about half a minute and a button that says nothing for that long reads as a hang. Quit and Uninstall ask the menubar to go rather than killing it (the login item is the app's own to drop, which nothing outside it can do), and a menubar too old to be asked is never signalled behind its back: the card marks it out of date, offers Update, and disables the Capacity Dock switch, Quit and Uninstall under one line, "Update the menu bar to use this". Asking one anyway gives up after five seconds with "The menu bar app did not respond. Update it and try again." rather than waiting on an answer that is not coming, and what the install actually put on disk decides the card, so a release older than the one these features ship in comes back as the outdated state rather than a cheerful Running. Uninstall takes the bundle away from wherever it was found and the card returns to Not installed. Both confirm in the card itself rather than in a system dialog, the way removing a plugin already does on this page. The Capacity Dock switch writes the menubar's own preference, which a current menubar acts on within a second. There is no link step (both apps already share the CLI, the cache and the config) and no login-item prompt, because the menubar registers itself on first launch. Failures read as sentences (offline, not published for this version, damaged download, no permission), never a stack trace. Install is behind a capability flag that a Mac App Store build turns off, where the card points at the website instead; nothing renders on Windows or Linux. (#1485, #1511)
  • Hovering a dollar amount in the desktop shows the tokens behind it. Hover or tab to any money figure that has token counts behind it (the Overview hero, the Yesterday, 7-day avg and Month to date figures, the daily chart bars and its Avg/day, Peak and Yesterday chips, the Sessions and Models rows and the two Compare tables), and a small popover lists input, output, cache read and cache write tokens plus the calls they came from. A figure with no token counts behind it shows nothing rather than a fabricated breakdown: projected month and the other estimates, percentages, savings, retry tax, plans and the by-project and by-PR rows are left exactly as they were. The amounts themselves are unchanged in size, weight and position, and the popover is drawn above the card so it never clips. The menubar payload's periodTotals now carries the same four counts, so the hero keeps its breakdown when its number comes from the machine-wide generation rather than the payload's own period. (#1472)
  • A model billed through a different door is its own row, on every surface. Claude Code with CLAUDE_CODE_USE_BEDROCK=1 and Hermes on its bedrock provider record Bedrock's id (anthropic.claude-haiku-4-5-20251001-v1:0, openai.gpt-5.6-luna) where a direct session records the vendor's; Hermes also records the door itself in billing_provider, which is how a Bedrock session is told apart from a direct one when the model id is the same. Those rows either surfaced as raw ids or, aliased to the friendly name, would have merged with the direct row, hiding which account the spend landed on, and so how much of the month was metered API usage on top of a subscription. Every report now keys model rows on one function, modelRowKey: the short name plus the door's label, so Haiku 4.5, Haiku 4.5 (Bedrock) and Haiku 4.5 (Bedrock us) (the cross-region profile, a dearer SKU) are three rows in codeburn models, the dashboard and the menubar payload's topModels alike, where before models and the menubar could disagree. The route rides on the call (route), and codeburn models --format json reports it per row. Pricing is untouched: it runs on the raw id. Daily cache moves to v33 (day.models keyed by the row key, so a column-sourced route survives into finalized days); Hermes sessions re-parse once. Closes #1449, #1450. (#1448) Thanks @ulises-c.
  • models, sessions, export and audit can be filtered by billing route and billing mode. --route direct|bedrock|openrouter and --billing metered|subscription cut to the matching calls, and a session that mixes them contributes only the calls that match, keeping its session linkage. Provenance comes from what the provider recorded rather than being guessed from the model id: Hermes included is subscription-covered and actual, an explicit $0 included, is metered; OpenCode's own provider column supplies the OpenRouter and Bedrock routes; everything else stays unknown. (#1486) Thanks @ulises-c.
  • Every codeburn serve --stdio response carries the generation it was built from. Each answer the resident process derives advances a counter, and the response reports it with the moment that derivation happened ({"generation":{"n":4,"at":"2026-09-17T08:12:44.019Z"}}); an answer served from the memo carries the stamp of the derivation it came from, not the moment it was handed over. A client holding several panels can tell which of them share one reading of the corpus and show a single clock for it instead of one per panel. The stamp deliberately does not describe a day range: a menubar payload answers for its period but also carries a year of daily history and the live session list, so the request's own period would have described only part of it. (#1467)
  • Grok Bot, xAI's desktop agent app, is a provider. CodeBurn reads the transcript mirror the app keeps in its Electron userData directory (sand-client-persistence), one session per bot, named after the bot the way the app's own sidebar names it. A conversation is grouped into one call per request, and a run the bot woke itself for (a Routine on a schedule, a handoff resumed, a message from another bot) is emitted without a human prompt, so the task classifier does not read it as something you asked for. The mirror records no tokens, no cost and no model id anywhere, so every figure is estimated from message text at xAI's published grok-4.6 rate and every cost is marked estimated; the mirror also keeps only the most recent entries per bot, so a lifetime total covers what is still there. This is the desktop app, not the Grok Build CLI, which stays the separate grok provider. codeburn quota gains a Grok Bot row: the app's "Weekly usage" reading is a Cursor dashboard call, and CodeBurn makes it with the Cursor IDE's own unencrypted token rather than decrypting Grok Bot's copy, so the row is Grok Bot's allowance only while the Cursor app is signed into the account Grok Bot uses, and says "not signed in" otherwise. (#1462)
  • Kimi Code sessions now show up in the live-sessions block. A Kimi session whose agents/*/wire.jsonl was written inside the liveness window is reported as kimicode, with its project, the model of its last request and the activity of its sub-agents folded in. (#1354)
  • Live Kimi Code sessions show their context ring. The row reads the context Kimi measured after the last turn and the window it requested from the tail of the session wire, the same numbers Kimi prints in its own footer. (#1358)
  • CodeBurn tells you when OpenAI banks a limit reset on your Codex account. These grants (the "banked" or "goodwill" resets that restore a rate-limit window early) are sometimes announced and sometimes not, and until now the only way to notice one was to go looking. The reset-credit inventory CodeBurn already reads on every Codex quota refresh is now compared against the previous reading: a credit seen for the first time posts one notification naming what was granted, when it landed and how many you have available to use. The first reading after connecting is a baseline, not news; a credit that disappears because you spent it says nothing; a failed fetch or a malformed payload is treated as no opinion rather than as an empty account, so reconnecting does not re-announce what you were already told; and the fired event is persisted next to the existing quota snapshots, so a relaunch does not repeat it. Settings → Notifications gains a switch for it, on by default. The count and the most recent grant also appear in the Codex Plan tab, in the agent-tab quota hover card, and as a Limit resets · … line in codeburn quota (text and --format json), worded identically on both sides. No new endpoint, no new polling loop and no new data source: this reads fields off a response already fetched on the existing cadence, per the rule #702/#724 established. CodeBurn never spends a credit: this is a notice only. Closes #725. (#1328)
  • Per-model token counts sit beside per-model cost in the desktop Overview and the macOS menubar Models rows. A menubar row gains a secondary line such as 12.3K in · 4.5K out · 67.8K cache read under the model name, with its cost staying on the primary line, and the Overview models table gains a Cache read column. The desktop Models tables keep observed token counts visible for models that have no attributed cost, where usage was previously hidden. Counts are carried through both fresh-session and durable-day aggregation using the same model-name grouping as cost and the same billable-output normalization, so reasoning tokens are added only where the provider reports them separately. Cache read means reused input; cache pricing was already included in attributed cost, so this exposes usage without changing any total, and cache write stays a separate payload field surfaced in the macOS accessibility description. The new fields are optional: older payloads still decode, a missing count stays unknown and renders as an em dash rather than a fabricated zero, a known zero prints as 0, and an alias merge whose contributors disagree presents no partial count. The all-provider Overview table now reads from the period-scoped model rows rather than a union of each day's top-five history, which makes its numbers period-accurate; the period list is uncapped (#1318), so the table keeps every model with usage in the period. Status snapshot revision 9 invalidates older cached payloads once so an unchanged session corpus also receives the new fields; daily and session cache formats and stored history are unchanged. (#1315, #1443) Thanks @ozymandiashh.
  • codeburn quota and the desktop Plans page report the ZCode (z.ai coding plan) allowance. The adapter calls the same usage endpoint the ZCode app's own coding-plan browser calls, reading its access token read-only out of that webview's local storage journal, newest journal first and last write of the key wins. The stored token carries no expiry claim, so a rejected login shows up only as a body-level code on an HTTP 200; that is read before the limits are parsed and reported as an authentication error rather than a parse failure. (#1347) Thanks @ozymandiashh.
  • Antigravity sessions report their tool calls, bash commands, MCP tools and skills instead of nothing. Token usage was read from the conversation database while the tool arrays were hardcoded empty, so codeburn today, codeburn tools and codeburn report showed zero tool activity for every Antigravity session. The provider now reads the steps table alongside gen_metadata and decodes the tool-call metadata, so run commands, MCP invocations and skills reach the breakdowns. Closes #1455. (#1456) Thanks @dcaldr.
  • Models are coloured by capability tier rather than by provider in the web dashboard. Flagship, Premium, Balanced, Fast and Other are assigned from the model itself, and the Spend charts, the Sankey flow and the Models tables use one colour per tier, so the same model reads the same everywhere. Closes #1162. (#1169) Thanks @ksp2000.

Added (macOS)

  • The menubar acts on a Capacity Dock change made from outside it, and can be asked to quit or uninstall itself. The desktop app's new Menu bar card turns the rail on and off by writing the menubar's own preference from another process. UserDefaults.didChangeNotification never carries a write made outside the process, so the rail sat where it was until the next launch; the dock now watches its one key with KVO, which cfprefsd does deliver across processes, and the rail appears or goes within a second. The app also answers one new key with a quit, and with an uninstall that first unregisters its login item: SMAppService speaks only for the app that calls it, so no other process can drop that registration on its behalf. Both clear the key before acting, so a command that outlives the process cannot quit the next launch. Nothing else reads the key, and a build that does not know it is simply signalled instead. (#1485, #1511)
  • Clicking a live session in the Capacity Dock card raises the terminal it is running in. The row's process is found from Claude Code's own ~/.claude/sessions/<pid>.json or, for Kimi Code, from the one kimi working in the session's directory, and the process tree is walked up to the application that owns it: Terminal.app and iTerm2 get the exact tab selected by tty, every other terminal is brought to the front. A row whose process cannot be found stays inert, with no pointer change and no tooltip. (#1363)
  • Only one copy of the menu bar app runs, however many ways it was launched. A second launch retires the copy already running instead of stacking a second Capacity Dock on the screen edge. Which copy stays is decided by the start time the kernel reports for each process, so both sides rank each other off the same number: the newest start wins and a process id only breaks an exact tie. A copy whose start time the kernel will not report is left out of the comparison altogether, neither retired nor able to retire anything, so the worst case is two menu bars you can see and close, and never none. A copy that ignores the polite quit is forced out a few seconds later, and --keep-both opts one launch out. (#1359, #1511)
  • The menubar tells you when a quota window crosses 80% and again when it hits its limit. Every connected provider's windows, the same list the menu-bar flame and the popover warning row are built from, are checked on the quota refresh that already runs, with no new polling: crossing 80% posts Claude · Weekly at 80%, reaching the limit posts Claude · Weekly limit reached. Each fires once per window per cycle, a window that hits its limit without ever having been seen at 80% posts only the limit notice, and the cycle's reset instant is part of the identity, so the next cycle re-arms both while an absent or failed fetch keeps what was already announced. The fired set is persisted, so a relaunch does not repeat a crossing. Notification authorization is requested on the first real crossing and never at launch. Settings → Notifications gains a switch, on by default. (#1361)
  • The menubar tells you when a vendor resets your quota early. Vendors sometimes reset a usage window ahead of schedule as a goodwill gesture; CodeBurn showed the new percentage but never said it had happened, so the free capacity went unused. Two signals now catch it on the refresh lifecycle that already runs, with no new polling: a reset time that jumps to a new cycle while the stored one still had time to run, and usage emptying (a fall of at least 40 points landing at or under 10%) while the advertised reset time stands still. A system notification through the existing notifier names the provider, the window and the lead, worded for the signal that saw it ("Claude's weekly limit reset 18h early. You're back to 100%." when the cycle rolled over, "Claude cleared your weekly usage 18h before its reset. You're back to 100%." when the counter emptied but the reset time held). One goodwill reset is one notification per provider however many windows it empties, and a reset already announced is never announced again after a relaunch, or when the vendor briefly serves the old cycle back. It stays silent on a normal scheduled reset, a plan change, clock or timestamp skew, a window appearing or disappearing between fetches, a window's first observation, a reconnect after a terminal failure or a fresh bootstrap, and a window whose length the adapter does not validate; a missing or malformed stored reading is no opinion rather than an event. Settings → General → Notifications turns the notification off, default on. Claude is the provider wired up today, because it is the one whose quota readings are persisted. Closes #725. (#1329, #1452) Thanks @ozymandiashh.
  • Codex gets the same early quota-reset notification Claude gets. Its windows are keyed by a pre-localization label, so a language switch or the "limit reached" suffix no longer drops the stored baseline, and when a provider reports absolute usage a spend-cap increase is no longer mistaken for a cleared counter. The detector had also dropped additional-limit windows that were already at 0 percent used before checking them, so a per-model window that reset early to zero was never recorded; those windows are included now. (#1339, #1490) Thanks @ozymandiashh.
  • The macOS menu bar tells you when an update is available. The background poll that already runs posts one notification per new version pair, app or CLI or both, deduped by a stored stamp so the repoll stays quiet until a newer release, and clicking it runs the same full update the Check for Updates button does. Settings > General gains a switch, on by default; with it off nothing is posted and no permission is requested. Authorization is asked the first time a notification would actually post, never at launch, and a manual check still shows its result in an alert rather than a notification. Closes #1261. (#1283)
  • The Capacity Dock shows today's cache-read tokens and tells you whether each quota window will last to its reset. The Today section gains a provider-scoped cache-read figure beside input, output and calls; a known zero prints as 0 while missing or incomplete historical accounting stays unknown rather than becoming a fabricated zero, and because cache reads were already priced into the burned figure this adds visibility without changing any total. Each quota window also gets a projection, reported in the dock's tooltip and under each bar in the agent-tab quota hover card: Lasts until reset, Runs out in 2d 8h, or, on windows of six hours or less where one burst would make a linear ETA cry wolf, the pace stage the Plan tab uses (On pace, 40% in deficit, 30% in reserve). The projection runs against the window length the provider adapter reports, never a length guessed from the display label, so a monthly cycle whose label happens to read Weekly is still paced against its month; it stays silent early in a window, on an exhausted window, without a reset time or a validated duration, and on stale, disconnected or older-than-ten-minute data. The window cells themselves stay at percent, label and countdown, with up to three windows sharing one row and a fourth wrapping to a second, so the card does not read as clutter. Status snapshot revision 7 invalidates older cached payloads without purging daily history, and no extra polling is introduced. (#1267, #1314, #1353) Thanks @ozymandiashh.
  • The macOS menubar item can show a second line. Settings → General → Display gains a "Second row" switch, off by default, and a picker for what that line shows: quota remaining with its reset countdown for whichever connected provider is nearest its limit, today's all-provider cost, today's total tokens, or the number of running sessions. Both lines render as one attributed title at 9pt with their line height clamped to 10pt, so the pair fits the standard 22pt menu bar, and the second line hides itself whenever its metric has no data yet, leaving the existing single-row figure exactly as it was. This is a deliberately small first slice of the multi-row layout request: no layout editor, no presets, no live preview, no per-item provider or period scoping. The setting persists as CodeBurnMenubarSecondRowEnabled and CodeBurnMenubarSecondRowMetric in the app's own defaults domain alongside the existing menubar period, scope and metric keys. Fixes #1252. (#1308, #1311)
  • The Capacity Dock gauge can report the short usage window instead of the weekly one, without expanding the dock. The resting rail shows one number per provider, and that number was always the weekly (else monthly) billing window. Clicking the provider already resting in the rail now switches its gauge to the provider's short rolling window (Claude's 5-hour limit, Codex's 5-hour or daily window, any Hourly, Daily or session row an adapter reports), and clicking again switches back. The choice is stored per provider under CodeBurnCapacityDockGlanceWindows, so Claude can sit on its 5-hour window while Codex stays weekly, and it survives relaunch. A per-model row such as Weekly · Opus is never read as a short window, a provider that reports only one window keeps the plain click-to-pin behaviour, and a stored horizon the provider stops reporting falls back to the window it does report rather than blanking the gauge to --. The rail's geometry is untouched, VoiceOver and keyboard users get the switch as a named action on the provider cell with the window named in the cell's value, and Escape or a click outside still unpins the dock. Fixes #1243. (#1309)

Added (Windows)

  • The Windows tray app is installed and configured from a card on the desktop app's Plugins page, the way the macOS menu bar is. The card installs, updates, opens, quits and uninstalls the tray companion, shows a Running dot with the installed version, carries the Capacity Dock switch, and opens the tray's own settings in the desktop app's Menu bar pane; the two switches that used to sit in the sidebar corner are gone. codeburn menubar --uninstall reads the product code from the install marker or the uninstall registry and runs msiexec /x. A failed removal never forgets a still-installed app, a cancelled elevation prompt is a no-op rather than a red failure, and a dev build sorts below any release, so a staged release still lights Update and a dev build never does. Exercised live on a Windows 11 x64 machine through a real MSI install. (#1507)
  • CodeBurn on Windows reads the agents you run inside WSL. An agent running in a distro writes its history to the distro's Linux home, not to the Windows user profile, so a Windows-only scan reported nothing for it. Each running distro's home directories are now extra provider roots for Claude Code and Codex, merged with the Windows sessions and read only. Only running distros are scanned by default, because reaching into a stopped one starts it: CODEBURN_WSL=all opts into stopped distros and off never runs wsl.exe at all. A distro that is not running is an unreachable root rather than a deleted transcript, so its usage keeps counting out of the cache and comes straight back when the distro starts, while a transcript actually deleted under a reachable home is evicted normally. codeburn doctor lists every WSL root it probed. Closes #1059. (#1062)

Changed

  • The resident codeburn serve process gives its memory back once the cold start is over. On a first run the desktop's serve child parses the whole corpus to seal the daily summaries, and then kept every byte of what it read: an unscoped session cache the load memo served to every later request whatever range it asked for, and the lifetime parse trees, with nothing to retire them once the polls stopped. On a 170 MB shard set that was 2,198 MB resident, unchanged across ten minutes of idle, against 364 MB of live heap. The cold start now releases what it can re-read from disk the moment its summaries are written, so the poll that follows loads only the month shards its own range covers (141,154 cached calls resident, down to 90,094; five lifetime parse trees, down to one day's), and then collects, because V8 hands the pages back only on a later collection and returns none of them on its own. Steady resident after a cold start falls from 2,198 MB to 700 MB, and a warm start, which has no fill to release after but the same opening burst behind it, from 900 MB to 781 MB. Nothing is dropped that the cache is the only record of: a held shard-publish window is published before the release, not discarded, and a durable provider's section never waits in that window at all. Answers are unchanged, every period is leaf-identical to before, cold and warm, and a poll costs the same; the whole cost is a one-off 1.6 CPU-seconds of collection after a cold start. (#1511)
  • The resident codeburn serve process stops re-reading the whole corpus, and re-writing a whole month of cache, on every poll. While the desktop app is open its serve child answers a status poll every 30-60 seconds, and as long as any agent is writing a transcript each of those polls re-walked every project directory and re-stat'd every session file (17,611 stats and 2,460 readdirs on a 4,530-transcript corpus) and then republished the current month's cache shard: 49 MB rewritten, and read back on the next poll, because one file gained a line. Both are now driven by the root watchers that already exist. The discovery sweep reuses the directory listing and the dev/ino/mtime/size of any path no filesystem event has named, re-reading only the paths the watcher named and the directories those sit in. Nothing derived is reused (every file is still reconciled against its cached fingerprint by the same code as before), and the sweep falls back to reading everything whenever the watcher cannot vouch for the answer: no watcher at all (every one-shot CLI run), a watcher that failed to arm or errored, an event that arrived unscoped or overflowed the tracked-path bound, a path outside every armed root, a WSL/UNC path, a SQLite-backed or virtual-suffix source whose fingerprint is not the path an event would name, the first parse after start, anything remembered longer than four and a half minutes, and a wall-clock jump the monotonic clock did not see. Shard publication is coalesced to at most once every ninety seconds in the resident process (longer than the slowest poll, or the window would expire before every poll and coalesce nothing), and is always flushed when the app closes its stdin. One-shot CLI runs keep writing immediately. A publish the window swallows writes nothing at all, so the on-disk cache is never newer-looking than its content: the cost of losing a window is that the next start re-parses those appends, and a held publish is dropped rather than written if another process has published since. Measured on a 4,530-transcript corpus with one session appended every three seconds, polling as the desktop does: 0.71 to 0.33 CPU-seconds per poll at a 30-second cadence, 17,611 to 11,592 stats, 2,460 to 324 readdirs, three shard publishes down to one. A quiet corpus was already free and still is. Time-to-reflect is unchanged at 1.23 s before and 1.19 s after, including for a new project directory, a new session file, a deleted file, a file replaced by rename, an append landing mid-poll, a change burst past the watcher's tracking bound, and a corpus changed while the watcher is unhealthy. (#1511)
  • Vercel AI Gateway spend is no longer added to your totals, because it was being counted twice. The gateway's reporting API returns one row per day per model: a cost, a token count and a request_count, with no request ids, no timestamps and no attribution. The requests behind those rows came from the local tools you pointed at the gateway (Claude Code via ANTHROPIC_BASE_URL, Codex, OpenCode, Cline/Kilo, Cursor), and every one of those tools writes its own session file that CodeBurn already parses. Nothing in a gateway row can be matched against those sessions, so there is no way to reconcile the two, which meant every headline, model row, daily row and history point carried the same spend twice for anyone with a gateway credential. If you use the gateway, your headline totals will go down. The gateway is still shown, always: its own provider row carries the full amount (marked "not in total" in the desktop app), the JSON report gains overview.excludedGatewayCost when something is excluded, and the terminal report and dashboard print a one-line footnote. --provider vercel-gateway is unaffected and reports the full amount. The rule lives in one place, the parse that every surface reads, so models, sessions, export, compare, compare-periods, spend, yield, audit, budget, the Ink dashboard's panels and the Teams sync push all agree with the headline instead of each report deciding for itself, and no detail panel (By tool, Top models, Top projects, By activity, the JSON projects[]) lists spend the total beside it is not counting. The gateway still appears once, clearly labelled: its own By tool row with no share, its own provider row in the payload and in --format json (overview.excludedProviders), and a one-line footnote under any total that left it out. The daily cache and the session cache are the deliberate exception: they keep storing the gateway slice, because a past day's aggregate row can never be fetched again. To count it in totals anyway, run codeburn gateway-totals include (exclude to undo, no argument to see the current setting). The exclusion is read-side only (the daily cache still seals the gateway slice on every day), so turning it on applies retroactively to sealed days without re-fetching anything, which matters because a gateway row cannot be re-derived from disk. Also fixed alongside it: request_count was parsed and then ignored, so a day+model row with 300 requests counted as one call; the gateway's call count now reflects the real figure on every surface. (#1511)
  • A Hermes mid-session price change lands on the day it is observed, and sealed days are never rewritten. This was already the behaviour; it is now the stated rule, documented where the observation is stamped (src/providers/hermes.ts). (#1511)
  • An open desktop window only refreshes the headline at the live cadence; the slower sections moved to slower tiers. Every refresh used to spawn a fresh act report process, run a yield pass and run a full optimize scan. The headline poll now asks for neither optimize findings nor a fresh act report; act report refreshes on mount, on a manual refresh and every 10 minutes, yield every 5, and the optimize scan runs at most once a day per query scope, stored with the time it was computed, reused only on the same local day, and re-asked for on the first render of a new local day so a window left open across midnight does not keep yesterday's savings under Today. A figure that is not live says how old it is, a failed scan shows its error instead of scanning forever, and no stored scan means no savings clause rather than a zero. On battery the live interval doubles, and arming a new cadence no longer refetches everything. The window also stops polling and animating while it is hidden and catches up the moment it comes back. Measured for an open window: 65 to 70 percent less CPU while a session is being written, 84 to 91 percent less when idle, with every CodeBurn process at 0 percent while hidden. (#1496, #1511)
  • Both apps retire the resident parser after 15 idle minutes and start a new one on demand. The renderer stops polling while the window is hidden, but the serve child stayed resident with its parsed cache, one to two GB on a large corpus. After 15 minutes without a request it is stopped the way a settings restart stops it, without spending the unexpected-death budget, and the next request starts a new resident through the existing lazy path rather than a one-shot, whose snapshot settle window would leave the figure a whole tick behind. A retired parser closes its stdin and gets a bounded grace to publish any held cache window before it is signalled, so retiring no longer discards a coalesced write: flushed 0 of 5 before, 5 of 5 after, for both clients. CODEBURN_SERVE_IDLE_MS and CodeBurnServeIdleSeconds tune or disable it. (#1511)
  • A menu bar poll is never answered from the on-disk status snapshot. Inside its settle window the snapshot returns the pre-change payload on purpose, and the resident process then memoized that answer as clean, so after a single write followed by quiet the headline stayed on the old figure until the next write or the five-minute memo cap. The snapshot is for one-shot runs; the resident process already holds the parsed state, so it skips it. (#1511)
  • The desktop app reads a size up. A token pass, not a redesign: the type ladder moves one rung (body 12.5 → 14px, secondary 11.5/10.5/9.5 → 12px, panel titles 13 → 15px, the page title 15 → 20px semibold), and a new --fs-eyebrow rung at 11px carries the uppercase, letter-spaced section labels, the only text in the app now under 12px besides keyboard chips and the two-letter provider monogram. The sidebar widens to 220px with 16px gutters, its rows grow to 36px with a 10px icon-to-label gap, 4px between rows and 20px between groups, and the shortcut chip leaves the row for its hover tooltip so a row is an icon and a name. Page padding goes 20 → 32px, card interiors 16 → 20px, and the Plugins empty state joins the normal left-aligned column instead of floating a centred card mid-page. Primary text is near-white in dark (#f4f4f5) and near-black in light (#121214), measured at 12.15:1 against its card in both themes. Card radii, the two-layer card rim and the black-frame-plus-charcoal-card structure are unchanged. Checked in the running app on all eleven sections at 900, 1200 and 1600px in both themes: no text under 12px, no card overlap, no horizontal overflow. (#1485)
  • The desktop app had a measured design pass: one type ladder, one screen header, one motion layer. Thirty-six audit items were fixed and re-measured by an independent pass. Eight type sizes instead of eighteen with nothing off the ladder, one header on all eleven screens, spacing on tokens, and zero clipping or overflow at 900, 1200 and 1680px in both themes. The light theme goes from seventeen contrast failures to none and is the default on a fresh install, with an explicit choice always honoured. Lucide icons replace text glyphs at one stroke and one size per context, Escape closes every overlay through one hook, and easing and duration tokens drive section fades, drawer and modal exits, popovers, hover and press states and one focus ring, all off under reduced motion and animating only opacity and transform. The frame is new: a near-black plane behind the sidebar and top bar, a three-step surface ramp, and a card system the Plugins and Plans pages are rebuilt on, with the Plans page's connection state no longer flipping a connected provider to disconnected on a single slow poll. (#1459, #1485)
  • The session drawer, the refresh indicator and the Plugins page lead with a sentence instead of a wall of figures. The drawer opens with one sentence putting the session's cost against the median of the list it came from, then three tiles, the model and task category bars, and the token and branch detail folded. The "Refreshing selected view" banners and the Overview hero caption that pushed content down during a refresh are gone: a 2px hairline under the top bar lights for every in-flight fetch in every section and a small icon beside "refreshed Ns ago" spins while busy, so a refresh moves nothing on screen and the screen reader announces only the state change. The empty Plugins screen is one centered card saying what the first plugin will do, with the existing install flow behind a link rather than a second floating button. (#1444, #1445, #1447)
  • Every desktop Settings pane keeps a bounded column and rows that do not squeeze. Cards stretched with the window, so on a wide display a control could sit 1300px from its label and an intro line ran past 140 characters. All panes now share one left-aligned column, 760px wide and 980px for Projects, Aliases and Pricing which hold wide rows, and one text measure; rows stack on their own width instead of on a viewport rule the 900px minimum window can never reach, and a long path in Projects no longer overflows its pane. The Privacy & data pane joins the same title-and-hint-on-the-left, control-on-the-right row pattern every other pane uses instead of its own layout, and its rows share one left edge with real icons. In dark mode the secondary button gains a face and a border from two new tokens, so it is no longer invisible on the plugin install dialog; light is unchanged. (#1501, #1511)
  • The desktop shell stops re-rendering the whole tree once a second. AppMain owned a wall-clock setInterval(1000) that existed only so the footer could print "refreshed Ns ago", but every tick re-rendered the sidebar, the hero, the daily chart, the heatmap and every table 60 times a minute whether or not any data had changed. The per-second tick now lives in a leaf RefreshedAt component that owns just that label, and the shell itself re-renders only on real state changes plus a 15-second day check that fires exactly when the local calendar rolls over. That rollover matters because the overview memo keys bake in a today/month boundary, so midnight must produce one re-render to keep "Today" honest. Renderer render-count regressions are pinned by a churn test that fails if the shell re-renders on the per-second tick or on same-day clock checks. (#1352) Thanks @ozymandiashh.
  • Devin is priced from tokens, like every other provider, and no longer needs to be configured before it appears. Devin's transcripts stopped carrying committed_acu_cost (it is null in current builds), so a provider that converted ACUs to dollars had nothing to convert and hid every session until a devin.acuUsdRate was set by hand. Cost now comes from each step's own prompt_tokens/completion_tokens/cached_tokens and the model that produced it (generation_model per step, falling back to the session's model), priced through the same tables as Claude Code and Codex. Devin reports OpenAI-style prompt tokens with the cached share counted inside them, so that share is subtracted from input rather than billed twice. A model the tables do not carry (today swe-2-high) costs $0 and warns once, rather than being priced off a neighbour. The devin.acuUsdRate config key is gone, along with the ACU Conversion field in the menubar's Settings → Devin tab that wrote it; the key is ignored if still present in ~/.config/codeburn/config.json, and the Devin tab is now the standard provider connection pane. Task text comes from the session's own user turns, which real transcripts mark with source: "user" rather than is_user_input, falling back to the sessions.db title and then to the session's first prompt_history prompt. (#1469)
  • Quota colours use one set of bands everywhere: green under 60%, yellow to 70%, orange to 95%, red from 95%. The flame, the popover rows and the Capacity Dock rings each had their own thresholds; the dock now reads the same severity the flame does. (#1355)
  • Superseded cache files are swept after 14 days. Session-cache, Codex result and Cursor result files left behind by a version bump can be 100MB or more each; they are removed once untouched for 14 days, at most once a day. The daily cache and the Hermes ledger are never swept, because they carry sealed days that a version bump adopts rather than re-derives. (#1511)

Fixed

  • OpenClaw and Pi lose the cost they reported the same way Warp did, and it is fixed the same way. Both write what a message actually cost into usage.cost.total, and neither was on the reported-cost pass-through, so every read, cold as well as warm, replaced it with a token estimate. On fixtures that is $0.011325 reported against $0.056495 billed for OpenClaw and $0.012908 against $0.409660 for Pi. The rule is now per call rather than per provider: a cost that came from the tool's own record is carried through the cache and served verbatim, while a call whose reported cost is absent or zero (Pi writes total: 0 for xai-oauth, OpenClaw omits the field) keeps storing no cost at all, so it is still re-priced from its tokens and a later pricing update reaches it. Both providers' parse versions move, so already-cached calls re-derive once and the daily cache rebuilds the affected days from the transcripts, which are still on disk. omp, which shares Pi's parser, is unchanged. The growing chain of provider names behind this decision is now one named REPORTED_COST_PROVIDERS set with the same membership. (#1511)
  • Warp's own billing figures now survive the cache instead of being replaced by a token estimate on every read. 0.9.25 taught the Warp parser to read what Warp actually charged a conversation (total_provider_cost_in_cents, then the total_charged_usage cents, then credits_spent at the published credit rate), but warp was missing from the reported-cost pass-through in the session cache, and every non-Claude/Codex provider reaches a report only through that cache, so the derived dollars were dropped on the cold run too, not just on a warm one. Every Warp read, in the CLI, the menu bar and the desktop app alike, was re-pricing the conversation from the token floor against the aliased LiteLLM model. On a three-conversation fixture that is $0.15 reported against $1.22 actually billed. A billing-derived cost is now carried through the cache and frozen at the billed amount; a conversation with no billing record keeps storing no cost at all, so it is still re-priced from its tokens and a later pricing update reaches it. Warp's parse version and the daily-cache version are bumped, so already-cached calls re-derive once and finalized days rebuild from Warp's durable sqlite. Call counts do not move, so no day can be double counted or lowered by the rebuild. (#1511)
  • A provider-scoped run no longer vouches for providers it never walked. SessionCache.complete was one flag for a scan that is scoped on two axes, so a Claude-only refresh could stamp the whole cache complete while another provider still had discoverable sessions, and those sessions stayed missing until a full refresh repaired it. Completeness now lives per provider section: a scoped run marks only the provider it walked, an unscoped run marks every section and the whole-cache flag, which is the only thing that can speak for a provider with no section at all, and a section with no flag of its own falls back to the whole-cache flag so an existing cache still reads. That replaces the first fix for this, which withheld the stamp by running a second unfiltered discovery in the stamp path and left scoped users re-entering cold hydration on every run. Addresses #912. (#1229, #1303) Thanks @earbona23.
  • The macOS menu bar finds a Nix-provided Node. The CLI is launched through /usr/bin/env, and on a nix-darwin machine node lives in the per-user profile, which was in none of the locations the app adds to PATH, so every spawn died with env: node: No such file or directory and the popover showed no data at all. It presented as a sudden break rather than a constant one, because a macOS update deletes the launchd user path configuration the app had been inheriting those directories from. The per-user profile directory is part of the augmented PATH now. (#1263) Thanks @sm0keyyy.
  • The macOS menu bar runs the CLI with the Node beside it, not a version manager's default. The published launcher starts #!/usr/bin/env node and the inherited PATH was searched first, so on an nvm machine whose default alias is older than the 22.13 the CLI requires every spawn exited with a version error and the popover read "Could not load Today" with nothing pointing at Node. When an executable node sits in the same directory as the resolved CLI, that directory moves to the front of PATH, promoted rather than duplicated if it was already there. (#1266) Thanks @TheCrazyAnt.
  • The interactive dashboard no longer runs out of heap after a few days. dist/cli.js never set NODE_ENV and React is external to the bundle, so Ink loaded React's development reconciler, which since React 19.2 calls performance.measure() on every render, commit and component; Node keeps those user-timing entries in a process-wide buffer that nothing trims, and the dashboard re-renders on every progress tick, so the buffer grew by about 740 entries a minute until the heap limit killed it. The launcher now defaults NODE_ENV to production, which an explicit value in the shell still overrides, and the guard tick clears the user-timing buffer. Fixes #1268. (#1269) Thanks @giovanninibarbosa.
  • A project keeps its identity across providers, and a session count says when it is only a lower bound. The same working directory could split across providers, or a legacy summary with no path could attach to the wrong project. Working-directory identity is preserved through discovery, parsing, caching and aggregation: the same directory coalesces across providers, distinct POSIX paths stay separate, Windows path normalization is preserved, and an ambiguous legacy summary is left unallocated rather than guessed. Where the available history cannot establish unique session identities the count reads "At least N" and the unsupported per-session average is omitted, and a combined-device view says "Session count unavailable" instead of printing a number it cannot stand behind. Project details and charts read the same identities, with collision-safe rows and full-path accessible labels. (#1271) Thanks @avs-io.
  • codeburn doctor names the search roots for seven more providers. Forge, Mux, Open Design, OpenClaw, ZCode, Zed and Zerostack exposed no filesystem roots, so doctor could not explain an empty or missing installation for any of them. Each now reports the same paths its discovery code reads, with OpenClaw reporting its agents directory so an existing empty root is distinguishable from a missing one. Vercel AI Gateway stays the network-only exception. Addresses #899. (#1272) Thanks @avs-io.
  • Three layout bugs in Settings and the web dashboard. Opening Privacy & data compressed the desktop Settings navigation rail and clipped its labels even at the default window width; the rail no longer shrinks while the content pane still can. At intermediate browser widths the dashboard header pushed the provider selector and the theme control off the page; overflow stays inside the header controls, labels stay on one line, and the controls take a full-width second row on mobile. And the dashboard's "Most reworked" list could keep a stale row across a period change when filenames repeat, because rows were keyed by path alone; the rank is part of each row's key now. (#1275, #1277, #1280) Thanks @avs-io.
  • Disconnecting Copilot in the macOS menu bar stays disconnected. Discoverable credentials reactivated quota tracking on the next refresh or relaunch, so the provider came back however many times it was switched off. The explicit opt-out is persisted, preserved through an automatic refresh, and cleared only when you connect again; credentials and historical usage are untouched, and the Plan tab, Settings and the Capacity Dock now tell an explicit disconnect apart from missing credentials rather than offering OAuth guidance for both. Refs #1273. (#1278) Thanks @avs-io.
  • The native Stats view stops calling a selected-period total "today". Lifetime totals were labelled "Sessions today" and "Calls today"; they read "Sessions" and "Calls", with the selected period still named above them. Values and the session-count qualifier are unchanged. (#1279) Thanks @avs-io.
  • Kiro sessions reach sync attribution instead of reporting nothing. All three kiro parsers wired the session's directory to projectPath, but attribution reads session.workingDirectory, so every kiro session failed the trusted-directory check, was dropped before the commit join, and codeburn sync push --attribution reported zero facts at every window. The path was being carried the whole time, just on the field nothing consumed; the parsers emit workingDirectory from the same provider-recorded value now, with a parse-version bump so warm caches re-parse. (#1282) Thanks @Enclavet.
  • DeepSeek Harness sessions of every generation are discovered and counted, and a skipped one reaches doctor. Discovery now finds session.jsonl and session.v<N>.jsonl, .zstd included, and takes the numerically highest generation within each session directory, never falling back to an older snapshot when the highest is unreadable or unsupported. v0 and v1 usage events and v2 and v3 embedded assistant streams are parsed, retry usage is preserved, inherited fork usage is excluded, and separate legacy and newer-format sessions all contribute: only multiple generations inside one session directory are alternatives. Every discovery path that drops a session for a format reason now reports it, so doctor no longer says OK while sessions are silently omitted, and on Node below 22.15, which has no built-in zstd, the notice prints once instead of once per session. Fixes #1281. (#1284, #1301) Thanks @xiaozhenliu.
  • The Capacity Dock rings stay fresh when a payload tick is skipped. A background tick skips the whole refresh when the local session directories are unchanged, but quota is server-side state, so a window reset or usage from another machine left the rings stale until the popover was opened. A skipped payload tick still runs the quota half, on the existing HTTPS path and the existing self-throttled cadence, so there are no extra spawns. Dock providers also refresh before the Claude and Codex awaits, so a slow or hung provider no longer delays the dock, and the display-asleep latch is replaced by a live check that cannot stay set forever if a wake event is missed. (#1300)
  • A day derived from an incomplete read is no longer frozen below what the sources on disk say. A day is derived into the durable cache once and then frozen behind the watermark, so one parse that missed sources became permanent and the Overview headline read below the Daily Activity rows printed under it. The read path already parses the dates it reports on, so it reconciles the two derivations per date and provider and keeps whichever explains more calls, ties going to the cache. Carry-forward is untouched, nothing live can outbid a day with no surviving source, and the write path is unchanged. Fixes #1217. (#1304) Thanks @therickfactr.
  • codeburn export -f json into a folder writes the file inside it. The desktop app passes the folder you picked, and the JSON writer appended .json to it, so choosing ~/Exports produced ~/Exports.json beside the folder rather than a file in it. An existing folder now gets a dated file inside it, codeburn-export-YYYY-MM-DD.json, matching the dated subfolder a CSV export already nests there; a path written with a trailing separator says folder too, and is created if it is not there yet; -o report still writes report.json. (#1511)
  • codeburn menubar replaces the copy you already have instead of adding a second one. The installer only ever looked at ~/Applications, so a CodeBurnMenubar.app in /Applications was left where it was and a second bundle, with its own login item, appeared alongside it. The install now goes to whichever copy is already there, in the same order the desktop app's Menu bar card looks. A folder it cannot write to is never escalated into: the install falls back to ~/Applications, the old copy is asked to quit, and it is named on stdout for you to move to the Trash. (#1511)
  • The export toast names the file or folder that was actually written. Both formats put the export inside the folder you pick, so the toast reported a path with nothing at it; it now reads the path the CLI prints. (#1511)
  • codeburn yield stops calling work abandoned that shipped through a merge the session's window missed. A session was labeled abandoned when no commit credited to it landed on main, but commits are credited by timestamp window, so a feature branch merged after the session ended, or merged as a GitHub squash, which lands the work under a brand-new SHA the window can never see, left fully shipped sessions reading as dead. When the session logs record the git branch it ran on (Claude Code captures this per turn; most providers do not), yield now rescues a session whose branch demonstrably shipped through a squash merge: the branch carries commits of its own, its tip's tree hash matches a commit on main (the squashed tree is byte-identical to the branch tip, with the tip different from the branch's base so an untouched or self-reverted branch cannot match), and at least one of those commits was made inside the session's own window, so the session contributed the work rather than merely having run on the branch. Every matching ref is examined (a stale local copy cannot hide a merged remote-tracking one). Abandoned and ambiguous sessions rescue to productive; reverted keeps its verdict, which already describes shipped-then-undone work. The rescue is confined to computeYield: sync attribution records keep the pure timestamp-window methodology, and the JSON methodology field is unchanged for compat. Fixes #1442. (#1476) Thanks @ozymandiashh.
  • Usage that prices at $0 is named instead of invisible, on the CLI and on every surface that already carried the count. codeburn models's default table drops every row under its $0.01 floor with nothing in the flow saying so. On the machine #1420 documents, 429.1M tokens across three routed models were absent from the table and the footer, and --unpriced was the only reveal, with no hint it existed. The default table keeps its floor and its numbers, but prints one line naming what the floor hid, 3 more models price at $0 (429.1M tok), pointing at codeburn models --unpriced to see them, also when the floor dropped every row and the table read "No model usage found"; an explicit --min-cost is the user's own cut and stays silent, and json, csv and markdown stay machine-clean. The menubar payload has carried an unpricedModels block since #638, but the popover, the tray and the dashboard never rendered it: the macOS popover's Models section, the Windows tray's Models section and the web dashboard's Top models panel each render one secondary line, N models unpriced, counted at $0 · M tokens, the wording the compare view and the MCP tables already use, with the exact ids, calls and token counts in the popover's accessibility label and the ids with their call counts in the tray row's tooltip. The popover line is localized (en, zh-Hans); older payloads without the block render exactly as before, and the desktop app is unchanged: its table already shows unpriced rows dimmed (#1443, #1474) and its compare view already lists the block. Fixes #1420. (#1491) Thanks @ozymandiashh.
  • The Models table shows every model that ran, including the ones under a cent. The CLI's models command defaults minCost to $0.01, and the desktop bridge passed neither --min-cost nor --unpriced, so the table silently dropped every row below a cent, which by construction excluded every unpriced row too (a 0 >= 0.01 filter), leaving #1443's dimming and add-alias affordances unreachable in the shipped app. codeburn:getModels now passes --min-cost 0 (and the demo bridge mirrors it), so sub-cent and unpriced rows arrive and render with their existing dim treatment; on a real lifetime corpus that recovers 10 rows and 2,160 calls the default filter hid (43 → 53 rows, verified in both themes). A row priced between $0.00 and $0.01 renders as "$0.00" without dimming: it is genuinely priced, just below the display floor. Fixes #1465. (#1474) Thanks @ozymandiashh.
  • OpenCode sessions stay counted after you archive them in the app. The provider's discovery query and its recursive subtree walks all filtered on time_archived IS NULL, with no stated reason, so archiving a session in OpenCode made it vanish from the sessions list, from per-project views, and from any cache rebuild, and on a fresh parse the lifetime totals themselves dropped. OpenCode's archive is organizational: the rows stay in the database, so the filters are gone and archived roots and children parse like any other session (verified against a copy of a real OpenCode database: three archived roots stopped exactly three sessions and 656 calls from counting). Kilo Code shares this parser and is covered by the same change. Both providers' parse versions move so a warm cache re-derives: an archived root came back on its own (it was evicted as an undiscovered source), but a root whose child was archived stayed a present, unchanged source and kept serving the parse that dropped the child. Fixes #1362. (#1475) Thanks @ozymandiashh.
  • OpenCode 2.x sessions show up again. OpenCode 2.x (mainline since 2.0.3) writes sessions to a new session_v2 table and messages to session_message, tagged by type and ordered by sequence, while the old session/message/part tables freeze at upgrade, so every session created after the upgrade was invisible. CodeBurn now branches per database on what actually exists: when the 2.x tables are there they are read and the frozen legacy tables are ignored, otherwise the legacy path runs exactly as before. Compaction requests keep their own cost and tokens counted, as they were on 1.x. Refs #1293. (#1436) Thanks @ozymandiashh.
  • An OpenCode 2.x database that was upgraded in place keeps its pre-upgrade history. The legacy session, message and part tables freeze at upgrade while new work goes to session_v2 and session_message, and discovery detected the v2 generation and read only that, so every session created before the upgrade that never migrated was silently dropped and lifetime totals were undercounted by exactly that frozen history. Legacy top-level sessions whose id is absent from session_v2 are listed too now and read from the legacy tables, while v2 sessions keep coming from session_message; a session present in both generations is listed once and resolves to v2, never summed and never read from the stale legacy row. A legacy-only database is unchanged and reports byte-identically. Fixes #1293. (#1488)
  • OpenCode's session-level fallback now bills reasoning tokens at the output rate. When none of a session's messages produce a priced call, codeburn falls back to the totals on OpenCode's session row, and that path handed tokens_output alone to the pricing table while still reporting tokens_reasoning separately, so a reasoning model's thinking showed up in the token breakdown but was never charged (4,000 reasoning tokens on Sonnet 4 left a $0.11 session priced at $0.05). OpenCode itself charges reasoning at the output rate and codeburn's per-message path already did; the fallback and the per-message path now both go through billableOutputTokens, the same rule the cache-rehydration path uses, so the three can't drift apart again. Displayed token totals are unchanged. Fixes #1334. (#1342) Thanks @ozymandiashh.
  • The Overview model table lists every model with usage in the period instead of stopping at twenty. The period-scoped model list the table reads is no longer cut at 20 rows by the payload: on a machine with 47 models every local and free model (qwen, llama, deepseek, gemini, grok) had dropped out of the table, exactly the rows whose $0 cost ranks them last, while the same change was busy making those models visible in the Models tables. Every consumer that wants fewer rows already slices its own (the desktop hero takes 8, MCP tables take 5 or the caller's limit) and the macOS menubar reads a per-session list, so nothing else renders more. Two smaller items from the same review: the menubar Models row's compact token count gains a billions rung, so a period cache-read total past a billion reads 12.3B instead of 12345.7M, with the exact value still in the row's accessibility text, and the CLI tables gain the same rung through formatTokens (a codeburn models totals row now reads 29.9B instead of 29870.8M); and the desktop Models tables stop dimming the calls column on unpriced rows, because calls are observed usage like the token columns, not a pricing artifact, while cost/saved keep their dashes and the add-alias affordance. In the shipped app unpriced rows do not reach that table yet: codeburn models defaults to minCost: 0.01 and drops rows whose cost and savings are both under $0.01, and the desktop's codeburn:getModels passes no --min-cost/--unpriced, so the change only takes effect once that filter is addressed (tracked separately). Status snapshot revision 9 invalidates older cached payloads once, so a warm snapshot cannot keep serving the truncated 20-row list; daily and session cache formats are unchanged. Fixes #1318. (#1443) Thanks @ozymandiashh.
  • An unpriced Bedrock model now reaches the Unpriced list instead of being treated as free local inference. Claude Code with CLAUDE_CODE_USE_BEDROCK=1 records Bedrock's foundation-model id, which ends in a -[v]<major>:<minor> version (anthropic.claude-haiku-4-5-20251001-v1:0, openai.gpt-oss-120b-1:0). The local-model heuristic read that colon as an Ollama :tag, so a Bedrock model with no pricing row was classed as expected-free: it was excluded from codeburn models --unpriced, from the unpriced warning and from the pricing-coverage denominator, and its metered spend showed as $0 with nothing flagging the gap. The version shape is now exempted before the tag rule runs; Ollama and LM Studio tags are unaffected. (#1463) Thanks @ulises-c.
  • Model tables add up to the headline above them. codeburn report --format json took its totals from the durable daily cache but built its model rows from surviving sessions, so on a long period the rows summed to less than the headline; they now come from the same days the headline and the daily rows do. Days recorded before CodeBurn tracked usage per model kept their cost and calls but had nothing to attribute them to, and that money simply went missing from every model table. It now shows as a row named "Unknown (carried)": the same figure, named instead of silently dropped. (#1468)
  • Codex sessions routed through codex-cliproxy-gateway now price as the model they wrap instead of silently reading $0. That gateway keeps Codex's own OpenAI OAuth routing native and forwards only cliproxy/* model ids to a local CLIProxyAPI, so every routed session lands in the rollout with a cliproxy/<id> model name, sometimes with a CLIProxyAPI provider path behind the wrapper, e.g. cliproxy/zcode/glm-5.3-flash. No such name matches a pricing row, so the tokens were counted but the dollars were not: the largest line of a day could sit at $0, and because the models table hides zero-cost rows, routed spend looked absent rather than unpriced. The cliproxy/ wrapper now peels like the other routing wrappers (OrcaRouter, Cline Pass, cmd/, …), and zcode joins the known vendor namespaces so a provider path strips to its priced glm-* leaf; an unknown vendor behind the wrapper still fails closed rather than pricing at a coincidental row, and routed rows keep their Codex provider while displaying the wrapped model's name. (#1364, #1439)
  • Codex sub-agent rollouts no longer double-count their parent's usage. A thread spawned as a sub-agent replays its parent's history, including every token_count event with its original timestamp, and names the parent under source.subagent.thread_spawn.parent_thread_id rather than forked_from_id, so the fork guard never saw it and the replayed prefix was counted again per child. The parent id is now read from either field, and the replayed events drop like a fork's. On a corpus with 381 spawned children this removed 405 duplicate calls and 403,712 duplicate cache tokens; rollouts without sub-agents are unchanged. Fixes #1380. (#1383)
  • Codex requests stay distinct when a rollout carries no cumulative usage total. The cumulative guard and the all-zero deduplication key collapsed subsequent requests whenever total_token_usage was absent, and keying on the last usage alone also lost distinct requests that happened to report equal token counts. The guard now applies only to a finite, non-negative reported total; absent, null and partial totals are treated as missing identity rather than as zero; and the fallback identity is the source path plus the physical record offset, which keeps equal-usage rows apart and stays stable across re-reads of the same file. No cross-file replay-deduplication claim is made when cumulative identity is unavailable. The Codex result cache and the provider and session caches are invalidated, so existing aggregates re-parse. (#1264) Thanks @roy-tong.
  • A Devin step run on gpt-5-3-codex-xhigh is priced at the Codex row it names, not the base gpt-5 one. Devin writes the model's minor version with a dash, and that spelling has no pricing row, so lookup fell through to gpt-5 and undercharged every Codex-backed step by $0.50 per million input tokens. The id is now normalised to gpt-5.3-codex-xhigh for pricing, the same rewrite the display name already used. (#1469)
  • Four provider parsing and pricing attribution bugs. Antigravity stores the command basename for a run command instead of the full command string, so a secret passed in a flag no longer reaches the on-disk session cache. A Copilot OTel skill or tool is credited once per trace instead of once per chat span, so a multi-span trace no longer multiplies its cost and call count in the skill and tool breakdown. The daily cache's carried-remainder row now carries a token shortfall alongside the cost and call remainder, so per-model token sums reconcile to the day total the way cost and calls already did. And Bedrock provisioned-model and custom-model ARNs are recognised as unpriced Bedrock usage instead of being hidden as local inference. Grand totals are unchanged, verified leaf by leaf against a copy of the real local usage cache. (#1489)
  • Copilot skills and shell commands are extracted, and their cost lands in the right period. Structured skill calls are read from VS Code chatSessions and the OTel spans, bash-style tool calls emit real command names instead of an always-empty shell list, and the original user prompt is taken from the message text rather than the system-heavy rendered text, so prompt attribution stops drifting. Stale Copilot caches and menu bar snapshots are invalidated, so the corrected figures are scoped by period rather than served from a cache that predates them. Fixes #1479, #1494. (#1480, #1495) Thanks @ksp2000.
  • A Claude Team seat is labelled Team Premium, not Max 5x. The plan label was derived from the rate-limit tier alone, and a Team Premium seat carries the default_claude_max_5x tier, so the tier scan hit max first and every surface called it Max 5x. The credential file's subscriptionType is the primary signal now and the tier only supplies the detail, applied in all four places that compute the label; a credential file without the field falls back to the existing tier-only rule, so nothing changes for older files. (#1492) Thanks @Swizzy.
  • A Warp database macOS will not let CodeBurn open no longer freezes the whole run. Warp keeps its SQLite file inside a Group Container, which macOS puts behind Full Disk Access. Without that permission, and with no way to show the prompt, the open never returns: codeburn status printed nothing at all, and the resident serve process the desktop and menubar talk to hung with it, so every surface went blank with no error. Any database under ~/Library/Group Containers is now opened first in a short-lived child process that can be killed; if that child does not come back within three seconds, or is refused, the database is skipped, one line on stderr says so and names Full Disk Access as the fix, and Warp is reported as a provider whose discovery failed rather than one with no sessions, so the rest of the run keeps its own numbers. The check is remembered for a minute, so the resident process does not repeat it per request and granting access is picked up on the next refresh without a restart. (#1481)
  • A blocked Warp read no longer freezes the resident serve process every minute, and a locked Grok Bot database no longer hangs its quota reader. The Full Disk Access probe ran synchronously on the serve request thread, so a denial cost about six seconds of every sixty; the recurring re-probe returns the last known verdict instantly now and refreshes off the request path, measured at about one millisecond. A probe child that cannot start on a Group Container path fails closed, with Warp skipped and the grant-access notice shown, rather than opening the file and wedging again. The desktop Grok Bot quota reader opened Cursor's database with no busy timeout, so a Cursor write lock could hang it; it waits the lock out with the same timeout the CLI uses. (#1487)
  • The desktop header lists every provider CodeBurn detected, not only the ones that billed something this period. hasUsage: false means a tool is installed and was idle in the selected window, which is a reason to grey the row, not to hide it: on Today the picker showed Claude alone while codeburn doctor listed sixteen. Idle providers now appear after the active ones, greyed, in the header picker and the Settings provider list. (#1467)
  • A partial first paint no longer replaces the finished numbers on screen. Only persistence checked whether a report was complete; rendering did not. So when the resident serve child re-derived the corpus after the local-day rollover, the partial payloads it served while indexing replaced a complete one: history.daily came back with about 27 cost-bearing rows instead of 125, and the streak, month-to-date and the medians computed off that same array were wrong with it. A finished answer is now never given up for an unfinished one, in the switch memo or on screen; the indexing banner reports the partial instead, and the next complete payload replaces it. The resident child's output memo is also keyed by the local date, so --period today cannot serve yesterday's window for up to five minutes after midnight. (#1467)
  • The desktop footer no longer reports a refresh that never happened. The clock read the age of a report snapshot restored from local storage, so a renderer restart made it announce a refresh from the previous day, and the restored payload had no row for today, so the streak pill read 0 beside a heatmap with no gaps. The clock now reports only fetches this run made, says "refreshing" while the first is in flight, and the streak counts from yesterday when today has not started. (#1467)
  • Every period headline in the desktop app comes from one aggregation. Each period held its own payload fetched at its own time, so switching period mixed snapshots minutes apart: the same six months read $20,770.90 on one refresh and $37,683.63 on the next, and Lifetime came back smaller than the six months it contains. The status payload now carries cost and calls for every headline window its live scan covered, the app reads the selected period from the newest generation it holds, and the footer clock is that generation's time. A window the scan did not reach is omitted rather than summed from the cache alone, and a filtered or provider-scoped view always shows its own numbers. (#1467)
  • The usage streak is one number for the machine. It was derived from the payload's own period- and provider-narrowed history, so the same pill read 21, 32, 38, 55 or 0 depending on which tab was open. The CLI now emits one streak taken from the hydrated daily cache across every provider. (#1467)
  • The Daily spend chart fits its card at every period. Minimum bar width plus gap times the day count overflowed the plot at 6M and Life, so bars drew outside the card and the peak guide pointed at a bar that was not on screen. Bar width and gap step down until the columns fit, a history longer than that folds into whole weeks, and the summary chips read the same series the bars do. (#1467)
  • Dropdown menus and popovers are no longer clipped by the card they sit in. The two-layer card anatomy clips its inner surface to keep the rounded corners, which cut the Spend project menu off after three rows. Every anchored surface now renders through a portal, opens under its trigger and flips above it when the space below is too short. (#1467)
  • The Spend "By branch" project picker lists repositories, not checkouts. A repository with a dozen review worktrees and benchmark clones filled the picker with identical rows; checkouts that share a git origin are one entry with the checkout count beside it and their branch rows summed. An identity with no recorded working directory is a session title rather than a project and no longer appears, throwaway checkouts collect under one entry at the bottom, and a project with no branch row in the range is left out. (#1467)
  • A period switch in the desktop app no longer re-reads every session transcript in the window. The optimize detectors run their own scan of every Claude transcript overlapping the period, separate from the parse, and it was re-read in full on every request - about six of the seven seconds a warm 30-day switch cost. A resident codeburn serve --stdio process now keeps each scanned file, keyed by the file's path and the range it was scanned for, so only files written since the last request are read again - and a file that was merely appended to, which is what a live transcript is, is read from the byte after the last complete line the previous read consumed rather than from the top, with the new lines merged into what was already scanned. A file that shrank, or whose first 4KB no longer hash to what they did when the offset was taken, is read in full instead; a line a writer has not finished is left for the next read; the recency flag every detector reads is stamped after the scan rather than during it, so a kept file is not pinned to the cutoff it was first read under, and the whole scan now shares one cutoff instead of taking a fresh clock reading per file. The memo is bounded by what it really retains, evicted by age and least-recent use, and dropped with the other in-memory memos when the process passes its resident-set guard. A file is scanned once into everything the detectors can ask of it, each item carrying the instant it happened, and the period's filter is applied to that record at query time - so one stored scan answers every period instead of each period holding its own copy and evicting the others. Measured on a 7GB corpus with the argv the desktop actually sends, with a session written between every request: rotating 7D to 30D to 6M, status --format menubar-json goes from 2933ms to 1534ms, 7902ms to 4586ms and 9167ms to 5174ms, with peak resident memory 1819MB against 2904MB. Staying on one period, 30D goes from 7253ms to 2387ms when an existing transcript is appended to (medians of 8; the branch's own spread is 0.6s to 3.1s, depending on how much was written between two requests) and from 7212ms to 2044ms when each request creates a new session file. A repeat of a query whose watched roots have not changed at all is answered from the memo outright. What is left of a warm 30D switch is split between rebuilding the period's sessions and running the detectors over it, neither of which reads a file. No cache format changed, so no cache version was bumped, and every payload is byte-identical to before. (#1467)
  • The resident codeburn serve --stdio process stopped re-deriving every period on every request. The desktop app's server keeps a memo per query and reuses it while no watched session root has changed, but SQLite rewrites a database's shared-memory index when it is read, so opening a provider database under a watched root reported a change: CodeBurn invalidated, on every request, the memo it had just produced. -shm writes are now ignored (a real write still lands in the database or its WAL, both watched), so a repeat query on an unchanged corpus is answered from the memo instead of re-deriving the whole period. Two further changes cut what a period switch costs when the corpus has changed: a change is now scoped to the days the changed file itself can hold (the days between its creation and its last write), so a finalized past range keeps its result when only today's sessions were written; and loaded cache shards stay parsed in the process, keyed by the shard file name (which carries a fresh nonce on every write, so a name that is still published names the same bytes), bounded by held bytes and evicted by age and least-recent use, so widening the range from 30 days to six months reads only the months it has not read yet. On a 7GB corpus a warm report --format json --period 30days goes from 2294ms to 1337ms and --period all from 3139ms to 1907ms; a repeat of a query whose watched roots have not changed since is answered from the memo rather than re-derived. No cache format changed, so no cache version was bumped; every payload is byte-identical to before. (#1467)
  • The dashboard no longer crashes with RangeError: Maximum call stack size exceeded on a large session history. Column widths were measured by spreading a per-session key list into Math.max(...), and those lists grow with every session, so past roughly 124,000 entries the call blew V8's argument limit and took the process down before the first frame rendered. The width measurements now fold over the values instead of spreading them, in the dashboard, the plain-text tables, the overview and the sessions report alike. Fixes #1453. (#1460)
  • The Overview no longer replaces itself with "This screen hit an error" when the act report has no totals. The realized-savings figure read actReport.data.totals.realizedCostUSD behind a guard on the payload only, so an act report without a totals block threw during render and the section's error boundary latched, and because the desktop app repaints the durable report snapshot instantly on the next launch, a single such payload persisted in localStorage (one written by an older build that answered {}) crashed the Overview on every relaunch. The read now tolerates a missing block and reports $0 saved, which is what the card already shows before the first act report arrives. (#1438)
  • The Overview side column stops painting over the models table. Between roughly 1000 and 1190px the Models card's automatic minimum width, about 490px of no-wrap content, exceeded its grid track, so the card overflowed and the side column drew across the Calls column and the sessions card; the viewport media query never caught it because the row sits inside a shell 200px narrower than the window. Cards take a zero minimum width and the body grid uses auto-fit columns with a 420px minimum, so the row stacks on its own width and a tight Calls column scrolls inside its card. Checked from 800 to 1920px in both themes: no overlap, no horizontal spill. (#1471)
  • The Spend page stops freezing with "Maximum update depth exceeded". The Sankey measured its labels, tightened the caps and remeasured, and on some label sets that cycle never settled. Each label is estimated from its full name first and caps tighten only per layout, so the measure loop is guaranteed to end. The app also bundles Inter for text and JetBrains Mono for figures ahead of the platform system fonts, keeping the per-locale CJK fallbacks, so Windows and macOS render the same faces. (#1507)
  • The startup splash flame floats on the dark canvas again instead of on a grey checkerboard. Re-encoding splash-loader.webm to halve its size dropped the VP9 alpha plane while leaving the container's alpha_mode flag set, so the checkerboard the source art carries under its transparent pixels became visible. The original alpha-carrying file is restored; the file is 2.5 MB again, because the same CRF with alpha preserved encodes no smaller. (#1482)
  • The macOS app icon is the flame at every size, not a bordered thumbnail in Finder lists and System Settings. electron-builder generated all ten icns sizes by downsampling one 1024 px master whose dark-bodied, binary-textured flame covers only the middle of its tile; below 64 px that collapses into a brown square. The bundle now ships a hand-built icon.icns whose 16, 32 and 64 px entries carry a solid orange flame on the same dark rounded tile, with 128 px and above unchanged from the master. (#1482)
  • Shared and MCP-redacted payloads no longer carry pull request urls or owner/repo labels. current.pullRequests survived both passes verbatim: each row's full GitHub url and its owner/repo#123 label named the repository to a paired peer device and to a name-redacted MCP client, the same class of leak #1337 closed for branch names. Sharing now drops the block outright, the way it already drops the project, session and per-branch rows. The MCP pass with include_project_names: false pseudonymizes url and label from the url, the aggregation key, with the same salted hash as the project and branch pseudonyms, domain-separated so rows referring to the same PR still line up while the cost, calls and totals stay untouched; include_project_names: true still sees the real urls and labels. Fixes #1346. (#1431)
  • Shared and MCP-redacted payloads no longer carry git branch names or the working directory of a session. Two identities were riding along in current: the topSessions rows added in #1316 carry projectKey, a dash-encoded absolute path such as -Users-<user>-Projects-<repo>, which redactProjectNames left verbatim even when an MCP client asked not to see project names, and the per-branch rows added in #788 carry the raw branch, which both sanitizeForSharing and the MCP pass let through, so a branch such as exp/extraction-arms reached a shared payload as typed. The MCP pass now hashes projectKey with the same salted pseudonym it already applies to topProjects[].id, hashes the drill-through sessionId on both topSessions and topProjects[].sessionDetails (a Codex id embeds the rollout timestamp the pass blanks out of date), and gives each distinct branch a stable branch-xxxxxx pseudonym, keeping the cost, calls and session counts untouched; a null branch stays null, because unbranched spend is not a name. Sharing drops the per-branch block outright, the way it already drops the project and session rows, since a pseudonymized branch tells a peer device nothing anyway. Nothing changes for the local menubar, the desktop app or the web dashboard, which read the unsanitized payload, and nothing changes for an MCP caller that passes include_project_names: true: it still gets every name as recorded. Closes #1325, #1321. (#1337) Thanks @ozymandiashh.
  • A pull request row now opens the sessions that produced it. Every other aggregate in the app drills through (a day bar, a model name, a task category, a project), but the Pull requests section only ever expanded and collapsed: the PR selection builder and the drill-through callback were threaded through the section four component levels deep and nothing ever called either, so the one report that names actual shipped work had no way into the sessions behind it. An expanded PR row now carries a View sessions for this pull request → control above its work breakdown, the same affordance a project row in Spend already offers; it selects the PR by its URL, the aggregation key the by-PR report is built on, and lands in Sessions with the chip applied and each session's per-turn contribution reconciled against its full cost. The control is its own click target rather than the row, so clicking it or pressing Enter on it never costs the expansion, and a legacy row with no per-turn detail offers it too. Two smaller items from the same review: the sessions list's return-to-the-first-page reset wrote internal state the app does not read while it owns the pagination depth, so revealing another 120 rows and then changing the sort or removing a chip left you deep inside a list that had just been reordered. The reset now rides the same commit as the change that caused it, and the depth a Back/Forward restore brings back is left alone. And the investigation chips were keyed by the label they display, which truncates a session id to 12 characters and a project to its last path segment: two chips that read alike collided into one React key, warned about it, and risked reconciling or removing the wrong one. They are now keyed by the full provider+id and project+branch identity the selection is deduplicated by. Fixes #1317. (#1350) Thanks @ozymandiashh.
  • A save is never held back for a provider whose cache entry is the only record of the spend. The resident process holds shard writes for up to 90 seconds, with an exception for durable-source providers that recognised Copilot by name alone. Amazon Quick Desktop is the same class, soft-deleting sessions from its own database, and its cache section already carried the durable flag; the hold check honours that flag now, matching the two other durable checks on the same path, so a hard kill inside the hold window cannot drop its spend. (#1514)
  • Exporting with a single provider selected no longer fails silently. The desktop dropdown offered provider display names while the CLI takes provider ids, so the export ran under a name the CLI did not know and wrote nothing without saying so. The ids are passed now, and a failed export raises a toast instead of nothing. (#1517)
  • First paint warms one period instead of forty-two reports. The prefetch warmed six periods across seven reports each, ran again on later poll cycles and kept going while the window was hidden: on a real install that was about 18 CPU-seconds a minute for five minutes and up to 2.8 GB. It now warms only the selected period, only while the window is visible, runs once, and keeps a finished warm across poll cycles. Measured idle on the same install after the first load: 0.0 to 0.8 CPU-seconds a minute, and 1.5 GB down to about 0.5 GB. (#1517)
  • Nine Settings actions that failed silently now say so. Their setters swallowed the rejection, so a write that never reached disk looked like it had worked; each one raises a toast on failure. (#1517)
  • The Claude plan card no longer claims "Not connected" before the keychain has been read. Not-yet-read is its own state now, with a Check now button, and a forced refresh no longer joins the background fetch it has just aborted, so the click gets an answer rather than waiting on a dead poll. Keychain-denied guidance survives the next poll instead of being overwritten by it. (#1517)
  • Retry tax is the cost of the edit turns that needed a retry, and can no longer exceed what you spent. It was retries multiplied by the average cost of an edit turn, which on a real corpus reported 114 percent of total spend. It is the cost of the edit turns that contained a retry now. No money total moves: a leaf diff on real data shows only the retry-tax leaves changing, and report -p month has zero differences. (#1517)

Fixed (menubar)

  • The menubar refreshes usage within five minutes while a session is running, instead of up to thirty. The background change guard only stats directories, so a session appending to its transcript looked unchanged and the app skipped every refresh for half an hour; the skip now lasts at most five minutes, and the Kimi Code session store joined the watched roots. (#1356)
  • The Capacity Dock card lists running sessions for providers whose CLI id differs from the dock id. The card matched live rows against the dock's own identifier ("kimi"), while the CLI reports "kimicode", so a running Kimi Code session read "none running"; the card now matches the provider's payload ids, which also lets the Cursor card count cursor-agent sessions. (#1357)
  • The menubar second row's quota line now names the provider the flame is warning about. "Quota remaining" picked each provider's headline window (weekly, else monthly, and only then the busiest one), which is the Capacity Dock's billing horizon, not a warning: on a machine with Cursor's API window at 100% and Claude's 5-hour window at 18%, the row reported Monthly 9.5% and Weekly 5% and read as if nothing were near a limit. Each provider now contributes its worst window, per-model rows included, which is exactly what the menu-bar flame already tints by, so the two surfaces agree. A provider backing off after a failed fetch keeps its last-known window instead of dropping out, so the row no longer vanishes, resizing the status item, for the length of a retry, matching what the dock shows dimmed. The row is also capped at 24 characters, shortening a long provider name ("GitHub Copilot 12% left · 6d 3h") rather than letting the second line more than double the item's width, and the two-line title now carries a VoiceOver label that reads as one phrase instead of a string with a newline in it. With the setting off nothing changed: the snapshot that feeds the row is no longer even built on a refresh, and the title is the single-row composition it has always been. Fixes #1310. (#1311) Thanks @ozymandiashh.
  • Every Copilot credential rung now follows its own GitHub host, not just the two read from files. #1286 taught hosts.json and apps.json to carry the host their token came from, but the rungs that carried none (COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN, gh auth token, and a token pasted into Settings) were still sent to api.github.com, so a user whose only Copilot credential is a GitHub Enterprise Cloud login got a 401 and a terminal failure with no way out. The environment rung now reads GH_HOST from the same environment as the token; the gh rung resolves the host from gh's own hosts.yml (GH_CONFIG_DIR, then XDG_CONFIG_HOME/gh, then ~/.config/gh), picking it the way gh picks the host for gh auth token and without a second subprocess; and macOS Settings gains a GitHub host field next to the pasted token, defaulting to github.com and saved in the same Keychain record as the token so the two can never drift apart. Settings refuses a host no endpoint can be derived from at the field rather than storing it, the connection row no longer promises api.github.com before anything has been fetched, and every one of these new host sources is character-validated before a URL is built, so a crafted value such as evil.com?.ghe.com fails closed with no request instead of pointing the Authorization header at a host of its own choosing. codeburn quota is unchanged: it still reads only the editor-plugin files, which already carry their host. Fixes #1306. (#1313) Thanks @ozymandiashh.
  • Copilot live quota works for GitHub Enterprise Cloud enterprises on a *.ghe.com host. Both readers hardcoded https://api.github.com/copilot_internal/user and threw away the host their credential came from, so a data-residency enterprise signed in on <tenant>.ghe.com could only ever report available: false with "Temporarily unavailable". A discovered credential now carries its host (hosts.json is keyed by host and newer apps.json files key by <host>:<app id>), and the request follows it: api.github.com for github.com and for any rung that carries no host of its own (an app-name apps.json key, COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN, gh auth token, a pasted token), and https://api.<tenant>.ghe.com/copilot_internal/user for an enterprise host. The token and the host always come from the same entry, with github.com preferred when several hosts are signed in and otherwise the first .ghe.com tenant in sorted order; a host neither rule can address, such as a self-hosted GitHub Enterprise Server install, fails with a message naming that host instead of sending the credential to dotcom, and unreachable-host and HTTP failures name the host that was tried. The macOS Settings connection row now says which host answered. Fixes #1286. (#1305) Thanks @ozymandiashh.
  • A quota window that resets now refreshes now. The menubar's 30s tick compared the reset times it already holds (Claude's 5-hour and weekly windows, Codex's two windows, every Capacity Dock provider's) against the previous tick, and forces a refresh for the provider whose window just rolled over instead of leaving the pre-reset percentage on screen for up to five minutes. Each reset instant fires once; everything else keeps the existing cadence. (#1360)
  • The popover's quota warning can be read in light mode, and says which limit it is warning about. The banner under the tagline painted its text in the same system yellow, orange or red as its 12% pill, so in light mode warning text measured 1.2:1 to 1.4:1 against the pill, and even red reached only 3.0:1; in dark mode red fell to 3.3:1. The pill keeps its hue, and the text and its glyph now take a per-appearance colour for each severity, a deep amber, rust or brick in light mode and a lifted orange or red in dark mode, that measures 5.3:1 or better everywhere. A test holds every severity at WCAG AA (4.5:1) against the pill composited over both ends of the popover's background in both appearances. The sentence also named no window. Its figure is the provider's worst window, so "Claude 70% of quota used" meant the 5-hour limit while Claude's own panel read weekly 34%, and it looked wrong. It now reads Claude · 5-hour 71% · resets in 3h 12m, with the countdown worded exactly as the Capacity Dock words it and one line per provider when several are warning. The countdown is left off when the provider reports no reset time or the reset has already passed. "Over limit" is kept for a window at or past 100%, where it used to cover anything from 90%. The flame's colour and the providers that trigger the banner are unchanged. Fixes #1343. (#1349) Thanks @ozymandiashh.
  • The Trend tooltip shows the day's total again under its model breakdown. Provider-filtered history now carries tokens, so with only Claude selected the tooltip header switched to a token count and the day's cost appeared nowhere, and the model rows could not stand in for it because only four of up to five models render. A Total row with the day's cost and tokens now closes the breakdown, as it does in the web dashboard, and the header figure it replaces is shown only for days without a breakdown. Fixes #1433. (#1434) Thanks @ozymandiashh.
  • Finding live Kimi Code sessions no longer walks the whole session store. The scan recursed through <root>/sessions looking for wire.jsonl, so every blob and file-history entry an agent had ever written was listed to find the wire files among them, about 18,000 entries for 2,000 wires, and roughly 250ms on every payload build. It now walks only the levels the layout actually uses (work dir, session, agent) and stats wire.jsonl there, which is about 23ms on the same store. A decoy wire.jsonl buried in blobs/ or file-history/ was also being reported as a session of its own, and no longer is; a missing store is still silent. (#1382)
  • The pointing-hand cursor no longer sticks after a session row disappears under it. The Capacity Dock's click-to-raise rows pushed the cursor on hover and popped it on exit, but a row that vanishes while hovered, because the session leaves the ten-minute window or the dock hides, never fires an exit, so the pushed cursor stayed on system-wide until something else replaced it. The row now pops what it pushed on the way out. Clicking such a row could also raise the wrong terminal: Claude Code writes a fresh ~/.claude/sessions/<pid>.json on every resume and never removes the old one, so several files name the same session and the first one the directory happened to list was taken. The newest is now taken instead, and if its pid fails the recycled-pid guard the row stays inert rather than falling back to a stale file. (#1382)
  • The menu bar's status-item recovery retries all fire on a persistent menu bar. The loop that re-places a parked status item gated on the menu bar being able to hide, which is never true when the bar is persistent, so one retry fired, the other two never ran, and the loop woke every 250ms for the rest of its two-minute window, about 477 no-op wakeups per launch. The gate arms only when the bar can actually hide: a persistent bar gets its three retries about 800ms apart and the loop ends in about 2.4 seconds, and an auto-hiding bar is unchanged. This does not by itself close #1148, whose remaining cause is system-side status-item state. (#1461)
  • The Capacity Dock glance popover is sized and padded in every state. The reconnect and disconnected blocks fell through an unpadded branch and ran to the panel edge, the action button sat in the corner with no inset, and a worst-case height reserve parked about 40pt of dead space above it. Every state goes through the padded notice section with the dock's 16pt inset now, the connection block is measured from the strings it actually wraps, the Today amount and its token column share a baseline, the action row has its own top gap and bottom inset, and a plan label that only repeats the provider name is dropped while a real plan such as Max 20x still shows. Reconnect drops from 269 to 259pt and the no-quota card from 186 to 122pt, with every state rendered in both locales without clipping. (#1470)
  • The Capacity Dock's reconnect text stops clipping at the smallest dock scale. It was drawn at a fixed size while its reserved height scaled with the dock, so at the 0.9 minimum it ran out of room; the text scales with the dock and the reserve is measured at the scaled font. (#1490)
  • The menu bar Settings window opens on screen. It was centered at a placeholder size and then grew, so it could land half off the display. It is created at its real size, centered on the active screen, and its frame is remembered only when it was fully visible. (#1517)
  • Changing the menu bar app's language is applied in place, so the data-access prompt does not come back every time. macOS resets the "access data from other apps" permission whenever the app quits, and macOS also holds whoever asked LaunchServices to open an app responsible for it, so a menu bar quit and reopened by the desktop app was a new process under a new responsible process and the prompt returned on every language change. The language is applied without a relaunch; a menu bar older than the release that ships this still gets the quit and reopen, and the desktop app waits for the replacement process rather than a matcher the dying one still satisfied. (#1511, #1517)
  • Simplified and Traditional Chinese resolve in the packaged menu bar. The .lproj directory names did not match what the packaged bundle looks up, so a zh-Hans or zh-Hant user got English out of a build that carried the translations. (#1517)
  • The change guard stops watching a Warp group folder that never existed. It stat'd a path Warp had not created on that machine, on every tick. WARP_DB_PATH is still honoured. (#1517)

Internal

  • Packaging no longer aborts on a checkout whose path contains a UUID. app/scripts/stage-cli.mjs resolved the CLI's production dependency closure by matching each npm ls --parseable line against the checkout's absolute <root>/node_modules/ prefix, and npm 11 and later redact UUID-shaped path segments to *** in that output, so on CI runners and scratch worktrees under a UUID directory the match found nothing and packaging died with the misleading "is the root npm installed?". The top-level package name is now read by position (after the N-th /node_modules occurrence, N counted off the real root path, which the redaction cannot move) instead of by absolute-prefix match, so redacted, Windows-separated and nested-checkout shapes all resolve; a missing npm_execpath also fails with its own message instead of degrading into the empty-closure one. Fixes #1466. (#1473) Thanks @ozymandiashh.
  • The localization catalog test can now see argument-order drift, and every multi-argument key spells its slots positionally. The catalog comparison matched specifier lists, so a key whose placeholders are all %@ passed even when the Chinese value bound the arguments into the wrong slots, exactly the drift that shipped in three early-reset keys and was only caught by rendering real data bilingually. All 42 multi-argument keys in both tables now use positional specifiers (%1$@, %2$lld, …), the parity check compares slot to verb so Chinese word order stays free while each slot keeps its type, and a new test fails on any multi-argument key that is not positional. The source scanner also stopped being blind to three shapes that put copy on screen looking like machinery: a ternary (Text(flag ? "A" : "B")), a concatenation (.help(prefix + " suffix")), and a literal bound to a let and passed by name. Text(verbatim:) stays the explicit opt-out, and the four provider-name fallbacks the new rules surfaced in HeatmapSection now route through the catalog as verbatim entries. Fixes #1331. (#1435)
  • Contribution rules and the pull request gate. Open pull requests per contributor are capped at five, a template-only description or an unreferenced large first-time change is closed by the gate, which accepts a short summary and counts only real issue references, and a UI pull request must carry before and after screenshots. The repository gained issue forms, a code of conduct, a support file, an editorconfig and a docs index, and Dependabot moved to monthly with two pull requests per package, majors excluded, now covering the Windows tray package and its Cargo crate. (#1367, #1368, #1369, #1371, #1379, #1381, #1418, #1437)
  • CI runs on Node 24 and 26 as well as the 22 line, and the slow suites got measured limits. The cache-lock step failed 6 of the last 60 runs with no output for five minutes, while a passing run takes 12 to 18 seconds on every Node version; each attempt is capped at 90 seconds and retried once, with a warning whenever that happens. On windows-latest a single CLI spawn takes 1.4 to 12 seconds, so the four-spawn status snapshot test and the c6 parser suite get limits taken from those runs, each with its reason, and both test jobs print their ten slowest cases. (#1299, #1511)
  • Five flaky or environment-dependent tests are fixed at the cause. A fresh cache-read seed could land outside the current UTC day, the ServeConnection cancellation tests raced under load, the Sessions drawer, TUI startup and dashboard Ctrl-C tests timed out instead of failing, and a project-filter test asserted an empty stderr that Node 24 and later fill with a deprecation warning. (#1298, #1344, #1345, #1432, #1477) Thanks @ozymandiashh.
  • Swift tests stop writing the real menu bar's files. ServeConnection takes its pid file as a parameter, so a test run no longer shares ~/.cache/codeburn/menubar-serve.pid with a running menu bar, and the reaper only signals a record that is a serve --stdio command. Four test files never removed their UserDefaults suites and the rest removed the domain but left the plist behind, piling up thousands of files; one shared helper forgets a suite and deletes its file, and every test that makes a suite uses it. The real-Keychain round trip runs only behind an explicit environment variable. (#1511)
  • The Windows tray app is on Tauri 2.11 on both the npm and the Rust side. (#1428)
  • The star history chart is fetched live instead of being generated, and the workflow that refreshed it publishes a review branch rather than opening a pull request. (#1274, #1414) Thanks @avs-io.

0.9.24 - 2026-09-04

Added

  • The CLI now computes the anonymous usage aggregate the desktop app and the Windows tray send, so both report the identical shape. codeburn status --format menubar-json gains a telemetrySnapshot block (schema 2) built by src/telemetry-snapshot.ts from the payload and its own turn-level reports. Its headline signal is the model x task cross: up to 8 models, each with a cost bucket, a turn bucket, a one-shot rate and up to 6 task categories with their share of that model's turns, alongside up to 12 task categories with their top models, providers, MCP servers, skills, tools, a session count and median-length bucket, and cache-hit and retry-tax shares. Every magnitude is a bucket label, never an exact dollar amount, count or duration, and every name is a model or configuration identifier: nothing in the block is derived from a working directory, project name, branch name, file path or message text. The desktop app sends the block verbatim as the once-daily usage_snapshot event when the CLI provides it and falls back to its own renderer-side builder for an older CLI, and it adds five name-only interaction events (optimize_apply, plan_set, export, compare_view, settings_change). Telemetry is off by default in the EU/EEA/UK/CH and when the region is unknown, on by default elsewhere (the first-run consent screen is not shown in this version; onboarding completes with that regional default), and switchable at any time in Settings > Privacy & data; README documents exactly what each event carries. STATUS_SNAPSHOT_VERSION moves to 4 so persisted status snapshots re-derive with the new field.
  • Bundled pricing is refreshed for this release. The primary LiteLLM snapshot moves from 4669 to 4739 models: 71 added, 1 removed (azure_ai/deepseek-v4-flash-0731, re-added under LiteLLM's new casing) and 18 repriced. The fallback table moves from 200 to 203 rows: 4 added (claude-fable-5.1:batch, gemini-3.8-flash:batch, glm-flash-latest), 1 removed and 4 repriced. Among models CodeBurn users actually run, kimi-k2.5, kimi-k2.6 and their Azure rows plus azure_ai/deepseek-v4-flash and azure_ai/deepseek-v4-pro gain a cache-write price; deepseek-v4-flash-0731 moves to $0.20 input and $0.40 output per million; mistral-medium, magistral-medium-latest and magistral-small-latest are repriced with a cache-write rate added; Qwen/Qwen3.8-2.4T-A95B drops to $2.00 input and $6.00 output per million with its cache-write rate halved; and gpt-realtime-2 output rises from $16 to $24 per million. New rows of note are gemini-3.8-flash, grok-build-latest, gpt-6-astra, glm-5p3-flash, azure_ai/kimi-k2.7-code and the us-gov Bedrock Claude Opus 4.8 and Sonnet 5 rows. (#1254)
  • codeburn quota prints live capacity for every coding tool you are signed in to on this machine. The new command lists each provider's quota windows as a table, or as machine-readable output with --format json. Ten adapters ship with it: Antigravity, Claude, ClinePass, Codex, Copilot, Cursor, Gemini, Grok, Kimi and Z.ai. It landed together with the Windows Capacity Dock, which is its first consumer. (#1228)
  • A provider that records tokens but no dollar cost stays visible in the agent tabs. The tabs filtered strictly on cost > 0, so Hermes on a flat subscription, free-tier models and any unpriced model vanished from the strip even though their usage existed. The menubar payload now carries a per-provider activity signal and the tab strip keys visibility off that instead: a provider with real usage stays listed at $0 recorded cost, while a provider merely discovered on disk stays hidden. The same rule is applied across the macOS menubar, the Electron renderer and the Windows payload, and an older payload without the signal keeps the previous detected-provider behaviour. (#1214) Thanks @MiguelMachado-dev.

Added (macOS)

  • The Capacity Dock hover panel is now a glance card with running sessions, today's spend and every quota window. The panel opens with the provider's logo, name and plan, then one pill per running session showing project and branch, model and elapsed time, context used and tokens left; the pill fills left to right to the context percentage in a severity tint, pills idle for more than two minutes dim, and the list scrolls past four. Below it a Today row reports what that provider burned with input, output and call count, and a column per quota window the provider reports gives the percent used, its label and the reset time, in four severity bands (yellow at 70, orange at 80, red at 90). Feeding it, codeburn status --format menubar-json gains an add-only liveSessions block covering sessions active in the last 10 minutes with model, branch, context tokens and window, start, last activity and idleSeconds, stripped by sanitizeForSharing and the MCP redactor, and costing about 115 ms warm on roughly 6900 transcripts. The Today figures are the hovered provider's, not the machine's: providerDetails entries gain optional inputTokens, outputTokens and sessions (absent rather than zero until a day reports them, STATUS_SNAPSHOT_RENDER_VERSION moves to 5), the section hides rather than borrow a global figure when the payload carries no breakdown, and a tile that stands for more than one CLI row now sums them, so Cursor covers cursor and cursor-agent, ClinePass covers cline and cline-cli, and Droid maps onto droid. The staleness line gets its own band with the same inset, padding and divider as every other block instead of sitting crammed under the header, and the panel now appears when a full-screen app is frontmost, where it had been ordered into a space you were not looking at and could be covered by any floating window. (#1237, #1246, #1247, #1249)
  • The macOS menubar can show your Grok Build quota. A native adapter reads the Grok CLI's own auth.json under $GROK_HOME read-only, keeps the bearer token in memory for the request only over an ephemeral cookie-free session, and never copies the credential into CodeBurn storage. It offers the same single-click connection flow as the other local providers and surfaces plan, quota usage, reset timing and actionable connection errors. (#1222) Thanks @avs-io.
  • The macOS menubar can show your Z.ai Coding Plan quota. A native adapter surfaces the 5-hour and weekly credit windows in Settings and the Capacity Dock, reusing an existing Pi Z.ai login read-only with a provider-scoped Keychain override as the fallback. A follow-up reads Z.ai's body-level code and success fields before parsing limits, so a rejected login is reported as an authentication error rather than a parse failure when the HTTP status is 200 but the body carries 401 or 403. (#1202, #1204) Thanks @MiguelMachado-dev.
  • The macOS menubar can show your Cursor quota. A CodeBurn-owned adapter discovers the existing Cursor app session in one click, reads Cursor's state SQLite read-only with a WAL-aware immutable fallback, validates the local token's subject and expiry, and keeps the token in memory only with no CodeBurn Keychain copy and no dependency on another provider's runtime. Monthly, Auto, API, on-demand and team-pool usage are normalized for the Capacity Dock, and Cursor appears as a live provider in Settings. (#1174) Thanks @avs-io.
  • The Capacity Dock size slider goes down to 60 percent. The lower bound moves from 70 to 60 percent in the same 5-point steps, and a persisted value below the old bound clamps to the new one. (#1209)

Added (Windows)

  • Windows gets a Capacity Dock, the edge-docked quota rail the macOS menubar has. A second frameless, transparent, always-on-top window rests against a screen edge with one capacity ring per provider reporting available: true; hovering widens the same window into a detail panel listing every quota window per provider with its label, percent used, plan and reset countdown, and the glance bubble from the macOS dock is there too. The dock drags and snaps to any of the four edges, slides in and out from the edge, persists its placement, and has settings for scale, edge, shades and gauge shape. Data comes from codeburn quota --format json, refreshed every five minutes and on demand by double-clicking the rail; a CLI too old to have the quota subcommand shows a quiet "CLI update needed" line instead of an error storm. The tray menu gains a "Show Capacity Dock" checkbox, off by default and persisted to ~/.config/codeburn/windows-dock.json, and switching it off closes the window outright rather than leaving a hidden live view running. The desktop app bundles and installs the tray MSI, owns the Menu bar and Sidebar switches in its bottom-left corner, shows the tray's Menu bar and Capacity Dock settings when they are on, and lists Plugins as coming soon on Windows; the Store route ships the tray exe inside the AppX. A second launch of the tray app could also deadlock it during startup and leave every later launch, --quit included, stuck as a windowless process; the single-instance callback now defers to the event loop and the desktop app waits for a quit to finish before relaunching. Verified on a Windows 11 ARM64 VM with installed release builds; multi-monitor and mixed-DPI behaviour is unverified on that single-display machine. (#1228)

Changed

  • The Linux snap now asks for only the paths its parsers actually open, and one credential file moved off the automatic grant. $HOME/.claude/.credentials.json leaves the auto-connect ai-agent-session-logs plug and becomes its own read-only single-file claude-quota-credentials plug with no auto-connect requested, so the optional Claude plan and quota tile now needs snap connect codeburn:claude-quota-credentials once; it already degrades gracefully when the file is unreadable. ~/.config/github-copilot is removed entirely because it holds the Copilot OAuth credential files, so JetBrains Copilot sessions are not discovered under the snap, which docs/providers/copilot.md now says. GitHub.copilot-chat was capitalised and matched nothing on a case-sensitive filesystem; it is corrected to lowercase and joined by the Code - Insiders and VSCodium variants. Grants are narrowed to what is read (~/.cline/data to its tasks subdirectory, .omp/agent and .pi/agent to their sessions subdirectories so .pi/agent/auth.json is no longer covered, .cursor/ai-tracking to ai-code-tracking.db, the claude-dev, roo-cline and kilo-code global storage entries to their tasks subdirectories, and the Antigravity roots to the five directories the parser reads) and added where a parser reads a path that was never granted, including Cline sessions, the clawdbot, moltbot and moldbot agent directories, Cursor workspaceStorage, ~/.copilot/session-store.db, IBM Bob, lingtai and zerostack. app/scripts/snap-grants.test.ts enforces the shape, including that no credential path appears in the auto-connect plug. On the CI side, snapcraft upload exits non-zero whenever the store answers "will need manual review", which every personal-files snap gets, so the publish job now treats that answer as a successful upload while any other failure still fails. (#1195, #1223, #1224)

Fixed

  • Hermes sessions on a flat subscription stop reporting $0.00, and provider totals stop leaking stale or false-zero figures into the menubar. Hermes writes estimated_cost_usd = 0.0 rather than NULL when its cost_status is unknown or included, and any non-null estimate was treated as a recorded amount, so the placeholder zero beat the token-based calculation forever; only a positive recorded estimate is trusted now, a zero falls through to the calculation, and a non-null actual_cost_usd still wins including an explicit $0. Around that, providerDetails.hasUsage becomes authoritative and fails closed for legacy payloads, provider history and cache are scoped to active provider periods so a stale provider cannot contribute a false zero, Hermes measured and estimated provenance is preserved through a version 3 session ledger with migration handling, provider paging arrows stay navigational, and versioned Claude family labels are derived so an observed id such as claude-fable-5-1 displays as Fable 5.1 without a hand-maintained entry. The refresh path snapshots its cache keys atomically and joins an all-provider refresh already in flight instead of launching competing work, and reconciles cost and call mismatches independently rather than accepting a partially stale payload. DAILY_CACHE_VERSION moves to 31, so settled days re-derive once on the first run after upgrading and then stay cached, and the upgrade-path check discovers the derived version instead of hard-coding one that drifts at every accounting bump. (#1213, #1234, #1235) Thanks @MiguelMachado-dev and @avs-io.
  • The desktop Trend and Spend charts group models the same way codeburn models does. The per-day rows behind those charts were built from the raw provider and model string while codeburn models merged by alias-resolved canonical id, so the same day's data produced two different ideas of what counts as one model: the Trend tooltip listed minimax/MiniMax-M3 and MiniMaxAI/MiniMax-M3 as separate rows against a single "MiniMax M3" row in the command. history.daily[].topModels now merges by the same normalized display name, and a merged row carries a new optional rawModels field so a cached and an uncached route stay visible; codeburn models gains the same field in its JSON, and the desktop tooltip appends the route list when more than one raw id fed a segment. Both changes are add-only to the CLI and app payload contract. Fixes #1239. (#1241)
  • codeburn plan estimates Copilot AI credits and says when the total is incomplete instead of printing a confident wrong number. Only the Copilot CLI's ~/.copilot/session-store.db rows carry an exact credit figure, so VS Code chat sessions, transcripts, the OTel database, JetBrains stores and the CLI session-state JSONL contributed nothing: the headline could read 3.9 / 20000 AI Credits while GitHub showed 19,326. Requests without an exact figure are now estimated from tokens at the model's listed API rate with 1 credit equal to $0.01, which is how GitHub bills credits including the cache-read and cache-write split, and the headline says so whenever anything is estimated, for example Copilot Max: ~9800 / 20000 AI Credits (estimated; 4 of 473 requests carry GitHub's exact figure). The bar and percentUsed follow the estimate while any request is unrated and the exact figure once every request carries one, and a session that carries any exact figure is never estimated on top of, so paired store rows and JSONL twins cannot double count. New add-only plan fields are estimatedCredits, creditRatedCalls, creditUnratedCalls and creditsNote; docs/providers/copilot.md documents which sources carry exact credits. Fixes #1199. (#1201)

Fixed (menubar)

  • The macOS menubar app no longer burns 5 to 7 percent CPU while sitting idle. Activity Monitor had it at roughly two thirds of Chrome's daily energy on the same machine, with about 650 wakeups a second and nothing on screen. Three causes are fixed. The status-item popover's hosting controller lived for the app's lifetime with the wordmark's repeating gradient sweep and the loading pulse inside it, so the closed, invisible popover re-rendered at display cadence; those animations now start when the popover appears and stop when it closes, and the popover's content view is built fresh on every open and released on close so nothing survives to churn. Every scaled Capacity Dock dimension is rounded to a whole point, because at any dock scale other than 100 percent the fractional sizes made SwiftUI's fitted content disagree with the pixel-aligned frame and re-run layout forever. And the dock's bubble panel is released on dismiss and rebuilt on the next hover rather than kept alive hidden. Measured on the same machine, idle CPU goes from 5 to 7 percent sustained to 0.1 percent, including the case that reproduced it most reliably, opening the popover during the first data load. (#1208, #1210, #1220)
  • Copilot quota is found when you are signed in through the Copilot CLI or gh rather than the legacy editor plugin files. The ring only looked for ~/.config/github-copilot/hosts.json and apps.json, which only older plugins write, so a fully signed-in user saw "No GitHub Copilot credentials found" with no hint that usage tracking was unaffected. Discovery is now a read-only chain, first hit wins and every rung tolerant of missing or malformed data: those two files, then ~/.copilot/config.json and settings.json (taking the first value with a GitHub access-token prefix and skipping ghr_ refresh tokens), then COPILOT_GITHUB_TOKEN, GH_TOKEN and GITHUB_TOKEN, then gh auth token cached for five minutes behind a 5 second deadline and never on the main thread, then a token pasted in Settings and kept in CodeBurn's own provider-scoped keychain item. The Copilot CLI's own Keychain item is deliberately not read because every read would raise the macOS access dialog, and VS Code's encrypted secret storage stays out of scope. The message now reads "No GitHub Copilot credentials found. Usage tracking still works." followed by the three ways to connect. Separately, a Free or Individual plan reports a premium_interactions window with entitlement 0, which both the menubar and the desktop decoder had drawn as a 100 percent used ring; windows with entitlement 0 or unlimited: true are now skipped in both readers. Fixes #1198. (#1200)
  • Grok unified-billing accounts show 0 percent after a weekly reset instead of an unrecognized-payload error, and only those accounts do. SuperGrok Heavy unified billing drops creditUsagePercent from its billing response after a weekly reset, and treating that as an unrecognized payload showed no usage where grok.com showed 0 percent, so an omitted percent inside an active billing period is now read as 0 percent used, matching Grok's own client; a published percent or a usable used-over-cap ratio still wins, and an expired window without a percent is never invented as 0. That rule is then scoped to unified-billing accounts only, since it had applied to any account with an open window; everyone else falls back to unknown as before. (#1242, #1253) Thanks @mh7whmyz6z-alt.
  • The CodeBurn wordmark stops vanishing from the popover header. The letters were drawn hidden and made visible only through an animated gradient masked by their shapes, so any frame where the mask or the gradient frame was not laid out yet, such as the first frame after opening or an animation retargeting on reopen, showed nothing at all. The letters now carry a static flame gradient fill of their own and the sweep is an overlay that can only brighten them, slowed from 1.5 s to 4 s each way. (#1240)
  • Checking whether a provider's credentials are present can no longer deadlock the app. The presence check and the credential store's mutation path took the same lock in opposite order; the acquisition order is now consistent so a presence read never waits on the write path. (#1212) Thanks @MiguelMachado-dev.
  • The Capacity Dock stops drifting out of position as you hover it. NSHostingView intrinsic window resizing is disabled for the rail and detail panels so AppKit is the sole owner of their geometry, and SwiftUI content-size updates can no longer race the hover frame animations and accumulate a position offset. (#1205) Thanks @MiguelMachado-dev.
  • Capacity Dock hover works over any frontmost app, not just Chrome. Hover was synthesized from a global mouse-moved monitor, and macOS only delivers those events while the frontmost application itself requests them, so hovering was dead over the Finder desktop or Terminal. A 10 Hz pointer poll on the main run loop now feeds the same passthrough and hover path the monitors use, with a same-position guard that makes idle ticks free and a lifecycle tied to the existing monitors so a disabled dock costs nothing. (#1197)

Fixed (Windows)

  • A Codex quota refresh can no longer overwrite a login that changed while it was in flight, and the tray and desktop apps stop losing each other's settings writes. A quota read refreshes the Codex token and writes the rotated one back; if you signed in as someone else, or signed out and in again, while that request was outstanding, the response belonged to the old login and overwrote the new credentials. The refresh now checks the account id, the refresh token it started from and the id-token subject before writing, and discards the response if any of them moved, in both copies of the reader. Separately the Rust tray app and the Node desktop app both write the same preference files, so a read-modify-write from two processes lost updates and a torn read parsed as {}, silently collapsing every dock setting to its default; one cross-process lock now covers all three write sites on both sides, including the dock's enabled, provider and placement writes. The cross-process test was proven to fail without the lock, and was later reduced from 100 rounds a side to 24 so that it stops starving itself against the production 2 second lock budget on a loaded CI runner, and it still catches a lost update as reliably as before. (#1248, #1251)

Internal

  • A repeatable performance harness with baselines pinned to main. An isolated-HOME fixture generator and metric runner (npm run perf:all) measure wait-path latency through the same CLI and serve arguments the desktop app and menubar already use: cold session parse, incremental reparse, first and warm period switches, cold-start wait paths, refresh and view-switch proxies, and RSS after a cold load. Baselines for each are pinned in perf/BASELINES.md against a 28 MB synthetic fixture, no product hot path is touched, and the generator refuses to write under a real home. (#1173) Thanks @avs-io.
  • Release builds are hermetic. The live LiteLLM update no longer runs as part of the release build path, the dashboard release build uses npm ci, and the reproducible-build contract is documented in CONTRIBUTING.md and pinned by tests. (#1236) Thanks @avs-io.
  • The cache-refresh lock heartbeat writes atomically, ending a long-running JSON flake. The heartbeat rewrote the lock file in place, so a concurrent reader could see an empty or partial body and fail with Unexpected end of JSON input. It now writes the next body beside the lock and renames over it, which is atomic on POSIX and a replacing move on Windows; a refused replace skips the tick and cleans up, absorbed by the nine-heartbeat staleness window. A new regression test doing 400 plain reads against a 1 ms heartbeat fails on the old code every run and passes with the fix. (#1196)
  • Real people and machines are scrubbed out of the test fixtures and docs. An audit of every commit since v0.9.23 found no credentials, hosts, customer data or committed logs, and five places where a real value stood in for an invented one: an absolute path into a private audit workspace and a personal detail in perf/README.md, a private product name as a fixture working directory, and the maintainer's OS username and two machine names inside tests that assert such strings are redacted. Every change is a rename, no assertion is weakened, and a device name that survived in codeburn-desktop-wireframes.html was caught in a follow-up. (#1250, #1253)
  • Maintainer docs match the tree again. The provider registry gains the missing Codebuff, Mux, Open Design and Zed rows with new pages for Codebuff and Open Design derived from their parsers, Gemini and Antigravity point at their real test files, the architecture page's eager provider list and test inventory are corrected, the Gemini page's described parsing behaviour is corrected, and CONTRIBUTING.md stops hard-coding a test-case count that rots on every PR and names the commands that actually work as the full-suite gate. (#1221)
  • Contributing rules for the full-suite gate, agent accountability and affiliation disclosure. Verification means the full suite on your branch against main with zero new failures, contributors answer posted review findings before opening more PRs and are accountable for what their agent submits, and a new-provider PR must disclose affiliation with the tool being added. (#1194)
  • A budget test no longer fails on every branch when CI runs before noon UTC. It seeded the current month's spend at today 12:00 UTC, which is in the future for any run between 00:00 and 12:00, and future-dated rows never surface in the overview, so the budget line never rendered. It now seeds five minutes in the past, pinned to the month start on the 1st. (#1216) Thanks @MiguelMachado-dev.
  • Two payload comparisons ignore the wall-clock fields the new liveSessions block carries. The menubar parity test builds the payload twice over one warm cache and expects a match, and idleSeconds differs by one across a second boundary, so stableShape now zeroes it the way it already blanks the generated stamp. The upgrade-path check comparing a payload built with and without workers strips liveSessions outright, since idleSeconds and lastActivityAt differ between the two invocations by design. (#1238, #1245)
  • An overridden Claude home is a strict credential-source boundary. Isolated tests and sandboxed probes no longer fall through to the operator's login Keychain, while normal file-then-Keychain discovery is unchanged when no override is active. (#1172) Thanks @avs-io.

0.9.23 - 2026-08-29

Added

  • codeburn sync push sends additive CB-3 usage-span fields from the Teams boundary spec section 7. Every new attribute is optional and emitted only when its value is proven, so a receiver ignoring unknown attributes loses nothing. Sessions with provider-recorded lineage (the #1140 field) now carry ai.work_unit_id (deriveTraceId of the root session, the same derivation trace ids already use, resolved by the #1145 work-unit resolver), ai.session_role (root|child), and ai.lineage_evidence (provider-recorded) - all three or none, never inferred, and a child whose parent is out of range, ambiguous, or cyclic fails closed with none of the three. Cache tokens ride as ai.cache_read_tokens / ai.cache_write_tokens, billable-consistent with ai.input_tokens via the display layer's max-of-both-vocabularies convention, each sent only when non-zero. ai.call_count and ai.session_duration_ms describe the span's session (span count, and last-minus-first provider-recorded event time, omitted when either timestamp is missing or unordered). ai.subscription_covered reports the plan/proxy-path machinery's decision and is omitted when it cannot decide (no plan match and no cwd to proxy-check). Each export batch also carries a codeburn.coverage_through resource attribute with the ISO date the local corpus is complete through, stamped only when a complete parse finalized the daily-cache watermark. Span ids, trace ids, and ledger keys are unchanged, so nothing re-sends; new string values pass the same #1128 serialization sanitizers, with the lineage trio falling together if any value fails. sync push --dry-run stays zero-push and now reports how many spans carry lineage, cache tokens, and each subscription_covered verdict plus the coverage watermark, and the field disclosure in docs/sync/README.md (which also back-fills the previously undocumented ai.speed and ai.cost_estimated) and docs/sync/DEVELOPER.md lists every new attribute. Implements slice CB-3 of the CodeBurn main/Teams boundary spec (CODEBURN-MAIN-TEAMS-BOUNDARY-v0.1). (#1135, #1140)
  • codeburn sync auto adds opt-in, consent-once automatic sync that never sends until you accept. sync auto enable --cadence daily|hourly [--attribution] [--accept] first prints the full plain-language disclosure - every outbound field carries a written meaning, pinned by a test asserting the meanings map covers CORE_SYNC_ATTRIBUTE_KEYS exactly - and without --accept stores nothing at all. Acceptance is recorded as a sha256 fingerprint over the org, destination, wire contract version, the sorted outbound field set, your work-matching choice, a 7-day scan scope, and the cadence; enable, run, and status build that input through one shared function so they can never disagree. The scheduler-invoked sync auto run sends only on an exact fingerprint match through the same push path as manual sync push (attribution included when and only when it was accepted); if the config is killed or was never accepted it writes a receipt and makes zero network calls, and if the accepted terms have since drifted it writes an acceptance-required receipt and again sends nothing until you re-accept. sync auto disable is a hard kill switch: it removes the scheduled job and stops all automatic outbound immediately, leaving manual sync push untouched. Every automatic attempt - sent, skipped, killed, or drift-blocked - is recorded as an append-only JSONL receipt beside the sync config. macOS installs a LaunchAgent; other platforms print a crontab line to add. Implements slice CB-4 of the CodeBurn main/Teams boundary spec (CODEBURN-MAIN-TEAMS-BOUNDARY-v0.1). (#1153)

Added (macOS)

  • The macOS menubar gains a native Capacity Dock - an optional floating usage widget that rests against any screen edge. It sits collapsed on a single provider and expands on hover or click into per-provider quota rings, opening its usage details inward from whichever screen edge it is docked to, and drags across edges with surface-tension shoulders, usable-screen recovery, and shape-aware click-through. Each ring reads a provider's quota - the weekly window when there is one, otherwise the monthly - as a headline gauge whose colour shifts by severity as it fills (green below 50%, then yellow, orange, and red), with plan labels and inline hide/connect actions. Connected subscriptions surface themselves automatically, up to five, until you curate the set yourself, after which your choice is kept. It ships Graphite and Liquid Glass appearances and circle or squircle gauges, with Apple-system typography, warm off-white text, reduced-motion behaviour, and VoiceOver labels. Provider credentials stay provider-scoped in CodeBurn-owned Keychain accounts, and catalog entries without a live quota adapter stay informational and can never claim a connection. (#1167)

Fixed

  • OMP-provider rows now respect the period filter, attribute cost correctly, and price Grok 4.6 at its high tier. codeburn models -p <period> had returned an identical OMP aggregate at every scope because the session-header branch dropped its timestamp, so an absent entry.timestamp handed the range filter an invalid date and the today row reflected a cumulative rather than a daily total; timestamps are now retained for date slicing. A valid alias plus price override had still produced a zero-cost OMP row because any finite msg.usage.cost.total was treated as authoritative and OMP writes cost.total = 0 for xai-oauth ids; a zero reported cost is now treated as absent and repriced through the alias table and overrides, while a nonzero reported cost keeps its existing behaviour. And Grok 4.6, which xAI prices in two tiers, gains its 200k prompt-token tier ($4.00 / $1.00 cached / $12.00 output, applied to every token in a request once prompt tokens - input plus cached input - reach 200,000 inclusively), where the bundled card had carried only the low tier and under-reported large-context traffic; an exact configured priceOverrides entry still wins over the built-in tier. CACHE_SCHEMA_VERSION moves to 3 so cached rendered costs recompute. (#1150)

Fixed (desktop)

  • The desktop app paints the last exact totals immediately while the selected period refreshes. On a large corpus, switching Today / 7D / 30D / Month / 6M / Life could make the app feel frozen while it rehydrated every detailed panel, and it could briefly show the previous period's total. It now paints the last exact cached headline at once with honest progress copy and drill-down skeletons behind it, pre-warms the standard horizons without a provider-by-period parse storm, and keys data, errors, and late responses to the selected period/provider so a total can never leak across filters. Partial resident hydration is rejected as an exact snapshot, boundary-sensitive Today/Month keys stay current, the session memo is capped at 96 LRU entries so date-boundary keys cannot grow forever, and progressive data is invalidated safely after configuration changes. Long activity timelines read better too: fixed weekday labels, month labels aligned to the scrolling heatmap, newest dates first, and readable long daily-spend labels. (#1157)
  • Every desktop destination stays useful across restarts instead of greeting you with a blank skeleton. Sessions, Spend, Optimize, Models, Compare, and Plans now persist versioned, compressed report snapshots per destination and paint the last exact same-key (period/provider/range) result immediately on a renderer restart, labelled as cached, then revalidate once. The in-memory LRU is bounded by both count (96) and serialized weight (12M characters) and durable storage to 72 snapshots, restart persistence is pre-capped at 300k raw JSON characters so multi-megabyte reports are never synchronously compressed on the renderer thread, and recompression and writes are skipped when a poll returns an unchanged report. Stale and partially hydrated results are rejected from restart persistence while still retained as last-good data in the live renderer, Today and Month snapshots are keyed to local calendar boundaries so yesterday's answer cannot appear under today's label, and horizons warm serially at background priority (Today to 7D to 30D to Month to 6M to Life) while normal Chromium background throttling is preserved. Quota endpoint discovery stays user-driven (opening Plans), report generations are invalidated after settings changes, and the footer now describes the selected destination and ages old snapshots in hours/days. Renderer storage contains report output only. (#1163)
  • The packaged desktop CLI recovers safely when its resident stdio child dies and no longer misroutes ordinary commands. When resident stdin closes the app now detaches the dead child and falls back to one-shot execution rather than turning a closed pipe into an EPIPE failure, and the generated Electron-as-Node launcher preserves serve --stdio dispatch without swallowing ordinary CLI arguments. The packaging gate now proves both an ordinary CLI command and a held-open resident ready handshake, running the packaged resident smoke under a disposable HOME/data/cache tree so packaging can never arm watchers against a developer's real corpus. (#1155)
  • Quitting CodeBurn reaps the resident CLI's whole process tree instead of leaking it. Release testing found live codeburn serve --stdio processes reparented to PID 1 after earlier runs, because a direct child-only signal cannot reach a provider parser or other grandchild - which could also strand owned cache locks. The resident CLI is now launched in its own POSIX process group and the complete owned group is terminated on normal quit (SIGTERM, then a bounded SIGKILL fallback) within Electron's existing before-quit window, with process-group ownership recorded in the serve PID file so a stale owned resident can be reaped safely. The grace period is short and bounded (750 ms cooperative plus 250 ms post-kill, inside the existing 1.5 s quit deadline); Windows retains direct-child termination since POSIX process groups are unavailable there. (#1166)

Fixed (TUI)

  • The interactive TUI is useful before a large corpus has finished re-indexing. It now paints an exact cached period first, clearly labelled as cached while source refresh continues, making Today, 7D, 30D, Month, 6M, and Lifetime immediately selectable from one normalized cached index, then stages cold-cache readiness in that order while parsing source contents once. Today falls back to 7D only when Today is exactly empty across calls, cost, savings, and tokens - unpriced or token-only activity keeps Today - and provider and period truth are preserved across rapid navigation, custom ranges, day mode, and in-flight reloads. Cached snapshots are kept out of the parse memo and the index is replaced atomically after reconciliation, and real q and Ctrl-C termination are preserved. On one heavy corpus first-visible dropped from a median of about 26s to about 3s. (#1159)
  • Rapidly resizing the terminal no longer leaves ghost panels below the frame. Resizing between wide and narrow layouts could leave pixels from the old dashboard under the newly rendered one, looking like duplicated panels even though the live layout was correct. Resize bursts are now coalesced and the stale viewport is cleared before the settled redraw, with one redraw forced even when Ink considers the next frame textually unchanged. The clear/redraw path stays disabled in screen-reader mode so accessible text is never erased. (#1156)
  • The TUI releases its cache locks on a confirmed exit. Both cache-lock families are now released before the TUI's direct process exit, so a hydrating.lock (and its sibling) can no longer be left behind after quit, while the existing first-q confirmation and second-q fast exit during indexing are preserved and the terminal is fully restored (raw mode, mouse tracking, and the alternate screen all off). Exit status is conventional: 130 for Ctrl-C, 0 for q. (#1165)

Fixed (menubar)

  • The macOS menubar recovers its status item when Tahoe parks it. The existing status item is given a stable AppKit autosave identity, and only the confirmed Tahoe parked geometry is detected; when the menu bar is revealed the app recovers with at most three supported visibility pulses and never removes or recreates the status item. (#1161)

0.9.22 - 2026-08-25

Added

  • codeburn sessions --by-work-unit groups the sessions report into provider-recorded work units. The new deterministic resolver (src/work-units.ts) folds each session carrying the #1140 lineage field under the orchestration root it names, giving one row per unit with the root's title and project, cost/calls summed over root plus children, and a child count, with the children listed indented beneath and standalone sessions rendered exactly as before. A work unit's id is deriveTraceId(rootSessionId), the exact trace-id derivation sync already uses, so a unit's identity matches the root's existing trace identity. Evidence stays strictly provider-recorded: a session without lineage is its own standalone unit with role unknown, a child that names an in-range parent folds even when the parent recorded nothing (one-sided evidence), and anything ambiguous fails closed per the boundary spec's MAIN-02 - a parent id outside the parsed window leaves the child ungrouped, cycles and self-references are broken and marked unknown, and duplicate or cross-provider id collisions never fold. --format json with the flag emits an add-only { sessions, workUnits } envelope: sessions is today's row array unchanged, workUnits is the resolver's full partition (standalone sessions included, so counts and totals reconcile). Default output without the flag is byte-identical, no wire/sync or daily-cache behavior changes, and a pinning test asserts the grouped footer total equals the ungrouped total. Implements slice CB-2 of the CodeBurn main/Teams boundary spec (CODEBURN-MAIN-TEAMS-BOUNDARY-v0.1). (#1140)
  • codeburn status --format menubar-json can serve from a disk-persisted snapshot. A background poll whose corpus has not changed since the last computation is answered from a per-query snapshot keyed on a corpus fingerprint (ordered source topology, per-provider env fingerprints, five config hashes, currency, pricing generation, cache and package versions - any mismatch is a miss), eliminating the per-poll re-parse on idle machines. A snapshot is only ever written for a complete, non-degraded parse: the save gate requires the payload's own captured markers (stale !== true, no hydration block), so a poll that went read-only under a held refresh lock can never pin its under-reported totals to disk - a regression holds the refresh lock and asserts no snapshot file is written. Serve-side and one-shot outputs are unchanged; the optimization currently engages for --no-optimize polls (the menubar background refresh). Thanks @dgabehar, hardening @ozymandiashh. (#999)
  • The menubar status snapshot now re-validates persisted records on load, mirroring the save gate. A snapshot record whose payload carries degradation markers (stale: true or a hydration block) is treated as a miss and recomputed instead of served, so a record that should never have been written (older build, hand-edited file) can never pin under-reported totals; the save gate from #999 is unchanged. Extending the snapshot to the default optimize path was evaluated on this branch and reverted in post-build review: scanAndDetect's findings depend on mutable state the corpus fingerprint never observes (~/.claude and project-level settings.json, CLAUDE.md, defined skills, agents, commands, MCP config), and re-deriving them on a snapshot hit requires the parsed corpus, which is the full parse the snapshot exists to avoid on exactly the cold processes it serves. The optimize path therefore still never reads or writes the disk snapshot, and one-shot and serve-child outputs are byte-identical to before for both optimize values. (#1135)
  • The parser captures provider-recorded session lineage instead of discarding it. Claude agent transcripts carry a provider-written parent session reference (and the parent side records the spawned agent id when compaction has not dropped it); Kimi Code subagent sessions live inside their parent session's own directory. Both now populate an optional lineage field on the parsed session model - parentSessionId, a root/child role, and evidence: 'provider-recorded' - persisted through the session cache, with claude and kimicode parse versions bumped for the one-time re-derive. Strictly provider-recorded evidence only: no inference from time adjacency or shared projects, and a session with no evidence carries no lineage field at all. Purely additive metadata: a pinning test asserts every report total is byte-identical with the field present or absent. Groundwork for spend attribution across delegated-agent families (a measured 5.1% of sessions carry direct lineage but 76% of 90-day spend). (#1140)

Fixed (macOS)

  • CodeBurnMenubar now launches mise-installed CodeBurn correctly from Spotlight. Spotlight gives GUI apps a minimal PATH, so a persisted mise use -g npm:codeburn launcher could be found but then fail with exit 127 when its shell wrapper tried to resolve node. The menubar child environment now includes mise's stable shim directory (including a custom MISE_DATA_DIR when available), matching the existing Volta/asdf/nvm handling without invoking a shell. (#1124)

Fixed (TUI)

  • The interactive dashboard stays responsive during the cold-start background fill, and Ctrl+C exits the TUI at any moment after paint. The progressive cold start introduced by #1109 paints the dashboard from recent files, then runs a background fill that parses the deferred history and writes the per-file session cache. On a real 21k-file corpus that fill's cache save publishes one JSON.stringify + fsync per provider shard in a tight loop, and the whole save held the event loop long enough that Ink's stdin reader never ran - a q or Ctrl+C pressed in the post-paint window was swallowed, not buffered, and the user saw a frozen terminal (#1139, #1141). saveCache now yields to the event loop between every shard write, so the Ink input handler fires between publishes. The yield sits BETWEEN shards, never inside one, so each shard's temp+rename atomicity and the lost-fence cleanup that owns the canonical envelope are preserved; nothing marked seen without being parsed, the kill-safe resume invariant from #1109 is intact, and the floored-parse memo key suffix keeps warm reads byte-identical. The fill still parses every deferred file, so money totals are unchanged. The dashboard's useInput now exits on Ctrl+C through the same path as q, so a raw 0x03 from the terminal lands as soon as the loop breathes. Pinning tests: tests/save-cache-yields.test.ts (a setImmediate probe must interleave with saveCache's shard writes) and tests/dashboard-exit.test.ts (a raw \x03 exits the dashboard the same way q does). The #1109 cold-start one-shots - report, sessions, models, json/csv/markdown, the desktop and menubar payloads - are unchanged.
  • The first q during the cold-start background fill renders feedback instead of a silent drain, and a second q exits the same way Ctrl+C does. With the #1142 input fix in place, q pressed during the post-paint fill landed instantly but the exit path drained the fill first - a deterministic ~16.5s silent wait on a 21k-file corpus (#1143). The fill's indexing signal already flows to the dashboard (the #1109 banner reuses it), so the useInput handler now arms a confirmation on the first q while a fill is active and renders Finishing background index so the next launch starts warm - press q or Ctrl+C again to quit now in the footer area, with the second q taking the abrupt path. Ctrl+C is unchanged: it always exits through the abrupt path, and because #1109 made that path kill-safe (nothing marked seen without being parsed, resume converges) the two are equivalent. When the fill completes, the confirmation clears itself so a later q cannot be trapped by a stale flag. q with no fill active exits immediately as before, with no status-line flicker. Pinning tests added to tests/dashboard-exit.test.ts.

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), and custom --credits N --provider copilot. Spend is sum(finite nanoAiu) / 1e9. Token-priced costUSD does not fill the credits bar. Claude / cursor / grok / custom-USD plans stay on costUSD. (#943)

  • codeburn models --unpriced. The dashboard warns about models that price at $0 and points at codeburn model-alias, but the list itself was hard to get out of the TUI. This filters the plain-stdout models report to exactly those rows, reusing findUnpricedModels so 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)

  • optimize spots the same long block pasted at the start of many sessions. The new recurring-context detector 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 into CLAUDE.md for you, so the finding asks Claude to give the block a permanent home (a CLAUDE.md rule, 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 marked estimated: provider usage is counted per API call, where the pasted block is mixed in with the system prompt, tool schemas and CLAUDE.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 optimize run, and told plainly whether they worked. After codeburn optimize --apply, every still-applied fix comes back in an Applied fixes section on subsequent codeburn optimize runs, carrying the verdict act report already 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 exact codeburn act undo <id> that puts it back), or measuring for anything younger than the 3-day measurement window. The numbers are measured - provider-counted usage over the post-apply window - not re-estimated. --apply now says when the re-measure will happen, --format json gains appliedFixes[] (add-only), and the same section appears in the dashboard TUI and the desktop app. New codeburn optimize --auto-revert undoes the fixes that measured no reduction at all through the same code path as codeburn act undo; it never touches partial or still-measuring fixes, and never auto-reverts a CLAUDE.md rule (it prints the undo command instead), matching the --yes guardrail.

  • 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 findings codeburn optimize --apply can write itself, Habits for the behavioural ones, FYI for 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 an mcp-deferral-off caused 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 marked measured (summed from provider-counted usage) or estimated (a schema-size or recovery-fraction model), with the split reported in the header as N measured · M estimated in place of the blanket "Estimates only." footer. Sessions whose cost the provider never reported are kept out of the cost-outliers peer comparison, and a provider that only ever estimates gets the finding marked estimated rather than dropped. --format json gains class and basis per finding plus summary.measuredSavingsUSD (existing fields unchanged), and the new docs/optimize.md covers what is scanned, exactly what --apply may write, and how to read the health grade.

  • CODEBURN_CACHE_SCOPE=all forces 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 resident codeburn serve alike. 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 -p came from the default rather than from you, with no --day and no --from/--to, and only on a TTY - so piped runs, --format json/csv/markdown, report, sessions and models stay 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 persisted defaultPeriod preference 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's model field. The orcarouter/ prefix is now a routing wrapper like cmd/ / antigravity/, so a routed spelling prices at its upstream LiteLLM row instead of $0; orcarouter/auto stays unpriced (the smart route lands on a Qwen/Llama flash model - a Sonnet alias would overprice it) and the fusion routes alias to their current openai/gpt-oss-120b target (live completion model field, 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 finds the sessions you ran inside WSL. An agent running in a WSL distro writes its history to the distro's Linux home, which Windows exposes as \\wsl$\<distro>\... and never under your user profile - so a Windows-only scan reported zero for everyone working that way. On Windows, discovery now also walks each distro's home/* and root directories for Claude Code (~/.claude) and Codex (~/.codex) history and merges it with your Windows sessions; the roots are additive, so they sit alongside CLAUDE_CONFIG_DIRS/CODEX_HOME rather than replacing them, and codeburn doctor lists each one so an empty result names the missing path. Distros are read from %SystemRoot%\System32\wsl.exe (absolute, so nothing dropped next to the CLI can impersonate it) with a 3-second timeout and a 60-second, mode-aware discovery TTL; container-runtime distros are skipped. Only running distros are scanned by default, because reaching into \\wsl$ boots a stopped distro and CodeBurn will not do that behind your back - CODEBURN_WSL=all opts into every installed distro, while CODEBURN_WSL=off immediately disables discovery and UNC access but keeps historical cache rows reportable. It is a read policy, not a cache input: changing it never discards retained history. Session fingerprints drop dev/ino for \\wsl$ paths, since the 9P share synthesizes them per mount and keying on them would re-read every WSL session on every run; mtime and size still catch both edits and appends. Stopping a distro does not lose its numbers: offline rows keep counting without statting the unavailable share, including when WSL was the provider's only source. Once the owning home is reachable, an ordinary deleted transcript is evicted normally (including Codex's raw result cache) instead of being mistaken for an offline root; Claude rows carrying PR attribution retain their existing historical exception. Everything is read-only, and the tray app inherits it unchanged. (#1059)
  • codeburn menubar installs 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 resolves windows-v<cliVersion>, falls back to a scan of the newest windows-v* release carrying both assets when that tag has none, downloads the .msi with 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 runs msiexec out 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; --force reinstalls.
  • 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 accepting status --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 .msi from the windows-v* tag, which codeburn menubar now 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 on session.shutdown rollups 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 push holds 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 - the session.shutdown rollup, 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-run reports the frozen count, and codeburn sync reset --confirm re-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_HOME relocates the root), both the default zstd logs and the uncompressed session.jsonl variant. A .zstd log 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+ for zlib zstd; below that dsh is skipped with a notice instead of counted as $0). One call per (turn, step), with the step's final assistant/message usage superseding the streamed assistant/chunk sample 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.json and calls copilot_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 to GetUserStatus), discovering it through ps/lsof with 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 Google ya29. and GitHub gh*_ 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 through copilot_internal/user and the editor plugin token in ~/.config/github-copilot (preferring the github.com host), and Antigravity through ps+lsof discovery 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: a fixed 260pt sidebar with native search, SF Symbol gradient chips, independently sourced provider marks 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 binary 0101 flame 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.sh now ships the SwiftPM resource bundle inside the .app - without it Bundle.module aborts at the first icon render, which the release workflow would have shipped. (#1133)

Changed

  • Report, sessions, overview, compare, export, report JSON and menubar localModelSavings now use billableOutputTokens for 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 existing modelBreakdown key (parser short name first, then the raw id) so a leftover raw-id bucket cannot mint a $0 Opus 4.8 Unpriced orphan. Daily cache v28 re-derives finalized days so report matches 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 serve child now does for its JSON clients what the TUI got in #1107: on a cold cache it answers the first status --format menubar-json from 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 optional hydration: { complete, indexedFiles, totalFiles } block, and the desktop app and web dashboard render indexing history · N/M files · totals below cover what is indexed so far until it reports complete: 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 emits hydration, 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 of hydration therefore always means complete, which is also what an older CLI's payload means. It is not stale (#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, one stat, one state.json read 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/readdir order 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 wasted subagents/ 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. Warm codeburn today on 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 throttled saveProgress callback 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. parseProviderSources now takes the same callback, invoked once per source right after that source's cache entry lands (mirroring scanProjectDirs' 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/model is 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 spellings kimi/, mimo/, zhipu/, litellm_proxy/ and openai_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>-shm and <db>-wal in the database's own directory, so a source on read-only media, under restrictive permissions, or inside a Flatpak/snap confinement failed with attempt to write a readonly database (or unable to open database file when a -wal was 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 with immutable=1, which costs nothing and cannot go stale; a database with a non-empty -wal is copied with its -wal into 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.usage records Grok CLI already writes into updates.jsonl (inputTokens, outputTokens, cachedReadTokens, cacheCreationTokens, reasoningTokens), deduplicated by prompt_id and emitted as one session-level call from the top-level totals. The previous parser reconstructed an estimate from the running _meta.totalTokens context 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. modelUsage only selects a priced attribution id; multi-model rate attribution stays out of scope, so one session is priced at one model's rate. costUsdTicks is 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 one turn_completed record, 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.json and 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 takes max(pendingFiles / 50, pendingBytes / 200 MB), and the per-thread memory budget is derived per parse as clamp(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 cold status drops 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 is min(cores - 1, min(0.25 × available, 2 GB) / 256 MB, pendingFiles / 50), where available is process.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=0 forces the serial parse and CODEBURN_PARSE_WORKERS=N forces N (capped at the core count), both bypassing every gate; CODEBURN_VERBOSE=1 prints 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/week skip 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_DIR when set, otherwise ~/.cache/codeburn. XDG_CACHE_HOME is 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 an indexing history · N/M files · totals below cover what is indexed so far banner 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 on isTTY with no --day and no --from/--to, so one-shot JSON/CSV/markdown, report/sessions/models and 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 parseAllSessions calls - 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. month goes from 2 parses to 1 and today from 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, warm today in the TUI goes 5.12s to 3.81s and month 5.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-files read of every AI tool's root - $HOME/.claude, $HOME/.codex, $HOME/.cursor and 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 own auth.json to 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: .lingtai is dropped, because its per-agent log directory needs a wildcard the interface has no form for, and optimize, context-budget and act no longer see the user-scope ~/.claude/settings.json, agents/, skills/ and commands/ - project-scope copies still work through the home plug. 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.lock behind 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 resident codeburn serve alike set CODEBURN_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 the process.exit an 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 report and plan were 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 sets CODEBURN_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. Resident codeburn serve requests 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 no ps and no stdin-close recourse after a crash - signalling only when that pid still runs that exact command. Separately, codeburn serve drains 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 .completeFileProtection outside 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 with O_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 same codeburn.loginItemRegistered guard still limits this to the first launch, so a login item you removed by hand stays removed. (#1026)

  • The resident codeburn serve child. 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 +40 rather than a silent shrink. Explicit $0 is treated as recorded. Ledger publication failures are retryable and hold the daily watermark. (#916)

  • codeburn doctor now probes the five remaining fixed-location providers. codebuff, devin, gemini, kiro, and mistral-vibe implement probeRoots() 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 or CODEBUFF_DATA_DIR pins one; Devin reports transcripts plus sessions.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-review consumes 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 is auto, not the synthetic warp. kimi-for-coding-highspeed (the SKU #968 was filed around) is now honestly $0. big-pickle was dropped: it appears under OpenCode, not as a cited ClinePass codename. codeburn model-flat-rate --remove now 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 as function_call arguments (#656). Codex has two other shapes for the same exec: its custom-tool transport records the shell tool as a custom_tool_call whose payload is an input program rather than arguments, and its item model repeats a finished command as event_msg/item_completed carrying a CommandExecution item with an argv command. 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's SKILL.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 key pi derives for a native skill read (#588) and the same vocabulary the Claude parser records from the Skill tool. A grep/rg/ls that merely mentions a SKILL.md is 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_VERSION 13 → 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-codex and gpt-5.6-codex-max now 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). getModelCosts already resolved both through the gpt-5.6 prefix 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. Raising MIN_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>.json the 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 satisfy input + output == total - but CodeBurn added reasoning_output_tokens to 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 raw reasoningTokens figure 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 shared billableOutputTokens helper, so a cold run and a warm run can never disagree. Separately, Codex's cache_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 missing gpt-5.6-codex snapshot 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/taskGeneratedTokens in the Codex parser and generatedTokens in the codex-tps live-throughput reader still summed outputTokens + reasoningTokens; both now go through the same billableOutputTokens('codex', …) helper #1075 introduced, so the numerator can never drift from the billed one. (2) Codex fires task_started before it assembles the request, so the gap up to the first request-context event (turn_context, world_state, event_msg/user_message, or a response_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-shot codex exec sessions that pay the gap on every task. The duplicated tool-interval clip/merge/cap logic in providers/codex.ts and codex-throughput.ts is now one function (mergeToolIntervals, exported from codex-throughput.ts), which also closes a live trap where task_complete's duration only parsed a plain number and silently dropped the {secs,nanos}/string forms mcp_tool_call_end already 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 earlier prevCumulativeTotal guard 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/toolWaitMs are 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 labelled Tok/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_meta for the first "model" string anywhere in the payload, so a nested base_instructions.provenance.model was read as if it were payload.model - and since the model is last-writer-wins state, that wrong value was credited to every call before the rollout's first turn_context and to every call after any mid-file session_meta (29 of 1380 rollouts on one real corpus carry a late session_meta, and 57 record usage before any turn_context). Direct payload fields are now read depth-aware, which is what the non-fast JSON.parse path 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_meta cwd / session id / originator follow the same depth-1 window as model. #1040 fixed nested provenance.model; the compact Buffer path still took the first cwd, session_id, originator, name, forked_from_id or model_provider anywhere in the payload, so a dynamic_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-call name on 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 with codeburn 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, and kimi-k3 still has none - which means a session Hermes left costless is priced from the shared tables and carries the estimated marker, exactly as mimo-v2-flash already did. The same change fixes a pre-existing crash that this alias did not introduce: the shipped mimo-v2-flash -> xiaomi/mimo-v2-flash alias already cycled through display-name resolution - strip the namespace, alias it back, take the leaf, repeat - so getShortModelName blew the stack on any real MiMo v2 Flash session and took every surface that names a model down with it, the models table included. Display-name resolution is now cycle-safe, and the mimo-v2-flash and mimo-v2.5 rows 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 json churned 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)

  • models and audit no longer show two identical Grok 4.5 rows. grok-4.5-build - the Grok Build harness's variant id - fell into the grok-4.5 display 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 as Grok 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.title is 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. detectContextBloat divided context by totalOutputTokens alone, 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 same output + reasoning sum 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 --unpriced below 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 --unpriced shows raw IDs instead of friendly names because model-alias keys on the raw ID. (#969)

  • codeburn models --unpriced --top N returned nothing for a --top N smaller than the number of priced models. --top is applied inside aggregateModels, 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.

  • optimize no longer treats subagent transcripts as your sessions. Claude Code writes each subagent's transcript to its own subagents/agent-*.jsonl file with isSidechain: true on 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, the low-worth-sessions, context-bloat, cost-outliers and capability-reliability detectors, the coaching notes, the file-churn table, the median time-to-first-edit, the worst one-shot category, and the model-default recommendation - plus duplicate-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-reads and read-edit-ratio still count calls made inside a sidechain, since reading node_modules or editing without reading is the same waste whoever does it and the CLAUDE.md rule 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, and isSidechain now 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 in status, 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)

  • optimize no longer offers claude mcp remove for claude.ai connectors, and its MCP schema-cost estimate is per session. A claude_ai_* namespace that no readable local MCP config claims is a claude.ai connector, managed through /mcp or 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 --apply plans 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 lowers mcp-low-coverage estimates 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. flatSlice skipped 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, v2 workspacePaths[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 a pendingRederive is 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 doctor reports 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 sync no longer puts a filesystem path or a raw output count on the wire. Every synced span carried ai.project as the slugified absolute path (-Users-you-Projects-thing), contradicting both docs/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, and codeburn 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 - and git.repo disambiguates them. Separately, ai.output_tokens was 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 same billableOutputTokens helper 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.project comes only from a provider-recorded absolute cwd, attribution resolves repositories only from that same trusted cwd and derives its ai.project from the normalized git.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 running sync; Hermes prompt-derived paths, including the legacy Windows Current working directory: messages, stay local grouping labels and never become outbound provenance - a deliberate privacy correction; Goose propagates its own sessions.working_dir field 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.project remains 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 --attribution copied ai.session_id onto session and commit spans even though deriveTraceId(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 on ai.session_id must key on traceId, 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 doctor names 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/.codex as 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 ~/.grok is the real store, and Codex discovery returns nothing for a home nested under ~/.buzz while a distinct primary Codex home exists. A sole Codex home that does live under .buzz is still counted. Spend stays on the Codex row; no Buzz parser and no rates were added. (#1099)

  • codeburn models resolves raw ids the way report does, 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, which cursor-agent and vercel-gateway also 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, so models --format json is not rewritten to a lexically smallest spelling, and a Codex credit row mixing rated and unrated buckets partial-sums the rated ones and reports creditsIncomplete rather than a confident total. audit still 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.shutdown rollup, and a journal whose legs carry sessionStartTime but 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" and CLAUDE.md into 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 all and --provider claude keep the shipped Claude and CLAUDE.md copy; codex says Codex and AGENTS.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 write CLAUDE.md only 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 resize event from process.stdout with the live columns/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)

0.9.20 - 2026-08-10

Added

  • Desktop panel fetches drop from seconds to milliseconds. The app, the web dashboard and the macOS menubar now hold one resident codeburn serve process instead of spawning a fresh CLI per panel: the parsed session cache stays warm in memory, panel bursts share one parse, filesystem watches over every provider's discovery roots let a no-change fetch skip the scan entirely, and the web dashboard prefetches every period tab at startup. Cold start, mixed CLI versions and any serve failure all fall back to the exact spawn behavior shipped today, and one-shot CLI output is byte-identical. (#956, #957, #959, #960)
  • Spend punchcard in the desktop app. The hour-of-day × weekday spend matrix from the web dashboard, on the Spend page, fed by a dedicated timeline fetch so every other panel keeps its lean payload. (#962)
  • Top pull requests in the menubar. The popover shows the period's top three PRs by attributed spend under Models; hidden when the payload carries none. The Workflow strip is retired from the popover — those metrics live in the desktop app, web dashboard and TUI, where there is room to read them. (#962, #963)
  • Mouse-wheel scrolling in the terminal dashboard. The viewport enables SGR mouse reporting while mounted (three lines per tick, clicks stay inert, tracking restored on quit); click-drag text selection needs Shift held while the dashboard is open. (#951)
  • Credit-metered ChatGPT workspaces (Business / Edu / Enterprise) now show their limit. These plans report no rate-limit windows, so the admin-set monthly allowance from spend_control.individual_limit is shown as a "Monthly usage limit" bar in the desktop app and the menubar. (#833)
  • Combined-device scope in the desktop Dashboard, mirroring the menu bar. A Local / Combined toggle aggregates paired-device usage in the Overview hero and the menu bar badge, degrading gracefully to the local figure when a peer is unreachable; the badge then shows a dimmed reachable/total marker so a momentary drop to the local number reads as "a peer is unreachable" rather than a glitch. (#866, #867, thanks @marcreynolds)

Added (CLI)

  • Cline CLI provider. The Cline command-line agent (npm cline, 3.x) stores sessions as ~/.cline/data/sessions/<id>/<id>.json + <id>.messages.json, a layout the existing Cline provider never scanned — it requires tasks/<id>/ui_messages.json — so every CLI session was silently reported as $0.00, with no warning even under --verbose. Added as its own cline-cli provider so the shared Cline-family parser (Roo Code, KiloCode, IBM Bob) is untouched; it mirrors the CLI's own root resolution (CLINE_SESSION_DATA_DIR → CLINE_DATA_DIR → CLINE_DIR → ~/.cline) and reports its probed root through codeburn doctor. Per-message cost is metered by the CLI, so cline-cli joins the reported-cost pass-through allowlist rather than being re-priced from tokens. (#874)
  • Codex throughput tracking: per-model Tok/s in the dashboard and report, active time excludes tool wait. (#805, thanks @ihearttokyo)
  • codeburn sync push --attribution (opt-in): sends git attribution spans — the session→commit correlation from codeburn yield (codeburn.session.attribution and codeburn.commit span types with normalized repo remote, commit SHAs, merged/reverted state, and PR links). Nothing new is sent without the flag; local-only repos and Windows filesystem paths are never emitted as repo identities, and sessions whose project path no longer resolves never inherit the push-time working directory's repo. See docs/sync/README.md "Git attribution".

Fixed (CLI)

  • Copilot claude-haiku-4.5 store rows now price correctly instead of $0. The Copilot session-store.db writes the model as claude-haiku-4.5 (tier-first, dot), but no pricing alias existed for that raw id, so calculateCost returned $0 while GitHub billed the real Anthropic rate (e.g., $0.0063536 for a sampled request). Added an alias to the existing correctly-priced claude-haiku-4-5 row (no new rate invented). The daily cache bumps from v26 to v27 to force re-derivation of already-finalized days, since this id has no self-heal via prefix fallback. (#1093)

  • A pull request no longer swallows a whole repo's spend. The working-directory correlation rule attributed every session sharing a checkout with a PR-linked session, with no time bound — a repo whose only captured PR link was pasted once attributed a month of unrelated work (129 of 131 sessions on real data) to that PR. Checkout evidence now only attributes sessions overlapping the linked sessions' own activity window. (#961)

  • Shell reads finally count as reads. rg, grep, cat, git log and friends were invisible to the read-edit-ratio detector (90%+ of real reads uncounted on bash-first workflows) while every Bash call counted as a verification step, so edit → grep → edit scored as rework. One shared read-shaped-command classifier fixes both detectors; unknown or mutating commands keep the old behavior. (#941, thanks @laulpogan)

  • Phantom corrections from injected skill prose. The user-correction detector matched "the wrong answer" inside a templated skill prompt, counting the same non-correction four times on real data; "answer" left the wrong- pattern list, concrete artifacts (wrong file, wrong approach) still count. (#952)

  • Pricing coverage is floored, never rounded. 99.6% coverage with unpriced calls outstanding rendered as the "100%" reserved for genuinely complete pricing, contradicting the unpriced-model warning on the same screen. Applies to the TUI panel and the web dashboard. (#786, #783)

  • Model efficiency one-shot rate showed 10000% in the web dashboard. The payload field is already a percent; the dash multiplied by 100 again. (#958)

  • Copilot CLI sessions report their input and cache tokens. The Copilot CLI writes the same producer: 'copilot-agent' in its session.start events that VS Code transcripts carry, so content-based detection classified every CLI session as a transcript and skipped its session.shutdown rollup — the only place the CLI records input, cache-read and cache-write tokens — leaving cache hit rate at 0.0% and dramatically underreporting cost. Whether a file is a transcript is now decided by where discovery found it, never by its contents. Resumed sessions, whose legs each append a cumulative rollup, are billed as per-leg deltas so a growing session never double-counts or goes stale; the GitHub Copilot desktop app writes the same session store, so its usage is covered by the same fix. The copilot session cache takes a parse-version bump and the daily cache bumps from v16 to v17 for the one-time re-parse that heals already-recorded days whose logs still exist. (#944)

  • Copilot CLI subagent runs are attributed to their agent. Newer CLIs announce delegation with subagent.started/subagent.completed rather than subagent.selected, so delegated turns lost their agent label; the label now also clears when the subagent completes instead of bleeding onto the parent's later turns. Rides the #944 re-parse, so already-cached sessions gain the attribution. (#944)

  • --project / --exclude now apply to the headline totals, not just the detail panels. The durable headline unions the carry-forward daily cache with today's live parse, and the cached days were sliced to the requested provider but never to the requested project — so the Overview panel counted excluded projects while By Project / By Activity / By Model (built from the name-filtered parse) left them out, and the two could not be reconciled. Cost, calls, sessions and savings are now sliced out of the per-project day stats the cache has carried since v15. Tokens, models and categories have no per-project split in the cache, so under a project filter they come from the (project-filtered) live parse instead; cached days — or provider slices — carried from before v15 have no project split at all, so they cannot be attributed to a filtered project, and the terminal overview now states how much was set aside rather than folding it into the total. (#864)

  • Codex parser corrections: fork-replay no longer double-counts patch_apply_end and mcp_tool_call_end; exec is normalized to Bash; custom_tool_call events are handled; token_count lines larger than 32 KiB now parse exact token counts instead of estimating. Codex session cache bumps from v7 to v8 for a one-time re-parse. Only tool attribution changes for ordinary sessions, leaving their cost identical; sessions that logged an oversized token_count line are repriced from exact counts instead of an estimate. (#805)

  • Midnight-straddling turns keep both halves. A turn whose calls span local midnight was attributed whole to its start day, so codeburn today under-reported until the turn ended and multi-day totals mis-split it. Calls are now range-filtered inside the turn so each day gets the calls that belong to it, and By Activity and the daily turn counts reconcile with the headline. (#853, thanks @KENSHI601)

  • --provider <x> no longer leaks Claude spend into the detail panels. A provider-filtered run still ran the Claude scan, whose orphan pass re-injected every cached Claude session, so By Project / By Model / By Activity showed Claude usage under, e.g., --provider cursor while the headline was correct. (#872, thanks @ozymandiashh)

  • A degraded session parse no longer freezes daily history. A read-only parse that served a stale or missing session file was treated as complete and finalized days it never covered, freezing warm-cache ingestion; a corrupt refresh lock is now recovered rather than ending ingestion, and a legitimately idle tail is no longer re-derived on every launch. (#856, thanks @avs-io)

  • Pi / Oh My Pi transcripts with a leading title record are discovered. OMP writes a type: "title" line before the session header; discovery now scans a bounded number of leading lines for the first session record instead of requiring it on the first physical line. (#846, #859, thanks @jbspeakr, @avs-io)

  • Nine providers served silently stale numbers after you pointed their env override at a different profile or root. Kiro, Grok, Kimi, Mux, Mistral Vibe, Zerostack, Codebuff, Goose and Crush each honor an env var that relocates where discovery looks, but the var was never declared in the provider env fingerprint, so the cache section survived the change and kept reporting sessions parsed from the old root — with no diagnostic anywhere. The fix declares those vars, the adjacent OS-set path variables that resolve a discovery root for Claude, IBM Bob, Open Design and Kilo Code on Windows and Linux, Cursor's parse-budget override, and the Vercel AI Gateway credential — which must invalidate the fingerprint because a read-only refresh serves the cached report and would otherwise keep reporting the previous account's usage after a swap. Your next run re-parses the fourteen file-backed providers whose declarations changed — the nine above plus Claude, Cursor, Open Design, IBM Bob and Kilo Code — once, and only once; the Vercel AI Gateway declaration is a read-only-path correction, not a migration (its report is re-fetched on every writable run anyway); Copilot is deliberately NOT included, because declaring its overrides would force a re-parse that can drop OTel history only the cache still holds; codeburn doctor names deliberate overrides including the XDG_* vars, never the Windows ambient APPDATA / LOCALAPPDATA, and redacts credential values. (#920)

Fixed (Desktop & Menubar)

  • Menubar icon on macOS 26.5.x: best-available fix for the never-rendering status item. The app now activates with the window server before creating its status item — the half of the original #147 fix that the ghost-item fix removed — with the activation policy pinned so neither historical bug can return. Falsifiable on this release by affected 25F80 machines. (#868, #955, analysis by @ozymandiashh)
  • Punchcard tooltips no longer crop at the container edges (top rows flip the tooltip below the cursor). (#963)

Fixed

  • Claude Desktop and Cowork sessions are discovered for Windows Microsoft Store (MSIX) installs. (#611)
  • Cline tasks are discovered in every VS Code variant (VS Code, VS Code Insiders, VSCodium), not just stable VS Code. (#874)

0.9.19 - 2026-07-20

One version across every surface: CLI, macOS menubar, and the desktop app all ship as 0.9.19.

Accuracy

  • Every surface now shows the same numbers. CLI, TUI, menubar, desktop app, and web dashboard totals all come from one durable aggregation path and match exactly, including history whose session logs have since been deleted; the terminal overview notes how much was preserved from expired logs. (#755, #760, #759)
  • Never lose history again. The daily cache carries forward every (day, provider) slice a re-parse can no longer derive, and adopts days from older cache generations instead of wiping them on schema changes. (#755)
  • True Lifetime period on the CLI, dashboard, desktop app, and menubar. The desktop tab formerly labeled "All time" showed a 6-month window; it now says "Last 6 months", and Lifetime is the real all-time view. (#753, #759)
  • Yield repo grouping is case-correct on macOS/Windows; skills usage is attributed regardless of turn category; daily-activity history scans are bounded. (#751, #745, #727)
  • Days before recorded history render as "No data recorded", never as a currency zero, in the desktop heatmap, daily charts, and web dashboard. Genuinely idle days keep their true zeros. (#765)
  • Incremental append parsing falls back to a full re-parse when a streamed assistant message restates across the append boundary, fixing a rare over-count on image-heavy sessions. (#772)
  • Turn-level stats (edit turns, one-shot, category counts) attribute to exactly one provider slice, so per-provider sums always equal day totals. (#762, thanks @ozymandiashh)
  • Workflow-intelligence accuracy pass from review: corrections count only follow-up prompts, file-churn paths are separator-normalized so the payload's basename privacy redaction works on Windows, pricing coverage excludes deliberately-free local models and reports null (never a fabricated 100%) when not computable, and time-to-first-edit is null rather than mismeasured on unparseable timestamps. (#763, review by @ozymandiashh)
  • Daily-history retention extended from 2 to 10 years so carried days can never age out of the durable record; quota pace guards non-finite inputs; the exchange-rate cache honors CODEBURN_CACHE_DIR. (#764, #766)

Added (CLI)

  • Quick Desktop provider — Amazon Quick Desktop usage from ~/.quickwork, with real metered costs and multi-profile discovery. (#735, thanks @gjmveloso, @Enclavet)
  • Kimi Code provider — Kimi Code CLI (kimi-k3) wire sessions from ~/.kimi-code. (#750, #747)
  • Workflow intelligence in optimize and the payload: user-correction rate, median time to first edit, most-reworked files, pricing coverage, and coaching notes. (#756)
  • Richer session capture: git branch, lines added/removed (counted from diffs, never stored as text), interruptions, tool errors, session titles, and PR links now land in the local cache for upcoming per-branch and code-impact reports. (#758)
  • Provider-agnostic quota window model with provenance; doctor warns when transcript retention is about to expire history. (#740, #757)

Performance

  • Large-line session parsing is ~2x faster (single-pass field extraction), and files that grew by append are parsed incrementally from the cached offset instead of from byte 0. (#752, #749)
  • Concurrent CLI, menubar, and MCP processes can no longer clobber each other's cache work: the warm session-cache refresh runs under a strict cross-process gate with heartbeat, staleness takeover, and a publication fence; a timed-out waiter serves the prior complete snapshot read-only instead of racing. (#743, thanks @avs-io)

Desktop app

  • Windows fixed: the CLI is now found on every Windows install (path handling was POSIX-only, breaking 100% of Windows installs). (#733)
  • In-app update notifications and an About-dialog update check; this is the first release existing 0.9.17 installs will be notified about. (#722, #738)
  • Faster and calmer on data-heavy machines: CLI spawns are capped and prioritized so clicks never queue behind background work, provider prefetch is paced, and the default period is Today. (#748)
  • Telemetry (opt-in, anonymous) reports per-provider spend buckets, richer error detail with a per-kind daily cap, and a reliable session-close beat. (#736, #742, #746)

Menubar

  • Codex quota windows show linear pace: deficit/reserve, projection, and run-out ETA. (#728, #726)

0.9.15 - 2026-07-02

Added (CLI)

  • codeburn context. See what fills a session's context window, by role, block type, and tool: an interactive terminal browser over Claude Code and Codex sessions that separates the live window from compacted history and anchors estimates to the exact API-reported context size. Also available as a Context page in the browser dashboard and scriptable via codeburn context <id> --json. (#592)
  • Zed provider. Zed's built-in agent is tracked: per-request token usage with full cache fields, topped up to each thread's exact cumulative counter and validated token-for-token against a real store. (#594, format documented by @chatzinikolakisk in #480)
  • codeburn audit. Per provider-and-model table of where every number comes from: calls, input, output, reasoning, cache read/write, cost. (#578)
  • User price overrides for any model via codeburn price-override. (#390, #560, thanks @ozymandiashh)
  • open-design provider for per-model usage tracking. (#559, thanks @ozymandiashh)
  • Browser dashboard: fully mobile responsive (#582, thanks @ele-yufo; #589), instant first paint with the local payload inlined, defaults to today, and fast-fails offline paired devices. (#573)

Fixed (CLI)

  • Cursor tokens are Cursor's own numbers. Input comes from the per-conversation context meter instead of text-length guesses, credited once per conversation on a stable anchor so daily history stays consistent across re-scans; tools and shell commands come from the agent stream; Composer house models price at Cursor's published rates; figures are flagged estimated where they are. (#574, #575; closes #326)
  • Copilot Chat users no longer see $0.00: VS Code core chatSessions journals are read for token counts. (#555, #563, thanks @ozymandiashh)
  • Codex sessions up to 4GB are parsed (streaming cap raised). (#569)
  • Devin supports ATIF v1.7 (#570, thanks @tvcsantos) and reports friendly GPT model names with effort tiers. (#585)
  • OpenCode skills and subagents breakdowns are populated. (#557, thanks @KevNev19)
  • Pi native skill loads classify as Skill, not Read. (#588, #590)
  • Cache read/write scoped to the selected period in web and devices CLI. (#583, #586, thanks @ozymandiashh)
  • Web rejects invalid dashboard periods instead of exiting. (#554, thanks @ozymandiashh)
  • Pricing: LiteLLM snapshot refreshed; MiniMax-M3 follows MiniMax's tiered pricing (standard tier $0.30/$1.20 per M). Daily cache bumped to v10 so history re-hydrates under the new Cursor accounting and pricing.

macOS menubar

  • Local/Combined usage toggle backed by combined multi-device data in menubar-json. (#566, #567, #568, thanks @ozymandiashh)
  • Update dialog detects a codeburn CLI too old to install menubar updates (pre-0.9.9) and shows the exact CLI upgrade command first. (#593)

0.9.14 - 2026-06-22

Added (CLI)

  • Browser dashboard. codeburn web serves a local React dashboard in your browser with the same task, model, tool, and project breakdowns as the TUI, plus charts. Data is read locally and the server binds to localhost. (#531, #533)
  • Combine usage across your devices. codeburn share exposes one device's usage over your local network (PIN-paired), and codeburn devices shows combined totals by machine. Devices can also be discovered and paired from the browser dashboard. (#532, #534, #536)
  • New providers: Grok Build (#521), ZCode (z.ai GLM-5.2) (#537), Hermes Agent (#544), Kiro CLI sessions (#502), and zerostack (#519, thanks @kevinpauer).
  • codeburn overview. Plain-text monthly usage summary that is copy-pasteable, with --no-color and --from/--to. (#528, #535)
  • Codex credit usage. Compute and surface Codex credit consumption alongside dollar cost. (#408, #495, #510)
  • MCP server usage in exports. codeburn export now includes per-MCP-server usage in both JSON and CSV. (#496, #514)
  • JSON output for optimize and yield. (#492, #500)
  • Claude-scoped agent-type breakdown in the report.
  • OpenCode 1.1+ file-based JSON sessions. (#523)
  • Copilot OTel cache-token parsing. (#477, thanks @steelp02; #498)

Fixed (CLI)

  • Model names in reports. Models priced through a sibling alias no longer show their internal pricing key: ZCode/Hermes GLM-5.2 and Grok Build display their real names, gpt-5.5 labels as GPT-5.5, and gpt-5.3-codex-spark is distinguished from base GPT-5.3 Codex. (#548, #550, #539 thanks @ozymandiashh)
  • Hermes lowercase glm-5.2 prices the same as GLM-5.2. (#545, thanks @ozymandiashh)
  • Daily cache purges cached today/future entries on hydration and is bumped to v9 so newly supported providers backfill across history without a manual cache clear. (#550)
  • Cursor scans the requested window instead of a blind 250k ROWID cap. (#482, #512)
  • cursor-agent ingests the workspace-less CLI transcript layout. (#542, thanks @ozymandiashh)
  • Claude Code project names no longer collapse to a parent folder, and stray .git directories no longer over-group projects. (#540, thanks @ozymandiashh)
  • Copilot shell commands and skills/agents display correctly. (#527, thanks @jonjozwiak)
  • Codex attributes MCP calls emitted as event_msg/mcp_tool_call_end. (#513)
  • Antigravity reads the current agy CLI on-disk layout. (#541, thanks @ozymandiashh)
  • Workflow/ultracode subagent usage is now counted. (#470)
  • --provider is validated and the non-TTY report is deterministic. (#501)
  • The dashboard plan banner is scoped to its own provider tab. (#524)
  • Test isolation and environment-collision fixes. (#530, thanks @tvcsantos)

Added (macOS menubar)

  • Custom daily budget. Set a custom daily budget amount; the alert respects the display metric (Cost or Tokens). (#497, #505, #506)
  • Agent tabs show every active agent for the selected range, ordered by usage. (#549)
  • Polished status-item menu and About tab (Star and Sponsor links). (#509)

Fixed (macOS menubar)

  • Keychain prompts. Stop repeated keychain prompts on token refresh; read the Claude keychain via the security CLI on silent refresh. (#490, #491)
  • Restore the right-click status-item menu on macOS 27. (#472, thanks @theparlor)
  • Support installer HTTP proxies. (#475, thanks @sleicht)
  • Surface the CLI's stdout/stderr on a decode failure so a stray banner is self-diagnosing. (#515, #547)
  • Reduce repeated status parsing and guard against clock skew. (#486, thanks @vaibhavarora14; #499)
  • The cost budget stays in USD and an empty custom budget is flagged. (#508)
  • Drop the tok suffix from the Total Tokens metric. (#511)

0.9.12 - 2026-06-09

Added (CLI)

  • MCP server. codeburn mcp runs a stdio Model Context Protocol server exposing get_usage and get_savings to AI agents, with project names pseudonymized by default (opt-in reveal). (#429)
  • New providers: Devin (#444), Antigravity IDE (#418), JetBrains — IntelliJ/DataGrip via Copilot (#433), coder/mux (#438), and an opt-in Vercel AI Gateway datasource via AI_GATEWAY_API_KEY (#432).
  • Automatic pricing gap-fill from models.dev and OpenRouter for models LiteLLM has not indexed yet (e.g. Claude Fable 5). (#457)
  • Proxy-aware cost attribution. codeburn proxy-path marks a project as routed through a subscription-backed proxy (e.g. Claude Code over GitHub Copilot); the full API-rate cost is reported as subscription-covered so the dashboard shows net out-of-pocket, leaving actual cost untouched. (#417, #459)
  • Local-model cost savings reports. New codeburn model-savings command maps a local-model name (e.g. llama3.1:8b) to a paid baseline (e.g. gpt-4o) so the dashboard can report the counterfactual spend the same tokens would have incurred on the baseline. The local call still costs $0; the new savingsUSD field tracks the avoided spend separately from costUSD everywhere a number is shown (dashboard, JSON/CSV exports, menubar payload, macOS menubar, GNOME extension, daily cache rollups). Historical savings are recomputed automatically when the baseline mapping changes (config-hash invalidation on the daily cache). Daily cache schema bumped to v8. (#421)
  • CNY currency support. (#430)
  • Contribution heatmap insight. (#437)

Added (CLI)

  • Hermes Agent provider. Track token usage, cost, and tool breakdowns for Hermes Agent sessions. Reads from ~/.hermes/state.db and per-profile databases. Supports session-level accounting with actual/estimated costs from Hermes, falling back to CodeBurn's model pricing table. Supersedes #386, closes #368.

Fixed (CLI)

  • Per-file parse isolation. A single malformed session file no longer aborts the run or empties the daily-history trend; parse failures are cached so broken files are not re-read every run. (#441, #450, #453)
  • Codex fork dedupe is content-addressed, fixing undercounting of divergent events. (#458)
  • Model-name matching on the version boundary so e.g. claude-opus-4-6 and claude-opus-4-8 no longer collapse to the same tier. (#417)
  • Vercel AI Gateway data now flows through aggregation instead of reporting $0; Fable 5 and Mythos 5 price correctly ($10/$50). (#432, #466)
  • Cache-read tokens are no longer double-counted in the models report. (#447)
  • Critical-path fetches (pricing, currency) now time out so a stalled network cannot wedge the CLI or menubar. (#445, #448)
  • Cursor lookback is period-aligned with a 6-month floor. (#432)
  • Antigravity hook stale path repair. codeburn antigravity-hook install now installs the statusLine command through a persistent codeburn binary from PATH and repairs older CodeBurn-owned hooks that pointed at stale local build artifacts, preventing agy from auto-disabling capture after MODULE_NOT_FOUND failures.

Added (macOS menubar)

  • App icon. (#455)
  • Configure CLAUDE_CONFIG_DIRS from Settings. (#434, #436)

Fixed (macOS menubar)

  • Refresh reliability. The app awaits the CLI's exit via its termination handler instead of blocking a queue thread, and caps concurrent CLI spawns — fixing the menubar wedging on "Loading…" after a long idle. (#462)
  • Recover from stuck loading when an in-flight refresh is orphaned across sleep/wake. (#412)
  • Use the correct currency enum in the Settings picker. (#435)

0.9.11 - 2026-05-27

Added (CLI)

  • MCP project profile advisor. codeburn optimize now flags MCP servers that are useful in one project but loaded into other projects where they are never invoked, with a project-scoping prompt that preserves the hot workflow while reducing cold-project schema overhead. Thanks @ozymandiashh. (#356)
  • MCP and skill reliability report. codeburn optimize now detects MCP servers and skills whose edit turns are disproportionately retry-heavy, using turn-level MCP/Skill call evidence and a shared-turn token estimate so one retry-heavy turn is not double-counted across multiple capabilities. Thanks @ozymandiashh. (#357)
  • VSCodium storage discovery. Copilot, Roo Code, and KiloCode now scan VSCodium and VS Code Insiders storage roots in addition to VS Code, so usage from VSCodium is included automatically. Thanks @ozymandiashh. (#233)
  • Tooling breakdowns in dashboard and menubar. New panels showing core tools, MCP servers, and shell command usage per session and across periods.
  • File-aware retry detection with typed ToolCall. One-shot rate now tracks which file was edited, so editing file A then file B after a shell step no longer counts as a retry. Claude and Codex extract file paths from tool inputs; Codex also parses patch_apply_end changes and JSON-encoded function_call arguments. Providers without file path data fall back to tool-name-based detection.

Fixed (CLI)

  • Codex 100% one-shot rate. Codex function_call arguments are JSON strings, not objects, and patch_apply_end stores file paths in changes object keys. Both are now parsed correctly.
  • Claude toolSequence missing from session cache. apiCallToCachedCall was not forwarding the toolSequence field, so all cached Claude sessions lost their tool ordering data.
  • Forge dedup key instability. The fallback deduplication key used the raw message array index, which shifts when messages are deleted between scans. Now uses a composite of model name and token counts. Also fixed a variable reference before its declaration that would crash at runtime when no tool call ID was present.
  • Session cache rejected subagentTypes field. The cache validator did not recognize the subagentTypes array, causing entries with this field to be silently dropped and reparsed on every run.
  • Conflicting date flags on status accepted silently. Passing --day with --from/--to, or --days with any other date flag, produced undefined behavior. Now exits with a clear error message.

Changed (CLI)

  • OpenCode provider uses shared SQLite parser. Delegates to sqlite-session-parser.ts (same module KiloCode uses), reducing the provider from 498 to 66 lines with no behavior change.

Added (macOS menubar)

  • Configurable menubar status period. The menubar dropdown now lets you choose which period (Today, 7 Days, Month, All Time) is shown in the status bar. Persisted via UserDefaults. Thanks @ozymandiashh. (#302)

Fixed (macOS menubar)

  • Loading watchdog killed healthy CLI fetches. The recovery loop ran every 8 seconds with no backoff. Each attempt reset the generation counter, discarding in-flight CLI responses (45s timeout) before they could finish. Replaced with exponential backoff (8s to 60s, 6 attempts max) that skips recovery when a fetch is already in flight. Shows an error overlay with a Retry button after all attempts are exhausted.
  • Multi-day cache key mismatch. selectedDay returned the earliest date instead of nil when multiple days were selected, and startInteractiveSelectionRefresh did not pass the day set to the cache key constructor. Both now match PayloadCacheKey normalization rules.
  • Dead code cleanup. Removed RefreshBackoff.swift, its test file, and a broken test that called methods deleted in #393.

0.9.10 - 2026-05-20

Added (CLI)

  • Agent and subagent tracking coverage across providers. Gemini sessions now emit one provider call per assistant message with token usage instead of one aggregate call per session, preserving per-message tools, bash commands, timestamps, and nearest user prompts. Existing cached aggregate Gemini entries are reparsed so the new per-message shape takes effect, and per-tool counts may increase because repeated tools are now attributed to the specific Gemini message that used them. Claude discovery also scans direct project-level subagents/*.jsonl files, and Codex agent tool normalization is covered by regression tests. Addresses #336. Thanks @ozymandiashh. (#340)
  • Optimize tab with retry tax, routing waste, and token display modes. New codeburn optimize surface in the dashboard and menubar, with daily budget alerts and project drill-down. (#349)

Fixed (CLI)

  • OpenCode child sessions are attributed to their root session. The OpenCode parser now walks the unarchived session.parent_id subtree so child and grandchild agent sessions contribute token and tool usage under the discovered root session while still excluding child sessions from top-level discovery to avoid double counting. Thanks @ozymandiashh. (#343)
  • OpenCode router sessions with missing usage are still reported. Some OpenCode router/provider combinations can persist assistant messages with text or tool activity but zero token and cost fields. The OpenCode parser now keeps those turns as zero-cost calls instead of dropping the session entirely. Closes #341. Thanks @ozymandiashh. (#342)
  • OpenCode and Goose sessions on fresh installs. Both providers returned zero sessions on first run when their on-disk directories did not yet exist. Discovery now treats missing directories as empty instead of erroring out. (#347)
  • One-shot rate detection for all non-Claude providers. Retry detection now sees multi-message flows correctly across providers, not only Claude. Follow-up to the v0.9.9 fix. (#355)
  • Cursor #cursor-ws= compound-path separator in fingerprintFile. session-cache.ts only handled the OpenCode : separator, so Cursor's workspace-aware paths could fall back incorrectly. The fingerprint now strips both # and : compound suffixes. Thanks @renerichter. (#358)
  • Per-provider multi-day data loss, division-by-zero, and decode fragility. Switching to Claude/Codex tab on 7-day/30-day/month periods previously only showed today's categories, models, sessions, and tokens because the cache shortcut only merged cost/calls. Per-provider periods now always do a full parse. Also floors maxCost at 0.01 to avoid NaN bar widths in ActivitySection and ModelsSection. (#362)
  • Kiro post-February 2026 storage discovery. The Kiro provider now keeps legacy .chat support while also discovering extensionless session index files and nested execution files. Modern execution JSON is parsed for identifiers, timestamps, model IDs, conversation text, structured tools, and estimated token usage. Thanks @ozymandiashh. Closes #329. (#339)

Fixed (macOS menubar)

  • Per-provider refresh latency. Switching provider tabs took ~24s on heavy histories. Now ~2s via session cache safety and reuse. (#344)

0.9.9 - 2026-05-15

Added (CLI)

  • IBM Bob provider. Discovers IBM Bob IDE task history, reuses the Cline-family parser for token/cost records, extracts model tags and workspace-based project names from session data. Closes #248.

Fixed (CLI)

  • One-shot rate detection for non-Claude providers. Gemini and Mistral Vibe now emit per-assistant-message calls grouped by user turn, so retry detection sees multi-message Edit -> Bash -> Edit flows instead of counting each message as an independent one-shot turn. Kiro and Goose record per-message tool ordering via toolSequence for the same effect on aggregated sessions. Vibe prefers meta.json.stats.session_cost over price-derived estimates when available. Session cache bumped to v2. Closes #351.
  • Reduced Claude parser OOM risk. Large Claude JSONL sessions retained full entry objects (text, thinking blocks, tool results) in memory during parsing, causing V8 heap exhaustion on heavy usage months. Entries are now compacted immediately after JSON.parse, keeping only the fields needed for cost/token aggregation. This is a mitigation - very heavy users may still need the streaming parser refactor planned next.
  • Eager daily-cache hydration caused OOM on most CLI commands. Eight commands (report, today, month, export, optimize, compare, models, yield) called hydrateCache() which parses a 365-day backfill, even though only status --format menubar-json consumes the daily cache. Removed from all paths that parse their own date ranges via parseAllSessions.
  • Session cache retained between status parses. The status --format json path parsed today and month ranges without clearing the in-process session cache between them, keeping both result sets pinned. Cache is now cleared after each period is consumed.
  • Claude 1-hour cache write pricing. 1-hour cache writes are now priced at 2x base input (previously used the 5-minute 1.25x rate for all writes). Daily cache bumped to v6 so stale totals are recomputed. Closes #276.
  • OpenCode MCP usage now counted. OpenCode stores MCP tool calls as <server>_<tool> names, which the shared MCP pipeline did not recognize. The provider now normalizes these to the canonical mcp__<server>__<tool> form so MCP breakdowns and optimize work correctly. Closes #308.
  • Antigravity Windows language-server discovery. Antigravity detection now supports Windows process discovery, --extension_server_port, --extension_server_csrf_token, --flag=value syntax, and both wrapped and unwrapped Connect-RPC response shapes. Closes #249.
  • Mangled project names in dashboard. The By Project and Top Sessions panels decoded slugs by splitting on -, which broke directory names containing dashes or dots (e.g. my-project rendered as my/project). Now uses the real project path instead. Closes #320.
  • Cursor undated bubble rows misattributed to Today. Bubble rows without a createdAt timestamp were defaulting to the current date, inflating Today's spend. Now skipped at both the SQL and application level.
  • Node version guard. Running on Node < 22.13.0 now prints a clear upgrade message instead of crashing with a cryptic node:sqlite parse error. Closes #319.

Fixed (macOS menubar)

  • All-provider refresh OOM. Refreshing with provider set to "All" could exhaust the V8 heap on accounts with heavy session history.
  • Tab refresh recovery. Switching tabs during a refresh no longer leaves the panel in a stale loading state.
  • Stale cache recovery. The menubar now detects and discards a corrupt or outdated on-disk cache instead of rendering zeroes until the next restart.
  • Refresh timer hardening. The 30-second auto-refresh timer is now cancelled on sleep/wake and restarted cleanly, preventing overlapping refreshes after lid-open.
  • Version display. The settings panel now shows the version without the v prefix for consistency with codeburn --version.

0.9.8 - 2026-05-10

Added (CLI)

  • Cline provider support. CodeBurn now reads Cline task usage from both VS Code globalStorage (saoudrizwan.claude-dev) and Cline's ~/.cline/data task root. It reuses the existing Cline-family parser for ui_messages.json usage entries, deduplicates migrated tasks by the newest ui_messages.json, and exposes Cline in CLI provider filters, docs, and the macOS menubar provider tabs. Closes #130.
  • Multiple Claude config directories. Set CLAUDE_CONFIG_DIRS to an OS-delimited list of paths (:-separated on POSIX, ;-separated on Windows) to scan more than one Claude data directory in a single run. Sessions across every configured directory roll up into one project row per project, so a user with ~/.claude-work and ~/.claude-personal who works on the same repo from both accounts sees one combined row rather than two split rows. ~ is expanded; missing or unreadable directories in the list are skipped instead of aborting the scan; if every listed entry is unreadable a one-line hint is written to stderr so a misplaced delimiter does not silently produce zero rows. Precedence: CLAUDE_CONFIG_DIRS > CLAUDE_CONFIG_DIR > ~/.claude. As part of this change ~ and ~/foo are now also expanded in CLAUDE_CONFIG_DIR (previously the value was passed through verbatim, which only worked when the shell expanded ~ before exporting). Closes #208.
  • codeburn models command. Per-model breakdown across all providers, one row per (provider, model), sorted by cost. Each row carries Input, Output, Cache Write, Cache Read, Total, and Cost columns plus a Top Task cell showing the dominant task category and its cost share (e.g. Coding (42%)). Pass --by-task to explode each model into one row per task type, with provider/model cells blanked on subsequent rows of the same group and a horizontal divider between groups. Filters: --period (default 30days), --from/--to, --provider, --task, --top, --min-cost, --no-totals. Output formats: table (Unicode box-drawn, default), markdown (GitHub-flavored, copy-paste friendly), json, csv. The table renderer auto-sizes every column to its content and drops cache columns first, then input/output, then top-task when the terminal is too narrow to fit the full set. Headers are cyan, totals row is yellow, provider name is dim. Inspired by tokscale's per-model table and ccusage's responsive cli-table3 layout, ported to plain Node with no new runtime dependency.
  • Per-day one-shot data in --format json. Each entry of daily[] now carries turns, editTurns, oneShotTurns, and oneShotRate (0-100, one decimal, null when no edit turns). Counts match the existing period-level activities[] rollup so a consumer can sum across days and reconcile. Closes #279.

Fixed (CLI)

  • Cursor sessions break down by project, not one row called "cursor". Cursor's chat history sat under a single dashboard row labeled cursor because the provider had no way to attribute bubbles to a workspace. The fix walks ~/Library/Application Support/Cursor/User/workspaceStorage/* for each workspace's workspace.json (folder URI) and composer.composerData (the composer ids opened in that workspace), then joins those composer ids against the global bubbles. Each workspace becomes its own project row, sanitized into the same slug shape Claude uses (e.g. -Users-you-myproject); composers that have no workspace mapping (multi-root workspaces, "no folder open" sessions, deleted workspaces) remain under a catch-all cursor row. As part of this the cursor parser now derives sessionId from the bubble row key (bubbleId:<composerId>:<bubbleUuid>) instead of the empty conversationId JSON field, which was always falling back to 'unknown'. Cursor result cache version bumped to 3 to invalidate prior caches that recorded the old session id. Closes the per-project half of #196.
  • Cursor cost shown for every model, not just Auto. Cursor emits model names in a claude-<dot-version>-<tier> shape (claude-4.6-sonnet, claude-4.5-opus, claude-4.5-opus-high-thinking, etc.) plus its own composer-1 house model, none of which match the canonical LiteLLM pricing keys (claude-sonnet-4-6, claude-opus-4-5). The alias map in src/models.ts filled some of these in v0.9.4 but missed the plain no-suffix forms (claude-4.5-opus, claude-4.5-sonnet, claude-4.6-opus), the haiku tier, the forward-looking 4.7 variant, and composer-1. The dashboard rendered $0 for sessions that used any unaliased model. Visible to users in #159 even after the v0.9.4 fix. Every Cursor variant in src/providers/cursor.ts:modelDisplayNames now has an alias and a regression test asserting non-zero pricing resolution. Closes #159.
  • Activity classifier no longer mislabels feature work as debugging. Messages like "add error handling", "create an issue tracker", or "implement the 404 page" used to land in the Debugging bucket because the classifier checked the debug-keyword regex (which matches error, issue, 404) before the feature regex. Now the keyword that appears earliest in the user message wins, so "add" beats "error", "create" beats "issue", etc. A real bug report ("login is broken, traceback below") still classifies as debugging because the debug word leads. Fixes the activity-misattribution half of #196.

Changed (CLI)

  • optimize suggestions now declare their destination. Every paste-style fix carries an explicit destination — claude-md (permanent project rule), session-opener (one-time paste at the start of a future session), prompt (one-time ask in the current chat), or shell-config (append to ~/.zshrc / ~/.bashrc). Output renders a clearly-labeled section header per destination so users no longer accidentally bake one-time session openers into their CLAUDE.md as permanent rules. Closes #277.

0.9.7 - 2026-05-07

Added (CLI)

  • MCP tool coverage detector. New optimize finding flags MCP servers whose tool inventory is largely unused. Inventory is observed from the Claude deferred_tools_delta JSONL attachments (exact tool names per session) instead of guessed at five tools per server. Token-savings estimates are cache-aware: schema bytes pay full input price on the first cache-creation turn of a session, then carry at the cache-read discount on subsequent turns, capped per call so we never claim more overhead than the call's own cache buckets could contain. Threshold:

    10 tools available, <20% coverage, observed in ≥2 sessions. Closes #2.

  • Session cost outlier detector. New optimize finding flags sessions costing more than 2x their peer-session average within the same project. Ignores sub-$1 outliers to avoid noise. Requires at least 3 sessions per project for a baseline.
  • Context bloat detector. New optimize finding flags sessions where effective input/cache tokens are large and disproportionate to output. Cache reads are discounted in the estimate to avoid overstating cheap cached context. The report highlights top sessions by imbalance, notes sharp growth from the previous project session (within a 7-day baseline window), and suggests starting fresh with only the current goal, relevant files, failing output, and constraints. Sessions flagged here are excluded from the cost-outlier finding so the same session is not listed twice.
  • Worth-it score detector. New optimize finding flags expensive sessions with weak delivery signals: no edit turns, repeated retries, or edit work that never landed in one shot, when no git/gh delivery command is observed. Framed as a conservative review candidate, not proof of waste. Sessions flagged here take priority and are excluded from both the context-bloat and cost-outlier findings so the same session is not listed more than once.
  • Per-model efficiency metrics. JSON report includes edit turns, one-shot rate, retries per edit, and cost per edit for each model.
  • Custom date range export. codeburn export --from --to exports a single custom period.
  • Live Claude quota bar. Menubar shows real-time quota usage inside the agent tab strip with OAuth refresh gate.

Fixed (CLI)

  • Invalid --format silently accepted. All commands now reject unknown format values with a clear error and exit 1 instead of silently falling back to the default.
  • Invalid --period silently accepted. getDateRange() no longer falls back to "week" on unknown periods. All period-accepting commands reject invalid values.
  • status help text. Description said "today + week + month" but only today and month were shown. Fixed to match actual output.
  • Windows Claude project paths. Claude Code project rollups now prefer the canonical cwd stored in session JSONL files instead of reconstructing paths from lossy directory slugs, and group case/slash variants together. Closes #217.
  • all period semantics unified between CLI and dashboard. The dashboard treated --period all as all-time (epoch start) while the CLI bounded it to the last 6 months. Both now consistently mean "Last 6 months". Period helpers (Period, PERIODS, PERIOD_LABELS, toPeriod, getDateRange) consolidated into cli-date.ts. Use --from / --to for unbounded historical ranges.
  • Popover anchor, tab strip flicker, and stale-data refresh. Batch of UI regressions from the menubar hardening round.
  • Validator hardenings. Batch of edge-case fixes from the multi-agent bug hunt.
  • Command injection in yield. yield now uses execFileSync instead of execSync to prevent shell injection via crafted branch names.
  • SHA-256 checksum verification. Menubar installer verifies download integrity before replacing the running app.

Fixed (macOS menubar)

  • Stuck loading spinner. The menubar ran --optimize on every 30-second background refresh. As sessions accumulated, optimize exceeded the 45-second timeout, and the loading overlay stayed forever with no fallback. Optimize is now stripped from all menubar fetches (use codeburn optimize in the CLI instead). On fetch failure with empty cache, the app retries without optimize so the spinner always clears.
  • Stale data after overnight sleep. Cache keys used the period enum (.today) not a calendar date, so data from yesterday persisted after midnight. Cache now tracks the current date and clears itself on day rollover. Wake-from-sleep additionally clears all cached entries before fetching fresh data.
  • Refresh button appeared to do nothing. Clicking refresh with stale cached data never showed the loading overlay because loading state only triggered on empty cache. Manual refresh and wake-from-sleep now explicitly request loading feedback.
  • Update button stuck spinning forever. performUpdate() only reset isUpdating on failure. On success the installer kills and relaunches the app, but if the process survives (pkill fails silently), the button stayed on "Updating..." permanently. Now always resets on termination and clears the update badge on success.

0.9.6 - 2026-05-03

Added (CLI)

  • Goose provider. New provider for Block's Goose AI coding assistant.
  • Antigravity provider. New provider for Antigravity IDE sessions.
  • Antigravity model aliases. gemini-3-pro, flash-image, flash-lite, and community-contributed Gemini model IDs.
  • GPT-5.5 display name for Codex.
  • Deno support. deno dx added as a run method.

Fixed (CLI)

  • Streaming dedup. Claude Code streams each message.id multiple times (start, intermediate, stop). The old keep-first strategy lost tool_use blocks and understated output tokens by ~6.3%. Now keeps last occurrence content with first occurrence timestamp for correct date bucketing.
  • $0.0000 display. Near-zero costs showed four decimal places instead of $0.00. Fixes #205.
  • ANSI escape stripping. Shell commands containing ANSI color codes now cleaned across all providers.
  • Antigravity dedup collision. Fixed key collision in session dedup. Added Codex ChatGPT Plus token estimation.
  • Codex large session validation. Reads full first line for session meta validation; caps read size and handles torn writes.
  • Codex fork dedup. Deduplicates forked Codex sessions to avoid double-counting.
  • Windows dashboard hang. Fixed ExperimentalWarning and dashboard freeze on Windows.
  • Hardcoded $ in forecast. Forecast comparison text now uses the configured currency symbol.

Fixed (macOS menubar)

  • Provider tabs showing $0.00 after idle. CLI timeout increased from 20s to 45s for cold file-cache latency. Loading overlay now appears when the all-provider payload confirms a provider has spend but its dedicated data hasn't loaded yet.
  • Refresh button blocked by in-flight requests. Manual refresh now bypasses the in-flight guard so users can always re-fetch.
  • Tab strip vs hero cost mismatch. Tab strip prefers the provider-specific payload cost when available, staying in sync with the hero section.
  • Ghost status item on macOS Tahoe.

0.9.5 - 2026-05-01

Added (CLI)

  • Homebrew. brew install codeburn (originally via tap, now in homebrew-core).
  • GPT-5.3 and DeepSeek display names. GPT-5.3, DeepSeek Coder, DeepSeek Coder Max, DeepSeek R1.

Fixed (macOS menubar)

  • Menubar refresh loop. Was a single-fire Task that never repeated; now a proper while loop with 30s interval and force: true.
  • Loading overlay flicker. Counter-based isLoading so concurrent fetches don't toggle the overlay.
  • Rapid tab switching race. Previous fetch is cancelled when switching tabs; stale results are discarded via Task.isCancelled.
  • Tab strip vs hero cost desync. Provider-specific and all-provider data now fetched in parallel so costs arrive from the same snapshot.
  • Stale menubar icon after wake. forceRefresh now fetches today/all in parallel alongside the current selection.
  • Accent color propagation. ThemeState is now @Observable; removes .id() view hierarchy teardown hack.
  • Currency flash on first switch. Symbol and rate now apply atomically — no more wrong-symbol-with-old-rate flash.
  • Export UI freeze. Uses terminationHandler instead of waitUntilExit; HHmmss in filename prevents overwrite on double-export.
  • CurrencyState concurrency. Proper @MainActor isolation with Sendable conformance; nonisolated on pure static functions.
  • Streak count. Iterates calendar days instead of sparse history entries so gaps correctly break streaks.
  • TrendBar chart flicker. Stable date-based identity instead of UUID.

0.9.4 - 2026-04-29

Added (CLI)

  • OpenClaw provider. Parses JSONL agent logs from ~/.openclaw/agents/ with legacy path support (.clawdbot, .moltbot, .moldbot). Token usage from assistant message usage blocks.
  • Roo Code provider. Reads Cline-family ui_messages.json from VS Code globalStorage/rooveterinaryinc.roo-cline/tasks/.
  • KiloCode provider. Reads Cline-family ui_messages.json from VS Code globalStorage/kilocode.kilo-code/tasks/.
  • Qwen CLI provider. Parses JSONL sessions from ~/.qwen/projects/<project>/chats/.
  • Droid provider. Parses sessions from ~/.factory/projects/.
  • Durable daily cache. Cache hydration extracted into shared ensureCacheHydrated() called by all commands. Schema migration fills missing fields instead of nuking the cache. Old cache versions backed up before reset. Atomic file writes with fsync.
  • Copilot auto-model buckets. Transcript inference uses auto-model naming for cleaner dashboard display.
  • Cursor model aliases. Built-in aliases for Cursor proxy model names.

Fixed (CLI)

  • Gemini provider updated for JSONL format. Supports Gemini CLI 0.39+ which switched from JSON to JSONL.
  • Duplicate hydrateCache() call in JSON reports. Removed redundant cache hydration inside runJsonReport().

Changed (CLI)

  • Daily cache version bumped to v4 with backward-compatible migration (v2+ supported).
  • LiteLLM pricing snapshot replaces hardcoded pricing for Qwen and new models.
  • 16 providers now supported (was 10).

Added (macOS menubar)

  • OpenClaw, Roo Code, KiloCode, Qwen, Droid tabs. Agent tab strip updated for all new providers.
  • Instant cached data display. Shows cached data immediately instead of blocking on CLI refresh.

Fixed (macOS menubar)

  • Menubar stops updating after first load. Background refresh was silently skipped by the cache TTL guard. Data loaded once, then froze. Fixes #179.
  • Menubar not dimming on inactive screens.
  • Performance improvements. Reduced unnecessary redraws and CLI invocations.

Added (macOS menubar)

  • Right-click context menu. Right-click the status bar icon for "Check for Updates" and "Quit CodeBurn".
  • Version label in footer.

Changed

  • README restructured with honeycomb provider hero image, 2x2 screenshot grid, and complete inline reference.
  • bunx codeburn added as alternative install option.

0.9.3 - 2026-04-28

Added (CLI)

  • Gemini CLI provider. Parses ~/.gemini/tmp/<project>/chats/session-*.json from Gemini CLI 0.38+. Uses real embedded token counts (input, output, cached, thoughts) with correct cached/fresh separation to avoid double-charging. Pricing for gemini-3.1-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash. Tool normalization (ReadFile->Read, SearchText->Grep, Shell->Bash). Closes #166.
  • Kiro provider. Parses .chat JSON session files with token estimation and auto-model naming (kiro-auto). Costed at Sonnet 4.5 rates via BUILTIN_ALIASES.
  • Copilot VS Code workspace transcripts. Copilot now reads transcripts from VS Code's workspaceStorage/*/GitHub.copilot-chat/transcripts/ in addition to the legacy ~/.copilot/session-state/ path. Tokens estimated from content length, model inferred from tool call ID prefixes. Fixes #161.
  • Auto-model naming. Cursor, Copilot, and Kiro store transparent model names (cursor-auto, copilot-auto, kiro-auto) instead of guessing the underlying model.

Fixed (CLI)

  • Cursor provider dropped all data older than 35 days. Hardcoded lookback silently excluded bubbles outside a 5-week window, making --period all return $0. Increased to 180 days. Fixes #159, fixes #163.
  • Cursor-agent subagent transcript discovery. Scans subagents/ subdirectories.

Added (macOS menubar)

  • Gemini, Kiro, Copilot, OMP tabs. Agent tab strip now shows all detected providers. Cursor + Cursor Agent merged into a single Cursor tab.
  • Accent color picker. 9 Apple-style system presets in the menubar header, persisted via UserDefaults.
  • Tab costs match selected period. Provider tab costs now reflect the active period (Today/7 Days/30 Days/etc.) instead of always showing today.

Changed

  • Daily cache version bumped to v4 (forces recompute with auto-model naming).
  • Cursor cache versioned to invalidate stale model names.
  • Case-insensitive provider key matching for tab cost lookups.

0.9.2 - 2026-04-28

Fixed

  • Cursor provider reported $0 on newer Cursor versions. Cursor v3 stores zero token counts in bubbles. Now estimates tokens from text length when counts are zero. Fixes #159.
  • Cursor provider dropped rows with NULL createdAt. The SQL filter silently excluded bubbles without a timestamp. Now includes them with a fallback timestamp. Fixes #163.
  • AgentKv entries with plain string content were skipped. Not all agentKv content is a JSON array; plain strings are now counted toward usage.
  • Subagent transcripts were not discovered. Transcripts inside subagents/ subdirectories are now picked up by the cursor-agent provider.

0.9.1 - 2026-04-25

Added

  • codeburn yield command. Correlates AI sessions with git history to categorize spend by outcome: productive (code shipped to main), reverted (commits later undone), or abandoned (work that never committed). Shows percentage breakdown so you know not just what you spent, but what happened to it. Accepts --today, --week, --month flags.

0.9.0 - 2026-04-24

Added (CLI)

  • Claude Max 5x plan preset. codeburn plan claude-max-5x sets a $100/month budget for heavy Claude Code users.

Fixed (CLI)

  • Cursor provider failed on newer versions. Cursor 0.50+ stores session data in agentKv:blob:* entries instead of bubbleId:*. Added fallback parser that extracts usage from the new format.
  • Cursor-agent provider missed Composer 2 sessions. Composer 2 stores transcripts in agent-transcripts/<UUID>/<UUID>.jsonl subdirectories instead of .txt files. Now scans both formats. Fixes #142.
  • Codex showed wrong model names. Model info is now extracted from turn_context entries, showing exact names like "GPT-5.4" instead of generic "GPT-5".
  • Codex edit detection showed 0 edit turns. Codex records file modifications as patch_apply_end events, not tool calls. Now tracks these events to enable one-shot rate and retry metrics.
  • Compare chart bar colors didn't match legend. Non-winning model bars were grayed out despite the legend showing both colors. Bars now always display their assigned colors.

Fixed (macOS menubar)

  • Menubar icon invisible on macOS Tahoe (26.x). Status item failed to render on macOS 26.4+ due to window server registration timing. Fixed by starting as regular app, activating, then switching to accessory mode after setup. Fixes #146.
  • High CPU usage (~14%). Removed duplicate refresh timer, increased LaunchAgent interval to 30s, added 5-second debounce on wake events.

0.8.9 - 2026-04-22

Fixed

  • Menubar showed stale prices. The "all providers" query used end: now while per-provider queries used end: endOfDay, causing sessions timestamped after the capture moment to be excluded from totals. Now uses periodInfo.range consistently across all queries.

Changed (macOS menubar)

  • Variable-width status item is now the default. The menubar pill hugs the rendered text in both compact and default modes instead of reserving a fixed 130pt slot.

0.8.8 - 2026-04-22

Fixed (CLI)

  • OOM crash on large session files. scanJsonlFile and parseSessionFile loaded entire files into memory via readViaStream (which defeated its own streaming by joining all lines back into one string). Switched both to the existing readSessionLines async generator that yields one line at a time. Contributed by @maucher (#132).

Added (macOS menubar)

  • Compact mode. Opt-in tighter menubar display: no decimals, variable width that hugs the text. Enable with defaults write CodeBurnMenubar CodeBurnMenubarCompact -bool true. Default off.

Fixed (macOS menubar, shipped alongside via mac-v0.8.8)

  • Plan tab never loaded on Claude Code 2.1.x. Keychain credential lookup filtered on kSecAttrAccount == "default", but Claude Code writes the macOS login username. Removed the hardcoded allowlist; the service name is sufficient to scope the query.
  • Four keychain prompts on debug builds. Collapsed two-phase keychain enumeration into a single SecItemCopyMatching call.
  • App Nap override not sticking. The beginActivity token was immediately overridden by AppKit. Now disables automaticTerminationSupport and suddenTermination at the process level.

0.8.7 - 2026-04-21

Added

  • MiniMax-M2.7 and MiniMax-M2.7-highspeed pricing. Added to FALLBACK_PRICING plus display names so MiniMax sessions show up with the right cost and readable labels when users route MiniMax through providers like OpenCode. Rates verified against MiniMax's live paygo pricing: base model $0.3/M input, $1.2/M output; highspeed $0.6/M input, $2.4/M output; cache read $0.06/M, cache write $0.375/M on both.
  • OMP provider (Oh My Pi). Auto-discovers sessions at ~/.omp/agent/sessions/*.jsonl and tracks them alongside Pi. Shares Pi's JSONL parser via a providerName parameter, so OMP rows keep their own omp: dedup prefix and never cross-dedupe with Pi on a shared conversationId namespace. codeburn report --provider omp filters to OMP only; the default combined view includes both. Contributed by @cgrossde (#59).
  • codeburn model-alias command. Maps any provider-emitted model name to a canonical pricing name so cost rows no longer read $0.00 when a proxy rewrites names. Aliases persist in ~/.config/codeburn/config.json under modelAliases. Usage: codeburn model-alias <from> <to> to set, --list to view, --remove <from> to clear. User aliases resolve before the built-in list. Contributed by @cgrossde (#59).
  • Built-in aliases for Anthropic-compatible proxy format. anthropic--claude-4.6-opus, anthropic--claude-4.6-sonnet, anthropic--claude-4.5-opus, anthropic--claude-4.5-sonnet, and anthropic--claude-4.5-haiku now resolve to canonical Claude names and price correctly with no user configuration. getCanonicalName also strips provider/ prefixes before alias resolution so double-wrapped forms like anthropic/anthropic--claude-4.6-opus work the same way. Contributed by @cgrossde (#59).

Fixed (CLI)

  • Prototype pollution in alias resolution. A model literally named __proto__ leaked Object.prototype through the ?? fallback chain in resolveAlias, which then crashed canonical.startsWith downstream. The resolver now uses Object.hasOwn checks for both user and built-in alias maps. Caught by the existing prototype-pollution test suite during the #59 merge.

Fixed (macOS menubar, shipped alongside via mac-v0.8.7)

  • Menubar label froze in the background and only refreshed when you clicked the icon. Three independent causes fixed:
    • prefetchAll on launch spawned four concurrent codeburn subprocesses that competed with the main refresh loop for disk and parser time. Removed; period tabs now fetch lazily on first click.
    • NSStatusItem sometimes deferred the status bar paint for an accessory app, so attributedTitle updates hit memory but not the screen until the popover opened. Explicit needsDisplay + display() after each update forces the paint.
    • The real root cause: macOS App Nap / Automatic Termination was suspending the app whenever the icon sat idle in the background, stretching the 15-second refresh Task's sleep indefinitely. Holding a ProcessInfo.beginActivity token for the life of the app opts out. Confirmed via log show: _kLSApplicationWouldBeTerminatedByTALKey now stays at 0.
  • Subprocess QualityOfService lifted to .userInitiated so codeburn runs at terminal speed when spawned from the menubar.

Skipped

  • 0.8.6 was never published to npm. The version was briefly planned and then skipped to align CLI and macOS menubar versioning at 0.8.7.

Notes

  • If you are on 0.8.5 and do not use MiniMax, Oh My Pi, or a proxy that rewrites model names to the anthropic--claude-X.Y-tier format, CLI behavior is unchanged and you can safely stay on 0.8.5.
  • macOS menubar users on mac-v0.8.6 or earlier should update: the refresh loop only ticks reliably from mac-v0.8.7 onward. The in-app update pill surfaces within 2 days, or quit and re-run npx codeburn menubar to pull immediately.

0.8.5 - 2026-04-21

Fixed

  • Stale Today totals after 0.8.2. The persistent source cache introduced in 0.8.2 caused Today's cost to under-report and sometimes drop between polls during active Claude Code sessions. The cache keyed entries on (mtime, size) fingerprints that diverged from Claude's append-mostly JSONL model, producing empty or partial entries that were served on subsequent polls. Reverted the cache rewrite to the v0.8.1 full-reparse path for Claude sessions. Both the menubar and codeburn status now return consistent, monotonically-increasing Today totals.
  • Menubar and terminal status disagreed on Today. A turn that straddled midnight (user message in one day, response in the next) was bucketed by user timestamp in one code path and by assistant timestamp in another, producing different Today values in the two surfaces. Both paths now count a turn on the day its first assistant call ran.
  • Kept from 0.8.2-0.8.4: subscription plan tracking, pricing accuracy and CSV injection hardening, cursor-agent provider, menubar prefetch and timezone alignment. Only the cache rewrite and its follow-up patches were reverted.

Removed

  • --no-cache flag on report, today, month, status, export, optimize, and compare. The flag existed to bypass the persistent source cache which no longer exists. If your scripts pass --no-cache, drop it; the parse runs fresh every time now.

Notes

  • 0.8.2, 0.8.3, and 0.8.4 on npm contain the buggy cache. Upgrade with npm i -g codeburn@latest or npm i -g codeburn@0.8.5.
  • This release uses a full reparse on every invocation, matching v0.8.1 behavior. On large corpora (5,000+ session files) expect 3 to 10 seconds per invocation. An incremental refresh design that preserves correctness is planned for a follow-up release.

0.8.0 - 2026-04-19

Added

  • codeburn compare command. Side-by-side model comparison across any two models in your session data. Interactive model picker, period switching, and provider filtering.
  • Compare view in dashboard. Press c in the TUI to enter compare mode. Arrow keys switch periods, b to return.
  • Performance metrics. One-shot rate, retry rate, and self-correction detection per model. Self-corrections are detected by scanning JSONL transcripts for tool error followed by retry patterns.
  • Efficiency metrics. Cost per call, cost per edit turn, output tokens per call, and cache hit rate.
  • Per-category one-shot rates. Breaks down one-shot success by task category (Coding, Debugging, Feature Dev, etc.) for each model.
  • Working style comparison. Delegation rate, planning rate (TaskCreate, TaskUpdate, TodoWrite), average tools per turn, and fast mode usage.
  • TUI auto-refresh enabled by default. Dashboard now refreshes every 30 seconds out of the box. Pass --refresh 0 to disable. Closes #107.
  • 36 comparison tests. Full coverage for metric computation, category breakdown, working style, self-correction scanning, and planning tool detection. Total suite: 274 tests.

Fixed

  • Planning rate showed ~0% in model comparison. Only counted EnterPlanMode (rarely used) instead of all planning tools (TaskCreate, TaskUpdate, TodoWrite, EnterPlanMode, ExitPlanMode). Now detects planning at the turn level across all five tool types.
  • Menubar "All" tab showed stale data. Three-layer caching (300s in-memory TTL, daily disk cache, 60s parser cache) prevented tab switches from showing fresh numbers. Cache TTL reduced from 300s to 30s, tab switches always fetch fresh data, background refresh interval reduced from 60s to 15s.

0.7.4 - 2026-04-19

Added

  • codeburn report --from/--to. Filter sessions to an exact YYYY-MM-DD date range (local time). Either flag alone is valid: --from alone runs from the given date through end-of-today, --to alone runs from the earliest data through the given date. Inverted ranges or malformed dates exit with a clear error. In the TUI, pressing 1-5 still switches to the predefined periods. Credit: @lfl1337 (PR #80).
  • avgCostPerSession in reports. JSON projects[] entries gain an avgCostPerSession field and export -f csv adds an Avg/Session (USD) column to projects.csv. Column order in projects.csv is now Project, Cost, Avg/Session, Share, API Calls, Sessions -- scripts parsing by column position should read by header instead. Credit: @lfl1337 (PR #80).
  • Menubar auto-update checker. Background check every 2 days against GitHub Releases. When a newer menubar build is available, an "Update" pill appears in the popover header. One click downloads, replaces, and relaunches the app automatically.
  • Smart agent tab visibility. The provider tab strip hides when fewer than two providers have spend, reducing clutter for single-tool users.

Fixed

  • Stale daily cache caused wrong menubar costs. The daily cache never recomputed yesterday once written, so a mid-day CLI run would freeze partial cost data permanently. The "All" provider view relied on this cache, showing wildly incorrect numbers while per-provider tabs (which parse fresh) were correct. Yesterday is now evicted and recomputed on every run.
  • UTC date bucketing instead of local timezone. Timestamps in session files are UTC ISO strings. Several code paths extracted the date via .slice(0, 10) (UTC date) while date range filtering used local-time boundaries. Turns between UTC midnight and local midnight were attributed to the wrong day -- the menubar showed lower today cost than the TUI. All date bucketing now uses local time consistently.
  • OpenCode SQLite ESM loader. node:sqlite is now loaded correctly in ESM runtime. Credit: @aaronflorey (PR #104).
  • Menubar trend tooltip per-provider views. Tooltip now shows the correct cost when a specific provider tab is selected.
  • Menubar (today, all) cache freshness. The cache entry powering the menubar title and tab labels is now kept fresh independently of the selected period/provider.
  • Agent tab strip restored. All detected providers are shown again after a regression hid them.
  • Plan pane button cleanup. Removed the broken "Connect Claude" button that opened a useless terminal session. The Plan pane now shows only a "Retry" button.

0.7.3 - 2026-04-18

Changed

  • Dropped better-sqlite3 in favor of Node's built-in node:sqlite. Removes the deprecated prebuild-install transitive dependency that npm warned about on every install (issue #75, credit @primeminister). End-user install is now 40 packages down from 167 and shows zero deprecation notices. The experimental-SQLite warning Node 22/23 normally prints on module load is silenced for this specific warning; other warnings pass through unchanged.
  • Minimum Node version raised to 22. Node 20 reached EOL on 2026-04-30; node:sqlite lives in 22+. Users on older Node get a clear upgrade message when a SQLite-backed provider (Cursor, OpenCode) is loaded.

0.7.2 - 2026-04-17

Added

  • Native macOS menubar app. Swift + SwiftUI app under mac/ replaces the SwiftBar plugin. Agent tabs, Today/7/30/Month/All period switcher, Trend/Forecast/Pulse/Stats/Plan insights, activity and model breakdowns, optimize findings, CSV/JSON export, instant currency switching, live 60s refresh.
  • codeburn menubar. One-command install: downloads the latest .app from GitHub Releases, strips Gatekeeper quarantine, drops it into ~/Applications, and launches it. --force reinstalls in place.
  • status --format menubar-json. Structured payload consumed by the native menubar app. Current-period totals, per-activity and per-model breakdowns, provider costs, optimize findings, and 365-day history.
  • Release workflow. .github/workflows/release-menubar.yml builds a universal .app bundle and zip on mac-v* tag push.

Changed

  • codeburn export -f csv now writes a folder of one-table-per-file CSVs (summary, daily, activity, models, projects, sessions, tools, shell-commands) plus a README.txt index. Each file opens cleanly as a single table in any spreadsheet.
  • codeburn export -f json upgraded to schema codeburn.export.v2 with currency metadata.

Fixed

  • codeburn status terminal Today/Month now buckets by local date instead of UTC, so spend shows correctly during the window between local midnight and UTC midnight.
  • FX rate validation. Frankfurter responses are checked to be finite and within [0.0001, 1_000_000] before they affect displayed costs.

Removed

  • SwiftBar plugin. src/menubar.ts, codeburn install-menubar, codeburn uninstall-menubar, and status --format menubar are gone. The native Swift app is the single menubar surface.

Security

  • codeburn export -o guard. Writes a .codeburn-export marker into every folder it creates and refuses to reuse non-marked directories or overwrite existing files, so a typo like -o ~/.ssh/id_ed25519 cannot delete a sensitive file.

0.7.1 - 2026-04-17

Security

  • External security audit closed. 1 HIGH, 2 MEDIUM, and 1 LOW finding fixed. Threat model: a compromised third-party AI CLI with write access to ~/.claude/projects/ dropping malicious session JSONL.
  • Prototype pollution blocked. Breakdown maps in parser.ts (model, tool, MCP, bash) now use Object.create(null) so attacker-controlled keys like __proto__ create own properties instead of mutating Object.prototype. Credit: @lfl1337 (PR #67).
  • Bounded session-file reads. New src/fs-utils.ts helper caps reads at 128 MB and switches to stream-based parsing above 8 MB. Applied to 13 reachable read sites across parser, Codex, Copilot, Pi, context-budget, and optimize. Credit: @lfl1337 (PR #67).
  • Menubar label sanitizer. SwiftBar directive-separator (|) and ANSI escape injection via crafted model or category names is now prevented by an allowlist ([A-Za-z0-9 ._/-]) plus 14-character truncation. Credit: @lfl1337 (PR #67).

Added

  • --verbose flag. Global CLI option that prints warnings to stderr on skipped (oversize) or failed session-file reads. Silent by default. Credit: @lfl1337 (PR #67).
  • 11 new security tests. tests/security/prototype-pollution.test.ts, tests/security/menubar-injection.test.ts, tests/fs-utils.test.ts. Total suite: 209 tests.

0.7.0 - 2026-04-16

Added

  • codeburn optimize command. Scans your sessions and your ~/.claude/ setup for 11 common waste patterns and hands back exact copy-paste fixes. Detection-only, never writes to user files. Supports --period (today, week, 30days, month, all) and --provider (all, claude, codex, cursor).
  • Setup health grade (A-F). Urgency-weighted rollup of all findings, with impact scored against observed waste so the most expensive issues rank first. High findings penalise more, medium less, low least.
  • Trend tracking. Repeat runs classify each finding as new, improving, or resolved against a 48-hour recent window, so fixed issues disappear instead of lingering as noise.
  • 11 detectors: files Claude re-reads across sessions, low Read:Edit ratio, projects missing .claudeignore, uncapped BASH_MAX_OUTPUT_LENGTH, unused MCP servers, ghost agents, ghost skills, ghost slash commands, bloated CLAUDE.md files (with @-import expansion counted), cache creation overhead, and junk directory reads.
  • Copy-paste fixes. Each finding comes with a ready-to-paste remedy: a CLAUDE.md line, a .claudeignore template, an environment variable, or a mv command to archive unused items.
  • In-TUI optimize view. Press o in the dashboard when the status bar shows a finding count, b to return. Same engine as the standalone command, scoped to the current period and provider.
  • Per-project context budget column. By Project panel now shows the estimated per-session context overhead for each project (system prompt + tools + CLAUDE.md + skills).
  • 34 filesystem-mocking tests. Tmpdir fixtures with os.homedir mocked via vi.mock cover the detector surface end to end. Total suite: 198 tests across 13 files.

Performance

  • mtime pre-filter + parallel reads + 60s result cache cut a cold scan from 12-17s to 6-7s on a 10k-session history.

0.6.1 - 2026-04-16

Added

  • JSON output on report, today, month. --format json writes the full dashboard (overview, daily, projects, models, activities, tools, MCP servers, shell commands, top sessions) to stdout. Contributed by @mallek.
  • Project filters. --project <name> and --exclude <name> on all commands (report, today, month, status, export). Case-insensitive substring match against project name and path. Both flags are repeatable. Contributed by @mallek.
  • claude-opus-4-7 model mapping and pricing. Displays as Opus 4.7 with the same Opus pricing as 4.6 and a 6x fast multiplier. Contributed by @mallek.
  • Unit tests for filterProjectsByName covering include/exclude semantics, case-insensitivity, path matching, and input immutability.

Fixed

  • Top Sessions panel truncating the calls column. Row width filled the full panel width without leaving room for the border and padding, so Ink truncated the last 4 characters -- landing exactly on the calls column and producing rows like $182.58 ... with no value.
  • SwiftBar custom plugin directory now honoured when installing the menubar widget. Reads the configured path from SwiftBar's defaults before falling back to the standard location. Contributed by @Galeas.
  • status --format menubar per-provider today totals now respect --project/--exclude. The main period blocks already did, the provider breakdown loop was the one spot that bypassed the filter.

0.6.0 - 2026-04-16

Added

  • GitHub Copilot provider. Parses ~/.copilot/session-state/*/events.jsonl and tracks model changes via session.model_change events. Picks up six new model prices (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-5-mini, o3, o4-mini). Contributed by @theodorosD. Note: Copilot logs only output tokens, so cost rows will sit below actual API cost.
  • All Time period (key 5). Shows every recorded session since CodeBurn started tracking. Daily Activity expands to every available day instead of the fixed 14- or 31-day window. codeburn report -p all also works from the CLI. Contributed by @lfl1337.
  • avg/s column in By Project. Average cost per session next to the existing total cost and session count. Surfaces projects where individual sessions are expensive even if the total is modest. Contributed by @lfl1337.
  • Top Sessions panel. Highlights the five most expensive sessions across all projects with date, project, cost, and API call count. Helps spot outliers that drag weekly or monthly totals. Contributed by @lfl1337.

Fixed

  • modelDisplayName now matches longest key first so gpt-4.1-mini resolves to GPT-4.1 Mini instead of GPT-4.1.
  • TopSessions handles missing firstTimestamp gracefully with a ---------- placeholder instead of rendering a stray whitespace row.

0.5.0 - 2026-04-15

Added

  • Cursor IDE support. Reads token usage from Cursor's local SQLite database. Shows activity classification, model breakdown, and a Languages panel extracted from code blocks. Costs estimated using Sonnet pricing for Auto mode (labeled clearly). Supports macOS, Linux, and Windows paths.
  • SQLite adapter with lazy-loaded better-sqlite3 (optional dependency). Claude Code and Codex users are completely unaffected if it is not installed.
  • File-based result cache for Cursor. First run parses the database (can take up to a minute on very large databases); subsequent runs load from cache in under 250ms. Cache auto-invalidates when Cursor modifies the database.
  • Provider-specific dashboard layout. Cursor shows a Languages panel instead of Core Tools, Shell Commands, and MCP Servers (Cursor does not log these).
  • Provider color coding in the dashboard tab bar (Claude: orange, Codex: green, Cursor: cyan).
  • Broader activity classification patterns: file extensions, script references, URLs, and HTTP status codes now trigger more accurate categories.
  • Debounced period switching. Arrow keys wait 600ms before loading data so quickly scrolling through periods skips intermediate loads. Number keys still load immediately.
  • Dynamic version reading from package.json (no more hardcoded version string).

Fixed

  • CLI --version reported stale 0.4.1 since v0.4.2. Closes #38.

0.4.4 - 2026-04-15

Added

  • Auto-refresh flag. codeburn report --refresh 60 reloads data at a set interval. Works on report, today, and month commands. Default off.
  • Readable project names. Strips home directory prefix from encoded paths, shows 3 path segments for more context. Home dir sessions display as "home".
  • Responsive dashboard reflows on terminal resize via Ink's useWindowSize hook. Width cap raised from 104 to 160 columns. Contributed by @AleBles.
  • Total downloads and install size badges in README.

Fixed

  • Agent/subagent session files were excluded, dropping ~46% of API calls. Subagent sessions live in separate subagents/ directories with unique message IDs and are now included. Closes #17.
  • Codex cache hit always showed 100%. OpenAI includes cached tokens inside input_tokens (unlike Anthropic). Normalized to prevent double-counting in cost calculation and cache hit display. Closes #21.
  • CSV formula injection. Cells starting with =, +, -, @ are prefixed with an apostrophe before CSV escaping. Contributed by @serabi.
  • Menubar "Open Full Report" and "Export CSV" actions broken for npm-installed users. Invokes resolved binary directly instead of assuming ~/codeburn checkout. Currency picker used nonexistent config currency subcommand. Contributed by @MukundaKatta. Closes #32, #27.
  • Activity panel moved from full-width to half-width row for better space usage on wide terminals.

0.4.1 - 2026-04-14

Added

  • Multi-currency support. codeburn currency GBP sets display currency (162 ISO 4217 codes). Exchange rates from Frankfurter API (ECB data, 24h cache). Applies to dashboard, status, menubar, and exports. Contributed by @BlairWelsh.
  • 30-day rolling window period (codeburn report -p 30days, key 3 in TUI). Distinct from calendar month. Contributed by @oysteinkrog.
  • Menubar currency picker with 17 common currencies.

Fixed

  • Export "30 Days" period now uses actual 30-day range instead of calendar month.

0.4.0 - 2026-04-14

Added

  • Codex (OpenAI) support. Parses sessions from ~/.codex/sessions/ with full token tracking, cost calculation, task classification, and tool breakdown.
  • Provider plugin system. Adding a new provider (Pi, OpenCode, Amp) is a single file in src/providers/.
  • TUI provider toggle. Press p to cycle All / Claude / Codex. Auto-detects which providers have session data on disk. Hidden when only one is present.
  • --provider flag on all CLI commands: report, today, month, status, export. Values: all (default), claude, codex.
  • Codex tool normalization: exec_command -> Bash, read_file -> Read, write_file/apply_diff/apply_patch -> Edit, spawn_agent -> Agent.
  • Codex model pricing: gpt-5, gpt-5.3-codex, gpt-5.4, gpt-5.4-mini with hardcoded fallbacks to prevent LiteLLM fuzzy matching mispricing.
  • CODEX_HOME environment variable support for custom Codex data directories.
  • Menubar per-provider cost breakdown when multiple providers have data.
  • 1-minute in-memory cache with LRU eviction for instant provider switching.
  • 10 new tests (Codex parser, provider registry, tool/model mapping).

Fixed

  • Model name fuzzy matching: gpt-5.4-mini no longer mispriced as gpt-5 (more specific prefixes checked first).

0.3.1 - 2026-04-14

Added

  • Shell Commands breakdown panel showing which CLI binaries are used most (git, npm, docker, etc.). Parses compound commands (&&, ;, |) and handles quoted strings. Contributed by @rafaelcalleja.

Changed

  • Activity panel is now full-width so the 1-shot column renders cleanly on all terminal sizes.

Fixed

  • Crash on unreadable session files (ENOENT). Skips gracefully instead.

0.3.0 - 2026-04-14

Added

  • One-shot success rate per activity category. Detects edit/test/fix retry cycles (Edit -> Bash -> Edit) within each turn. Shows 1-shot percentage in the By Activity panel for categories that involve code edits.

Fixed

  • Turn grouping: tool-result entries (type "user" with no text) no longer split turns. Previously inflated Conversation category by 3-5x at the expense of Coding, Debugging, and other edit-heavy categories.

0.2.0 - 2026-04-14

Added

  • Claude Desktop (code tab) session support. Scans local-agent-mode-sessions in addition to ~/.claude/projects/. Same JSONL format, deduplication across both sources. macOS, Windows, and Linux paths.
  • CLAUDE_CONFIG_DIR environment variable support. Falls back to ~/.claude if not set.

Fixed

  • npm package trimmed from 1.1MB to 41KB by adding files field (ships dist/ only).
  • Image URLs switched to jsDelivr CDN for npm readme rendering.

0.1.1 - 2026-04-13

Fixed

  • Readme image URLs for npm rendering.

0.1.0 - 2026-04-13

Added

  • Interactive TUI dashboard built with Ink (React for terminals).
  • 13-category task classifier (coding, debugging, exploration, brainstorming, etc.) using tool usage patterns and keyword matching. No LLM calls.
  • Breakdowns by daily activity, project, model, task type, core tools, and MCP servers.
  • Gradient bar charts (blue to amber to orange) inspired by btop.
  • Responsive layout: side-by-side panels at 90+ cols, stacked below.
  • Keyboard navigation: arrow keys switch Today/7 Days/Month, q to quit.
  • Column headers on all panels.
  • Bottom status bar with key hints (interactive mode only).
  • Per-panel accent border colors with rounded corners.
  • SwiftBar/xbar menu bar widget with flame icon, activity breakdown, model costs, and token stats. Refreshes every 5 minutes.
  • CSV and JSON export with Today, 7 Days, and 30 Days periods.
  • LiteLLM pricing integration with 24h cache and hardcoded fallback. Supports input, output, cache write, cache read, web search, and fast mode multiplier.
  • Message deduplication by API message ID across all session files.
  • Date-range filtering per entry (not per session) to prevent session bleed.
  • Compact status command with terminal, menubar, and JSON output formats.