* 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.
340 KiB
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 legendreportalready used.report,overviewand 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 gainsestimatedCostUSDandisEstimatedon eachmodelsrow,estimatedCostandisEstimatedon eachsessionsrow, andestimatedCostUSDon eachproviderDetailsrow ofstatus --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 overviewprints 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 atcodeburn 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_DIRhonored, comma-separated): one JSON document per thread with a per-request usage ledger (tokens.input/output, cache tokens joined from the billed message throughtoMessageId) 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/--towith the range you exported, re-import overlapping exports without double counting, and--removeto 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 itsprompt_tokensandcompletion_tokenscolumns 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'sopenrouter/deepseek/deepseek-v4-prorow could claim thedeepseek/deepseek-v4-proslot 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 repriceszai/glm-4.6from the vercel_ai_gateway resale row ($0.45/$1.8 per million) to the official row ($0.6/$2.2), and movesmistral/codestral-embedandmistral/mistral-embedto 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.6base input moves to $1.25/M;claude-opus-4is 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 asmodel_usageentries, not assistant messages, and the parser read only messages, so that spend was missing. On one real week that was 605finddecisions 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 asomp:find,omp:cache-warmand 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
mturns 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-summarycarries 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 quotaread the credential from the Windows profile only, so a WSL-only login showed nothing. Both now also read~/.claude/.credentials.jsonin 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=offskips 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 fromagent-traces.dbwent tocopilot-chat. A conversation is now matched to the workspace whosechatSessionsfolder holds its session id, and named from that workspace'sworkspace.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 fromagent-traces.dbkeepcopilot-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 usedprovider/modeland now uses the bare id too. Turns throughgoogle-vertexandgoogle-vertex-anthropicland 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 menubaropens 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 menubarspawned the tray exe insideWindowsApps, 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
avgCostPerSessionout 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, withsessionCountBasisstill 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 --forceas its own child, andpkill/pgrepskip their own ancestors unless told otherwise, so the running app was never stopped: the bundle was replaced andopenre-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/modelid that failed pricing for ids likeopencode/claude-haiku-4-5orgithub-copilot/gpt-5so 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 todaydrops 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,statusand 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 corpusoverview -p todaydrops 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 quotaand the desktop app say so when it cannot be. Both providers read the OAuth credential from~/.claude/.credentials.jsonand 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 gotTemporarily 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'sexpiresAthas 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
estimatedandunpricedcolumns 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-buildpriceable 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-latestprimary does not answer a baregrok-latestquery, 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 (anebius/MiniMaxAI/MiniMax-M3row 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,yieldandspendnow declare--projectand--excludeand 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.comparescopes its self-correction scan to the filtered projects, andwebwarns 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 --contributionsreports 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-jsonreports 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-periodsruns 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-jsonwith--from,--to,--categoryand--project-iddefines 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
AppleLanguagesinto 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 aLocalizable.stringscatalog 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, soEEE MMM dreadsSat, Sep 12in en_US andSat 12 Sepin en_GB. Strings thecodeburnCLI 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 menubarthe 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 withoutnpm install -g codeburnfirst. 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 menubarnow 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
periodTotalsnow 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=1and Hermes on itsbedrockprovider 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 inbilling_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, soHaiku 4.5,Haiku 4.5 (Bedrock)andHaiku 4.5 (Bedrock us)(the cross-region profile, a dearer SKU) are three rows incodeburn models, the dashboard and the menubar payload'stopModelsalike, where beforemodelsand the menubar could disagree. The route rides on the call (route), andcodeburn models --format jsonreports it per row. Pricing is untouched: it runs on the raw id. Daily cache moves to v33 (day.modelskeyed 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,exportandauditcan be filtered by billing route and billing mode.--route direct|bedrock|openrouterand--billing metered|subscriptioncut 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: Hermesincludedis subscription-covered andactual, 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 --stdioresponse 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 separategrokprovider.codeburn quotagains 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.jsonlwas written inside the liveness window is reported askimicode, 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 incodeburn 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 readunder 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 as0, 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 quotaand 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 toolsandcodeburn reportshowed zero tool activity for every Antigravity session. The provider now reads thestepstable alongsidegen_metadataand 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.didChangeNotificationnever 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:SMAppServicespeaks 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>.jsonor, for Kimi Code, from the onekimiworking 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-bothopts 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 postsClaude · 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
0while 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 readWeeklyis 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
CodeBurnMenubarSecondRowEnabledandCodeBurnMenubarSecondRowMetricin 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,Dailyor session row an adapter reports), and clicking again switches back. The choice is stored per provider underCodeBurnCapacityDockGlanceWindows, so Claude can sit on its 5-hour window while Codex stays weekly, and it survives relaunch. A per-model row such asWeekly · Opusis 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 --uninstallreads the product code from the install marker or the uninstall registry and runsmsiexec /x. A failed removal never forgets a still-installed app, a cancelled elevation prompt is a no-op rather than a red failure, and adevbuild 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=allopts into stopped distros andoffnever runswsl.exeat 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 doctorlists every WSL root it probed. Closes #1059. (#1062)
Changed
- The resident
codeburn serveprocess 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 serveprocess 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 viaANTHROPIC_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 gainsoverview.excludedGatewayCostwhen something is excluded, and the terminal report and dashboard print a one-line footnote.--provider vercel-gatewayis unaffected and reports the full amount. The rule lives in one place, the parse that every surface reads, somodels,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 JSONprojects[]) lists spend the total beside it is not counting. The gateway still appears once, clearly labelled: its ownBy toolrow 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, runcodeburn gateway-totals include(excludeto 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_countwas 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 reportprocess, 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 reportrefreshes 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
servechild 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_MSandCodeBurnServeIdleSecondstune 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-eyebrowrung 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 leafRefreshedAtcomponent 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 adevin.acuUsdRatewas set by hand. Cost now comes from each step's ownprompt_tokens/completion_tokens/cached_tokensand the model that produced it (generation_modelper 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 (todayswe-2-high) costs $0 and warns once, rather than being priced off a neighbour. Thedevin.acuUsdRateconfig 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 withsource: "user"rather thanis_user_input, falling back to thesessions.dbtitle and then to the session's firstprompt_historyprompt. (#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 writestotal: 0for 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 namedREPORTED_COST_PROVIDERSset 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 thetotal_charged_usagecents, thencredits_spentat the published credit rate), butwarpwas 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.completewas 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 machinenodelives in the per-user profile, which was in none of the locations the app adds toPATH, so every spawn died withenv: node: No such file or directoryand 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 augmentedPATHnow. (#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 nodeand the inheritedPATHwas 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 executablenodesits in the same directory as the resolved CLI, that directory moves to the front ofPATH, 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.jsnever setNODE_ENVand React is external to the bundle, so Ink loaded React's development reconciler, which since React 19.2 callsperformance.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 defaultsNODE_ENVtoproduction, 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 doctornames 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 readssession.workingDirectory, so every kiro session failed the trusted-directory check, was dropped before the commit join, andcodeburn sync push --attributionreported zero facts at every window. The path was being carried the whole time, just on the field nothing consumed; the parsers emitworkingDirectoryfrom 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.jsonlandsession.v<N>.jsonl,.zstdincluded, 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 jsoninto a folder writes the file inside it. The desktop app passes the folder you picked, and the JSON writer appended.jsonto it, so choosing~/Exportsproduced~/Exports.jsonbeside 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 reportstill writesreport.json. (#1511)codeburn menubarreplaces the copy you already have instead of adding a second one. The installer only ever looked at~/Applications, so aCodeBurnMenubar.appin/Applicationswas 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 yieldstops 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--unpricedwas 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 atcodeburn models --unpricedto see them, also when the floor dropped every row and the table read "No model usage found"; an explicit--min-costis the user's own cut and stays silent, and json, csv and markdown stay machine-clean. The menubar payload has carried anunpricedModelsblock 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
modelscommand defaultsminCostto $0.01, and the desktop bridge passed neither--min-costnor--unpriced, so the table silently dropped every row below a cent, which by construction excluded every unpriced row too (a0 >= 0.01filter), leaving #1443's dimming and add-alias affordances unreachable in the shipped app.codeburn:getModelsnow 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_v2table and messages tosession_message, tagged by type and ordered by sequence, while the oldsession/message/parttables 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,messageandparttables freeze at upgrade while new work goes tosession_v2andsession_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 fromsession_v2are listed too now and read from the legacy tables, while v2 sessions keep coming fromsession_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
sessionrow, and that path handedtokens_outputalone to the pricing table while still reportingtokens_reasoningseparately, 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 throughbillableOutputTokens, 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.3Binstead of12345.7M, with the exact value still in the row's accessibility text, and the CLI tables gain the same rung throughformatTokens(acodeburn modelstotals row now reads29.9Binstead of29870.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 modelsdefaults tominCost: 0.01and drops rows whose cost and savings are both under $0.01, and the desktop'scodeburn:getModelspasses 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=1records 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 fromcodeburn 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 jsontook 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 acliproxy/<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. Thecliproxy/wrapper now peels like the other routing wrappers (OrcaRouter, Cline Pass,cmd/, …), andzcodejoins the known vendor namespaces so a provider path strips to its pricedglm-*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_countevent with its original timestamp, and names the parent undersource.subagent.thread_spawn.parent_thread_idrather thanforked_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_usagewas 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-xhighis priced at the Codex row it names, not the basegpt-5one. Devin writes the model's minor version with a dash, and that spelling has no pricing row, so lookup fell through togpt-5and undercharged every Codex-backed step by $0.50 per million input tokens. The id is now normalised togpt-5.3-codex-xhighfor 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
chatSessionsand 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_5xtier, so the tier scan hitmaxfirst and every surface called it Max 5x. The credential file'ssubscriptionTypeis 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 statusprinted nothing at all, and the residentserveprocess the desktop and menubar talk to hung with it, so every surface went blank with no error. Any database under~/Library/Group Containersis 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
serveprocess 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: falsemeans 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 whilecodeburn doctorlisted 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.dailycame 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 todaycannot 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 --stdioprocess 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-jsongoes 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 --stdioprocess 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.-shmwrites 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 warmreport --format json --period 30daysgoes from 2294ms to 1337ms and--period allfrom 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 exceededon a large session history. Column widths were measured by spreading a per-session key list intoMath.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.realizedCostUSDbehind a guard on the payload only, so an act report without atotalsblock 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 inlocalStorage(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.webmto halve its size dropped the VP9 alpha plane while leaving the container'salpha_modeflag 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.icnswhose 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.pullRequestssurvived both passes verbatim: each row's full GitHub url and itsowner/repo#123label 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 withinclude_project_names: falsepseudonymizesurlandlabelfrom 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: truestill 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: thetopSessionsrows added in #1316 carryprojectKey, a dash-encoded absolute path such as-Users-<user>-Projects-<repo>, whichredactProjectNamesleft 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 bothsanitizeForSharingand the MCP pass let through, so a branch such asexp/extraction-armsreached a shared payload as typed. The MCP pass now hashesprojectKeywith the same salted pseudonym it already applies totopProjects[].id, hashes the drill-throughsessionIdon bothtopSessionsandtopProjects[].sessionDetails(a Codex id embeds the rollout timestamp the pass blanks out ofdate), and gives each distinct branch a stablebranch-xxxxxxpseudonym, keeping the cost, calls and session counts untouched; anullbranch 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 passesinclude_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 monthhas 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.jsonandapps.jsonto 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 toapi.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 readsGH_HOSTfrom the same environment as the token; theghrung resolves the host from gh's ownhosts.yml(GH_CONFIG_DIR, thenXDG_CONFIG_HOME/gh, then~/.config/gh), picking it the way gh picks the host forgh auth tokenand without a second subprocess; and macOS Settings gains a GitHub host field next to the pasted token, defaulting togithub.comand 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 promisesapi.github.combefore 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 asevil.com?.ghe.comfails closed with no request instead of pointing the Authorization header at a host of its own choosing.codeburn quotais 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.comhost. Both readers hardcodedhttps://api.github.com/copilot_internal/userand threw away the host their credential came from, so a data-residency enterprise signed in on<tenant>.ghe.comcould only ever reportavailable: falsewith "Temporarily unavailable". A discovered credential now carries its host (hosts.jsonis keyed by host and newerapps.jsonfiles key by<host>:<app id>), and the request follows it:api.github.comforgithub.comand for any rung that carries no host of its own (an app-nameapps.jsonkey,COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN,gh auth token, a pasted token), andhttps://api.<tenant>.ghe.com/copilot_internal/userfor an enterprise host. The token and the host always come from the same entry, withgithub.compreferred when several hosts are signed in and otherwise the first.ghe.comtenant 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>/sessionslooking forwire.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 statswire.jsonlthere, which is about 23ms on the same store. A decoywire.jsonlburied inblobs/orfile-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>.jsonon 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
.lprojdirectory 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_PATHis still honoured. (#1517)
Internal
- Packaging no longer aborts on a checkout whose path contains a UUID.
app/scripts/stage-cli.mjsresolved the CLI's production dependency closure by matching eachnpm ls --parseableline 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_modulesoccurrence, 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 missingnpm_execpathalso 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 aletand 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-latesta 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
ServeConnectioncancellation 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.
ServeConnectiontakes its pid file as a parameter, so a test run no longer shares~/.cache/codeburn/menubar-serve.pidwith a running menu bar, and the reaper only signals a record that is aserve --stdiocommand. Four test files never removed theirUserDefaultssuites 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-jsongains atelemetrySnapshotblock (schema 2) built bysrc/telemetry-snapshot.tsfrom 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-dailyusage_snapshotevent 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_VERSIONmoves 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.6and their Azure rows plusazure_ai/deepseek-v4-flashandazure_ai/deepseek-v4-progain a cache-write price;deepseek-v4-flash-0731moves to $0.20 input and $0.40 output per million;mistral-medium,magistral-medium-latestandmagistral-small-latestare repriced with a cache-write rate added;Qwen/Qwen3.8-2.4T-A95Bdrops to $2.00 input and $6.00 output per million with its cache-write rate halved; andgpt-realtime-2output rises from $16 to $24 per million. New rows of note aregemini-3.8-flash,grok-build-latest,gpt-6-astra,glm-5p3-flash,azure_ai/kimi-k2.7-codeand the us-gov Bedrock Claude Opus 4.8 and Sonnet 5 rows. (#1254) codeburn quotaprints 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-jsongains an add-onlyliveSessionsblock covering sessions active in the last 10 minutes with model, branch, context tokens and window, start, last activity andidleSeconds, stripped bysanitizeForSharingand 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:providerDetailsentries gain optionalinputTokens,outputTokensandsessions(absent rather than zero until a day reports them,STATUS_SNAPSHOT_RENDER_VERSIONmoves 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 coverscursorandcursor-agent, ClinePass coversclineandcline-cli, and Droid maps ontodroid. 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.jsonunder$GROK_HOMEread-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
codeandsuccessfields 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 fromcodeburn quota --format json, refreshed every five minutes and on demand by double-clicking the rail; a CLI too old to have thequotasubcommand 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,--quitincluded, 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.jsonleaves the auto-connectai-agent-session-logsplug and becomes its own read-only single-fileclaude-quota-credentialsplug with no auto-connect requested, so the optional Claude plan and quota tile now needssnap connect codeburn:claude-quota-credentialsonce; it already degrades gracefully when the file is unreadable.~/.config/github-copilotis removed entirely because it holds the Copilot OAuth credential files, so JetBrains Copilot sessions are not discovered under the snap, whichdocs/providers/copilot.mdnow says.GitHub.copilot-chatwas 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/datato itstaskssubdirectory,.omp/agentand.pi/agentto theirsessionssubdirectories so.pi/agent/auth.jsonis no longer covered,.cursor/ai-trackingtoai-code-tracking.db, the claude-dev, roo-cline and kilo-code global storage entries to theirtaskssubdirectories, 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, CursorworkspaceStorage,~/.copilot/session-store.db, IBM Bob, lingtai and zerostack.app/scripts/snap-grants.test.tsenforces the shape, including that no credential path appears in the auto-connect plug. On the CI side,snapcraft uploadexits 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.0rather than NULL when itscost_statusisunknownorincluded, 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-nullactual_cost_usdstill wins including an explicit $0. Around that,providerDetails.hasUsagebecomes 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 asclaude-fable-5-1displays asFable 5.1without 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_VERSIONmoves 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 modelsdoes. The per-day rows behind those charts were built from the raw provider and model string whilecodeburn modelsmerged by alias-resolved canonical id, so the same day's data produced two different ideas of what counts as one model: the Trend tooltip listedminimax/MiniMax-M3andMiniMaxAI/MiniMax-M3as separate rows against a single "MiniMax M3" row in the command.history.daily[].topModelsnow merges by the same normalized display name, and a merged row carries a new optionalrawModelsfield so a cached and an uncached route stay visible;codeburn modelsgains 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 planestimates 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.dbrows 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 read3.9 / 20000 AI Creditswhile 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 exampleCopilot Max: ~9800 / 20000 AI Credits (estimated; 4 of 473 requests carry GitHub's exact figure). The bar andpercentUsedfollow 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 areestimatedCredits,creditRatedCalls,creditUnratedCallsandcreditsNote;docs/providers/copilot.mddocuments 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
ghrather than the legacy editor plugin files. The ring only looked for~/.config/github-copilot/hosts.jsonandapps.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.jsonandsettings.json(taking the first value with a GitHub access-token prefix and skippingghr_refresh tokens), thenCOPILOT_GITHUB_TOKEN,GH_TOKENandGITHUB_TOKEN, thengh auth tokencached 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 apremium_interactionswindow with entitlement 0, which both the menubar and the desktop decoder had drawn as a 100 percent used ring; windows with entitlement 0 orunlimited: trueare 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
creditUsagePercentfrom 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.
NSHostingViewintrinsic 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 andservearguments 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 inperf/BASELINES.mdagainst 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 inCONTRIBUTING.mdand 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 incodeburn-desktop-wireframes.htmlwas 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.mdstops 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
liveSessionsblock carries. The menubar parity test builds the payload twice over one warm cache and expects a match, andidleSecondsdiffers by one across a second boundary, sostableShapenow zeroes it the way it already blanks thegeneratedstamp. The upgrade-path check comparing a payload built with and without workers stripsliveSessionsoutright, sinceidleSecondsandlastActivityAtdiffer 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 pushsends 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 carryai.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), andai.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 asai.cache_read_tokens/ai.cache_write_tokens, billable-consistent withai.input_tokensvia the display layer's max-of-both-vocabularies convention, each sent only when non-zero.ai.call_countandai.session_duration_msdescribe the span's session (span count, and last-minus-first provider-recorded event time, omitted when either timestamp is missing or unordered).ai.subscription_coveredreports 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 acodeburn.coverage_throughresource 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-runstays 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 undocumentedai.speedandai.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 autoadds 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 coversCORE_SYNC_ATTRIBUTE_KEYSexactly - and without--acceptstores 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-invokedsync auto runsends only on an exact fingerprint match through the same push path as manualsync 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 disableis a hard kill switch: it removes the scheduled job and stops all automatic outbound immediately, leaving manualsync pushuntouched. 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 absententry.timestamphanded the range filter an invalid date and thetodayrow 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 finitemsg.usage.cost.totalwas treated as authoritative and OMP writescost.total = 0forxai-oauthids; 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 configuredpriceOverridesentry still wins over the built-in tier.CACHE_SCHEMA_VERSIONmoves 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
EPIPEfailure, and the generated Electron-as-Node launcher preservesserve --stdiodispatch 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 --stdioprocesses 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 boundedSIGKILLfallback) within Electron's existingbefore-quitwindow, 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
qandCtrl-Ctermination 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-qconfirmation and second-qfast 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 forCtrl-C, 0 forq. (#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-unitgroups the sessions report into provider-recorded work units. The new deterministic resolver (src/work-units.ts) folds each session carrying the #1140lineagefield under the orchestration root it names, giving one row per unit with the root's title and project, cost/calls summed over root plus children, and a child count, with the children listed indented beneath and standalone sessions rendered exactly as before. A work unit's id isderiveTraceId(rootSessionId), the exact trace-id derivation sync already uses, so a unit's identity matches the root's existing trace identity. Evidence stays strictly provider-recorded: a session without lineage is its own standalone unit with roleunknown, a child that names an in-range parent folds even when the parent recorded nothing (one-sided evidence), and anything ambiguous fails closed per the boundary spec's MAIN-02 - a parent id outside the parsed window leaves the child ungrouped, cycles and self-references are broken and markedunknown, and duplicate or cross-provider id collisions never fold.--format jsonwith the flag emits an add-only{ sessions, workUnits }envelope:sessionsis today's row array unchanged,workUnitsis the resolver's full partition (standalone sessions included, so counts and totals reconcile). Default output without the flag is byte-identical, no wire/sync or daily-cache behavior changes, and a pinning test asserts the grouped footer total equals the ungrouped total. Implements slice CB-2 of the CodeBurn main/Teams boundary spec (CODEBURN-MAIN-TEAMS-BOUNDARY-v0.1). (#1140)codeburn status --format menubar-jsoncan serve from a disk-persisted snapshot. A background poll whose corpus has not changed since the last computation is answered from a per-query snapshot keyed on a corpus fingerprint (ordered source topology, per-provider env fingerprints, five config hashes, currency, pricing generation, cache and package versions - any mismatch is a miss), eliminating the per-poll re-parse on idle machines. A snapshot is only ever written for a complete, non-degraded parse: the save gate requires the payload's own captured markers (stale !== true, nohydrationblock), so a poll that went read-only under a held refresh lock can never pin its under-reported totals to disk - a regression holds the refresh lock and asserts no snapshot file is written. Serve-side and one-shot outputs are unchanged; the optimization currently engages for--no-optimizepolls (the menubar background refresh). Thanks @dgabehar, hardening @ozymandiashh. (#999)- The menubar status snapshot now re-validates persisted records on load, mirroring the save gate. A snapshot record whose payload carries degradation markers (
stale: trueor ahydrationblock) is treated as a miss and recomputed instead of served, so a record that should never have been written (older build, hand-edited file) can never pin under-reported totals; the save gate from #999 is unchanged. Extending the snapshot to the default optimize path was evaluated on this branch and reverted in post-build review:scanAndDetect's findings depend on mutable state the corpus fingerprint never observes (~/.claudeand project-levelsettings.json,CLAUDE.md, defined skills, agents, commands, MCP config), and re-deriving them on a snapshot hit requires the parsed corpus, which is the full parse the snapshot exists to avoid on exactly the cold processes it serves. The optimize path therefore still never reads or writes the disk snapshot, and one-shot and serve-child outputs are byte-identical to before for both optimize values. (#1135) - The parser captures provider-recorded session lineage instead of discarding it. Claude agent transcripts carry a provider-written parent session reference (and the parent side records the spawned agent id when compaction has not dropped it); Kimi Code subagent sessions live inside their parent session's own directory. Both now populate an optional
lineagefield on the parsed session model -parentSessionId, a root/child role, andevidence: 'provider-recorded'- persisted through the session cache, with claude and kimicode parse versions bumped for the one-time re-derive. Strictly provider-recorded evidence only: no inference from time adjacency or shared projects, and a session with no evidence carries no lineage field at all. Purely additive metadata: a pinning test asserts every report total is byte-identical with the field present or absent. Groundwork for spend attribution across delegated-agent families (a measured 5.1% of sessions carry direct lineage but 76% of 90-day spend). (#1140)
Fixed (macOS)
- CodeBurnMenubar now launches mise-installed CodeBurn correctly from Spotlight. Spotlight gives GUI apps a minimal PATH, so a persisted
mise use -g npm:codeburnlauncher could be found but then fail with exit 127 when its shell wrapper tried to resolvenode. The menubar child environment now includes mise's stable shim directory (including a customMISE_DATA_DIRwhen available), matching the existing Volta/asdf/nvm handling without invoking a shell. (#1124)
Fixed (TUI)
- The interactive dashboard stays responsive during the cold-start background fill, and
Ctrl+Cexits the TUI at any moment after paint. The progressive cold start introduced by #1109 paints the dashboard from recent files, then runs a background fill that parses the deferred history and writes the per-file session cache. On a real 21k-file corpus that fill's cache save publishes one JSON.stringify + fsync per provider shard in a tight loop, and the whole save held the event loop long enough that Ink's stdin reader never ran - aqorCtrl+Cpressed in the post-paint window was swallowed, not buffered, and the user saw a frozen terminal (#1139, #1141).saveCachenow yields to the event loop between every shard write, so the Ink input handler fires between publishes. The yield sits BETWEEN shards, never inside one, so each shard's temp+rename atomicity and the lost-fence cleanup that owns the canonical envelope are preserved; nothing marked seen without being parsed, the kill-safe resume invariant from #1109 is intact, and the floored-parse memo key suffix keeps warm reads byte-identical. The fill still parses every deferred file, so money totals are unchanged. The dashboard'suseInputnow exits onCtrl+Cthrough the same path asq, so a raw 0x03 from the terminal lands as soon as the loop breathes. Pinning tests:tests/save-cache-yields.test.ts(asetImmediateprobe must interleave withsaveCache's shard writes) andtests/dashboard-exit.test.ts(a raw\x03exits the dashboard the same wayqdoes). The #1109 cold-start one-shots - report, sessions, models, json/csv/markdown, the desktop and menubar payloads - are unchanged. - The first
qduring the cold-start background fill renders feedback instead of a silent drain, and a secondqexits the same wayCtrl+Cdoes. With the #1142 input fix in place,qpressed during the post-paint fill landed instantly but the exit path drained the fill first - a deterministic ~16.5s silent wait on a 21k-file corpus (#1143). The fill's indexing signal already flows to the dashboard (the #1109 banner reuses it), so theuseInputhandler now arms a confirmation on the firstqwhile a fill is active and rendersFinishing background index so the next launch starts warm - press q or Ctrl+C again to quit nowin the footer area, with the secondqtaking the abrupt path.Ctrl+Cis unchanged: it always exits through the abrupt path, and because #1109 made that path kill-safe (nothing marked seen without being parsed, resume converges) the two are equivalent. When the fill completes, the confirmation clears itself so a laterqcannot be trapped by a stale flag.qwith no fill active exits immediately as before, with no status-line flicker. Pinning tests added totests/dashboard-exit.test.ts.
0.9.21 - 2026-08-24
Added
-
Copilot AI Credit plans.
codeburn plan set copilot-pro(1,500 credits / $15 equivalent, not the $10 sticker),copilot-pro-plus(7,000),copilot-max(20,000), andcustom --credits N --provider copilot. Spend issum(finite nanoAiu) / 1e9. Token-pricedcostUSDdoes not fill the credits bar. Claude / cursor / grok / custom-USD plans stay oncostUSD. (#943) -
codeburn models --unpriced. The dashboard warns about models that price at $0 and points atcodeburn model-alias, but the list itself was hard to get out of the TUI. This filters the plain-stdoutmodelsreport to exactly those rows, reusingfindUnpricedModelsso local, free, aliased and price-overridden models are treated the same way the warning treats them, and defaulting that mode's min-cost to 0 so $0 rows are not pre-filtered away. Thanks @kocaemre. (#969) -
optimizespots the same long block pasted at the start of many sessions. The newrecurring-contextdetector groups sessions by their opening block - normalized for whitespace and ANSI, hashed over the first 2 KB - and reports a block of at least 1.5 KB that opens 5 or more sessions, with the top three by tokens, their session counts and the project each is confined to. It is a habit, not an apply-able fix: CodeBurn will not move your own text intoCLAUDE.mdfor you, so the finding asks Claude to give the block a permanent home (aCLAUDE.mdrule, or a file read on demand) and hand back a one-line pointer to open sessions with instead. Savings count the repeats only, never the first paste, and are markedestimated: provider usage is counted per API call, where the pasted block is mixed in with the system prompt, tool schemas andCLAUDE.md, so the block is sized from its own bytes. Injected system reminders and slash-command wrappers are not pastes and are skipped, and neither is a prompt a program wrote - an SDK session or a subagent task - read from the entry's flags, which survive the parser's large-line path. The opening block comes from the session scan that already runs, so nothing extra is read from disk. -
Applied fixes get re-measured on every
optimizerun, and told plainly whether they worked. Aftercodeburn optimize --apply, every still-applied fix comes back in anApplied fixessection on subsequentcodeburn optimizeruns, carrying the verdictact reportalready computes from the same reconciliation:worked(at least 70% of its window-scaled estimate realized),partial(something, but under that),no-effect(no measured reduction, printed with the exactcodeburn act undo <id>that puts it back), ormeasuringfor anything younger than the 3-day measurement window. The numbers are measured - provider-counted usage over the post-apply window - not re-estimated.--applynow says when the re-measure will happen,--format jsongainsappliedFixes[](add-only), and the same section appears in the dashboard TUI and the desktop app. Newcodeburn optimize --auto-revertundoes the fixes that measured no reduction at all through the same code path ascodeburn act undo; it never touchespartialor still-measuring fixes, and never auto-reverts aCLAUDE.mdrule (it prints the undo command instead), matching the--yesguardrail. -
Optimize findings say what to do with them and where their number came from. Every finding now carries a class and a basis, and every surface groups by it:
Fix now (apply-able)for findingscodeburn optimize --applycan write itself,Habitsfor the behavioural ones,FYIfor informational ones whose cost may be justified. A finding only counts as apply-able when a plan can actually be built for that instance, so anmcp-deferral-offcaused by Vertex policy or a shell-profile override is grouped as a habit rather than promising a fix that does not exist. Alongside it, each finding is markedmeasured(summed from provider-counted usage) orestimated(a schema-size or recovery-fraction model), with the split reported in the header asN measured · M estimatedin place of the blanket "Estimates only." footer. Sessions whose cost the provider never reported are kept out of thecost-outlierspeer comparison, and a provider that only ever estimates gets the finding markedestimatedrather than dropped.--format jsongainsclassandbasisper finding plussummary.measuredSavingsUSD(existing fields unchanged), and the newdocs/optimize.mdcovers what is scanned, exactly what--applymay write, and how to read the health grade. -
CODEBURN_CACHE_SCOPE=allforces a full session-cache read. A ranged query reads only the month shards that can contribute a turn to it, which is a real behaviour change on a warm cache; this is the escape hatch for the case where a number looks wrong and you want to know whether the scoped read is why. Set it and every load ignores its scope and reads every shard, one-shot runs and the residentcodeburn servealike. It is a read policy, not an input to any cache fingerprint: setting or unsetting it re-parses nothing and invalidates nothing. -
The interactive dashboard opens on Today, and falls back to 7 days when today is empty. Only the CLI TUI ever defaulted to 7 days; the desktop app and the web dashboard already opened on Today and needed the fallback half alone. The TUI takes the auto path only when
-pcame from the default rather than from you, with no--dayand no--from/--to, and only on a TTY - so piped runs,--format json/csv/markdown,report,sessionsandmodelsstay on week, byte for byte. The probe costs nothing: it is the today slice of the parse the dashboard already runs, and it counts sessions rather than projects, because a project can survive the slice on a subagent anchor with no sessions behind it. The period strip shows what actually opened, keys 1-6 and every explicit selection win over the fallback, and the app's persisteddefaultPeriodpreference always wins. On a cold cache an empty today costs one extra repaint - the first paint's file floor is Today's, so the 7-day view is re-painted once on the wider floor from the cache pass one just wrote, about 2.3s on a 16.7k-file corpus and only when the cache is empty and the day unused. (#1113)
Added (OrcaRouter)
- OrcaRouter sessions now price and label like the model they route to. OrcaRouter is a gateway that exposes route ids (
orcarouter/auto,orcarouter/fusion, …) plus plain upstream ids (deepseek/deepseek-v4-pro), and reports the upstream id that actually ran in the completion response'smodelfield. Theorcarouter/prefix is now a routing wrapper likecmd//antigravity/, so a routed spelling prices at its upstream LiteLLM row instead of $0;orcarouter/autostays unpriced (the smart route lands on a Qwen/Llama flash model - a Sonnet alias would overprice it) and the fusion routes alias to their currentopenai/gpt-oss-120btarget (live completionmodelfield, 2026-08); and an unknown vendor nested inside the route still fails closed. Daily cache v29 so warm days re-derive the new prices.
Added (Windows)
- CodeBurn 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'shome/*androotdirectories for Claude Code (~/.claude) and Codex (~/.codex) history and merges it with your Windows sessions; the roots are additive, so they sit alongsideCLAUDE_CONFIG_DIRS/CODEX_HOMErather than replacing them, andcodeburn doctorlists 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=allopts into every installed distro, whileCODEBURN_WSL=offimmediately 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 dropdev/inofor\\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 menubarinstalls and launches the tray app on Windows. The same command that installs the macOS menubar now does the Windows one, through the same pinned-release path: it resolveswindows-v<cliVersion>, falls back to a scan of the newestwindows-v*release carrying both assets when that tag has none, downloads the.msiwith the same retry and backoff, and verifies its sha256 before anything executes it - a mismatch aborts without ever handing the file to the installer. It then runsmsiexecout of%SystemRoot%\System32(never a bare name, so nothing dropped next to the CLI can impersonate it) with/i <msi> /passive /norestart, treats exit 3010 as installed-pending-restart and 1602 as a cancelled install rather than failures, and launches the exe named by the product's Uninstall registry key. An already-installed matching version skips the download and just launches;--forcereinstalls.- A menubar app for Windows.
windows/is a Tauri 2 tray app - Rust binary, React popover - that puts today's spend in the notification area and mirrors the macOS menubar screen for screen: agent tabs, period switcher, Trend, Forecast, Pulse, Stats and Plan insights, activity and model breakdowns, optimize findings, CSV/JSON export, launch at login, currency, and theme. Windows has no menubar title, so the number lives in a second tray icon rendered from the system font at the panel's native icon size (Settings can turn it off; the tooltip always carries it). It reads everything through the CLI like the macOS and GNOME clients do, and gates on codeburn 0.9.9 or newer - the first release acceptingstatus --format menubar-json --no-optimize- showing a setup screen with the install command until it finds one. Refresh follows popover visibility the way the macOS app does: 60 s with optimize findings while open, 2 minutes for today's total while closed, and immediately on open when what you are looking at has gone stale. The Claude quota view never spends Claude's single-use refresh token; on a 401 it re-reads Claude Code's own credential file for a token it has already rotated, matching the macOS client. Ships as an unsigned.msifrom thewindows-v*tag, whichcodeburn menubarnow installs for you. The same crate still builds and runs a tray on Linux, but that stays experimental and unreleased -gnome/is the supported Linux surface.
Added (CLI)
- Copilot input/cache tokens are read per request from
~/.copilot/session-store.db. Previously, codeburn relied onsession.shutdownrollups from the Copilot CLI and GitHub Copilot desktop app. Those rollups are written only after a clean shutdown, stamp all usage on the shutdown day, and reset their counters at in-session compaction - so a crash could lose an entire session's input/cache usage, and even cleanly-closed long sessions were silently truncated. On one machine with long history, reading the per-request rows recovered about 35% of actual Copilot spend. Covered sessions now use per-request tokens with their real timestamps, counted exactly once against existing rollups and never added as extra calls or turns. Pre-store CLI sessions continue using the unchanged rollup path, and a locked or unreadable store defers only its own re-read instead of prematurely sealing daily history. Copilot reasoning tokens are also no longer double-billed: they are a subset of output already priced through the per-turn calls. This triggers a one-time re-parse, with the daily cache bumped to v26 to re-derive finalized days. Because that reconciliation keeps changing while a session is live,codeburn sync pushholds a Copilot session back until it has been quiet for 24 hours and then sends it once, final - the sent-ledger is append-once, so a value sent mid-reconciliation could never be corrected at the receiver (#988). Sync also pins each Copilot session to whichever of the two shapes it was first synced in - thesession.shutdownrollup, or per-request rows plus a residual - because a usage span cannot be retracted and sending the same tokens in the other shape would double them at the receiver permanently. That holds in both directions: a session synced before this release keeps its rollup and never sends rows, and a session synced as rows never sends the rollup that starts serving again once the 90-day age-out prunes them.--dry-runreports the frozen count, andcodeburn sync reset --confirmre-pushes everything under the new breakdown for anyone who can clear the receiver too. (#946) - DeepSeek Harness (
dsh) is now a supported provider. Reads DeepSeek's open-source agent harness from~/.dsh/sessions(DSH_HOMErelocates the root), both the default zstd logs and the uncompressedsession.jsonlvariant. A.zstdlog is a concatenation of independent zstd frames, one per write batch, so it is decoded frame by frame behind a structural frame scan and a torn trailing frame from a crashed writer is ignored rather than failing the file (needs Node 22.15+ forzlibzstd; below that dsh is skipped with a notice instead of counted as $0). One call per(turn, step), with the step's finalassistant/messageusage superseding the streamedassistant/chunksample of the same call rather than adding to it, the model taken from the message that served the step, and reasoning tokens billed at the output rate. DSH records tokens but no cost, so calls are priced from the shared tables. The events a forked session replays from its parent are skipped, since codeburn already counts the parent's own log. The session format is pinned at version 0 upstream with no compatibility implied, so a log stamped with any other version is skipped with a notice instead of read under today's assumptions.
Added (Desktop & Menubar)
- Live quota for Gemini, Copilot and Antigravity in the desktop app. The app's quota module knew claude and codex only; it now carries three more providers, all read from local credentials the tools already wrote - no cookies, no logins, no credential writes anywhere. Gemini goes through Code Assist (
loadCodeAssist/retrieveUserQuota) using the Gemini CLI's own on-disk OAuth material, refreshing a token only when the CLI's documented env overrides are set and keeping the refreshed token in memory; a retired consumer tier degrades to a terminal failure carrying migration guidance rather than a wrong number. Copilot reads the editor-plugin token from~/.config/github-copilot/hosts.json→apps.jsonand callscopilot_internal/user, re-reading once on a 401 - an internal API that may drift, so every failure degrades to an honest connection state. Antigravity probes the local language server over loopback only (RetrieveUserQuotaSummary, falling back toGetUserStatus), discovering it throughps/lsofwith regex-validated pids and relaxing TLS strictly for 127.0.0.1 self-signed certificates. Settings gains a per-provider toggle, defaulting on for detected providers; a disabled provider's fetcher never runs, so it reads no credential and makes no request. Provider display names come from a map rather than hardcoded pairs, sign-in guidance is honest per provider, and error sanitization now redacts Googleya29.and GitHubgh*_token shapes. Kimi live quota already exists in the macOS menubar and was audited rather than duplicated. (#1114) - The macOS menubar reaches quota parity with the desktop app, and gets a System Settings-style Settings window. The three providers above are ported into the menubar read-only from the app's Electron services, on the same load-state machine Kimi, Claude and Codex already use: Gemini through Code Assist and
~/.gemini/oauth_creds.json(in-memory refresh only, handling Google's CLI-OAuth tier retirement), Copilot throughcopilot_internal/userand the editor plugin token in~/.config/github-copilot(preferring the github.com host), and Antigravity throughps+lsofdiscovery and Connect-RPC probes over loopback HTTPS with the self-signed-certificate trust override scoped strictly to 127.0.0.1. Settings is rebuilt as a System Settings-style window: 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 binary0101flame as the app icon, the full-colour flame in the About hero and a solid silhouette in the About sidebar row; one tagline everywhere; and em-dashes removed from UI copy.package-app.shnow ships the SwiftPM resource bundle inside the.app- without itBundle.moduleaborts at the first icon render, which the release workflow would have shipped. (#1133)
Changed
-
Report, sessions, overview, compare, export, report JSON and menubar
localModelSavingsnow usebillableOutputTokensfor displayed output. Cost already did. Exclusive providers (Grok and the rest) were under-reporting output by exactly their reasoning tokens; optimize added reasoning on top of output for the inclusive set{claude, codex, copilot}and double-counted it. Displayed aggregates now bill per call while the provider is known, joined onto an existingmodelBreakdownkey (parser short name first, then the raw id) so a leftover raw-id bucket cannot mint a $0Opus 4.8Unpriced orphan. Daily cache v28 re-derives finalized days soreportmatches the live parse. Thanks @saulcanina. (#1115) -
The desktop app's cold start paints as soon as the selected period is readable, instead of after the whole history. The resident
codeburn servechild now does for its JSON clients what the TUI got in #1107: on a cold cache it answers the firststatus --format menubar-jsonfrom the files whose mtime allows them to hold data the requested period can show (the same floor and 48h clock-skew margin), then indexes the rest behind the answer. The payload says so in-band through a new optionalhydration: { complete, indexedFiles, totalFiles }block, and the desktop app and web dashboard renderindexing history · N/M files · totals below cover what is indexed so faruntil it reportscomplete: true. On a 21k-file corpus the app's first payload lands in 7.3s instead of 31.3s, fully indexed at 36.2s. Only the resident serve process ever emitshydration, and only it may answer partially - its clients poll, so they converge. Every one-shot output is unchanged and always a full parse:--format json/csv/markdown,report,sessions,models, MCP, the Swift menubar and GNOME clients, and the desktop app's own spawn fallback, none of which have a second poll to converge with. Absence ofhydrationtherefore always means complete, which is also what an older CLI's payload means. It is notstale(#1100) and never sets it: a first paint is fresh but partial, where a stale payload is complete but old. Nothing is stamped complete until the fill has actually parsed everything, so a process killed mid-fill comes back cold and finishes the job. (#1110) -
The discovery sweep issues its metadata syscalls concurrently. Every dated command re-walks and re-stats every provider tree before it can decide what is already cached, and that sweep was strictly serial: one
readdir, onestat, onestate.jsonread at a time, per provider, one provider after another. On a 21k-file / 9-provider corpus it owned most of a warm run's wall clock while the machine sat idle waiting on the kernel. Provider discovery now runs across providers at once, and the four walks that dominate it (claude, codex, kimicode, grok) plus the Claude project-dir walk and both fingerprint passes fan out through a shared bounded-concurrency helper. Order is unchanged everywhere - each level is re-concatenated in registry/readdirorder before anything reconciles against the cache - so what is discovered, in what sequence, is byte-identical to the serial walk. Two smaller cuts ride along: the Claude walk reads directory entries with their types so a plain file no longer costs a wastedsubagents/probe, and the Codex result cache (a single file that can reach hundreds of MB) now shares one in-flight load between concurrent readers instead of letting each one re-read and re-parse it. Warmcodeburn todayon that corpus: 5.30s to 2.99s; a cold parse 34.7s to 31.1s. (#1104) -
A kill mid-way through a non-Claude provider phase no longer restarts that whole phase.
scanProjectDirs(Claude) has long taken a throttledsaveProgresscallback so a killed cold parse resumes from a warm cache;parseProviderSources(every other provider - codex, cursor, gemini, and the rest) did not, and only persisted at the whole-provider boundary. On a large single-provider corpus (a multi-GB codex history is the common case) an app-timeout SIGKILL, crash, or force-quit during that phase discarded everything parsed since the last provider finished, forcing the entire phase to re-parse from zero on the next run.parseProviderSourcesnow takes the same callback, invoked once per source right after that source's cache entry lands (mirroringscanProjectDirs' placement, outside the per-file try/catch), on the same file-count/wall-clock throttle. A file only ever gets a fingerprint once it has fully parsed, so a mid-file kill can never leave a half-parsed file's entry looking complete on resume. -
Routed model ids price as the model they wrap, and an unknown vendor prefix no longer prices by blind stripping. Token-plan and gateway spellings of the same model (
omniroute:,cp/,cline-pass/,cline-free/,cmd/,antigravity/,orcarouter/) are peeled and the remaining id is priced, so a Cline Pass or OmniRoute session shows a~estimate instead of $0. In exchange,provider/modelis no longer treated as authority on its own: the leading segment is stripped only when it is a namespace the bundled pricing catalog itself uses (anthropic/,openai/,google/,x-ai/,qwen/,moonshotai/,nousresearch/,xiaomi/,z-ai/, and every other vendor prefix in the LiteLLM snapshot), one of the routing wrappers above, or one of the client-side spellingskimi/,mimo/,zhipu/,litellm_proxy/andopenai_like/. Anything else stays unpriced and is reported as unpriced rather than inheriting the price of a same-named cloud row, and local-runner prefixes (ollama/,lmstudio/,hosted_vllm/,local/) are excluded on purpose so an unlisted local tag can never invent cloud spend. A user price override for the bare id wins over the catalog row a routed spelling would otherwise hit. -
SQLite providers now survive read-only database parents. A read-only SQLite open is not read-only on disk: on a WAL database SQLite must create
<db>-shmand<db>-walin the database's own directory, so a source on read-only media, under restrictive permissions, or inside a Flatpak/snap confinement failed withattempt to write a readonly database(orunable to open database filewhen a-walwas present without its-shm), and both discovery sites swallowed it - the provider read as "not installed" rather than as an error. That covers cursor, cursor-agent, opencode, goose, warp, kilo-code, zerostack and the copilot agent-traces database. The direct open stays the fast path and is byte-identical when it succeeds. When it fails for want of sidecars: a database with no WAL frames to lose is opened in place withimmutable=1, which costs nothing and cannot go stale; a database with a non-empty-walis copied with its-walinto the CodeBurn cache and read there, so its un-checkpointed rows are never silently dropped. The copy costs one database's worth of disk and is taken once per change - it is keyed by the main-plus-WAL fingerprint, published under a fingerprint-stamped name so a refresh never overwrites a copy another process is reading, and superseded copies are evicted once a day has passed without a read, keeping at most one predecessor. If the cache itself cannot be written, the database is skipped with a notice naming it and the reason rather than in silence. The original provider database is never opened writable or modified. -
Grok Build now reads the CLI's own completed-turn usage instead of estimating it. Usage comes from the
turn_completed.usagerecords Grok CLI already writes intoupdates.jsonl(inputTokens,outputTokens,cachedReadTokens,cacheCreationTokens,reasoningTokens), deduplicated byprompt_idand emitted as one session-level call from the top-level totals. The previous parser reconstructed an estimate from the running_meta.totalTokenscontext counter, so existing Grok totals will change materially on upgrade - on one real 568-session corpus cache-read went from 150K to 96.3M tokens, total tokens from 20.0M to 113.9M, and cost from $36.98 to $56.79. Cache read and cache creation are subsets of input and reasoning is a subset of output, so reasoning is clamped to the record's reported output and split back out to match this repo's exclusive-reasoning contract.modelUsageonly selects a priced attribution id; multi-model rate attribution stays out of scope, so one session is priced at one model's rate.costUsdTicksis ignored because its scale is undocumented. Sessions with no usable record - older CLI versions - keep the old context-curve heuristic and stay flagged estimated. In a session that has at least oneturn_completedrecord, turns without one are not counted at all (their tokens are dropped rather than estimated), and the session is marked estimated instead of claiming full provider coverage. Cached Grok sessions re-parse once. The daily cache re-derives once on first run after upgrade: this is a global re-derivation of every day and every provider, since the daily cache has no per-provider invalidation, but it reads the warm session cache rather than re-parsing transcripts, so it costs seconds (~3s on the corpus above), and the superseded cache file is retained on disk as the baseline for days no source can still re-derive. (#998) -
Codex rollouts parse across worker threads too, and the workload gate now takes bytes or files. Codex is the bigger half of a real cold parse - a 4 GB rollout corpus against 1.8 GB of Claude sessions - and it was still decoding one file at a time. A whole-file rollout decode now runs on the same pool, against an empty dedup set, and comes back with the calls, the dedup keys it claimed, and the codex-cache entry it would have written; the parent installs all three in the serial loop's order, so
codex-results.jsonand every payload come out byte-identical to a serial run. Cross-file state stays where it was: a forked rollout replaying its parent's token_count history collides on the parent's keys and is re-parsed in-process, and no worker ever touches the cache module's per-directory state. Files the Codex cache can serve exactly or resume into from a byte offset never reach a worker - they read a few KB and the resume state belongs to the parent. The workload gate is now pending BYTES alone (200 MB), not file count: 250 pending files holding under a megabyte between them spawned threads that made the run ~5% slower, while a few hundred huge rollouts were being turned away. The count takesmax(pendingFiles / 50, pendingBytes / 200 MB), and the per-thread memory budget is derived per parse asclamp(256 MB, 2 × average pending file + 128 MB, 1 GB)rather than a flat 256 MB - a 260 MB rollout peaks near 430 MB in its worker and scales linearly with the pool, so the flat figure over-subscribed exactly the workload this adds. The decision is per provider, and at most one pool is alive at a time. -
A large cold Claude parse now runs across worker threads. Reading, decoding and line-parsing a session JSONL is per-file work that never touches anything shared, so it moves onto
worker_threads; each worker ships its parsed turns back as a JSON string and the parent installs them in the exact order the serial loop would. Everything with cross-file state - the streaming-message dedup, canonical project paths, spawn links, PR correlation, progress saves - stays on the main thread, and a file whose message ids were already claimed by an earlier file (or whose worker failed) is simply re-parsed in-process, so the session cache and every payload are identical either way. On a 6 GB corpus a coldstatusdrops from 27.5s to 14.8s with peak RSS up 2.27 GB → 2.52 GB. Threads only engage for a genuinely large cold parse: never with under 200 MB behind the pending whole-file re-parses, 2 or fewer cores, or under 4 GB of available memory - so warm and incremental runs are untouched and spawn nothing. Otherwise the count ismin(cores - 1, min(0.25 × available, 2 GB) / 256 MB, pendingFiles / 50), where available isprocess.availableMemory()(cgroup-aware in containers) rather than free memory, which on macOS reports free pages and would switch the feature on and off between runs.CODEBURN_PARSE_WORKERS=0forces the serial parse andCODEBURN_PARSE_WORKERS=Nforces N (capped at the core count), both bypassing every gate;CODEBURN_VERBOSE=1prints the resolved count and why. -
A warm launch rewrites only the month that changed, and a ranged query reads only the months it can report on. Per-provider shards still meant one appended session republished that provider's entire history - 95 MB for Claude on a 6 GB corpus. Each provider's shard is now split again by the UTC month of the cached session's FIRST turn, a bucket that never moves as a session grows, so an append rewrites one month. Every shard records the newest month it holds, which lets
--period today/weekskip the shards that cannot contribute a turn to the range; the skipped months stay on disk untouched across the save, and providers whose cache is the only surviving record (durable) or whose parse fingerprint moved are always read in full. Remaining shards are read concurrently. Existing v8 and v7 caches are re-laid-out losslessly on first load and the old layout removed once the new one is published: nothing re-parses. -
A warm launch rewrites only the provider that changed. The session cache was a single blob, so any provider appending a few KB republished the whole thing - 147 MB of stringify + fsync on a 6 GB corpus, ~18% of a warm run. It is now a version-suffixed directory holding one shard per provider plus a small envelope, written per provider and published by a single envelope rename. An existing v7 cache is re-laid-out losslessly on first load and the old file removed once the new layout is on disk: nothing re-parses. One unreadable shard now costs that provider a re-parse instead of discarding every provider's history, and partial saves during a cold parse are triggered every 2000 files rather than every 5 seconds, so a slow cold parse no longer rewrites the growing cache on a wall clock.
-
An appended Codex rollout parses only its tail. Rollout files are append-only and the active ones run to hundreds of MB, but the Codex result cache keyed on mtime + size alone, so any growth re-read the file from byte 0. Each entry now records a restart point at the last task boundary - byte offset plus the state the single-pass decode carries across it - and a grown file with the same inode resumes there, producing output identical to a full re-parse. An entry without a usable restart point simply re-parses in full once and gains one.
-
A date-ranged report classifies only the turns it keeps. Every cached turn went through the turn classifier - category, retries, edit detection, and a full reconstruction of its API calls - before the date slice discarded most of them, so a week view paid to classify all of history to keep a few percent of it. The keep/drop decision is now taken on the raw cached turn and only the survivors are classified, still from their complete call list, with the branch and pull-request carries still walking the full ordered turn list. Output is byte-identical.
-
One rule for every cache file.
CODEBURN_CACHE_DIRwhen set, otherwise~/.cache/codeburn.XDG_CACHE_HOMEis no longer consulted; the sync ledger, the only file that ever honored it, is merged into the canonical location on first read and the legacy copy is retired, so nothing is re-uploaded after the move. (#972) -
A cold interactive launch paints the dated view first and indexes the rest behind it. An empty cache parsed all ~21k files before painting a dashboard that shows 7 days. On a cold interactive TTY launch with a dated default view, the first paint now parses only the files that can hold in-range data -
fp.mtimeMs >= rangeStart - 48h, the clock-skew margin - which is safe because a session log's last event is never later than its mtime, so an older file provably cannot move the dated view; a file that already has a cache entry is never deferred, and network sources always load. The deferred files are parsed by the existing background reload without blocking keys, writing the per-file cache exactly as a full cold parse would and refreshing the panels when it lands, with anindexing history · N/M files · totals below cover what is indexed so farbanner until it converges. Nothing is marked seen without being parsed: a first paint that deferred anything cannot stamp the cache complete, so a run killed mid-fill re-enters cold and converges, and the floored parse's memo key carries a:paint<floor>suffix so it can never be served to an unfloored request. The path is gated onisTTYwith no--dayand no--from/--to, so one-shot JSON/CSV/markdown,report/sessions/modelsand every serve, menubar and app payload never enter it and never return partial data. On a real 21k-file corpus with an empty cache, time to first paint drops from 36.3s to 9.9s, and full indexing finishes at 47.5s instead of 36.1s - about eleven extra background seconds, paid while you are already looking at data. Two deliberate limits: a period switch during the fill queues behind it rather than slicing partially, and the plan-usage bar can read low until the fill lands, which the banner covers. (#1109) -
The dashboard parses once per launch instead of up to three times. Rendering the dashboard issued three
parseAllSessionscalls - the scan, the plan usages and the durable overview - whose ranges differed only in their ends, about a second apart, so the exact-key memo never hit and each one paid a full parse. The dashboard now declares its widest range up front, and any request inside it that is a pure narrowing - same start, an end no later, the same month-shard scope - is served by slicing that single parse.monthgoes from 2 parses to 1 andtodayfrom 3 to 2; the plan window starts on the 1st while the scan starts at midnight, and two different starts cannot be merged under the rule that makes this provably lossless, because a wider parse reads files a narrower one never sees and its dedup seeding can drop an in-range turn the narrower parse keeps. Alongside it the cross-provider PR correlation stops re-filtering the session list per child and the launch list per candidate, using a one-shot agent index and a windowed scan over a sorted array instead; the match sets are identical. On a frozen corpus, warmtodayin the TUI goes 5.12s to 3.81s andmonth5.46s to 3.41s, with every checked warm and cold output byte-identical. (#1108)
Changed (Linux packaging)
- The snap asks for the log directories it reads, not each tool's whole home. The first Snap Store submission declared a
personal-filesread of every AI tool's root -$HOME/.claude,$HOME/.codex,$HOME/.cursorand the rest - and that interface is recursive, so it granted read of every credential file those roots hold. Each entry now names the subdirectory the provider actually opens (.claude/projects,.codex/sessions,.cline/data,.vibe/logs/session,.dsh/sessions,.kiro/sessions,.quickwork/{profiles.json,sessions,metrics},.config/Claude/local-agent-mode-sessions,.config/Open Design/{runs,data/runs,namespaces}), two are single files (.forge/.forge.db,.zcode/cli/db/db.sqlite), and the editor entries name only the extension folders holding transcripts instead of the editor's whole configuration. Five providers that were missing entirely and would have shown no data are declared - opencode, crush, goose, kilo, kimi-code - and four roots stay roots only because the file the provider opens sits directly in them (.config/github-copilot,.local/share/{opencode,crush,kilo}). One credential file is now requested openly rather than implicitly:.claude/.credentials.json, read-only, for the live plan gauge. Codex's equivalent would need write access to the Codex CLI's ownauth.jsonto rotate the token, so neither it nor a Codex root is declared and the Codex live gauge is disabled under$SNAP; Codex usage and cost are unaffected, they come from the session rollouts. Two consequences inside the snap:.lingtaiis dropped, because its per-agent log directory needs a wildcard the interface has no form for, andoptimize,context-budgetandactno longer see the user-scope~/.claude/settings.json,agents/,skills/andcommands/- project-scope copies still work through thehomeplug. Nothing outside the snap changes.
Fixed (Desktop & Menubar)
-
The menubar stops killing its own cold cache rebuild. On a large corpus after the 0.9.20 cache-version bump, the menubar never completed a single fetch: every child was killed at exactly 45 seconds, and each kill left the
session-refresh.lockbehind for the next one to wait out. Four things were wrong and all four are fixed. The 45-second cap was a TOTAL-runtime kill, so it is now the same no-output watchdog the desktop app got in #1096 - spawns and the residentcodeburn servealike setCODEBURN_PROGRESS=1, the window restarts on every byte of stdout or stderr (the CLI heartbeats every 10 seconds while parsing, and now also while WAITING on the refresh lock, which was the silent stretch that mattered), and only a genuinely mute child is killed, with the same 10-minute cold floor until the first payload lands and the same 15-minute absolute ceiling behind it. Kills are SIGTERM first and SIGKILL only after a 5-second grace, and the refresh lock now arms the same signal cleanup the hydration lock has, so a killed holder unlinks its own lock instead of leaving one. A lock that was abandoned anyway is recovered immediately rather than after 90 seconds: a waiter takes over a lock whose recorded holder pid is gone, or whose heartbeat has frozen, and the waiter's own budget is now derived from the stale window so it can never again expire before the gate it is waiting for opens - a live holder, whose heartbeat keeps the mtime fresh and whose pid answers, is never taken from. The resident child's fixed 60-second warm request cap becomes a silence window that each progress frame restarts, and a spent restart budget is a five-minute cooldown rather than leaving the resident dead for the rest of the app run. Serve orphans get three new backstops: the app closes its end of a retired child's stdin (dropping the handle was not enough - the pipe stayed alive inside the Process), reaps every child it started synchronously at quit before the async shutdown can be skipped, and records the child's pid and command line so a serve orphaned by a crash is reaped on the next launch. On the CLI side a serve child's final exit no longer runs through theprocess.exitan in-flight request has monkeypatched, and its post-drain cleanup is bounded like the drain. (#1117) -
A long panel query is no longer killed for being slow. The desktop app capped every read at 45 seconds of TOTAL runtime, so on a slow machine
optimize,yield,models,sessions,spend,audit,act reportandplanwere SIGKILLed mid-parse and the panel painted a red "timed out" that a 60-second poll then reproduced forever. That cap is now a no-output watchdog: the window restarts on every byte the child writes, so only a genuinely silent child times out, with a 15-minute absolute ceiling still catching a livelocked one. Silence now means stopped rather than slow, because the parse itself heartbeats: every read spawn setsCODEBURN_PROGRESS=1, and under it a running parse emits a keepalive line every 10 seconds - the stretch that mattered was a cold parse's inter-provider cache save, measured at 31.6 seconds of total silence on a large corpus, which the old scan-progress stream did not cover at all. A served request heartbeats for its whole duration, parse and the aggregation and serialization after it alike; a one-shot spawn heartbeats through the parse, and the roughly 8-second tail that follows it stays well inside the window on its own. Residentcodeburn serverequests reset their window on each frame of their own response. Alongside it: the cold-cache floor now covers EVERY read while the first hydration is still running, not just the overview, so a section that starts polling the moment the app is ready is not killed waiting behind that parse; a read that times out while the hydration is still going keeps the indexing splash instead of painting an error panel, bounded so that an install which can never hydrate still surfaces a real error once the cold window has elapsed rather than sitting behind the splash forever; and a read killed for timing out, or a resident child replaced by a settings mutation, is sent SIGTERM first and SIGKILL only after a 5-second grace, so the child can unlink its own cache refresh lock rather than leave it for the next parse's stale-pid takeover (quit stays a hard kill, its flush budget being shorter than the grace). A hydration that genuinely needs more than 15 minutes is still ended by the absolute ceiling, but it no longer starts from nothing next time: the partial cache saved along the way means successive polls converge instead of each repeating the whole scan. The app also records the resident child's pid and full command line, and reaps a serve orphaned by a previous crash on the next launch - on Windows too, where there is nopsand no stdin-close recourse after a crash - signalling only when that pid still runs that exact command. Separately,codeburn servedrains an in-flight request before exiting on stdin close, bounded at 45 seconds so a wedged request cannot turn the child into the orphan the drain prevents. -
The menubar's copies of your Claude and Codex credentials move out of Application Support and into the login Keychain. Connecting a provider used to leave the copied OAuth material in
~/Library/Application Support/CodeBurn/*-credentials.v1.json, written world-readable (0644) because macOS ignores.completeFileProtectionoutside iOS. The copy now lives in a CodeBurn-owned login-Keychain item, and the first read after upgrading migrates the old file: it is reopened withO_NOFOLLOW, refused if it is a symlink or not owned by you, repaired to 0600 before a single secret byte is read, written to the Keychain, read back and compared, and only then unlinked - a failed or unverified write leaves the (now 0600) file in place so a retry can still find it, and the next read retries the cleanup. Where both a Keychain item and an old file exist, the one that expires later wins before anything is removed, so an item left behind by a much older build cannot displace a fresher token. Claude's entry no longer stores a refresh token at all - the CLI owns that grant and the menubar never spends it - and any refresh token in a historical blob is dropped on read. Disconnect only reports success once the material is actually gone; if the delete fails it says so and leaves the provider connected so you can retry. Keychain reads are non-interactive and are skipped outright while the login Keychain is locked, so a background quota refresh can never raise an unlock panel. (#1037) -
First launch no longer asks to control System Events. The macOS menubar registered its login item by driving System Events over AppleScript, which made macOS put up an Automation consent dialog the first time the app ran. It now registers itself through
SMAppService.mainApp, an in-process call that needs no Automation grant; there is no AppleScript fallback, so a failure logs and leaves the login item unset rather than bringing the prompt back. The samecodeburn.loginItemRegisteredguard still limits this to the first launch, so a login item you removed by hand stays removed. (#1026) -
The resident
codeburn servechild. The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972) -
The desktop app's Pull requests tab says why it is empty. A PR row exists only where a transcript actually contains a pull-request URL, and the default period is Today, so a week of work through providers that never write those URLs left the tab blank with copy that named neither the period nor the reason - indistinguishable from a broken extractor. The empty note now names the selected period, and when the All period does hold rows it says how many and points at the period control. Attribution itself is untouched: no link is inferred, no PR is invented, and the default period is unchanged. (#1098)
Fixed
-
Hermes sessions that keep running after a day is sealed no longer lose later token and cost growth. A sidecar ledger under the CodeBurn cache records last-seen lifetime totals per
(profile, sessionId)and emits observation-time deltas (weight 0) so a sealed day stays put while today's growth still lands. An all-zero reset is visible to the cursor after discovery filtering, so a later 40 is+40rather than a silent shrink. Explicit$0is treated as recorded. Ledger publication failures are retryable and hold the daily watermark. (#916) -
codeburn doctornow probes the five remaining fixed-location providers.codebuff,devin,gemini,kiro, andmistral-vibeimplementprobeRoots()through the same resolvers discovery uses, so a silent zero is distinguishable from a missing install. Codebuff reports all three manicode channels unless a factory orCODEBUFF_DATA_DIRpins one; Devin reportstranscriptsplussessions.db, not the parent; Gemini reports only~/.gemini/tmp; Kiro reports pre-filter candidates (empty CLI/v2 skipped, empty agent/workspace fall back); Mistral Vibe reports the joined sessions dir. Missing defaults still appear. Thanks @therickfactr. (#899) -
Subscription SKUs are classified from real product ids, and a false-positive built-in can be opted out.
codex-auto-reviewconsumes ordinary Codex usage (openai/codex#32224) and is priced as GPT-5.5 on #1056, so treating it as $0 hid real spend - it left the flat-rate list. Warp's product id isauto, not the syntheticwarp.kimi-for-coding-highspeed(the SKU #968 was filed around) is now honestly $0.big-picklewas dropped: it appears under OpenCode, not as a cited ClinePass codename.codeburn model-flat-rate --removenow opts out of a built-in, so a wrong classifier entry can warn again without waiting for a release. The daily-cache config hash now always includes the flat-rate section (even when empty), so the first run after upgrade re-derives every stored day once from the warm session cache. (#968, #1050) -
Codex MCP and skill usage is attributed from every shape Codex records a shell command in.
mcp-cli call <server> <tool>was only recognized when the command arrived asfunction_callarguments (#656). Codex has two other shapes for the same exec: its custom-tool transport records the shell tool as acustom_tool_callwhose payload is aninputprogram rather thanarguments, and its item model repeats a finished command asevent_msg/item_completedcarrying aCommandExecutionitem with an argvcommand. Both reached the Bash counter and neither reached the matcher, so a CLI-wrapped MCP call stayed absent from the MCP breakdown exactly as before the fix. All three shapes now feed one classification pipeline. The same pipeline learns skills: Codex has no skill tool, so loading one is a shell read of the skill'sSKILL.md, and those reads landed entirely under Bash with the Skills dimension empty. A read counts as a skill load only when the command segment starts with a file-reading binary (cat/bat/sed/head/tail/less/more) and the path it reads ends in<name>/SKILL.md; the skill is<name>, the same keypiderives for a native skill read (#588) and the same vocabulary the Claude parser records from theSkilltool. Agrep/rg/lsthat merely mentions aSKILL.mdis a search near the file, not a skill load, and stays plain Bash. This is attribution only - no call, token or cost figure moves, and a command carried by both a response item and an item-model item is attributed once. On a 1,397-rollout corpus: Skills went from empty to 7 skills over 35 turns (55 attributions), Bash was unchanged at 42,170, and cost, calls, tokens, sessions, daily, models and projects came back identical. Cached Codex sessions re-parse once (CODEX_CACHE_VERSION13 → 14 and the codex parse version both move; without them the fix is invisible on a warm cache). Thanks @chr-evensen. (#478) -
gpt-5.6-codexandgpt-5.6-codex-maxnow have their own pricing rows. Neither id is in LiteLLM yet, and both were missing from the bundled snapshot - flagged during #1075 verification on a real corpus (285 sessions, 5,446 calls).getModelCostsalready resolved both through thegpt-5.6prefix fallback, so live pricing was already correct once a session priced fresh; every prior Codex-suffixed id LiteLLM does carry bills identically to its bare-model sibling of the same generation (gpt-5-codex==gpt-5,gpt-5.1-codex==gpt-5.1-codex-max==gpt-5.1,gpt-5.2-codex==gpt-5.2,gpt-5.3-codex==gpt-5.3), which is the evidence both new rows mirror rather than inventing a rate. The gap that does not self-heal is the daily cache: it has no per-provider invalidation, so a day finalized while either id had no billable rate keeps that $0 forever. RaisingMIN_SUPPORTED_VERSION(v23 -> v24) forces the one-time re-derivation, a lossless no-op for days already correct. (#1077) -
Mixed-version installs no longer thrash the Codex / Cursor / Antigravity result caches. Daily and session caches already own a version-suffixed file so an old desktop binary and a newer CLI cannot clobber each other. The three per-provider result caches still used one unsuffixed filename with an internal version field, so a v10 and a v11 binary rewrote the same
codex-results.json(and the Cursor / Antigravity siblings) on every run and each re-parsed its whole corpus. They now write*-results.v<n>.jsonthe same way the daily cache does. The unsuffixed file is left for older binaries; a matching-version copy is adopted once and never overwritten. (#1082) -
Codex spend no longer counts reasoning tokens twice, and cache writes are priced only where OpenAI actually charges for them. OpenAI bills reasoning tokens as part of
output_tokens, not on top of it - on a 1,396-rollout corpus all 134,316 events carrying a total satisfyinput + output == total- but CodeBurn addedreasoning_output_tokensto output when pricing a Codex call and again in the models, audit and per-model displays. Every Codex number was therefore too high: on that corpus cost by $166.03 (3.5%) and displayed Output tokens by 34.6% ($4,713.12 -> $4,547.09; 22.6M -> 16.8M output tokens). The rawreasoningTokensfigure is unchanged and still reported on its own; only the double-count is gone. Both places that price a Codex call - the parser and the cache-rehydration re-price - now go through one sharedbillableOutputTokenshelper, so a cold run and a warm run can never disagree. Separately, Codex'scache_write_input_tokens(new in codex PR #33454) was never read and cache-creation tokens were hardcoded to 0; they are now carved out of the uncached-input bucket and clamped so they can never exceed it. That carve-out happens only on models whose pricing source publishes a real cache-write rate - gpt-5.6 and its terra/sol/luna variants charge 1.25x input for a cache write, everything before it charges nothing extra - because CodeBurn fabricates a 1.25x rate when a source omits one, and charging that would have invented a surcharge on gpt-5.5, gpt-5.4, gpt-5.3-codex and gpt-5. On models without an explicit rate the tokens stay in the plain input bucket and the price is unchanged to the cent. The field is new enough that today's impact is $0 on that corpus. Codex sessions re-parse once and the daily cache re-derives once off the warm session cache (a global re-derivation of every day and every provider, since it has no per-provider invalidation); no other provider's numbers move. Days whose Codex transcripts have since aged out are held by the same never-lose guard #1040 relies on: a re-derivation that finds fewer calls than the settled baseline keeps the older, pre-fix (double-counted) total rather than truncating it, so those days do not pick up the repricing until their sources are re-derived with equal or greater evidence. Long-context pricing tiers from the same report are tracked separately in #1076 and the missinggpt-5.6-codexsnapshot rows in #1077. Thanks @chr-evensen. (#1075) -
Codex Tok/s no longer counts reasoning tokens twice or credits harness startup as model time. Two distortions in the same metric, found and fixed together because they share the same cache-invalidation and test surface. (1) #1075 fixed the reasoning-token double-count for cost, but
activeGeneratedTokens/taskGeneratedTokensin the Codex parser andgeneratedTokensin thecodex-tpslive-throughput reader still summedoutputTokens + reasoningTokens; both now go through the samebillableOutputTokens('codex', …)helper #1075 introduced, so the numerator can never drift from the billed one. (2) Codex firestask_startedbefore it assembles the request, so the gap up to the first request-context event (turn_context,world_state,event_msg/user_message, or aresponse_item/message) was pure CLI/harness startup counted as active model time - the active window now starts at that first event instead, which matters most for one-shotcodex execsessions that pay the gap on every task. The duplicated tool-interval clip/merge/cap logic inproviders/codex.tsandcodex-throughput.tsis now one function (mergeToolIntervals, exported fromcodex-throughput.ts), which also closes a live trap wheretask_complete's duration only parsed a plain number and silently dropped the{secs,nanos}/string formsmcp_tool_call_endalready tolerated. (A third suspected distortion - fork-replay dedup dropping a token_count event's tokens from the numerator without shrinking the window to match - was investigated and retracted: the earlierprevCumulativeTotalguard already discards a repeated running total before dedup is ever reached, so a real Codex writer never produces a partial drop; the dedup site now carries a comment recording this so the trip isn't repeated.) Display only, no cost or token-count impact - verified byte-identical on the same real corpus. Combined effect on a real Codex corpus (original bug -> all fixes): GPT-5.5 37.8 -> 28.2 tok/s (-25.5%), Codex Auto Review 23.3 -> 20.0 (-14.2%), GPT-5.6 Sol 43.2 -> 33.9 (-21.4%), GPT-5.6 Luna 53.5 -> 49.0 (-8.5%), GPT-5.4 68.0 -> 43.6 (-35.9%), GPT-5.4 Mini 54.9 -> 55.6 (+1.1%, the harness-startup correction outweighing the reasoning-count correction for this model on this corpus).activeGeneratedTokens/activeDurationMs/toolWaitMsare stored verbatim in both the Codex result cache and the session cache rather than re-derived on read, so none of this self-heals: Codex sessions re-parse once (one cache-version bump covers both fixes, since they touch the same fields). The dashboard's per-model column stays labelledTok/s- a wider label had zero room at the standard three-column layout, verified by breaking a real width-budget test - but the legend beneath it now reads "Effective Tok/s: generated tokens ÷ time the agent spent waiting on the model, tool execution excluded. Includes prefill, request assembly and reasoning. Not comparable to vendor decode-speed figures." (#1079, #1088) -
Codex calls attributed from session metadata no longer carry a stale model. The Buffer fast path scanned
session_metafor the first"model"string anywhere in the payload, so a nestedbase_instructions.provenance.modelwas read as if it werepayload.model- and since the model is last-writer-wins state, that wrong value was credited to every call before the rollout's firstturn_contextand to every call after any mid-filesession_meta(29 of 1380 rollouts on one real corpus carry a latesession_meta, and 57 record usage before anyturn_context). Direct payload fields are now read depth-aware, which is what the non-fastJSON.parsepath always did. Codex sessions re-parse once (~9s on a 4 GB rollout corpus) and the daily cache re-derives once off the warm session cache, a global re-derivation of every day and every provider since it has no per-provider invalidation; it moves per-model attribution, and clears any rollup an earlier parse change had left stale. Days whose transcripts have partly aged out are held by the never-lose guard: on a real 110-day cache no day lost value and none disappeared - 100 days came back identical and 9 grok days rose by $19.80 in total. Thanks @timdp. (#1040) -
Codex
session_metacwd / session id / originator follow the same depth-1 window asmodel. #1040 fixed nestedprovenance.model; the compact Buffer path still took the firstcwd,session_id,originator,name,forked_from_idormodel_provideranywhere in the payload, so adynamic_tools[].name(or any same-named nested key) could steal the top-level field. Those strings now use the existing payload-depth-1 scan. Function-callnameon other event types is unchanged. Codex sessions re-parse once. (#1045) -
Plan rows for sticker-price presets read as a budget instead of live provider quota. There is no Grok quota endpoint, so a SuperGrok row was parsed API-equivalent spend divided by the plan's sticker price on a monthly reset - but the TUI labelled that math "plan" and "reset", which next to a client showing xAI's real weekly window read as CodeBurn being wrong. The bars and the arithmetic are unchanged; the words are not. Both the dashboard and the desktop app now say the number is an API-equivalent monthly budget and not a live provider window, in the same wording on both surfaces, and for every preset rather than as a SuperGrok special case. The window is anniversary-based (
plan.resetDay, settable withcodeburn plan set --reset-day), so it is called a budget reset rather than a calendar one. The row was also shortened to fit 80 columns: at that width the percentage and the projected month were being truncated away, including on custom plans, whose label carries the provider. -
MiMo sessions price from the LiteLLM Xiaomi rows, and MiMo v2 Flash no longer crashes the display path. Hermes / Xiaomi token-plan sessions store the bare id (
mimo-v2.5-pro,mimo-v2.5) while LiteLLM namespaces its row (xiaomi/…), so those models reported $0. They now alias to the existing snapshot rows - no invented rate, andkimi-k3still has none - which means a session Hermes left costless is priced from the shared tables and carries the estimated marker, exactly asmimo-v2-flashalready did. The same change fixes a pre-existing crash that this alias did not introduce: the shippedmimo-v2-flash -> xiaomi/mimo-v2-flashalias already cycled through display-name resolution - strip the namespace, alias it back, take the leaf, repeat - sogetShortModelNameblew the stack on any real MiMo v2 Flash session and took every surface that names a model down with it, themodelstable included. Display-name resolution is now cycle-safe, and themimo-v2-flashandmimo-v2.5rows are named rather than shown as raw slugs. -
A date-ranged run no longer republishes the month shards it never read. A scoped load leaves an out-of-range month on disk, so the files it holds have no visible cache entry and the reconcile re-parses them - re-deriving the entry the shard already stores. That re-parse marked the unloaded month dirty, and the save merged and republished it under a fresh nonce name on every single run, byte-identical content and all, so a repeated
codeburn status --format jsonchurned old months (on a real corpus: claude/2026-03, cursor/2026-02 and warp/2026-03 renamed every run) and left the retired shards for the sweeper. A merge into an unloaded month that neither adds, changes nor removes an entry now keeps the published shard, so unchanged months keep their names and their bytes. (#1032) -
modelsandauditno longer show two identicalGrok 4.5rows.grok-4.5-build- the Grok Build harness's variant id - fell into thegrok-4.5display entry by prefix, and since rows bucket by model id, not display name, the two came out as visually identical rows with different numbers. The variant now shows asGrok 4.5 (build). Display only: no id is rewritten and no cost moves. (#1029) -
An upgrade no longer loses history for days whose transcripts have only PARTLY aged out. The never-lose contract carried a cached (day, provider) slice forward only when the re-derivation found NOTHING for it, but transcripts expire per FILE rather than per day: on a day whose sources are mostly gone, a handful of turns from surviving later files still bucket onto it, so the fresh slice came back non-empty but truncated and REPLACED the full cached one. On a real cache upgrading from the last shipped daily-cache version, 2026-07-16 fell from $1,685.17 / 12,530 calls to $385.44 / 560 calls, and 13 days lost $2,765.75, 19,209 calls and 520 sessions in total. A fresh slice now replaces a settled baseline slice only when it carries at least as many CALLS - the same or more evidence; fewer calls means the source set demonstrably lost data, and the baseline is kept whole. The comparison is on calls alone: cost and tokens are re-priced accounting on the same evidence, which is exactly what a legitimate re-derivation changes (the Grok accounting fix keeps its per-day calls and is unaffected), and session counts drift down by a few on days whose sources are entirely intact. Days inside a 7-day settle window stay authoritative - their session files are still on disk, so a shrink there is a real change rather than expiry. The trade-off is deliberate and matches the direction this cache has always chosen: a future fix that legitimately REDUCES calls on a settled day keeps the older, higher value until that day is re-derived at an equal or greater call count. The timezone-change re-derive gets the exact form of the same rule - what the fresh parse can no longer explain under the old bucketing is added on top of the fresh slice instead of being dropped - and the cross-file adoption union is unchanged, where the newer schema still wins per (day, provider).
-
The session chart legend now leads with a visible session disambiguator and title instead of the project path. Every series in a monorepo shared the same project prefix, so the only thing separating them was a truncated hex fragment - and per-application cost attribution is the main reason to open that chart.
SessionSummary.titleis already parsed and already rendered in the Context tab; the legend now puts the short session id first, prefers the title, and falls back to the previous project-based label when a session never produced one. Titles come from transcripts, so they are stripped of ANSI and control characters and capped before they reach either the legend or the tooltip. (#997) -
Context-bloat detection now counts reasoning tokens as generated output.
detectContextBloatdivided context bytotalOutputTokensalone, but reasoning is stored beside output rather than inside it, so for every reasoning-bearing provider the detector saw a fraction of the tokens actually generated and invented findings - a session whose real ratio was 20:1, under the 25:1 threshold, was reported as 133:1 and "high impact". It now uses the sameoutput + reasoningsum the reports use, which corrects grok, codex, kiro, hermes, qwen and cursor-agent alike. -
The unpriced-models warning in the dashboard is now readable at every terminal width. It lived in a fixed-width panel with an inline model list and a fix command, so it clipped mid-name at 80 columns and clipped earlier at 200, where the three-column layout narrows each panel - neither the affected models nor a runnable command survived. The panel line is now a pointer,
! N unpriced: codeburn models --unpriced(shortened to! N: codeburn models --unpricedbelow 45 columns of panel), and the model list moves to that command's plain output, which is full width, copyable, and lists every model rather than the first two. The command's hint no longer reads as an unconditional instruction to alias: a subscription or flat-rate model is correctly $0, and mapping it onto another model's per-token rate would invent spend that was never billed. Provider-supplied model IDs are now stripped of terminal control characters in every human-readable report rather than only on the unpriced path, and--unpricedshows raw IDs instead of friendly names becausemodel-aliaskeys on the raw ID. (#969) -
codeburn models --unpriced --top Nreturned nothing for a--top Nsmaller than the number of priced models.--topis applied insideaggregateModels, before the unpriced filter, on rows sorted cost-first - and unpriced rows are $0 on both, so they sorted last and the slice removed exactly the rows the flag exists to show. A user with unpriced models was told they had none. The slice now runs after the filter - and after ranking, because unpriced rows tie at $0 on both keys, so slicing them in aggregate order kept whichever models happened to appear earliest in the transcript rather than the largest. The order now matches the one the unpriced-models warning shows. (#969) -
Old durable sources remain visible while they still exist. The 90-day session-cache age-out now applies only after a durable source disappears from discovery, so an unchanged older Copilot source keeps reporting usage and reuses its persisted fingerprint instead of being reparsed and immediately discarded. (#987) On long-lived machines this makes previously dropped history reappear, so lifetime totals can jump once after upgrading.
-
optimizeno longer treats subagent transcripts as your sessions. Claude Code writes each subagent's transcript to its ownsubagents/agent-*.jsonlfile withisSidechain: trueon every entry, and optimize counted each one as a user-started session. That inflated the session count in the header and fed the session-level detectors a population that fails their tests by construction: a sidechain is handed a large context and returns a short answer (context-heavy), and it never commits or opens a PR because its parent does (low-worth). Excluded from sidechains now: the header session count, thelow-worth-sessions,context-bloat,cost-outliersandcapability-reliabilitydetectors, the coaching notes, the file-churn table, the median time-to-first-edit, the worst one-shot category, and the model-default recommendation - plusduplicate-reads, because a subagent starts on a fresh context and re-reading what its parent read is a necessary read, not a repeat. Everything else keeps the full population:build-folder-readsandread-edit-ratiostill count calls made inside a sidechain, since readingnode_modulesor editing without reading is the same waste whoever does it and theCLAUDE.mdrule they suggest binds subagents too, and so do the MCP, cache-bloat, ghost-command and configuration-overhead findings. Classification is sticky across the whole file, so calls that appear before the first marked entry are reclassified too, andisSidechainnow survives the compact parser's 32 KB large-line path and warm-cache range rebuilds. Nothing is deleted from spend: sidechain tokens, calls and cost stay in every total and instatus, and the optimize result cache keys on sidechain identity so a run cannot be served a pre-fix result. Absent markers still read as user-started, so no cache re-parse is needed. (#974) -
optimizeno longer offersclaude mcp removefor claude.ai connectors, and its MCP schema-cost estimate is per session. Aclaude_ai_*namespace that no readable local MCP config claims is a claude.ai connector, managed through/mcpor claude.ai Settings rather than as a local MCP server (a local server that carries the prefix keeps its removal command and gains a same-name connector note); low-coverage findings now render them as a manual follow-up and build--applyplans only for exact local server names found in readable MCP config, so mixed findings remove only the local subset and the "apply-able" subtotal counts only that subset. The same change replaces the old global schema-cost cap with per-session, per-server proportional attribution - a more accurate model that lowersmcp-low-coverageestimates for everyone, connectors or not (on a large corpus roughly by half). (#975, #991) -
Bash command splitting was quadratic on long whitespace-heavy commands. The separator regex retried its leading
\s*from every offset; matching the separator alone and widening over whitespace by hand makes cold parse ~24% and warm ~40% faster on large corpora, output unchanged. -
Cold parse no longer retains full message bodies through cached previews.
flatSliceskipped its Buffer round-trip for strings already within the bound, but provider adapters pre-truncate user-message previews with.slice(0, 500)before the cache-site call - those pre-sliced views are still V8 SlicedStrings pinning their large parent, so the retention that OOM'd cold parses of large histories survived. The round-trip now always runs. -
Kiro sessions carry the real
projectPath(CLI meta.cwd, v2workspacePaths[0], workspace sessions'workspaceDirectory), so git-repo attribution can resolve them; previously they were attribution-blind. Bumps the kiro parse version, so the first run after upgrade re-parses kiro history once, and kiro sessions in linked git worktrees now group under the main repo. -
A ranged parse can no longer write a day outside the range it was asked for. A turn that straddles local midnight keeps its original anchor through range slicing, so the day aggregator could emit a residue day just outside the parsed window - cost, calls and tokens all zero, category counts only - and both ranged call sites, the re-derive and the gap fill, wrote it into durable history. The merge guards below defuse the overwrite, but the residue should never reach the cache at all, so both ranged sites now filter to the days actually in range. The aggregator and the slicer are untouched, so whole-corpus callers are unaffected. (#1131)
-
The daily cache's gap-fill path can no longer overwrite a good day with a degraded parse, and a day left holding only midnight residue heals itself.
ensureCacheHydrated's gap fill wrote days into durable history through a blind overwrite with no completeness gate, so a parse degraded by the refresh lock - the menubar refreshing concurrently is the ordinary way this happens - could permanently replace a settled day with its undercount; the re-derive path had completeness and partial-survival guards, the gap path had neither. A gap merge is now completeness-gated: a complete parse wins per (date, provider) through the same guarded merge the re-derive uses, while a partial parse can only fill gaps and never shrinks the baseline, and apendingRederiveis no longer silently dropped. A day holding only a straddling turn's category counts, with cost, calls and tokens at zero, now pulls the watermark back so the next launch re-derives it - bounded to the settle window and never the oldest cached day.codeburn doctorreports cache health read-only, listing residue-only dates and failed or empty session-cache entries, so the next report of this shape is one command rather than archaeology. There is no cache-version bump: every affected cache heals on its next launch. (#1129) -
codeburn syncno longer puts a filesystem path or a raw output count on the wire. Every synced span carriedai.projectas the slugified absolute path (-Users-you-Projects-thing), contradicting bothdocs/sync/README.md("paths stay local") and the compatibility contract's "safe project basename"; the wire name is now the leaf directory name, derived at the single choke point after the parse, andcodeburn yield's local output is untouched. Two directories with the same leaf collapse to one wire identity - that is what the contract's project identity means - andgit.repodisambiguates them. Separately,ai.output_tokenswas the raw output field, so exclusive-reasoning providers were under-reported on the wire by exactly their reasoning volume (measured on one corpus: an opencode session short by 37,848 tokens, its summed reasoning); it now goes through the samebillableOutputTokenshelper the display layer took, leaving the inclusive providers byte-identical. Cost is unaffected, being priced before export. Neither field is part of a span id, so nothing is re-sent and nothing duplicates at the receiver: usage span ids and commit attribution keys carry no project, and session attribution re-emits once per session under an upsert keyed on (org, trace). (#1126) -
Outbound project metadata now requires proven provenance instead of failing open. The basename mapper above still accepted whatever the cache held, so a Claude path slug, a generated session title, a home root, a relative path, a provider's own storage directory, a credential-shaped basename or a provider-owned container path could all become an outbound project label. The privacy decision moves to the OTLP boundary and demands explicit provenance: a usage span's
ai.projectcomes only from a provider-recorded absolute cwd, attribution resolves repositories only from that same trusted cwd and derives itsai.projectfrom the normalizedgit.repo, and a legacy cached cwd with no provenance fails closed. A relative cwd can never be resolved against the directory that happens to be runningsync; Hermes prompt-derived paths, including the legacy WindowsCurrent working directory:messages, stay local grouping labels and never become outbound provenance - a deliberate privacy correction; Goose propagates its ownsessions.working_dirfield while/sessions/<container>paths are rejected centrally; and provider, model, tool and span-name identifiers are sanitized at serialization, with real routed identifiers still supported.ai.projectremains optional, so a receiver must accept an unattributed span; where a trace carries both labels and they differ, the attribution repository basename is authoritative and the provisional usage basename must not be counted as a second project. (#1128) -
Attribution spans no longer carry the session id in cleartext.
codeburn sync push --attributioncopiedai.session_idonto session and commit spans even thoughderiveTraceId(sessionId)already keys both usage and attribution spans, and usage spans never carried the field; it is dropped from both, and the docs now match the wire. This is hygiene rather than a leak - the id existed on the wire only as hash input before attribution shipped - but a receiver that keyed session rows onai.session_idmust key ontraceId, the same id usage spans already carry. The join to usage is unchanged, and the local sent-ledger keys still use the session id. (#1073) -
codeburn doctornames Buzz as a launcher instead of showing it as a second Codex tree. Buzz sits on top of Codex, so a Buzz usage parser or a walk of~/.buzz/.codexas a second Codex home would count the same billed seat twice - but doctor only knew providers, so a nest holding no usage store of its own read as "nothing found". Buzz is now listed as a launcher billed through Codex, with no session count, as is Grok Bot when~/.grokis the real store, and Codex discovery returns nothing for a home nested under~/.buzzwhile a distinct primary Codex home exists. A sole Codex home that does live under.buzzis still counted. Spend stays on the Codex row; no Buzz parser and no rates were added. (#1099) -
codeburn modelsresolves raw ids the wayreportdoes, and stops splitting one SKU across two rows. The report bucketed by raw id after asking each provider for a label, so a provider whose local table missed fell back to the raw id (gpt-5.6-sol,accounts/fireworks/models/kimi-k2p6) - and a later merge keyed on the display string, which summed genuinely distinct SKUs whose labels happen to collide into a single row. Provider-first resolution stays and local labels still win (the Cursor estimated suffix, provider overrides); a provider that merely echoes the raw id now falls through to the global short-name table, whichcursor-agentandvercel-gatewayalso consult after their own transform. Rows merge on provider plus alias-resolved canonical id, so a path-form id merges with its bare slug and display names go back to being cosmetic: two SKUs that only share a label stay two rows. The first-seen raw id is kept, somodels --format jsonis not rewritten to a lexically smallest spelling, and a Codex credit row mixing rated and unrated buckets partial-sums the rated ones and reportscreditsIncompleterather than a confident total.auditstill buckets by raw id, because it recomputes rates per id. (#1053) -
A resumed Copilot CLI session's stampless shutdown legs no longer collapse onto one day. Each leg of a resumed session appends its own
session.shutdownrollup, and a journal whose legs carrysessionStartTimebut no per-leg timestamp fell back to that start time - identical across every leg - so all of them billed onto one date and, with that stamp in the dedup key, onto one row. The timestamp fallback is now the event's own stamp, then the last event seen, and only then the session start, so a stampless leg lands on its last stamped event; the occurrence keys (copilot:<sid>:shutdown:<model>:<n>) are unchanged, so nothing already synced is re-sent under a new name. (#1054) -
codeburn optimize --provider <x>no longer tells you to ask Claude. Provider filtering already skipped the Claude-only detectors, but the cross-provider ones -retry-heavy-capabilities,low-worth-sessions,context-heavy-sessions,cost-outliers,mcp-project-scope- baked "Ask Claude" andCLAUDE.mdinto their fix labels, and the CLI and TUI destination headers said Claude whatever the run was scoped to, with the JSON report carrying the same copy through. One remediation table now drives all of them: an unscoped run,--provider alland--provider claudekeep the shipped Claude andCLAUDE.mdcopy;codexsays Codex andAGENTS.md, the only other instruction file CodeBurn already names; every other provider gets its display name and a generic "project instructions" rather than an invented filename. Apply plans still writeCLAUDE.mdonly for Claude-only findings, and the paste-destination values are unchanged. (#1049) -
Resizing the terminal or zooming its font produces one settled reflow instead of a burst of intermediate widths. CodeBurn re-rendered on every
resizeevent fromprocess.stdoutwith the livecolumns/rows, and debouncing that listener alone cannot stop Ink, which runs its own resize handler. During a burst the dashboard now sees a frozen stdout facade and a private resize emitter, and on settle it re-renders once and lets Ink see a single resize - no write interceptor, which was the earlier shape's trap: swallowing frames still advanced Ink's last-output state, so a settle producing the same string wrote nothing and lost a mid-burst update forever. One transient old-width frame during the burst is accepted. Static and non-interactive output and the periodic refresh are unchanged. (#1038)
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 serveprocess 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_limitis 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/totalmarker 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 requirestasks/<id>/ui_messages.json— so every CLI session was silently reported as $0.00, with no warning even under--verbose. Added as its owncline-cliprovider 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 throughcodeburn doctor. Per-message cost is metered by the CLI, socline-clijoins 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 fromcodeburn yield(codeburn.session.attributionandcodeburn.commitspan 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, socalculateCostreturned $0 while GitHub billed the real Anthropic rate (e.g., $0.0063536 for a sampled request). Added an alias to the existing correctly-pricedclaude-haiku-4-5row (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 logand 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, soedit → grep → editscored 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 itssession.startevents that VS Code transcripts carry, so content-based detection classified every CLI session as a transcript and skipped itssession.shutdownrollup — 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.completedrather thansubagent.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/--excludenow 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_endandmcp_tool_call_end;execis normalized to Bash;custom_tool_callevents 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 todayunder-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 cursorwhile 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 doctornames 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
optimizeand 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;
doctorwarns 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 viacodeburn 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 webserves 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 shareexposes one device's usage over your local network (PIN-paired), andcodeburn devicesshows 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-colorand--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 exportnow includes per-MCP-server usage in both JSON and CSV. (#496, #514) - JSON output for
optimizeandyield. (#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
.gitdirectories 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
agyCLI on-disk layout. (#541, thanks @ozymandiashh) - Workflow/ultracode subagent usage is now counted. (#470)
--provideris 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
securityCLI 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
toksuffix from the Total Tokens metric. (#511)
0.9.12 - 2026-06-09
Added (CLI)
- MCP server.
codeburn mcpruns a stdio Model Context Protocol server exposingget_usageandget_savingsto 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-pathmarks 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-savingscommand 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 newsavingsUSDfield tracks the avoided spend separately fromcostUSDeverywhere 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.dband 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-6andclaude-opus-4-8no 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 installnow installs the statusLine command through a persistentcodeburnbinary from PATH and repairs older CodeBurn-owned hooks that pointed at stale local build artifacts, preventingagyfrom auto-disabling capture afterMODULE_NOT_FOUNDfailures.
Added (macOS menubar)
- App icon. (#455)
- Configure
CLAUDE_CONFIG_DIRSfrom 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 optimizenow 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 optimizenow 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_endchanges and JSON-encodedfunction_callarguments. 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_endstores file paths inchangesobject keys. Both are now parsed correctly. - Claude toolSequence missing from session cache.
apiCallToCachedCallwas not forwarding thetoolSequencefield, 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
subagentTypesfield. The cache validator did not recognize thesubagentTypesarray, causing entries with this field to be silently dropped and reparsed on every run. - Conflicting date flags on
statusaccepted silently. Passing--daywith--from/--to, or--dayswith 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.
selectedDayreturned the earliest date instead of nil when multiple days were selected, andstartInteractiveSelectionRefreshdid not pass the day set to the cache key constructor. Both now matchPayloadCacheKeynormalization 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/*.jsonlfiles, 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 optimizesurface 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_idsubtree 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 infingerprintFile.session-cache.tsonly 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
maxCostat 0.01 to avoid NaN bar widths in ActivitySection and ModelsSection. (#362) - Kiro post-February 2026 storage discovery. The Kiro provider now keeps
legacy
.chatsupport 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 -> Editflows instead of counting each message as an independent one-shot turn. Kiro and Goose record per-message tool ordering viatoolSequencefor the same effect on aggregated sessions. Vibe prefersmeta.json.stats.session_costover 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 onlystatus --format menubar-jsonconsumes the daily cache. Removed from all paths that parse their own date ranges viaparseAllSessions. - Session cache retained between status parses. The
status --format jsonpath 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 canonicalmcp__<server>__<tool>form so MCP breakdowns andoptimizework correctly. Closes #308. - Antigravity Windows language-server discovery. Antigravity detection now
supports Windows process discovery,
--extension_server_port,--extension_server_csrf_token,--flag=valuesyntax, 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-projectrendered asmy/project). Now uses the real project path instead. Closes #320. - Cursor undated bubble rows misattributed to Today. Bubble rows without
a
createdAttimestamp 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:sqliteparse 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
vprefix for consistency withcodeburn --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/datatask root. It reuses the existing Cline-family parser forui_messages.jsonusage entries, deduplicates migrated tasks by the newestui_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_DIRSto 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-workand~/.claude-personalwho 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~/fooare now also expanded inCLAUDE_CONFIG_DIR(previously the value was passed through verbatim, which only worked when the shell expanded~before exporting). Closes #208. codeburn modelscommand. 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-taskto 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(default30days),--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 ofdaily[]now carriesturns,editTurns,oneShotTurns, andoneShotRate(0-100, one decimal,nullwhen no edit turns). Counts match the existing period-levelactivities[]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
cursorbecause the provider had no way to attribute bubbles to a workspace. The fix walks~/Library/Application Support/Cursor/User/workspaceStorage/*for each workspace'sworkspace.json(folder URI) andcomposer.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-allcursorrow. As part of this the cursor parser now derivessessionIdfrom the bubble row key (bubbleId:<composerId>:<bubbleUuid>) instead of the emptyconversationIdJSON 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 owncomposer-1house model, none of which match the canonical LiteLLM pricing keys (claude-sonnet-4-6,claude-opus-4-5). The alias map insrc/models.tsfilled 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, andcomposer-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 insrc/providers/cursor.ts:modelDisplayNamesnow 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)
optimizesuggestions 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), orshell-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
optimizefinding flags MCP servers whose tool inventory is largely unused. Inventory is observed from the Claudedeferred_tools_deltaJSONL 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
optimizefinding 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
optimizefinding 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
optimizefinding flags expensive sessions with weak delivery signals: no edit turns, repeated retries, or edit work that never landed in one shot, when nogit/ghdelivery 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 --toexports 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
--formatsilently accepted. All commands now reject unknown format values with a clear error and exit 1 instead of silently falling back to the default. - Invalid
--periodsilently accepted.getDateRange()no longer falls back to "week" on unknown periods. All period-accepting commands reject invalid values. statushelp 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
cwdstored in session JSONL files instead of reconstructing paths from lossy directory slugs, and group case/slash variants together. Closes #217. allperiod semantics unified between CLI and dashboard. The dashboard treated--period allas 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 intocli-date.ts. Use--from/--tofor 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.
yieldnow usesexecFileSyncinstead ofexecSyncto 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
--optimizeon 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 (usecodeburn optimizein 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 resetisUpdatingon 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 dxadded as a run method.
Fixed (CLI)
- Streaming dedup. Claude Code streams each
message.idmultiple 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.0000display. 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
ExperimentalWarningand 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
isLoadingso 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.
forceRefreshnow fetches today/all in parallel alongside the current selection. - Accent color propagation.
ThemeStateis 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
terminationHandlerinstead ofwaitUntilExit; HHmmss in filename prevents overwrite on double-export. - CurrencyState concurrency. Proper
@MainActorisolation withSendableconformance;nonisolatedon 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 messageusageblocks. - Roo Code provider. Reads Cline-family
ui_messages.jsonfrom VS CodeglobalStorage/rooveterinaryinc.roo-cline/tasks/. - KiloCode provider. Reads Cline-family
ui_messages.jsonfrom VS CodeglobalStorage/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 insiderunJsonReport().
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 codeburnadded as alternative install option.
0.9.3 - 2026-04-28
Added (CLI)
- Gemini CLI provider. Parses
~/.gemini/tmp/<project>/chats/session-*.jsonfrom 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
.chatJSON session files with token estimation and auto-model naming (kiro-auto). Costed at Sonnet 4.5 rates viaBUILTIN_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 allreturn $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 yieldcommand. 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,--monthflags.
0.9.0 - 2026-04-24
Added (CLI)
- Claude Max 5x plan preset.
codeburn plan claude-max-5xsets 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 ofbubbleId:*. 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>.jsonlsubdirectories instead of.txtfiles. Now scans both formats. Fixes #142. - Codex showed wrong model names. Model info is now extracted from
turn_contextentries, 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_endevents, 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: nowwhile per-provider queries usedend: endOfDay, causing sessions timestamped after the capture moment to be excluded from totals. Now usesperiodInfo.rangeconsistently 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.
scanJsonlFileandparseSessionFileloaded entire files into memory viareadViaStream(which defeated its own streaming by joining all lines back into one string). Switched both to the existingreadSessionLinesasync 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
SecItemCopyMatchingcall. - App Nap override not sticking. The
beginActivitytoken was immediately overridden by AppKit. Now disablesautomaticTerminationSupportandsuddenTerminationat the process level.
0.8.7 - 2026-04-21
Added
- MiniMax-M2.7 and MiniMax-M2.7-highspeed pricing. Added to
FALLBACK_PRICINGplus 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/*.jsonland tracks them alongside Pi. Shares Pi's JSONL parser via aproviderNameparameter, so OMP rows keep their ownomp:dedup prefix and never cross-dedupe with Pi on a sharedconversationIdnamespace.codeburn report --provider ompfilters to OMP only; the default combined view includes both. Contributed by @cgrossde (#59). codeburn model-aliascommand. Maps any provider-emitted model name to a canonical pricing name so cost rows no longer read$0.00when a proxy rewrites names. Aliases persist in~/.config/codeburn/config.jsonundermodelAliases. Usage:codeburn model-alias <from> <to>to set,--listto 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, andanthropic--claude-4.5-haikunow resolve to canonical Claude names and price correctly with no user configuration.getCanonicalNamealso stripsprovider/prefixes before alias resolution so double-wrapped forms likeanthropic/anthropic--claude-4.6-opuswork the same way. Contributed by @cgrossde (#59).
Fixed (CLI)
- Prototype pollution in alias resolution. A model literally named
__proto__leakedObject.prototypethrough the??fallback chain inresolveAlias, which then crashedcanonical.startsWithdownstream. The resolver now usesObject.hasOwnchecks 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:
prefetchAllon launch spawned four concurrentcodeburnsubprocesses that competed with the main refresh loop for disk and parser time. Removed; period tabs now fetch lazily on first click.NSStatusItemsometimes deferred the status bar paint for an accessory app, soattributedTitleupdates hit memory but not the screen until the popover opened. ExplicitneedsDisplay+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.beginActivitytoken for the life of the app opts out. Confirmed vialog show:_kLSApplicationWouldBeTerminatedByTALKeynow stays at 0.
- Subprocess
QualityOfServicelifted to.userInitiatedsocodeburnruns 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-tierformat, CLI behavior is unchanged and you can safely stay on 0.8.5. - macOS menubar users on
mac-v0.8.6or earlier should update: the refresh loop only ticks reliably frommac-v0.8.7onward. The in-app update pill surfaces within 2 days, or quit and re-runnpx codeburn menubarto 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 andcodeburn statusnow 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-cacheflag onreport,today,month,status,export,optimize, andcompare. 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@latestornpm 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 comparecommand. 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
cin the TUI to enter compare mode. Arrow keys switch periods,bto 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 0to 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 exactYYYY-MM-DDdate range (local time). Either flag alone is valid:--fromalone runs from the given date through end-of-today,--toalone runs from the earliest data through the given date. Inverted ranges or malformed dates exit with a clear error. In the TUI, pressing1-5still switches to the predefined periods. Credit: @lfl1337 (PR #80).avgCostPerSessionin reports. JSONprojects[]entries gain anavgCostPerSessionfield andexport -f csvadds anAvg/Session (USD)column toprojects.csv. Column order inprojects.csvis nowProject, 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:sqliteis 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-sqlite3in favor of Node's built-innode:sqlite. Removes the deprecatedprebuild-installtransitive 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:sqlitelives 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.appfrom GitHub Releases, strips Gatekeeper quarantine, drops it into~/Applications, and launches it.--forcereinstalls 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.ymlbuilds a universal.appbundle and zip onmac-v*tag push.
Changed
codeburn export -f csvnow writes a folder of one-table-per-file CSVs (summary,daily,activity,models,projects,sessions,tools,shell-commands) plus aREADME.txtindex. Each file opens cleanly as a single table in any spreadsheet.codeburn export -f jsonupgraded to schemacodeburn.export.v2with currency metadata.
Fixed
codeburn statusterminal 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, andstatus --format menubarare gone. The native Swift app is the single menubar surface.
Security
codeburn export -oguard. Writes a.codeburn-exportmarker into every folder it creates and refuses to reuse non-marked directories or overwrite existing files, so a typo like-o ~/.ssh/id_ed25519cannot 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 useObject.create(null)so attacker-controlled keys like__proto__create own properties instead of mutatingObject.prototype. Credit: @lfl1337 (PR #67). - Bounded session-file reads. New
src/fs-utils.tshelper 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
--verboseflag. 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 optimizecommand. 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, uncappedBASH_MAX_OUTPUT_LENGTH, unused MCP servers, ghost agents, ghost skills, ghost slash commands, bloatedCLAUDE.mdfiles (with@-importexpansion counted), cache creation overhead, and junk directory reads. - Copy-paste fixes. Each finding comes with a ready-to-paste remedy: a
CLAUDE.mdline, a.claudeignoretemplate, an environment variable, or amvcommand to archive unused items. - In-TUI optimize view. Press
oin the dashboard when the status bar shows a finding count,bto 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.homedirmocked viavi.mockcover 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 jsonwrites 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.7with the same Opus pricing as 4.6 and a 6x fast multiplier. Contributed by @mallek. - Unit tests for
filterProjectsByNamecovering 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 menubarper-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.jsonland tracks model changes viasession.model_changeevents. 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 allalso 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
modelDisplayNamenow matches longest key first sogpt-4.1-miniresolves toGPT-4.1 Miniinstead ofGPT-4.1.TopSessionshandles missingfirstTimestampgracefully 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
--versionreported stale 0.4.1 since v0.4.2. Closes #38.
0.4.4 - 2026-04-15
Added
- Auto-refresh flag.
codeburn report --refresh 60reloads data at a set interval. Works onreport,today, andmonthcommands. 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 currencysubcommand. 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 GBPsets 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, key3in 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.