* Copilot: count every agent round and exact credits in VS Code chat sessions
Copilot Chat 0.69 keeps only the last model call of an agent request in result.metadata. Read the request's modelTotals (input, cache reads, output over every call) first, then root completionTokens, then metadata. Carry copilotCredits as nanoAiu so plan credits are exact. Bump the copilot parse tag so cached chat sessions re-parse.
modelKeyMatches no longer lets claude-sonnet-4 capture claude-sonnet-4-6.
* Copilot: bill chat-session credits exactly and skip agent-host twins
A chat-session request that records copilotCredits now costs credits x $0.01 through costFromBilling; requests without it stay priced from tokens. A chat session whose id matches a session-state dir is left to the agent-host path so it is counted once.
* Copilot: drop agent-host twins at serve time, rate chat requests one by one, re-derive past days
A chat-session journal request is left out when the same session has billed agent-host usage (session-state calls with tokens or nanoAiu, or session-store rows). Plan credits rate chat-session requests individually, so older requests in a mixed session keep their estimate. DAILY_CACHE_VERSION 75 with copilot pending re-derive so settled days pick up the new counts.
Electron's Node 24 undici asserts and kills the process when a server closes the socket while a piped download is paused on the file write. The menu bar zip is now read whole and written once.
When the CLI is 0.9.27 or newer, the menu bar update pill checks and updates every surface through codeburn update, and polls every 6 hours. Older CLIs keep the previous path.
The desktop app runs codeburn update for the CLI and menu bar on launch and after it updates itself, and the update banner installs and restarts in one click with a Try again path if the install fails.
Adds codeburn update [--check] [--cli|--menubar|--desktop|--all] [--local] [--json] with a fixed JSON step/result contract that the desktop app, menu bar and Windows tray call, so every surface updates in one click without Terminal. Also documents the npm-first release order and adds always-latest /download links.
* fix(pr): push evidence beats the carried pr-link, show every PR fact
Claude Code's pr-link tags a session with its latest PR and CodeBurn carried
that tag to every later turn and subagent run. A run or turn that pushes the
head branch of a PR opened in the same data (paired gh pr create --head) now
goes to that PR, same repo only; spend left only on the tag is carriedUSD.
Rows also get runList (why each run counts), a locally evidenced state and
follow-up round times, and a call opening several PRs pairs its pr-link with
the last create. The PR card states every unknown and the expanded view shows
timeline, runs, cost by phase and work as compact tables.
* fix(app): full text on hover for cropped PR cells, plainer wording for same-session spend
Truncated PR labels, run descriptions, table labels and summary figures now show their full text in the shared tooltip surface while cut off. The carried-from-session-tag wording becomes 'from other work in the same session' in all six locales.
* fix(pr): explain carried spend without the pr-link jargon
* fix(clinepass): say when the Cline login expired and how to renew it
An expired saved Cline login only said "Run cline to refresh it". It now
reads "Cline login expired 3h ago" (age from the saved expiry, when known)
and asks to send a message in Cline or run `cline auth cline`, then press
Retry. Still a retryable state, so the last quota is kept. CLI and menubar.
* fix(menubar): title an expired Cline login instead of "Retrying"
A provider failure can now carry a short status title. The expired Cline
login sets "Cline login expired", which the settings pane and the dock
caption show in place of the generic retrying text. It stays a retryable
state and keeps the last quota; the title clears with the error.
* fix(clinepass): match the expired-login hint to the visible control
"Then press Retry" only fits where a Retry button shows: the settings pane
with no last quota. Everywhere else, including the CLI, the hint now ends
with "CodeBurn picks up the new login on its next refresh."
* fix(menubar): check Cursor when its settings pane opens, keep the dock seed stable
Generic providers were only refreshed while selected in the dock, so a
signed-in Cursor outside a full dock read "Not connected". Opening a
provider's settings pane now runs a quiet probe that records nothing on
failure. Auto-seed also keeps the providers already in the dock and only
fills free slots, so a newly connected provider no longer pushes one out or
resets the preferred card. Remaining edge: after an explicit Disconnect,
reopening the pane re-probes, and a free dock slot would take it back.
* fix(menubar): keep a disconnected provider off until the user connects it
The explicit-disconnect flag Copilot already persists is now per provider.
Disconnect sets it for generic providers too, Connect clears it, and while
it is set the settings pane's quiet probe and the dock auto-seed skip that
provider, so reopening the pane no longer silently reconnects Cursor.
* fix(menubar): only the Disconnect action opts a provider out
Clear Override and Remove saved override share the disconnect path but
mean "go back to automatic", so they no longer set the explicit-disconnect
flag; the pane probe and dock auto-seed keep working after them.
* fix(menubar): the quiet pane probe hides only a missing sign-in
The Settings pane probe recorded nothing on any failure, so an expired login
opened before any refresh read "Not connected". Fetch failures now carry
whether the provider had no credentials or local data at all; the quiet probe
suppresses only that case and records every other failure like a refresh.
* fix(menubar): keep the not-signed-in flag clear of the ClinePass failure fields
Command Code's credits endpoint only reports monthly credits left, so the
monthly window was dropped. For an active subscription on a known plan the
plan price now comes from a small table (the API has no monthly cap field):
pool = max(plan, left), used = pool - left, reset = currentPeriodEnd.
Unknown plans and purchased/free credits stay out. CLI and menubar mirrored.
* fix(pr): a subagent whose description names a PR folds into that PR
A subagent run folded into whatever PR its parent's last pr-link carried, so
review runs for #1703, #1712, #1728 and #1729 landed on a stale closed#24.
The Agent description ("Review PR 1729 ...") is now read from the subagent's
.meta.json at report time; a single PR it names outranks the carried PR.
* fix(pr): date each PR by its own turns and count its agent runs
Every PR row carried the whole session's first and last timestamp, so one
long orchestrator session stamped the same span and "1 session" on all of
them. Rows now span their first to last attributed turn or folded run, and
a new `runs` field counts contributing sessions plus folded subagent runs.
* feat(pr): local one-shot, follow-up rounds, review runs, merge time and cost split per PR
Each PR row now carries signals read from cached commands, no GitHub calls:
openedAt (gh pr create paired with the PR's first pr-link), follow-up rounds
(pushes naming its --head branch, or from a run whose description names it,
in 10-minute bursts), oneShot only when the open and its branch were seen,
reviewRuns and reviewCostUSD, mergedAt and timeToMergeMs, a build/review/fixes
cost split that sums to the row, and linkEvidence to flag pr-link-only rows.
* feat(session-why): PRs the session opened, their follow-up rounds and review runs
The why view now carries prWork: each gh pr create whose output printed a PR
URL, the pushes after it on its --head branch (or from a helper whose
description names that PR) in 10-minute rounds, and the helpers that read a
PR without committing or pushing, with their cost.
* perf(pr): only parse commands that can name a push or a PR
* feat(app): local PR facts on PR cards and PR work in the session view
Each PR card gets one line of facts from the CLI's local signals: the review
run and its cost, one shot or follow-up rounds, and merge time after open.
Unknowns are left out; a pr-link-only row says so. The count shows agent runs
instead of sessions. The expanded card splits spend into building, review runs
and after first review, and lists the review runs. The session view lists the
PRs it opened and the review runs it spawned.
* perf(pr): read subagent descriptions only for PR attribution
Every report, status and menubar refresh read each subagent's .meta.json
(about 2,000 files in 30 days) and the cross-provider correlation scanned
every folded run's commands for PR signals it never uses. Sessions now carry
the sidecar path; the description is read on demand, memoized by mtime, only
for folded runs and runs that push, and correlation skips the command scan.
The by-pr footer now counts runs to match the Runs column.
Keychain Access exports the Developer ID p12 with RC2-40 encryption and a 1-iteration sha1 MAC, which the runner's security import rejects as a wrong password. Both workflows rewrap it with openssl@3 -legacy (3DES, 2048 iterations) under a masked one-off password; the desktop workflow now signs from its own keychain because electron-builder's temporary keychain failed to unlock. Verified: both workflows green on dispatch, signed, notarized and stapled.
When an Antigravity response left out the model, the parser crashed on the legacy .pb path and the whole session was skipped. It now falls back to the chat's own model and passes it through the live model map.
Maps the Gemini 3.8 Flash placeholders M318/M319/M320 to the high/medium/low ids from Antigravity's model catalog, and the gemini-3-flash-a/-d family strings to gemini-3-flash-preview (LiteLLM prices, nothing hardcoded). Parse version, results cache v9 and daily cache v74 bumped so cached rows re-parse; calls and cost only rise.
Thanks @dcaldr.
* 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.