* perf(cursor): stop JSON and menubar answers waiting on cursor.com
report/today/month --format json, overview and status --format menubar-json
awaited the hourly Cursor usage download (15 s cap, 45 s on the first
backfill) before parsing. The download now runs beside the command and its
import applies after the command (serve: between two requests, then its
memos are dropped), so the next run shows it. Dashboards still wait.
* test(cursor-sync): keep the in-process sync enabled when the shell disables it
* test(cursor-sync): wait for the serve memo instead of a fixed answer count
The serve test expected the second answer to be memoized, but serve only
memoizes once its root watchers are armed, which on a slow CI runner lands
after that answer. Keep asking until two answers share a generation. The
spawn now imports the fake fetch by file URL so Windows accepts the path,
and the in-process test points USERPROFILE at the temp home too.
Warm reports re-derived the display name of every cached call, a linear
scan of the short-name table each time (about 0.5 s on a lifetime run).
The name depends only on the aliases and the pricing namespaces, so it is
memoized per model id and cleared when either changes.
The sessions.db query returned each message's whole metadata blob and
parsed it in JS (617 MB on a real store) to use five fields. The query now
builds an object of just those fields; a missing or non-object metadata
still comes back NULL.
The fork-parent memo lived in module state, so every parse worker indexed
the Codex home and read the same parent rollouts again (58 reads of 10
parents, 8 GB for 1.28 GB on a real corpus). Workers now ask the main
thread over a message port, which keeps one memo for the whole parse.
A preset subscription plan (SuperGrok, Cursor Pro, Google AI) past its price
showed "$X over" in red and "On pace to exceed", but a flat subscription is
not billed overage. Those cards now read "13.6x your plan's price in API
value" with a neutral bar and projection; custom plans keep the cap wording.
#1657 put the ~ mark on estimated rows, but the headline total stayed bare on
the desktop hero, the mac hero, the Windows hero and tray text, and GNOME.
Each now applies the same 1% rule to the provider split's estimated portion
(the desktop helper moves from the VS Code summary into lib/format). Combined
device totals carry no estimated split and stay unmarked.
Switching period or provider kept the previous model pair in state, so the
report query fired once with the new period and the old models before the
facets effect reset them, and the CLI exited 1 with "model not found". Both
Cohorts and Classic now request a report only for models listed in the
current period's facets.
The low-coverage and project-scope findings only see servers whose tools were
deferred, yet charged 400 tokens per tool and counted every call as both a
cache write and a cache read. A deferred tool is one name line (~9 tokens),
and each call now pays it as a read, or as a write only when it read no cache.
The delivered check read bashBreakdown keys, which are base names (git), so
git commit / gh pr create never matched; it now reads the full command from
each call's tool sequence (Kimi Code now records one) and allows git global
options. Devin and cursor-agent edit tools count as edits, and the retry
share divides by edit steps instead of user prompts.
* fix(cursor-agent): leave Cursor IDE chat transcripts to the cursor provider
Cursor IDE agent chats write agent transcripts too, so on days without a
Cursor import they were counted by both cursor (state.vscdb) and
cursor-agent. Discovery now skips a transcript or store whose id is an IDE
composerData key; cursor keeps them with per-bubble times and models.
Daily cache v73 lets cursor-agent days shrink once.
* fix(cache): drop old cursor-agent slices the IDE parse now explains
On an existing cache, settled days kept their old cursor-agent slice after
v73 because the fresh parse no longer has one there and the merge carries
missing slices forward. While cursor-agent is pending its v73 re-derive, a
day the fresh parse explains with a cursor slice and no cursor-agent slice
drops the old cursor-agent slice; days with no fresh cursor slice keep it.
The session cache keys the Cursor parse on the DB fingerprint alone, but the
parse stopped at the requested range start, so a narrow run (7 days) was served
to every wider range afterwards (30 days showed 2 calls instead of 1,680). The
parse now always reads the full 6 month lookback; the cursor parse version is
bumped so cached narrow entries re-parse.
Claude calls never used the LiteLLM above-threshold rates, so Haiku 5.5
requests over 100k prompt tokens (input + cache read + cache write) billed
at the base rate. claude joins TIERED_PRICING_PROVIDERS and the transcript
paths pass the provider. Long-prompt test fixtures move to Sonnet 4.6, which
has no tier, so their round figures hold.
When the Frankfurter fetch failed (or under CODEBURN_FX_NO_FETCH) with no
fresh cache, the rate fell back to 1 but the foreign code and symbol stayed,
so USD amounts showed as EUR/JPY. Now the last cached rate of any age is used,
and with none the display falls back to USD code and symbol.
The comma-grouping regex in overview and devices also matched the fraction,
so $0.0028 rendered as $0.0,028. A lookbehind now skips digits after the
decimal point.
* fix(cache): drop stale cursor-agent slices on days a Cursor import covers
On a day the import fully covers, the parse drops local cursor-agent (and
Grok Bot, when the store has its rows) calls, so the fresh day has no slice
for them and the merge carried the cached one on top of the imported rows.
Adoption and the complete re-derive now drop those slices. Bump to v72 so
caches that already carry them get repaired.
* test(cache): keep Grok Bot slices an import holds no Grok Bot rows for
* fix(cache): adopt the top cache file's timezone, not the machine's
Adoption kept only files in the machine's timezone whenever any file had it, so an older file in that zone pushed out the newest complete one and every day only it still held. A first run under TZ=UTC on a cache with one old UTC file dropped 46 days.
Adopt the top-priority file's timezone and let the existing timezone re-derive re-bucket the result.
* fix(cache): re-bucket an adopted older cache once after a timezone change
Adopting only older-version files built the cache with an empty savings hash,
so hydration skipped the tz subtraction and the partial-survival guard kept
old-zone days over the fresh ones, counting the hours between the two
midnights twice. Adoption now keeps the top file's hash. A tz residual over a
fresh slice with data also replaced that slice instead of adding to it.
* fix(cache): never let a timezone change inflate when the savings hash also changed
With a tz change and a savings hash change in the same re-derive (every
0.9.25 upgrade, since builtin price overrides changed the hash), the tz
subtraction was skipped and the partial-survival guard put old-zone slices back
over fresh ones, counting the hours between the two midnights twice. Now the
old-tz re-aggregation still runs: a baseline slice any surviving source explains
is dropped whole and the fresh slice wins, with no guard swap. compare
--with-history now passes the tz aggregator too.
Tradeoff: in this case the source-gone part of a partly surviving (day,
provider) is lost, since old-priced and new-priced cost cannot be split
(subtracting would carry the price drift as spend). With a matching hash the
exact subtraction still keeps it.
* fix(cache): keep a tz residual's breakdown within its own totals
The tz subtraction reduces an old slice's totals but its project, model and
category maps only key by key, so keys a fresh parse no longer produces (pre-v67
label-only projects, a project now split per worktree, renamed models) stayed
whole and the summed residual stacked them on the fresh slice. Scale each map
back to the residual's totals, keeping the old keys and whole counts.
The seal-time re-derive parsed from today-7, so a turn crossing that midnight was cut and its later part re-anchored to the window start, while the day before kept the whole turn. Category cost, turn and session counts on the window start were counted twice.
Parse one day earlier when re-deriving already sealed days and keep only the window. A stale watermark still parses from its first unsealed day, as in 0.9.25.
With a day set, buildPayloadProjects also added live cost for any project
identity the days did not hold. Live per-call resolution (#1689) and bare
cursor-agent labels can put sealed calls under a new identity, so they were
counted twice and project rows summed above the period total. Rows now take
cost, savings and calls from the same days as the headline; live still
supplies sessions, details and paths.
The walk-up skipped a .git without an origin and kept climbing, so a
local-only repo nested in another repo, or any folder under a home dotfiles
repo, took the ancestor's origin and merged into that project row.
Now it stops at the first .git (its origin or none) and never uses home as
the repo of a folder below it.
The tier threshold summed input and cache read but left out cache-write
tokens, which codex splits out of input_tokens on gpt-5.6+ and gpt-6. A
280k-token prompt with 80k cache writes priced at base rates. OpenAI also
bills 272,000 exactly as short context, so the tier now starts above the
threshold. The audit recompute follows the same rule.
The dot-minor lookahead from #1531 stops claude-opus-4 matching
claude-opus-4.7, and 4.7 was not in the token list, so those turns fell
back to copilot-anthropic-auto (Sonnet 4.5 rates). Add the token and bump
the copilot session-cache parse tag so cached turns re-parse.
cli_error gains duration, exit, a locally classified reason and the scoped provider. New provider_read_fail (desktop) and update_result (desktop, menubar, tray) events, enums only.
For a bundled CLI the not-found panel now tells desktop users to reopen or reinstall the app instead of installing from npm. Strings in all six locales.
Lockfile-only updates: @hono/node-server 1.19.17 in the CLI; xmldom, postcss, browserslist, baseline-browser-mapping and shell-quote in the desktop app. No major bumps.
invalidateProviderDays no longer skips when the build has no daily-cache file yet, so the first Cursor import clears the covered days. cursor joins the pending re-derive list at v71 so stuck caches repair once.
Regenerated with npm run bundle-litellm. Sonnet 5.5 cache read is now the official $0.10 per million; adds Haiku 5.5, GPT-6.1 Sol and other upstream rows.
Cache adoption keeps one timezone and prefers complete parses. Sealing a day re-derives the last seven, and a sealed day only takes a fresh parse with at least as many calls.
Adds codeburn project link <folder> <project>, project unlink <folder> and
project links, so a folder that is not a checkout of a repo can join that
repo's project row. Links live in config.json, apply at report time
wherever rows are grouped or filtered, cover subfolders (longest link
wins), and never touch the day cache, so unlink works on cached days.
Top sessions in the JSON report and the menubar/desktop payload ranked each
session by its own cost, so a parent showed without its subagents and a
subagent could show as its own row. Both producers now go through
foldedSessionRows, the same fold the sessions list uses. The Overview model
label matches a top session by provider and id, since its cost and calls
now include subagents.
Co-authored-by: Resham Joshi <resham@codeburn.app>
* feat(app): transient project selector in the desktop top bar
Scopes every report to one project path for the current session through
the same narrowing the VS Code workspace scope uses. The saved project
filter is never written; a restart is back on all projects.
* fix(app): let the name pickers ellipsize before the top bar overflows
* fix: key the daily project split per real path, not per label
A project label can span several paths: every Claude session started from the
home folder shares -Users-<name>, while its cwd puts it in a different project.
The day rollup keyed by label kept the first path it saw, so a rooted
--project headline took the whole label bucket on some days and missed it on
others. Key per (label, path), read old label-keyed days unchanged, bump the
daily cache to v68.
* fix(app): list only selectable, non-zero projects in the top bar picker
Rows with no session cwd (the Cursor and Grok Bot imports, provider label
fallbacks) report their label as the path, so the picker offered names the
bridge refuses and dash-to-slash guesses like eywa/lab. Keep only absolute
paths, and drop rows whose lifetime cost rounds to $0.00. The old test that
rooted a slashless Codex path is replaced: Codex now records the absolute cwd,
and a slashless path is always the label fallback.
* fix(app): show a toast when a project pick is refused
The pick was dropped silently. Also pin that an encoded -Users-... label
never reaches the picker list.
* feat: group projects by git repository
Clones and worktrees that share an origin remote are one project in every
project list (report JSON, By Project, the top bar picker, the terminal
dashboard), with the checkouts listed under the row. A --project path inside
a checkout scopes to the whole repository; --exact-project keeps the old
single-folder match. The origin is learned while the folder exists, kept in
git-origins.json and on each day's project entry (daily cache v69), so a
deleted scratch clone stays in its repository. The report JSON project list
now comes from the durable day set, so a row costs what selecting it reports.
* fix: a picked project row selects that row exactly
The top bar pick sends =<path>: a checkout in a repository still selects the
whole repository, and a plain folder selects itself without the folders under
it, which are rows of their own. So the picker row and the headline it opens
are the same set. Typed --project paths keep their prefix match. Days adopted
from an older cache learn the origin of every checkout that still exists, and
lookups outside a parse are saved on exit.
* fix: key the status snapshot on --exact-project
* feat: one Temporary folders row for scratch folders outside a repository
Every project path under a temp root (os.tmpdir(), /tmp, /var/folders and
their /private spellings) that is not a checkout of a known repository folds
into one row, in the report JSON, By Project, the terminal dashboard, the top
bar picker and the Settings Projects pane. Its pattern @temp selects or hides
all of them. The Projects pane leaves out rows that round to $0.00 unless a
saved pattern names them, and a saved pattern naming any checkout of a
repository row now names the whole row in the renderer too.
* feat: group a deleted sibling folder with its repository by folder name
A deleted folder with no recorded origin, named <checkout>-<suffix> or
<checkout>_<suffix> next to exactly one repository's checkout, joins that
repository row. The checkout entry carries matchedByFolderName and the Spend
drill-down labels it. Existing folders and names two repositories could claim
are left alone.
* feat: the longest checkout name wins a folder-name match
A deleted folder that fits several sibling checkouts joins the one with the
longest name; it is skipped only when the longest fits belong to two
repositories. The report project type declares matchedByFolderName.
* fix(claude): attribute each call to the project of its own cwd
* test: fold the temp roots the tests set, compare checkout ids as identities (Windows)
* test: compare folder-name checkout ids as identities (Windows)
* fix(claude): keep subfolder cwds in their project, count and show a split session once
* fix: an exclude path hides that checkout only, repository rows count a session once
* feat: mark folder-name matches in the CLI report and the project picker
* fix(claude): a deleted subfolder stays its own project
* feat(app): transient project selector in the desktop top bar
Scopes every report to one project path for the current session through
the same narrowing the VS Code workspace scope uses. The saved project
filter is never written; a restart is back on all projects.
* fix(app): let the name pickers ellipsize before the top bar overflows
* fix: key the daily project split per real path, not per label
A project label can span several paths: every Claude session started from the
home folder shares -Users-<name>, while its cwd puts it in a different project.
The day rollup keyed by label kept the first path it saw, so a rooted
--project headline took the whole label bucket on some days and missed it on
others. Key per (label, path), read old label-keyed days unchanged, bump the
daily cache to v68.
* fix(app): list only selectable, non-zero projects in the top bar picker
Rows with no session cwd (the Cursor and Grok Bot imports, provider label
fallbacks) report their label as the path, so the picker offered names the
bridge refuses and dash-to-slash guesses like eywa/lab. Keep only absolute
paths, and drop rows whose lifetime cost rounds to $0.00. The old test that
rooted a slashless Codex path is replaced: Codex now records the absolute cwd,
and a slashless path is always the label fallback.
* fix(app): show a toast when a project pick is refused
The pick was dropped silently. Also pin that an encoded -Users-... label
never reaches the picker list.
* feat: group projects by git repository
Clones and worktrees that share an origin remote are one project in every
project list (report JSON, By Project, the top bar picker, the terminal
dashboard), with the checkouts listed under the row. A --project path inside
a checkout scopes to the whole repository; --exact-project keeps the old
single-folder match. The origin is learned while the folder exists, kept in
git-origins.json and on each day's project entry (daily cache v69), so a
deleted scratch clone stays in its repository. The report JSON project list
now comes from the durable day set, so a row costs what selecting it reports.
* fix: a picked project row selects that row exactly
The top bar pick sends =<path>: a checkout in a repository still selects the
whole repository, and a plain folder selects itself without the folders under
it, which are rows of their own. So the picker row and the headline it opens
are the same set. Typed --project paths keep their prefix match. Days adopted
from an older cache learn the origin of every checkout that still exists, and
lookups outside a parse are saved on exit.
* fix: key the status snapshot on --exact-project
* feat: one Temporary folders row for scratch folders outside a repository
Every project path under a temp root (os.tmpdir(), /tmp, /var/folders and
their /private spellings) that is not a checkout of a known repository folds
into one row, in the report JSON, By Project, the terminal dashboard, the top
bar picker and the Settings Projects pane. Its pattern @temp selects or hides
all of them. The Projects pane leaves out rows that round to $0.00 unless a
saved pattern names them, and a saved pattern naming any checkout of a
repository row now names the whole row in the renderer too.
* fix(claude): attribute each call to the project of its own cwd
* test: fold the temp roots the tests set, compare checkout ids as identities (Windows)
* fix(claude): keep subfolder cwds in their project, count and show a split session once
* fix: an exclude path hides that checkout only, repository rows count a session once
* fix(claude): a deleted subfolder stays its own project
* fix(claude): attribute each call to the project of its own cwd
* fix(claude): keep subfolder cwds in their project, count and show a split session once
* fix(claude): a deleted subfolder stays its own project
* feat(sessions): fold subagent sessions into their parent row
The default sessions list (table, JSON, TUI, contributions, desktop) now shows
one row per parent session with its provider-recorded subagents summed in and
listed under subagents. Grouping reuses the work-unit resolver, so only rows
move; totals are unchanged. --by-work-unit keeps the flat rows.
* test: reconcile the session diagnosis with the folded sessions row
* fix(sessions): find and open a parent row by its subagents, count subagents apart from top-level rows
Kimi Code now writes cwd instead of workDir. Without it the project path
was decoded from the wd_ directory name, so /x/crewroom-app-mem5 became
crewroom/app/mem5 and never joined its repository. Cost is unchanged;
only the per-project split moves.
* feat(desktop): one-click updates through electron-updater on the update-feeds release
* feat(tray): signed one-click updates through tauri-plugin-updater; feed publishing and release docs
* ci(tray): build without the updater key until it is configured
* chore(windows): set the tray updater public key
* ci(windows): skip updater signing in the release build smoke
* ci(windows): build without updater artifacts when no signing key is available
* ci(windows): turn off updater artifacts through TAURI_CONFIG, not CLI flags pwsh mangles
* ci(windows): run tauri build under bash so --no-sign reaches the CLI (pwsh drops the -- separator)
* feat(menubar): CI Developer ID signing, team-pinned installs, update feed
- release-menubar.yml imports the Developer ID cert, signs with hardened
runtime and timestamp, notarizes with an App Store Connect key, staples,
and fails when the signing secrets are missing
- the installer refuses bundles not signed by team XRVP7P7F9M or rejected
by Gatekeeper
- a rolling update-feeds prerelease carries menubar-latest.json, written
last after the published zip is checked; installer and update check read
it before scanning mac-v releases
- the update pill skips the CLI step for the desktop app's bundled CLI or
a CLI with no package manager, explains npm permission errors, and
allows longer update timeouts
- RELEASING.md matches what CI does and lists the required secrets
* docs(menubar): DISTRIBUTION.md points at CI signing; retry the feed's asset check
* fix(menubar): popover shows only the Update button; command footer appears only after a failed update
* feat(app): transient project selector in the desktop top bar
Scopes every report to one project path for the current session through
the same narrowing the VS Code workspace scope uses. The saved project
filter is never written; a restart is back on all projects.
* fix(app): let the name pickers ellipsize before the top bar overflows
* fix(app): list only selectable, non-zero projects in the top bar picker
Rows with no session cwd (the Cursor and Grok Bot imports, provider label
fallbacks) report their label as the path, so the picker offered names the
bridge refuses and dash-to-slash guesses like eywa/lab. Keep only absolute
paths, and drop rows whose lifetime cost rounds to $0.00. The old test that
rooted a slashless Codex path is replaced: Codex now records the absolute cwd,
and a slashless path is always the label fallback.
* fix(app): show a toast when a project pick is refused
The pick was dropped silently. Also pin that an encoded -Users-... label
never reaches the picker list.
A project label can span several paths: every Claude session started from the
home folder shares -Users-<name>, while its cwd puts it in a different project.
The day rollup keyed by label kept the first path it saw, so a rooted
--project headline took the whole label bucket on some days and missed it on
others. Key per (label, path), read old label-keyed days unchanged, bump the
daily cache to v67.
* fix(codex): mask fork replay records against the parent rollout
A fork's replay burst was dropped whole, losing calls that exist only in
the fork. Drop a burst record only when the parent rollout recorded it
before the fork, and count the rest as their own calls. Parent missing or
a burst that does not open with a parent record keeps the old behavior.
Bumps CODEX_CACHE_VERSION to 22 and DAILY_CACHE_VERSION to 66.
* fix(codex): bump the codex parse version for fork masking, fix the comments
Cached forks that dropped the whole burst need to re-parse, so the codex
parse version gains codex-fork-mask-v1. The burst records that now count
are the parent's work its rollout kept only inside a running total, not
fork-only usage.
* fix(grok): read per-inference usage from logs/unified.jsonl
Grok Build keeps logging every inference to ~/.grok/logs/unified.jsonl after
its session dirs are gone, so CodeBurn showed $0 for that usage. Read
shell.turn.inference_done records (prompt includes cached, completion includes
reasoning), with the model from the session's model changed event and the
project from session created. Sessions that still have a session dir are
skipped so they are not counted twice. DAILY_CACHE_VERSION 65.
* fix(grok): prefer the unified log over session-dir rollups
A session with inference records in logs/unified.jsonl is now read from the
log, one call per request, and its session dir is skipped. Session dirs only
count for sessions the log no longer holds. This keeps history when session
dirs are deleted after the log was cached, and prices grok-4.6's long-context
tier per request instead of on a session total. Logged calls can move to an
earlier day than the rollup, so grok joins PENDING_REDERIVE at 65.
* fix(grok): re-read skipped session dirs after log rotation, keep their tools
A session dir the unified log holds parses to nothing, and its fingerprint
does not track the log, so a rotated or truncated log left that empty
result cached. Empty grok session-dir entries now re-parse.
The skipped dir's summary, tools, bash commands and subagents now ride on
that session's first logged call, and its model and cwd fill in when the
log names neither. A call whose model nothing names is priced as
grok-build and marked estimated.
* fix(models): price kimi-for-coding by the model it served on the call date
kimi-for-coding is a moving alias. It priced as retired K2 Thinking for
every call. It now prices as K2.5 from 27 Jan 2026, K2.6 from 13 Apr and
K2.7 Code from 12 Jun (K2.8 Preview from 11 Sep has no published price,
so it stays on K2.7 Code). kimi-for-coding-highspeed prices at Moonshot's
K2.7 Code HighSpeed rate instead of $0. Daily cache v64.
* fix(models): mark K2.8 Preview kimi-for-coding calls as estimated
From 11 Sep 2026 kimi-for-coding served K2.8 Preview, which has no
published rate and prices as K2.7 Code. Those calls now carry the
estimated flag. The legacy kimi provider prices by the dated model too.
* fix(models): flag the K2.8 Preview stand-in where every call is read
cachedCallToApiCall now marks kimi-for-coding calls from 11 Sep 2026
estimated, so the flag reaches every provider and cached reads without a
parse bump. Calls with a reported cost are left alone. The kimi provider's
own flag and its parse version entry go, as they only duplicated this.
Cline CLI's metrics.inputTokens is the full prompt, cache reads and writes
included; Cline's own pricing bills max(0, input - cacheRead - cacheWrite)
at the input rate. We treated it as uncached input, so every cache read was
also charged at the full input rate. The split now happens in parseMetrics,
covering per-message and rollup calls.
Bumps the cline-cli parse version so cached calls re-parse and the daily
cache to v63 so settled days re-derive. Call counts are unchanged.
* feat: Google AI plan presets and the Antigravity tier label (#1614)
Plan presets google-ai-pro, google-ai-ultra-5x and google-ai-ultra-20x
at Google's US monthly prices, scoped to Antigravity usage.
The Antigravity quota readers (CLI, desktop, menu bar) now also ask
GetUserStatus when the summary answers and label the windows with
userTier.name. planStatus.planInfo is not used: it says Pro on a free
tier account.
* docs: Antigravity probe comments and README preset list
* feat(quota): Command Code quota and ClinePass from the Cline sign-in
Command Code: reads the CLI's ~/.commandcode/auth.json key and the billing
credits and subscription endpoints. 5-hour and weekly windows as used of
cap, remaining credits as a note, plan from planId. Live in the Mac dock
from the local CLI, in codeburn quota and the Windows dock.
ClinePass: without a saved or env key, falls back to the Cline CLI's
providers.json session (accessToken, then apiKey, then auth.apiKey, with
Cline's path overrides). Read-only; an expired session stops before the
network and says to run cline. The Mac catalog adds the local CLI source
so Automatic connects without a key.
* fix(quota): keep retrying after an expired Cline sign-in
Running cline refreshes the session file CodeBurn only reads, so an
expired or rejected Cline session is now a retryable failure that keeps
the last quota and shows the guidance, on the Mac and in the CLI. A
rejected API key and a missing credential stay terminal.
* test(quota): build expected Cline paths with path.join for Windows