* 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.
* 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
* refactor(app): move the bridge handlers out of main.ts
createBridgeHandlers and the argv builders now live in
app/electron/bridge-handlers.ts with no electron import, so a host other
than Electron can run the same channel to argv mapping. main.ts keeps its
default deps and re-exports, so the desktop app and its tests are unchanged.
Two additive hooks for that host: scopeProjectFilter narrows the saved
project filter before it becomes argv, and CODEBURN_NODE_BIN runs the
bundled CLI with a named Node instead of Electron-as-Node.
* feat(app): let the renderer run inside an editor
The bridge can now say it is the VS Code host. When it does, the chord
shortcuts and their hints step aside for the editor's own, Settings hands
theme, language, default period and refresh to the editor's settings,
the top bar shows a this workspace / all projects switch, and the editor
can send a section, a period or a refresh to an open dashboard.
formatResetTime moves to lib/format.ts so the status bar and the summary
use the Plans wording. New strings are in all six locales. Nothing
changes in the desktop app.
* feat(app): build the renderer for VS Code webviews
app/vite.vscode.config.ts builds two webview entries into
vscode/dist/webview: the whole dashboard and the activity bar summary.
renderer/vscode/bridge.ts implements window.codeburn over postMessage,
forwarding only the channels in channels.ts; telemetry, update checks and
the desktop companions are answered in the webview. localStorage is
prefixed per scope so one workspace's cached views never paint in
another's, and the theme follows the editor's light, dark and high
contrast kinds through its --vscode-* tokens.
* feat(vscode): CodeBurn extension for VS Code and its forks
A status bar item with today's spend (~ when estimated) and a tooltip
with the week, the month, this workspace, the top model and plan quotas;
an activity bar summary; the desktop dashboard in an editor tab, scoped
to the open workspace; eight commands and eight settings.
The bundled CLI runs as the desktop's resident serve child through the
shared client. Current VS Code, Cursor and Antigravity ship Node 24, so
it runs on the editor's own Node; an older editor gets a system Node
22.13+ or a degraded run that says what it cannot read. A workspace
folder maps to a rooted --project the way the CLI keys sessions,
worktrees folding into their main repository.
npm run package:vscode builds the .vsix. Unit tests cover the bridge
host, status bar formatting, workspace mapping, settings and runtime
choice; test:smoke drives a real VS Code against a fixture home.
* ci, docs: build, test and package the VS Code extension
A vscode job typechecks, unit tests, packages the .vsix, runs the smoke
test in VS Code under xvfb and uploads the .vsix. docs/vscode.md covers
the architecture, the measured editor runtimes and how to test.
* fix(vscode): keep the currency setting out of workspace settings
Changing codeburn.currency runs the CLI's own currency command, which
every CodeBurn app shares, so a repository's .vscode/settings.json must
not be able to set it: the setting is application scoped now.
The smoke test also checks that the dashboard's renderer booted inside
the webview and called the host, not only that its tab opened.
* feat(vscode): version the extension 1.0.0, apart from the CLI
The extension carried the CLI's 0.9.25. It now has its own version,
1.0.0. The host passes it to the webview, and the dashboard's nav
footer, About dialog and Settings show it there instead of the
renderer's package version; Settings adds "Bundled CLI 0.9.25" beneath
it. The desktop app is unchanged. Nothing in the build or packaging
writes the extension's version from the CLI.
* fix(vscode): run the smoke test in Cursor, keep the demo shim out
Cursor 3 opens its Agents window on a fresh profile, which starts no
extension host, so the smoke run against Cursor.app hung; it now passes
--classic for Cursor. The webview build no longer copies
renderer/public/demo-shim.js, a browser demo harness, into the .vsix.
* feat(vscode): a quiet star line in the summary, and a Star on GitHub command
From the third day of use, the activity bar summary ends with the
desktop's "Enjoying CodeBurn? Star us on GitHub" line, in the same six
languages, with a close button. Clicking it or closing it hides it for
good (kept in the extension's globalState). It is never a notification.
The new "CodeBurn: Star on GitHub" command opens the repository page.
No tracking parameters, no telemetry.
* docs(vscode): marketplace screenshots in the README
Five screenshots of the extension on real data (Overview in dark and
light, the why it cost view, the status bar hover and the activity bar
summary), 1600 px wide, each with a one-line caption. They live in
vscode/media/screenshots, stay out of the .vsix, and the package script
points vsce at the raw GitHub URLs on main.
* fix(vscode): review fixes for settings trust, serve lifecycle and the tooltip
- codeburn.provider and codeburn.quotaProviders are application scoped,
like currency, so a repository's settings cannot turn quota requests
back on, even in Restricted Mode.
- deactivate removes the serve pid file once the child is stopped, and
changing codeburn.nodePath restarts the serve child on the new Node.
- codeburn.currency is applied at startup when the CLI's currency
differs, not only when the setting changes.
- The dashboard's Settings no longer shows per-plan quota switches in the
editor, where the editor setting rewrites them on every boot; it points
to the editor settings instead.
- Workspace and model names are escaped in the status bar tooltip.
- The README lists the dashboard actions that reach the network.
- The staged CLI drops its dependencies' tests and TypeScript sources
(466 files, 9.9 MB to 9.2 MB).
* feat: Command Code support
Reads the cmd CLI's session transcripts at
~/.commandcode/projects/<slug>/<id>.jsonl (CODEBURN_COMMANDCODE_DIR moves
the root). Checkpoint and prompt sidecars are skipped.
usage.inputTokens includes cacheReadTokens, so fresh input is
inputTokens - cacheReadTokens. The recorded costUsd is kept per call
(costFromBilling); a call without one is priced from tokens and marked
estimated. Forks copy parent message ids into a new file, so calls dedupe
by message id across sessions.
Desktop: Electron sandbox read path, macOS and Windows change watchers,
Windows tab colour. No quota adapter.
* fix(command-code): dedupe on meta.messageId, not the 8-hex line id
The top-level id is randomUUID().slice(0, 8) and only unique within one
file, so calls from unrelated sessions could collide and vanish.
meta.messageId is a full uuid that survives forks. Fixture ids now use
the real 8-hex shape.
* test(command-code): check sidecar skipping by file name, not a posix path
* Restore usage from Mistral Vibe Unified Harness sessions
* fix(mistral-vibe): read unpinned Vibe 2.26 unified sessions exactly
An unpinned session has session_metadata.active_model null, so the parser
dropped it entirely. Fall back to config.toml's active_model, then Vibe's
default mistral-medium-3.5.
Split the rotated journal prefix into as many calls as runtime state lists
completions, so call counts match. Drop the tool group prefix from history
names (file_system.bash) and attach a turn's tools to its first call only.
Re-derive settled days (DAILY_CACHE_VERSION 58). Adds a redacted fixture from
a real 2.26.0 session.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(antigravity): count cache reads and price the standalone app's model
The standalone Antigravity app (2.19) stores usage as ModelUsageStats in
gen_metadata. Field 5 (cache_read_tokens) was never read, on the SQLite
path or the RPC path, so every cached prompt token was dropped. Fields 9
and 10 are thinking and response output, the reverse of what the SQLite
path assumed. The rows carry only MODEL_PLACEHOLDER_M16 and
"gemini-pro-default", so the model priced at $0.
Read cache read (and cache write) in both paths, read 9/10 in the right
order, drop the input fallback to field 1 (the model enum), and map the
placeholders seen in the app's own model catalog, flagged as estimated.
Bump the antigravity results cache, its parse version and the daily cache.
* fix(antigravity): keep .pb history across the cache bump, step times, 200k tier
With the results cache at v8 and Antigravity closed, .pb cascades had no
entry and no server, so they vanished until the app was next opened.
Serve the v7 entry meanwhile, stored with mtimeMs -1, and preload the
results cache so the session cache reparses such sources and the next run
with the server up re-fetches them with cache reads.
Standalone .db rows without created_at take their first step's time
(steps.metadata #1), which equals created_at wherever both exist, instead
of the file mtime.
Price antigravity at the Gemini long-context tier: add it to
TIERED_PRICING_PROVIDERS and pass the provider at both calculateCost sites.
Cursor Agent transcripts dated every turn at the session's last write. A
synced usage export ends its coverage at its newest billed event, so a
session that started before it and finished after it kept all its local
estimates on top of the billed rows. Date transcript turns by their
prompt's <timestamp> tag instead, like store.db turns already are. On a
real account this removed 101 double-counted calls on the last synced day,
and 2,748 of the 2,767 calls stamped on 22 Sep moved back to 17-20 Sep,
where Cursor billed them.
Synced plan rows carry no dollars (Included, Free), and Auto and Grok Bot
rows never name the model that served them, so their list price is a
stand-in: mark them costIsEstimated. Named models stay exact.
Daily cache to v58; cursor-agent joins PENDING_REDERIVE at 58 since a day's
call count can drop when turns move to their prompt day.
Session-store rows carried output only for compaction requests; every
other request's output was left to the per-turn assistant.message or the
shutdown rollup residual. CLI 1.0.8x and ACP hosts (JetBrains AI Chat via
copilot --acp) write no per-turn output, and an ACP session usually ends
without session.shutdown, so its whole output was lost. Rows now carry
their own output_tokens; at serve time a row that pairs with a per-turn
call carrying output (older CLIs) serves with output 0, so output counts
once in every shape.
grok-4.6 priced at $1.25/M input: LiteLLM has no bare grok-4.6 row and
the stripped azure_ai/grok-4.6 row claims the bare id ahead of
xai/grok-4.6, in the bundler and the live fetch alike. A builtin price
pins xAI's list rate ($2/$6/$0.50 per M), which is what GitHub bills.
On real data every Copilot model's cost now equals GitHub's
total_nano_aiu to the digit.
* fix(copilot): count VS Code agent-host requests and their output
VS Code now runs Copilot's agent through the Copilot CLI engine. Its
assistant.message events carry no outputTokens (CLI 1.0.8x writes the same
shape), so every request was skipped and the shutdown rollup, which assumed
per-turn events own output, dropped it: 0 calls and 0 output.
Tokenless messages now count as calls with zero tokens, and a leg with no
per-turn output takes its output from the rollup. The rollup also carries
modelMetrics totalNanoAiu, so the session's credits are exact. Tokens and
credits appear when the session ends.
* test(copilot): serve an open agent-host session
* fix(copilot): serve store-session output via the residual, count credit requests
CLI 1.0.8x sessions with session-store rows lost their output: the rows
replace the rollup and carry output only for compaction requests. The
per-leg residual now carries rollup output minus compaction-row output.
The credits note counted store rows, rollups and per-turn twins as separate
requests. It now counts requests by behavioral weight, rated when the
session carries an exact figure.
* fix(copilot): keep paired store-row credits, running nanoAiu delta
A behavioral store row (compaction or crash tail) made the credit sum drop
every paired row. Store rows now always count; a rollup counts only when
nothing behavioral carries nanoAiu.
totalNanoAiu is a running bill that does not reset at compaction, so its
per-leg delta uses its own sentinel instead of the token reset.
copilot's PENDING_REDERIVE contract moves to 56: a store row and its
message straddling midnight can move one call to the next day.
* feat(cursor): show the cursor.com sync in the apps, with an off switch
status --format menubar-json gains an additive cursorSync block (enabled,
state, lastSuccessAt, and a fixed errorCode/error on failure), read from
the sync sidecar, the config and the Cursor app's login. It never syncs,
never makes a request and never carries the token, the account hash or
server text. It is absent when Cursor is not signed in and has never
synced, and it is attached after the snapshot and per-device payloads.
With the Cursor provider selected, the desktop overview, the macOS menu
bar and the Windows tray show one line under the total: "Synced from
cursor.com 12 min ago", or the error in a muted warning colour. A
"Sync Cursor usage from cursor.com" switch writes config cursorSync:
Settings > Providers > Cursor on the desktop, the Cursor settings pane
on macOS and Windows. Desktop and mac strings are in all six locales;
the tray stays English until its localization lands.
* fix(cursor): guard config writes, show the env override in the switches
The tray's shared config writer now refuses to write over a config.json
that does not parse or is not an object (it used to replace it with an
empty map plus the one key), which also protects the currency, budget
and Claude config directory writers. The desktop writer rejects null
and array configs the same way.
With CODEBURN_CURSOR_SYNC=0 set, the desktop, mac and tray switches show
"Turned off by CODEBURN_CURSOR_SYNC=0" and are disabled. A failed mac
save now says so under the switch. The tray hero note uses the warning
ink and maps errorCode to its own messages.
* Sync Cursor usage from cursor.com automatically (#1545)
Reports that show Cursor dollars download the account's own usage export
with the Cursor app's session, at most every 15 minutes, and merge it into
the Cursor import store as source-tagged events. Manual CSV imports are
never replaced; synced rows dedupe against them by hash. codeburn import
cursor --sync forces one. Off with cursorSync: false or
CODEBURN_CURSOR_SYNC=0.
* Cursor sync: hourly with jitter, keep every account's history
The throttle is now an hour plus up to 5 minutes of jitter, and three
failed requests in a row back off 6 hours. After the first month, a sync
starts at the day of the newest synced event, and a quiet account
remembers that it synced, so closed days are downloaded once.
Synced rows and ranges carry the hashed account. An account change no
longer drops the earlier account's rows; the new account's window starts
after their coverage. A closed synced day stays in the daily cache when
a later sync fails or the store goes missing.
* Cursor sync: longer first backfill, safer merges, provider gate
The month-long first backfill gets 45s (it took 11-12.5s on a real
account and could hit the old 15s cap); incrementals keep 15s. The sync
heartbeats through the download for the app watchdogs. An incremental
starts the day before the newest synced event so late rows land, and
rows from before the window are ignored instead of failing the sync.
A manual import of a row a sync stored first turns it into a manual
row, so a later sync can never delete what manual coverage relies on.
The sidecar records the account only after the request. Commands
filtered to a non-Cursor provider no longer sync.
* Cursor sync: manual coverage wins inside its range
A sync skips rows dated inside a manual import's coverage, so a row
Cursor revised there is not priced twice. Rows after the manual
coverage end still land.
* Read DSH session format v4 written by dsh 0.2.0-rc.2
dsh 0.2.0-rc.2 stamps every session log v4, so discovery skipped each one
and DeepSeek Harness contributed no usage anywhere (#1584).
Admitting v4 only after verifying the reader's whole consumption surface
against DSH's official sessionFormatCatalog (recovery: 'strict',
validation: 'current', @deepseek-ai/dsh-session-format-catalog@0.2.0-rc.2),
per the semantic-update rule docs/providers/dsh.md prescribes for format
bumps: dense seq, usage at assistant/message data.usage (plus a new
informational totalTokens sum) or the embedded stream, tagged end-seed
inheritance, and llm/retry-started attempt slots are all unchanged. What
v4 does change (tool-result role lift, plugin: tag namespacing, surfaceOp
append/replace metadata, new header fields, untagged end-seed markers in
unseeded sessions) is ignored or already handled.
Adds catalog-validated v4 fixtures (same buckets as v0-v3 plus the
informational totalTokens) and extends the per-version parse tests to v4.
Bumps the dsh parse version so cached skip-era sessions recompute.
Verified end to end against a real 0.2.0-rc.2 log: doctor reports the
session readable, and the parsed totals (12 calls, input 11783, output
3699, cache read 107520) match the official catalog artifact exactly.
* daily cache v52; drop unused field
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
Local Cursor rows are priced from IDE composer bubbles, which never see
the context Cursor re-sends on every request, so the figure can sit far
below the dashboard bill (about 2% on the account that reported #1545),
yet overview printed it like any exact total. The import from #1558
answers this for the time an export covers; outside it nothing said the
number was not the bill.
overview now prints one dim line under the bottom line whenever the
period still holds local cursor/cursor-agent dollars that no import
covers — covered local calls are already dropped at serve time, so any
non-imported Cursor call that survives the walk is an uncovered
estimate. The line names the amount and points at
codeburn import cursor <file.csv>.
* Add regressions for redirected editor storage roots
* Honor redirected data roots for Copilot and Cursor
* Verify redirected Copilot OTel discovery on Linux
* Reproduce Cursor cache collisions across storage roots
* Bind Cursor result caches to their database and backfill redirected history
* Verify redirected discovery against the latest daily cache
* Update env guard and document editor storage overrides
* Add Hermes Windows home regression tests
* Resolve Hermes home from Windows local app data
* Re-derive Hermes data from Windows home
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* Add regressions for request-level Copilot journal usage
* Read request-level input tokens from Copilot journals
* Refresh cached Copilot journal usage and settled days
* Use local date keys in the journal cache regression
* feat(devin): read usage from sessions.db
Devin keeps every request in sessions.db message_nodes; the transcript
exports hold only a few sessions. Usage now comes from the store, one
source per non-hidden session, read in node_id keyset batches with only
the needed JSON fields pulled. Sibling retries share a request_id and
dedup on it; off-main-chain requests count. SQLITE_BUSY is rethrown so
the refresh retries. Transcripts are read only when the database is
missing or has no message_nodes, so the two never double count.
The legacy metadata.metrics path no longer carves cache reads out of an
input_tokens count that never included them.
sessions.db sources rely on the WAL-folded fingerprint instead of a
forced reparse. Parse version sessions-db-v1; daily cache v43 with devin
in PENDING_REDERIVE, since transcript-era days dated steps on the
session's last activity.
* fix(devin): take only text user content as the prompt
* test(daily-cache): include devin in pending re-derive expectations
* Add regression coverage for Claude queued prompts
* Count queued Claude human prompts in turn grouping
* Cover queued prompts through Claude session cache
* Reparse cached Claude turns for queued prompts
* Refresh daily categories after Claude turn regrouping
* fix(claude): count only human queued prompts, skip peer agent messages
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* feat: Amp support - read the local thread mirror (#1572)
Amp (Sourcegraph's coding agent) mirrors every thread it serves as one
JSON document per thread at ~/.local/share/amp/threads/ (AMP_DATA_DIR
honored, comma-separated). Each file is { id, messages,
usageLedger?{events} }; the ledger's events are the per-request record
(tokens.input/output, cache tokens joined from the billed message
through toMessageId -> messages[].messageId), with a per-assistant-
message usage fallback for threads without a ledger and a total-only
record billing as output.
Amp bills a subscription and records no charged dollars, so every call
is priced from the underlying model's token rates and marked
costIsEstimated; the recorded credits field is ignored (no published
per-credit dollar rate). Thread files carry no cwd, so sessions group
under the provider name. Registered eager; parse version
usage-ledger-v1; no artwork yet (mono-initial fallback, like Codebuff).
Surfaces: CLI registry + doctor probe roots, Electron sandbox read
paths + menubar catalog (Amp after Cursor, matching the Swift order),
Windows tray tab color + usage_guard root, GNOME prefs dropdown, macOS
ProviderConnectionCatalog, docs/providers/amp.md, README tool list,
CHANGELOG.
* fix: address Amp provider review feedback
* fix(openclaw): read the per-agent sqlite transcript store the 2026-09-01 migration writes
OpenClaw 2026.8.1 moved sessions from agents/<agent>/sessions/*.jsonl to
agents/<agent>/agent/openclaw-agent.sqlite, so discovery resolved to zero
sources and doctor reported the provider empty (#1259).
transcript_events keeps each JSONL envelope verbatim in event_json, so the
JSONL event reducer is extracted and shared: both eras parse, dedup and
cost identically, and migrated sessions keep their original ids. Sessions
are listed per store and parsed in keyset batches of 2000 rows so a
gigabyte store never loads whole. agent-schema 23 lets a row carry its
event as an event_zstd BLOB instead; those decode on Node 22.15+ and skip
with one notice below that.
Fixes#1259
* test(openclaw): batch fixture inserts in a transaction; hash id-less events before the created_at fallback
---------
Co-authored-by: ozymandiashh <ozymandiashh@users.noreply.github.com>
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* feat(dock): one Claude ring per config directory, with its own sessions and today (#1523)
Rate limits are per account, so a machine with a work and a personal
~/.claude cannot be served by one Claude ring. When the CLI knows two or
more Claude config directories:
- `codeburn quota --format json` adds a top-level `claudeProfiles` array,
one entry per directory (`providers[]` unchanged). Each entry carries
the directory's own windows, read from its own `<dir>/.credentials.json`
(the macOS Keychain fallback stays reserved for the default ~/.claude),
plus that directory's own today totals from a single Claude-only,
cache-backed parse of today's range, filtered per source id exactly like
the menubar's per-config payloads.
- Live sessions carry `claudeConfigSourceId`, so a per-profile ring claims
only its own sessions. Claude Desktop sessions keep their own bucket id
and belong to no profile.
- The Windows Capacity Dock gains a `claudeProfiles` preference (combined,
default; separate draws one captioned ring per directory). Profile rings
keep addressing the `claude` provider id for the provider set, glyphs,
colors and the connect button; the settings row appears only when the
CLI reports more than one profile.
- Captioned rows are taller by two new metrics (caption line + gap),
mirrored in TS and Rust: the page tells Rust the rail is captioned in
the layout request, and the frame carries the effective row extent the
hit-test uses. Non-captioned rails are pixel-identical to before.
- The desktop app's shared dock-prefs schema parses and sanitizes the new
key, so its pane cannot strip it from windows-dock.json.
Closes#1523
* review: opt-in profile reads, today timeout, missing-dir filter (#1570)
- `quota --claude-profiles` gates the per-directory reads: without it the
payload lists profiles ({id,label,path}) only, so a combined rail and
WSL users pay nothing; the Windows dock passes the flag only while its
claudeProfiles preference is separate, and an unknown option from an
older CLI reads as CliOutdated.
- The default ~/.claude profile reuses the claude provider row's result —
no second request, no second macOS Keychain read.
- claudeTodayBySource races the same timeoutMs and answers an empty map
on loss, so a silent child cannot cost the dock every provider's quota.
- Missing config directories are filtered before the >1 check.
- `captioned` applies only while a Claude ring is in the selected set;
profile rows tolerate a list-mode answer; comments trimmed; merge with
#1560 keeps both keepExpanded and claudeProfiles.
* test(quota): set USERPROFILE for the default-dir check; drop issue refs
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
Consumption-share split in `codeburn models`, reworked per review:
- First-party routes only: the split applies to `dsh` (DeepSeek API) and
`zcode` (Z.ai plan) calls. The same model behind a gateway never had
the vendor discount, and reported-cost providers may already carry it;
the gate set stays disjoint from REPORTED_COST_PROVIDERS.
- The numbers agree by construction: both legs are shares of the stored
list-rate cost (peakUSD + offPeakUSD == costUSD, pinned in tests).
The 0.5x off-peak discount stays on the vendors' bills; nothing is
repriced here. peakCostMultiplier and OFF_PEAK_MULTIPLIER are gone.
- Calendars: unverified 2027 projections dropped; the Z.ai
2026-09-25..10-07 all-day off-peak promotion is honored as a temporary
range; deepSeekHolidayCoverageYears() plus a test fail when the
current year falls out of the holiday list.
- Output shape: JSON carries the five peak keys only on rows with a
split, so a report without DeepSeek/Z.ai first-party usage keeps its
exact historical shape; the Z.ai quota note is removed.
- The `codeburn peak` live-status command is left out (scheduling tool,
not usage tracking); it survives on a separate branch for its own PR.
* menubar: dock preference for the hover bubble's surface
* menubar: let the dock's hover bubble keep its own surface
* menubar: setting for the dock's hover bubble surface
* docs: dock hover bubble surface setting
* feat(cursor): import Cursor's usage export to replace local estimates (#1545)
* Price Cursor-hosted grok-4.6-high like grok-4.6 in the Cursor import
* Skip no-op Cursor re-imports and price cursor-grok-4.6-high ids locally
* test(cursor-import): write agent fixtures under os.homedir() so Windows finds them
* fix(cursor-agent): read CLI sessions that exist only in ~/.cursor/chats store.db (#986)
A session with no exported transcript is read from its store.db; one with
a transcript is read from the transcript alone, so nothing counts twice.
Store turns follow the transcript accounting, take their time from the
prompt's <timestamp> tag and their model from the reasoning block, and
merge every conversation root so summarized history is kept. Daily cache
moves to v39 so settled days re-derive.
* cursor-agent: price store.db turns at the Cursor (auto) rate like transcripts
* feat(copilot): legacy JSON chat session parser and auto-mode resolution
* fixed copilot naming
* revert: remove unrelated jetbrains and env-isolation changes
* revert priority change
* fix(copilot): route legacy JSON chat sessions cleanly and fix calculateCost web search param
* fix(copilot): legacy chat session parsing and pricing
* fix(copilot): strip github.copilot-chat/ namespace from legacy model ids
found github.copilot-chat prefix in my chat sessions
* fix(copilot): revert comments
* fix(copilot): model names
* fix(copilot): model display name edge cases
* feat(copilot): only keep legacy parser source
* fix(copilot): legacy sessions skip binary tool outputs and use recorded thinking tokens
A screenshot or other binary tool result was base64-decoded as UTF-8 and
char-counted as input, so one 200 KB PNG billed 50k input tokens. Only
text/* payloads count now.
A round's recorded thinking.tokens was compared with the char estimate of
the same text via Math.max, so the estimate won whenever it was larger.
Each round now uses its recorded count when present and the estimate only
otherwise, summed across rounds.
Docs list the flat .json files and the transcript gate covering them.
* fix(models): price Copilot's dotted spellings of Claude 3.5, 3.7 and Opus 4.1
Copilot writes claude-3.5-sonnet, claude-3.7-sonnet,
claude-3.7-sonnet-thought and claude-opus-4.1, which resolved to no
pricing row and billed $0. They alias to the dashed rows now.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(opencode): price Vertex turns on a Vertex row and keep an unpriceable recorded cost through the cache (#1547)
OpenCode records the cost of each turn. For an id CodeBurn cannot price the
parser fell back to that cost, but the session cache stored no cost and
re-priced the cached tokens on every read, so the call showed $0. The
recorded cost now rides on the cached call as fallbackCostUSD and applies
only while the tokens price at $0, so a later catalog row or price
override still wins.
The session-level rollup used provider/model while per-message turns use
the bare id since #1543; it now uses the bare id too. google-vertex and
google-vertex-anthropic map to a metered Vertex route.
OpenCode and Kilo Code re-parse once; daily cache v37 re-derives settled days.
* fix(opencode): keep a declared-free id at $0 instead of its recorded cost
A model the user marked flat-rate, priced at a zero rate, or that looks
local prices at $0 on purpose. The recorded-cost fallback now skips those
ids at parse time and on cache read, through one guard in models.ts.
* fix: count every Cursor Agent 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 while the display stays truncated at
500 chars. Parse version bumped so cached sessions re-parse.
* cursor-agent: a transcript that ended before any assistant message is not an unrecognized format
* cursor-agent: bill the user text once per user message, not once per assistant message
* cursor-agent: re-derive finalized days and cached sessions under the new accounting
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(opencode): keep pre-upgrade history on OpenCode 2 databases and price v2 turns like 1.x
OpenCode 2 copies 1.x sessions into session_v2/session_message on first
start, but the copy drops compaction summary and task-tool turns, skips
messages it cannot decode, and can stop partway. A session present in both
generations now reads its frozen legacy turns first and adds only the v2
turns legacy does not hold (migrated rows keep their ids, so dedup skips
the copies).
v2 turns also used a provider/model id that failed pricing for ids such as
opencode/claude-haiku-4-5; they now use the bare id 1.x stored.
Reported by a user.
* Changelog: OpenCode 2 unpriced ids showed as $0
Crush's project registry has used a ProjectList wrapper since it was introduced, so loadRegistry parsed a real registry as zero entries and reported no usage. Accept the wrapper alongside the array and keyed-object shapes the parser already handles, and correct the registry-format description in the provider docs.
* docs: move the README reference manual into docs/
* assets: designed README images and their sources
* README: tell the story first, keep the manual in docs
* 0.9.25: correctness + provider fixes, energy guard, logo, default DMG
CLI/engine fixes: local-model savings count reasoning; ZCode reasoning billed;
codex-credits version-boundary tiering; GLM alias repointed to z.ai discounted rate;
openclaw/pi/qwen timestamp mtime fallback; hermes delta timestamp;
sqlite/warp/hermes busy+permission reclassified as failed (no sealed empty);
codex-cache dev+ino; cursor-cache WAL; antigravity server TTL; quota timeout+
grokbot footer+claude near-expiry; serve RSS guard env+hysteresis; CSV export
subfolder; providers pane fixed window; Warp credit-based cost.
Desktop/menubar: unified flame logo; default DMG; docs signed+notarized.
* menubar: one running instance, self-relaunch on language change
The single-instance guard only retired strictly older copies, so two login
items starting in the same instant both survived, and a copy whose bundle
identity did not survive install was never seen at all. The decision is now
total (newest launch wins, ties break on pid) and peers are matched on bundle
id or executable name. --keep-both opts one launch out.
Language changes no longer have the desktop app quit and reopen the menu bar,
which made macOS attribute the new process to the desktop app and re-ask for
the data-access grant. The menu bar relaunches itself via the existing remote
command key; older menu bars that do not watch the key fall back to the old
path after 5s.
* desktop: export toast shows the saved path; resource buckets on app_close
The export toast showed the folder that was picked, not the dated subfolder
the files land in. The handler already parsed the CLI's saved line; it now
returns that path and the toast uses it.
app_close carries cpu/mem (and serveCpu/serveMem when serve reported) as bucket
labels, sampled on the existing flush interval. serve adds cpuSec and peak rssMb
to its answer frames. Unmeasured figures are omitted, never sent as zero.
* menubar guard never leaves zero instances; installer replaces in place; JSON export into folder
Guard: an unreadable launch date on either side could make two copies retire
each other. Unreadable peers now count only when their pid is lower, and a copy
that cannot read its own date retires lower pids only, so a disagreement keeps
at least one instance.
Installer: an existing bundle in /Applications or ~/Applications is replaced
where it lives instead of a second copy being added. The old bundle is moved
aside and restored on any failure; a cross-volume move falls back to a copy that
is re-verified with codesign. An unwritable location is never escalated: the
install goes to ~/Applications and the leftover is named.
Export: -o pointing at an existing folder writes the JSON inside it. Short
sessions omit the cpu buckets. setLanguage reopens the menu bar if its own
relaunch never comes back. AppRelaunch passes the bundle path as an argument.
* menubar guard orders by kernel start time; installer opens the copy that exists and recovers a killed install
Guard: launch dates could be unreadable and pid order can disagree with start
order after pid reuse, so two copies could still retire each other. Ordering now
uses the kernel-reported process start time, which both sides read identically.
A copy whose start time cannot be read is left out, so the worst case is two
menu bars, never none.
Installer: with the only copy in an unwritable /Applications, a plain run tried
to open a path that did not exist. It now opens the installed copy. A placement
killed mid-way is recovered on the next run, a concurrent install is refused,
and a failed restore names where the previous bundle is. Process matching uses
the exact executable name.
Export: a trailing separator names a folder. Telemetry cpu is measured from
telemetry start.
* installer: a recycled pid can no longer block installs; language change waits for the replacement process
An aside left by an earlier install counted as an active install whenever its
pid was live, so once that pid was reused every install was refused for good.
It now also has to be recent (ctime within 10 minutes), a pid we may not signal
is not ours, the refusal names the file, and an orphan that cannot be cleared is
reported instead of aborting the install.
setLanguage confirmed the relaunch with a matcher the dying process still
satisfied, so the reopen fallback never fired. It now waits for a pid that was
not running before.
* desktop: only the headline refreshes live; act, yield and the optimize scan move to slower tiers
An open window ran act report (a fresh CLI process each time), yield and an
optimize scan on every refresh. The headline poll now passes --no-optimize and
takes the snapshot path; act report refreshes on mount, on manual refresh and
every 10 minutes, yield every 5. The optimize scan runs at most once a day per
query scope, is stored in userData with its time, and is forced fresh by the
Optimize page or a manual refresh. Figures that are not live show their age,
and no stored scan means no savings clause rather than a zero.
On battery the live interval doubles; the chosen cadence stays the base.
* desktop: a stored optimize scan is only valid on the day it was computed; cadence changes no longer refetch
The scan cache is keyed on the argv, which says 'today' or '30days', not a date,
so yesterday's scan was served as today's. It is now reused only on the same
local day. Stored findings in Signals carry their age, a failed scan shows the
error instead of scanning forever, and the cold-boot label uses the time the app
received the payload.
usePolled fetched immediately whenever its interval changed, so every power
transition (and launching on battery) refetched every live hook and could spawn
an extra act report. Fetching and timer arming are now separate effects.
Optimize's yield is on the same 5 minute tier as Overview.
* desktop: re-ask for the optimize scan on the first render of a new local day
An app left open across midnight kept yesterday's stored savings under Today,
because the scan was only requested on mount, scope change or manual refresh.
The local day is now one of the hook's triggers; the previous day's figure is
dropped while the new scan runs.
* serve: a menubar poll is never answered from the status snapshot
Inside its settle window the status snapshot returns the pre-change payload on
purpose. The resident serve 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 now skips it.
* tests stop writing the real serve pid file; superseded caches are swept; installer notes reach the desktop card
ServeConnection takes its pid file as a parameter, so the Swift tests no longer
share ~/.cache/codeburn/menubar-serve.pid with a running menu bar, and the
reaper only signals a record that is a serve --stdio command.
Superseded session-cache, codex-results and cursor-results files (100MB+ each)
are removed once untouched for 14 days, at most once a day. daily-cache and the
hermes ledger are never swept: they carry sealed days a version bump adopts.
The menu bar installer's leftover-copy advice is emitted as a marked line under
CODEBURN_PROGRESS and shown on the desktop card. The skipped-refresh NSLog is
debug only. The app's vitest workers turn off Node's built-in web storage.
* menubar: anonymous usage statistics, under the same consent as the desktop and the Windows tray
The Swift menu bar sent nothing, so every codeburn-menubar row was a Windows
tray. It now follows the desktop's telemetry.v1.json when that exists (read
only: same install id, same on/off, onboarded required) and otherwise keeps its
own state with the same region default (off in EU/EEA/UK/CH and when the region
is unknown). Settings gains one Privacy toggle, disabled when the desktop owns
the choice. Events use the tray's names; the daily usage snapshot is the CLI's
block passed through a sanitizer and is left to the desktop when it is the
consent source. Debug builds never send.
A resident CLI that gets disabled for the run now says so in the log.
* menubar statistics: private network session, abandoned batch on opt-out, a desktop No survives the desktop going away
The sender used URLSession.shared, so a server or CDN cookie and the default
User-Agent rode along and would have outlived an opt-out. It now owns an
ephemeral session with no cookies, no cache, a fixed agent string and no
redirects. A batch that is out when the toggle goes off is dropped rather than
re-queued under the fresh id. The desktop's answer is recorded locally, so a
missing or damaged state file never falls back to the region default, and the
standalone id is persisted when minted. The toggle stays usable as a local off
switch under desktop consent. Copy states what this app actually sends.
* vercel gateway spend is shown on its own row and kept out of totals by default
The gateway reports daily aggregates per model, with nothing to match against
the local tools that were pointed at it, so every headline counted that traffic
twice. Totals now leave the gateway slice out when all providers are shown; the
row keeps its full amount and is marked not in total, --provider vercel-gateway
is unchanged, and codeburn gateway-totals include folds it back in. The daily
cache still stores the slice, so the choice applies retroactively. Reports with
no gateway credential are byte-identical.
The gateway's request_count now feeds the call count instead of each daily row
counting as one call. Hermes: a mid-session price change lands on the day it is
observed and sealed days are never rewritten; stated in the code and changelog.
* gateway exclusion applies to every report through the parse, and the detail panels follow the headline
The first pass narrowed only the day totals, so By tool, top models, top
projects, activities, status json projects and any project-filtered table still
carried gateway spend next to a headline that did not, and By tool shares could
pass 100%. The rule now lives in parseAllSessions: with all providers in scope
and the opt-in off, aggregate-only sessions are dropped after the memo, and the
callers that derive cached days opt out so stored slices are unchanged.
day.projects is reduced like models. The gateway stays visible once, as a
labelled not-in-total row with no share, and standalone reports note the
excluded amount on stderr so machine output is unchanged.
* dashboard: the background history index follows the gateway exclusion too
The index keeps the whole corpus so it can fill the daily cache, but its
per-period projection handed that unfiltered list to the project, model and
activity panels, so they regained gateway spend once the index landed.
* ci: a hung lock suite fails in 90s and retries once with a warning; slow Windows tests get measured limits
The cache-lock step failed 6 of the last 60 runs with no output for five minutes
after the vitest banner; passing runs take 12-18s on every Node version. Each
attempt is now capped at 90s and retried once, with a warning whenever that
happens. On windows-latest a single CLI spawn takes 1.4-12s, so the four-spawn
status snapshot test and the c6 parser suite get limits taken from those runs,
each with its reason. Both test jobs print their ten slowest cases.
* desktop: Privacy & data uses the same rows as the rest of Settings
The pane mixed two static claims with two controls in its own layout. It now
uses the about-row pattern every other pane uses: title and hint on the left,
one control on the right. The claims became the subtitle. Rows: anonymous usage
statistics (toggle, link to what is sent), device sharing state with Manage,
local report snapshots with Clear, export with Open export.
* desktop: Privacy & data keeps one bounded column and rows that never squeeze
The card stretched with the window, stranding each control hundreds of pixels
from its text. This pane's heading, subtitle and card now share a 760px column
and one text measure. Rows stack on their own width, because the window can
never be narrow enough for the old viewport rule to fire; row buttons do not
wrap. The what-is-sent link sits on its own line, and the snapshot and export
descriptions fit their measure and say this machine.
* menubar tests clean up their scratch defaults and leave the real Keychain alone
Four test files never removed their UserDefaults suites and the rest removed the
domain but left the plist, so thousands of files piled up in
~/Library/Preferences. A shared helper forgets a suite and deletes its file, and
every test that makes a suite now uses it. cfprefsd still rewrites an empty plist
when the test process exits, so a run leaves about a hundred 42-byte files with
no test data in them. The real-Keychain round trip only runs with
CODEBURN_TEST_REAL_KEYCHAIN=1.
* desktop: the resident parser retires after 15 idle minutes and restarts on demand
The renderer stops polling while the window is hidden, but the serve child stayed
resident with its parsed cache, one to two GB on a large corpus. After 15 minutes
without a request it is stopped the way a settings restart stops it, without
spending the unexpected-death budget, and the next request starts a new resident
through the existing lazy path, never a one-shot. CODEBURN_SERVE_IDLE_MS tunes or
disables it.
* desktop: every settings pane keeps a bounded column, and dark buttons have an edge
Cards stretched with the window, so on a wide display controls sat up to 1300px
from their labels and intro lines ran past 140 characters. All panes now share
one left-aligned column (760px, 980px for projects, aliases and pricing, which
hold wide rows) and one text measure. Rows stack on their own width instead of a
viewport rule the 900px minimum window could never reach, and a long path in
Projects no longer overflows the pane.
In dark mode the secondary button matched or sat below its surface, and was
invisible on the plugin install dialog. It gets a face and a border from two new
tokens; light is unchanged. The onboarding consent label matches Settings.
* desktop: this device shows its fingerprint on its own line
The local name and the fingerprint are two adjacent inline spans, so they read as
one run of text.
* menubar: the resident parser retires after 15 idle minutes and restarts on demand
The menu bar kept its serve child, and the parsed cache with it, for the whole
run, even with the popover closed and nothing changing. After 15 minutes without
a request the child is retired without counting as a death, and the next request
starts a new resident through ensureStarted, never the one-shot fallback, whose
snapshot settle window would make the figure lag a whole tick. The window sits
above the longest gap between real requests, so a child stays warm while work is
happening. CodeBurnServeIdleSeconds tunes or disables it.
* serve: a poll re-checks only what the watcher saw change, and shard writes are coalesced
With a session being written, every poll re-listed about 2,400 directories and
re-fingerprinted about 6,000 transcripts, then rewrote the whole current-month
shard (49MB on a large corpus) and read it back on the next poll. The resident
process now reuses the previous listing and fingerprints for paths its root
watcher proves untouched, and falls back to the full sweep whenever the watcher
is missing, unhealthy, overflowed, armed after the sweep, does not cover the
path, the memo is older than 4.5 minutes, or the process was suspended. Shard
publication is held for up to 90s and flushed when stdin closes; a held write is
dropped rather than written over something another process published. One-shot
runs are unchanged. Active poll cost 0.71 to 0.33 CPU-s at a 30s cadence;
time-to-reflect unchanged at about 1.2s across append, new dir, new file, delete,
rename-over, in-flight, watcher overflow and unhealthy watcher.
Changelog: state the telemetry default as it is in this version.
* a retired parser gets to publish its held shards, and durable providers are never held
Both apps retired the serve child with SIGTERM before its stdin-close flush could
run, so every retire discarded the coalesced window. They now close stdin, give
the child a bounded grace to publish and exit, and signal it only if it is still
running; a settings restart does the same. Measured: flushed 0/5 before, 5/5
after, for both clients.
copilot's cache entries are the only record once Copilot prunes its own files, so
a held window that was dropped (retire, crash, kill -9) before the source was
pruned lost that spend for good. A window that a durable provider dirtied is
published on the poll that parsed it, as before coalescing existed. The RSS guard
publishes a held window before it clears the caches instead of discarding it.
* serve: release the lifetime parse after the fill, and make the collection real
The RSS guard dropped its memos and then called globalThis.gc, which exists only
under --expose-gc; no launcher passes it, so the collection had never run and the
freed pages never went back. On a cold start the resident idled at 2.2GB with
364MB of live heap: clearing every memo moved RSS by 0MB, a collection alone by
700MB.
After the background fill has sealed its summaries the process now publishes any
held shard window, drops the memos it can re-read from disk (the cold start had
loaded the cache with scope 'all', which then served every later poll), and
collects; it collects again a minute later for as long as a pass returns 64MB,
at most four times. Cold idle 2198MB to 700MB, warm 900MB to 781MB. Reports are
leaf-identical cold and warm, time-to-reflect unchanged, a released month scope
reloads in 0.3-0.5s.
* warp: the billed cost reaches reports and survives the cache
The provider derived a dollar cost from Warp's own billing record (provider cents,
charged usage, or credits), but warp was missing from the reported-cost
pass-through, so the cached call kept no cost and every report, cold or warm,
priced the conversation from its tokens. A fixture billed at $1.22 reported
$0.15. A call whose cost came from a billing record now keeps it; a call that
only has the token floor stores none and is still re-priced when prices change.
Warp entries re-parse once (provider parse version), and the daily cache moves to
v34 so days whose Warp source still exists re-derive; call counts do not move, so
nothing is double counted or dropped.
* openclaw and pi: a reported cost survives the cache like every other reported cost
Both providers read the cost their tool recorded for a message, and both were
missing from the reported-cost pass-through, so reports priced every call from
tokens instead. Fixtures reported $0.011 against $0.056 (openclaw) and $0.013
against $0.410 (pi). A call with a reported cost now keeps it; a call with none,
or an explicit zero, stores none and is still re-priced when prices change. omp
had been added to the list in #1150 and pi, the same parser, was never revisited.
The provider list is one named set, the unreachable providerCallToTurn is gone,
and both providers re-parse once; the v34 daily cache re-derives their days with
call counts unchanged.
* test: pin parse count per report and per resident poll
* feat(windows): discover provider roots inside WSL distros
Agents run inside a WSL distro write their history to the distro's Linux
home, exposed to Windows as \\wsl$\<distro>\... and never under the user
profile, so a Windows-only scan reported zero sessions for them.
src/wsl.ts lists distros through %SystemRoot%\System32\wsl.exe (absolute,
so nothing dropped next to the CLI can impersonate it) with a 3s timeout,
decodes its UTF-16LE output, drops container-runtime distros, and
enumerates each distro's home/* and root. Running distros only by
default: touching \\wsl$ boots a stopped distro. CODEBURN_WSL=all|off
opts in or out. Inert off win32 and memoized per process.
claude and codex append <wslHome>/.claude and <wslHome>/.codex to their
existing root lists, so multi-root discovery, probeRoots() and doctor
cover them unchanged. Roots are additive, never an override.
CODEBURN_WSL stays out of PROVIDER_ENV_VARS: cache entries are keyed by
source path, so it can only add or drop paths, never make a kept entry
stale, while declaring it would force a re-parse on every toggle.
Fingerprints drop dev/ino for \\wsl$ paths in both caches. The 9P share
synthesizes them per mount, so keying on them would re-parse every WSL
session on every run; mtime+size still catches edits and appends.
Refs #1059
* test(windows): cover WSL root discovery and the 9P carve-outs
UTF-16 output parsing, utility-distro filtering, CODEBURN_WSL modes, UNC
classification, claude/codex root injection and discovery, probeRoots,
fingerprint reconciliation with zeroed dev/ino, and the win32 guard that
keeps a Linux cwd from being walked on Windows.
Refs #1059
* docs(windows): document WSL discovery and CODEBURN_WSL
Refs #1059
* fix(windows): harden WSL discovery from adversarial review
Probe \\wsl$\ before \\wsl.localhost\. The older spelling works on every
WSL build; probing the newer one first sends builds without it through
MUP, SMB and DNS resolution for a host named "wsl.localhost" — seconds
of stall per distro, every process.
Only accept lines shaped like a distro name. With nothing installed,
wsl.exe prints prose on stdout whose lines otherwise read as distro
names and get probed over UNC. A name must be a single token with none
of the characters Windows bans from a path component and no trailing
period, which discards that message in any UI language.
Do not evict a \\wsl$ cache entry for being undiscovered, and serve it
in write mode too. A stopped distro drops its whole root out of
discovery; treating that as a deletion re-parsed everything over 9P on
the next start and made totals drop while the read-only serve path still
showed them. The exemption is scoped to isWslUncPath — an ordinary path
whose file is gone is still evicted.
Also: getCachedCodexProject was the one fingerprint site still keying on
dev/ino for WSL paths; doctor now prints why it probed no WSL roots; and
a distro contributing several homes labels them per user rather than
letting makeUniqueLabels number them.
Refs #1059
* fix(windows): re-validate running WSL distros on a TTL before UNC access
wslHomes() memoized the running-distro homes for the process lifetime. In
long-lived serve/menubar use a distro cached as running could be shut down
and the next refresh would still touch its \\wsl$ UNC path — which can hang
win32 fs calls — while a distro started after an empty discovery stayed
invisible until restart.
Discovery now caches for a 60s TTL only; on expiry the wsl.exe -l --running
probe (timeout-bounded) re-validates the running list before any UNC path
is touched, and a failed probe fails closed to native-only discovery rather
than serving stale homes. Covers both shutdown and later-start transitions.
Addresses the blocking review finding on PR #1062.
* fix(windows): harden WSL cache reconciliation
* fix(windows): bound WSL orphan refresh to the discovery TTL
refreshWslHomes re-spawned wsl.exe on every full parse for a stopped-but-
cached distro, ignoring the 60s TTL — up to ~3s twice per parse in the
resident serve/menubar. Reuse the TTL-bounded wslHomes() probe instead; the
same TTL already reflects the only transition the orphan check must catch,
offline->running.
* test(windows): skip on-disk POSIX-worktree walk case on real win32
The fixture builds a worktree under tmpdir() and treats it as a POSIX cwd the
win32 guard should reject. On a real Windows runner tmpdir() is a C:\ path, so
the guard rightly walks it and canonicalizes to the main repo, failing the
assertion. The case is only constructible on a POSIX host (mocked win32); the
literal /home/... test already covers the guard on Windows.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
* feat(cli): filter usage by billing route and mode
Carry provider-observed billing provenance through Hermes and the session cache, register the real OpenRouter route, and add call-level route/billing filters to models, sessions, export, and audit.
Keep unknown billing unclassified, preserve existing pricing, and repair legacy included Hermes ledger baselines without rewriting genuine multi-observation history.
* feat(opencode): preserve OpenRouter route provenance
Carry OpenCode providerID through legacy SQLite, v2, file, and session-rollup paths so OpenRouter calls remain distinct from direct traffic. Invalidate OpenCode and KiloCode caches and cover strict route matching.
* feat(opencode): classify Amazon Bedrock route
Map OpenCode's exact amazon-bedrock providerID to the existing metered Bedrock route, including model families that cannot rely on Anthropic/OpenAI ID shapes. Invalidate OpenCode and KiloCode caches across the route expansion.
* docs(opencode): document billing route provenance
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
Adds a live quota gauge for the z.ai ZCode coding plan on all three surfaces: the CLI plan view, the desktop Plans page, and the mac menu bar. It reads the provider's own remaining-allowance endpoint, so it is quota-only and touches no cost or spend paths. One decoder is shared between Z.ai and ZCode, and ZCode is hidden when a Z.ai login already covers the same endpoint. The fetch is bounded with graceful disconnected, waiting and error states, and no socket is opened without a login. The logo shows on every surface, and ZCode is listed in the desktop supported-provider list to match the menu bar catalog.
Archiving a session in OpenCode only sets time_archived, but the discovery query and all three recursive session_tree walks filtered on it, so the session vanished from the sessions list, from per-project views and from any cache rebuild. All four predicates are gone; the rows stay in the database, so archived roots and children now parse like any other session. Kilo Code shares this parser and is covered.
Real data, a copy of a 193-session legacy OpenCode database: with nothing archived, main and the branch both read $109.91 / 4,009 calls / 177 sessions.
With three roots flagged archived, main fell to $100.75 / 2,875 / 174 while the branch held $109.91 / 4,009 / 177 — the gap is exactly those three sessions, $9.16023 and 1,134 calls, and nothing else.
The opencode and kilo-code parse versions move as well: an archived root came back on its own, but a root whose child was archived stayed a present, unchanged source, so a warm cache kept serving the parse that dropped the child.
That case measured $104.48 / 3,752 on a main-seeded warm cache and stayed there on the branch until the bump, which restores $109.91 / 4,009.
Tests 4,278 pass, locks 36, tsc clean; the three archived assertions fail on main across both schema generations. CI 19 green.
Fixes#1362.
Devin transcripts carry no committed_acu_cost, so the ACU path priced every session at zero and devin.acuUsdRate hid them all until it was set by hand. Cost now comes from each step's tokens and its generation_model through the shared pricing tables, with the cached share carved out of OpenAI-style prompt_tokens; swe-2-high stays unpriced at $0. Task text is the user turn, then the sessions.db title, then prompt_history. The menubar ACU field, its config store and the config key are gone. Separate commit: gpt-5-3-codex ids resolve to the gpt-5.3-codex pricing row instead of the base gpt-5 row.
OpenCode 2.x (mainline since 2.0.3) writes sessions to session_v2 and messages to session_message, tagged by a type column and ordered by seq, while the legacy session/message/part tables freeze at upgrade. Every session created after the upgrade was invisible to CodeBurn.
The parser now branches per database on sqlite_master: when the v2 tables exist they are read and the frozen legacy tables are ignored, otherwise the legacy path runs exactly as before. The two generations are never joined.
v2 payloads normalize into the shared message/part shape, so tokens, cost, model refs, tools, bash commands, user attribution, the parent_id child-session walk and the session-level rollup fallback behave as they did on 1.x.
Compaction rows carry their own cost and tokens in 2.0.3 and were counted as assistant messages on 1.x, so they are counted here too; a running compaction has no usage and yields nothing.
Verified against a real 1.x database: the opencode report is unchanged at $109.91, 4,009 calls, 177 sessions.
Fixes#1293.
The same model billed through a different door is its own row on every surface.
The route rides on the call as a field: the model id names it when the door renames the model (Bedrock's <vendor>.<model>[-vN:M], with the cross-region profile kept as its own SKU), and the provider's own column names it when it does not (Hermes billing_provider). Only bedrock is registered; openrouter and bedrock-mantle wait for real sessions.
Every report keys a model row on one function, modelRowKey: short name plus the door's label. models-report folds on that row key rather than the route id, so two doors sharing one label can never print two rows a reader cannot tell apart. codeburn models --format json carries route per row.
Daily cache moves to v33: day.models is keyed by the row key so a column-sourced route survives into a finalized day, and hermes joins the pending-rederive table. Hermes sessions re-parse once.
Pricing never consults the route and runs on the raw id. granular-history still keys on the raw model id, so the desktop Trend timeline is the one surface outside this.
Verified on a real cache: 142 finalized days, $37,898.851498 and 272,985 calls unchanged.
* Extract Antigravity tool calls, bash commands, MCP tools, and skills from SQLite steps
Parse tool execution events from the SQLite steps table (step_type == 132) grouped by turn boundaries (step_type == 15), mapping run_command to bashCommands, call_mcp_tool to mcp__<server>__<tool>, view_file to skills, and invoke_subagent to subagentTypes. Bump CACHE_VERSION to 6 and add synthetic unit test coverage.
* feat(antigravity): index steps via gen_metadata packed varints and support repeated tools
- Decode packed varint stepIndices in gen_metadata Field 2 for direct step lookup, replacing heuristic turn grouping based on step_type 15.
- Read repeated Field 4 ToolCallMetadata entries per step and ignore send_message.
- Retain SQLITE_BUSY error propagation for WAL concurrency during active sessions.
- Bump session cache key to worktree-project-grouping-v6.
- Update docs and unit tests to reflect realistic step types and schema.
* fix(antigravity): drop unused step_type, simplify skill pattern, and loop all stepIndices chunks
Adds xAI's Grok Bot desktop app as a provider: per-bot sessions with estimated usage, and the weekly allowance read through the Cursor IDE login on the CLI, desktop Plans screen and menubar. No row unless the app is installed. Two independent review rounds on real data.