* Store the session cache in day pieces with a per-provider index
Layout v10: a provider's files live in pieces by the UTC day of their
newest call, each with a key file, and an index row per file (piece,
fingerprint, call span, v9 month) read on every load. A ranged load reads
the index, the key files and only the members it reports on; a save
rewrites only the pieces it touched. Rows keep v9's full-load order and
month scope, so orphans, the first-paint snapshot and dedup see what they
saw on v9. The v9 month shards re-lay out once, streaming, and are removed
after v10 publishes.
* Skip cached transcripts outside a narrow query's months when unchanged
v9 could not see them, so every narrow query re-parsed each older Claude
transcript it discovered and republished the cache. The index knows their
fingerprint; an unchanged one needs nothing, a changed one still re-parses
as if uncached.
* Read piece members in batches through one buffer, and full loads concurrently
* Write piece key files without a sync; rebuild lost keys from the piece
* Changelog: day pieces
* Seed dedup with the keys of unchanged transcripts outside the query's months before a whole-file parse
* Upgrade-path check: verify the v10 envelope, indexes, pieces and key files
* fix(menubar): always emit avgCostPerSession for older menubar apps
Menubar 0.9.14-0.9.24 decode topProjects[].avgCostPerSession as required,
so omitting it for partial session counts broke Today with any 0.9.25 CLI.
Emit the 0.9.24 value again and keep sessionCountBasis so 0.9.25+ hides
a partial average. Add a test that checks the payload against the
required keys of every released menubar app.
* fix(menubar): restart the app after an in-app update
The app runs `codeburn menubar --force` as its child, and pgrep/pkill
skip their own ancestors, so the running app was never stopped and
`open` re-activated the old copy. Find the app with pgrep -a; when it is
an ancestor, swap the bundle while it runs, exit, and let a detached
helper stop it and open the new bundle. The terminal path stops the app
first as before.
Reported in #1541, thanks @AleksandrLiadov.
* Write session shards one member per line
A shard is still one JSON object, so older builds read it unchanged, but it
is written through a stream and never exists as one string. Past V8's string
limit the save used to throw and the cache silently never persisted; reads of
such a shard now go member by member.
* Hold cached files a ranged query cannot report on as stubs
A ranged load streams its in-scope shards and keeps in full only the files
with a call in the range, spawn anchors, failure markers, turn-less files and
WSL paths. Every other file becomes a stub: its fingerprint, bucket, PR flag,
dedup keys per turn and where its member line lives. A stub stands in for its
entry everywhere but the summaries it cannot contribute to: reconcile, the
seenMsgIds and parser dedup pre-seeds, the query-time key replay in serve
order, orphan surfacing, eviction and the emptiness checks. A stub whose file
changed loads its member first, so the append or re-parse runs as before, and
a wider request in the same process loads the stubs it now needs.
A bucket holding stubs is rewritten as a full load would write it, copying
the stubs' lines from the shard they came from; unchanged, the published
shard is kept. A single-line shard from an older build is rewritten on the
next save. Providers whose parse reads cached entries no report shows
(hermes, quickdesk, antigravity, devin, gemini) still load in full.
* Discover codex projects from a small index
Discovery only needs each rollout's project, but it loaded the whole codex
results file (hundreds of MB on a large corpus) on every run to get it.
flushCodexCache now also writes codex-projects.v15.json, keyed by the same
fingerprint, and discovery reads that. A missing index is built once from the
results file.
* Load the session cache once for status today and month
status asks for today, then the month. A command can now declare the union of
its ranges, so the first load already holds the month in full and the second
request finds it there; each request keeps its own month scope and summaries.
* Release the pre-lock cache view before reloading
Once the refresh lock is acquired or another process completed the refresh,
the snapshot loaded before the wait is never used again, but it stayed
referenced while the reload read a second view.
* Test the first-paint snapshot serves stubs in load order
* Parse a shard whole when the range covers all its months
A range that spans a shard's months needs nearly every member, and streaming
it one member at a time only added allocation churn (about 200MB more RSS on
report -p all over a 1GB cache). The streaming reader also reuses one read
buffer instead of allocating a fresh one per chunk.
* Parse a shard whole once the range reaches its oldest month
The previous rule also required the range to reach past the shard's newest
month, which a range ending now never does, so report -p all still streamed
every shard.
* Carry a shard line that spans reads as decoded text
Members of a megabyte and more span the 1MB reads, and joining their pieces
as Buffers allocated off-heap memory that only a GC returns: report -p all
over a 1GB cache peaked about 170MB above main in RSS. Decoding the pieces
with a StringDecoder keeps them on the heap and brings every streamed range
below main, so the whole-file parse for wide ranges goes again.
* Changelog: bounded session-cache load
* fix: four cost accuracy bugs in export and pricing
- export daily rows bucket each call on its own day (same rule as the day
aggregator) and no longer skip a turn without a timestamp
- cache-write tokens on a non-Anthropic model with no published write rate
bill at the input rate instead of a fabricated 1.25x (calculateCost and
audit); daily cache v35 and pricing schema 4 so stored figures re-derive
- OpenCode/Kilo messages with output but no usage, and Copilot transcript
calls with no token count, are marked estimated
- export records keep unrounded cost and gain estimated/unpriced columns
* fix: plain-decimal CSV costs, re-parse cached copilot transcripts for the estimated flag
* ci(upgrade-path): compare copilot records at cent precision across the export rounding change
* 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
* fix(quota): recover the Claude credential from the source it came from
Both Claude quota providers resolve the OAuth credential from
~/.claude/.credentials.json and fall back to the macOS Keychain, which is
where Claude Code keeps it on a normal Mac. Every recovery re-read went to
the file whatever the credential had come from, so for a Keychain user it
read nothing and the one place a renewed token can appear was never
consulted: a lapsed login left the retry a dead end indefinitely.
A recovery now re-reads whichever store the credential came from. When it
cannot recover - the credential is unchanged and its expiresAt has passed -
the provider reports terminalFailure with a line telling the user to run
Claude Code once, rather than the transientFailure the desktop app's quota
cards hold the last connected numbers through. A 401 on a credential still
within its life keeps its transient handling and its backoff.
The desktop app carries its own copy of this provider, where the same
file-only re-read sat in two branches - the 401 and the one that re-reads a
credential within five minutes of expiry - and both are fixed.
* fix(quota): keep keychain reads on forced refresh and hold reconnect prompt
Drop the near-expiry keychain re-read from the desktop provider; a keychain
credential is re-read only after a 401, which only a forced refresh reaches.
Hold terminalFailure through keychainUnchecked background polls.
---------
Co-authored-by: review-bot <resham0209@gmail.com>
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.
* fix: open menu bar Settings centered on the active screen
The window was positioned at a 520x380 placeholder while SwiftUI's own
minimum is 880x620, so the grown window kept its top-left corner and ran
off the bottom-right edge. Size the window from the view's minimum, place
it with a tested pure function, and honour a saved frame only while it
still fits on one of today's screens.
* fix: say where the telemetry toggle is managed when the desktop app owns it
A desktop telemetry.v1.json with enabled:false locks the menu bar toggle
off, so clicking it does nothing. Name the desktop app's Privacy & data
pane in the caption, and pin the four desktop/veto combinations plus the
standalone case in tests.
* fix(desktop): export the provider id, and never swallow a rejected setter
The export pane built its provider list from `current.providers`, which is
keyed on the lowercased display name. Five of the seventeen providers on a
real corpus ("cursor agent", "grok build", "kimi code", "hermes agent",
"grok bot") are not valid ids, so vProvider rejected them as bad-args.
`exportNow` had a try/finally and no catch, so the rejection produced no
toast and no file: the button did nothing. Use detectedProviders, which
already resolves ids from providerDetails, and toast on rejection.
The telemetry consent toggle swallowed its rejection the same way.
* fix(desktop): warm only the selected period, and only while visible
The post-paint sweep warmed all six standard periods x seven reports plus
the provider overviews: up to 48 serve-routed queries, ~18 CPU-seconds a
minute for five minutes and 2.79 GB peak RSS on a real corpus, and it ran
with the window hidden. It now warms the selected period's reports (its
own overview is already under that memo key from the visible poll) and
the other providers' headlines for that same period, holding before every
request while a user fetch is in flight or the window is hidden. Other
periods take the ordinary on-demand path.
Also corrects two comments that claimed background priority lets a click
jump ahead: spawnCli takes the serve fast path before priority is read,
and the resident child answers FIFO.
* fix(desktop): answer the click, report the write, tell the truth about the Claude login
Nine bridge setters in Settings attached only .then(), so a rejected
envelope produced no toast and the control read as if it had ignored the
click. One shared helper toasts the rejection, per pane fallback wording.
Telemetry save() swallowed write errors while setEnabled returned the
in-memory status, so the UI could show ON while the file the macOS menu
bar app reads still said OFF. The setters now carry persisted:boolean and
the renderer toasts when it is false; the write goes through the existing
atomic writeFileAtomic (temp file, rename) so a crash cannot tear it.
The Claude card called a logged-in Claude Code 2.x user "disconnected"
forever, because the credential lives only in the keychain and the first
poll never reads it. A new darwin-only keychainUnchecked state says we
have not looked yet and offers the forced read, keeping the login
instructions for the genuinely-logged-out case. ConnectAffordance's
toggle is renamed to what it does: it discloses instructions.
* fix: retry tax is the cost of retried edit turns, not a multiple of spend
* stop the change guard watching a Warp path it can never stat
* apply a menu bar language change in place instead of relaunching
* keep honoring WARP_DB_PATH in the change guard
* restart a menu bar too old to watch AppleLanguages
* one rebuild per language change, and no global bundle in tests
* fix(desktop): a forced quota refresh must not join the poll it just aborted
Check now / Refresh / ⌘R invalidated the quota service and then handed the
caller the background fetch it had just aborted, so every provider failed
its generation check and resolved disconnected: the click repainted every
card as logged out without ever reading the keychain, and Claude showed
"Not connected. Log in with the Claude CLI." A call now joins an in-flight
run only when that run is at least as privileged, so a keychain-allowed
call starts its own; a second forced click still joins the first, which
keeps the single macOS prompt. A superseded run hands its caller the run
that replaced it instead of its own abort artefacts.
retainOnFailure also holds accessDenied through a keychainUnchecked poll,
so the "Allow in keychain" guidance is not replaced by "Check now" one
cycle after the user denied the prompt.
* fix(desktop): stop the sweep churning, and never spend a query on an abandoned one
The prefetch effect depended on the detected-provider entries, whose costs
move on every overview poll, so it was torn down and restarted each cadence
tick: a warm slower than the interval was discarded and re-issued forever.
It now depends on the provider ids alone, and a finished warm is kept for
its own period/provider key even if the effect was torn down meanwhile —
only a config-generation change makes a result wrong.
Both loops re-test cancellation and the memo after the hold, which has no
bound, so an abandoned sweep issues nothing and a section the user visited
meanwhile is not parsed again. The hidden half of the hold waits on
visibilitychange instead of waking the renderer every two seconds for a
window nobody is looking at.
* fix(desktop): never offer a provider id the CLI will reject, and keep the keychain note
detectedProviders applied the round-trip filter only to the legacy
providers map; a providerDetails id with a space reached the picker and the
export as a --provider value the main process rejects.
The Plans settings row showed the "not read your login yet" line without
the note that macOS may ask once for keychain access, which the Plans card
does show.
* 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): configure the menu bar and Capacity Dock as a Plugins card
Windows now configures its tray companion from a card on the Plugins page, the
same place macOS does, instead of two switches in the sidebar corner.
- WindowsCompanionCard mirrors the macOS MenuBarCard: Install/Reinstall,
Update, Open (show the tray popover), Settings (opens the desktop Menu bar
settings pane), Quit (stop without uninstalling), Uninstall, plus the
Capacity Dock toggle, a Running dot, the version, and an info popup.
- MenubarCompanion.status() now reports installed/running/version/outdated/
canInstall, and new discrete install/open/quit/uninstall actions back the
card. uninstall only forgets the tray once the CLI removal succeeds, and a
cancelled elevation prompt is treated as a no-op rather than a failure.
- isNewerVersion sorts a dev build below any release, so a staged release still
lights Update and a dev build never does.
- codeburn menubar --uninstall reads the product code from the install marker
or the uninstall registry and runs msiexec /x.
- Remove the bespoke sidebar CompanionSwitches and their dead CSS.
- A dev-only CODEBURN_MENUBAR_RESOURCES override lets the tray install run
without packaging; the CLI still validates the staged path in full.
* feat(desktop): fold the Capacity Dock settings into the Menu bar pane
One tray rail entry instead of two: MenuBarPane and CapacityDockPane render
together in the Menu bar pane with a clear gap between the two cards, so the
card's Settings button opens both. Drops the now-unreachable 'dock' pane value.
* fix(desktop): stop the Spend cost-flow Sankey from re-rendering forever
The label-fit effect measured the already-truncated text, so a capped label
read as short, un-capped, overflowed the cap, and re-capped without end
(Maximum update depth exceeded). It now estimates each label from its full
name via data-name, and caps only tighten per layout, so the measure, cap and
remeasure cycle is guaranteed to settle instead of oscillating.
* feat(desktop): bundle Inter and JetBrains Mono for one UI on every OS
The app used each platform's system font (SF Pro on macOS, Segoe UI on
Windows), so the two looked different. Bundle Inter for text and JetBrains
Mono for figures and lead the --sans and --mono stacks with them, keeping the
per-locale CJK fallbacks, so Windows and macOS render the same faces.
* desktop: gate tray installed state on support; drop strings and channels the sidebar switches left behind
* 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>
* chore(pricing): refresh the bundled snapshot with long-context tiers
Regenerated from live LiteLLM/models.dev/OpenRouter: 5,938 primary
entries (1,210 added, 11 removed, 298 repriced vs the 0.9.24 bundle)
and 51 fallback entries; 394 primary entries carry a tier slot. The
Grok Build family upstream is what makes grok-4.6-build priceable,
shifting the Grok parser's authoritative-model pick to the real
modelUsage id (pin updated with the reasoning in the test).
* chore(pricing): refresh the bundled snapshot; a refresh can no longer drop a model's pricing
Regenerated from live LiteLLM/models.dev/OpenRouter: 5,938 primary
entries (1,210 added, 298 repriced vs the 0.9.24 bundle), fallback 51
from the gap-fill plus 9 carried forward. Upstream actually removed
four entries (friendliai llama-3.1 bare and prefixed); seven more are
excluded by the per-token-output filter, which is a rule change and is
called out as one.
Two bundler rules from review:
- vendor-prefixed aliases no longer displace a richer entry for the
same model: completeness (published cache slots) wins over
first-write, so the new nebius/MiniMaxAI/MiniMax-M3 without
cache-read rates cannot replace the publisher's entry that carries
them;
- previously-priced fallback entries carry forward when neither the
new primary nor the gap-fill covers their key exactly or
vendor-prefixed - the form the resolution pipeline queries. Nine
models would otherwise have lost all pricing; a dated variant
sharing the bare name does not count as coverage because it does
not answer the undated query.
The Grok Build family upstream makes grok-4.6-build priceable, so the
Grok parser's authoritative-model pick prefers the real modelUsage id
(pin updated with the reasoning in place).
* fix(pricing): exact-key coverage carry, slot-fill-only completeness guard, regenerated bundle (#1483)
- coveredByKey is exact-key only: the resolver never reaches vendor/<id> for
a bare query, so the old endsWith clause dropped 96 priced fallback ids.
- Pass 2 stripped writes are strictly slot-filling: a richer upstream row may
only fill missing cache slots when input/output rates are identical, never
re-price a filled slot (was re-pricing 43 input/output + 34 cache rates).
- Carry now rescues dropped primary rows as well as fallback entries (twelve
ids this cycle), so a refresh cannot leave a previously-priced model null.
- Regenerated: 5,942 primary + 212 fallback; 159 carried; zero coverage
regression and zero guard-caused rate changes vs upstream/main, verified by
resolving every id from either bundle through the real getModelCosts.
* test(models): glm-5.3 prices at its own row after the refresh (#1483)
Upstream LiteLLM now publishes a distinct z-ai/glm-5.3 rate, so the
prefixed spelling no longer equals the glm-5p2 sibling. Assert against
the snapshot's own row (the gpt-5.6-codex pattern) so the test survives
later upstream reprices and the z-ai/ -> zai/ namespace rename.
* quota: Claude plan label prefers the subscription type
* desktop: Claude plan label prefers the subscription type
* menubar: Claude plan label prefers the subscription type
* mac: Claude plan label prefers the subscription type
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.
Sessions read as abandoned when no commit credited to them reached main, but squash merges land work under a new SHA and late merges fall outside the session window, so shipped work looked dead. When a session's turns record the git branch, yield now rescues an abandoned or ambiguous session to productive only when the branch has its own commits, its tip tree matches a commit on main, and at least one of those commits was authored inside the session's window. Ancestry is not used, so a parked ref with no in-window commit is never rescued, and reverted keeps its verdict. Confined to computeYield; the JSON methodology stays timestamp-window. Adds tests including the parked-ref trap and the per-session contribution negative.
The codeburn models default table dropped every row under its $0.01 floor with nothing saying so, hiding usage that prices at $0 for lack of pricing data. The default table keeps its floor and its numbers but prints one line naming what the floor hid: N more models price at $0 (M tok), with codeburn models --unpriced to see them. An explicit --min-cost stays silent; json, csv and markdown are unchanged. The macOS popover, Windows tray and web dashboard render the existing unpricedModels block as one secondary line. Verified: machine formats byte-identical, the hidden count matches --unpriced, totals unchanged.
The Warp Full Disk Access probe ran synchronously on the serve request thread, so a denial froze the whole serve for about six seconds every sixty seconds; the recurring re-probe now returns the last known verdict instantly and refreshes off the request path. A probe child that cannot start on a Group Container path now fails closed, skipping Warp with the grant-access notice, instead of opening the file and re-wedging. The desktop Grok Bot quota reader opened Cursor's database with no busy timeout, so a Cursor write-lock could hang it; it now uses the same busy timeout the CLI uses and waits out the lock. The FIFO-based preflight test is skipped on Windows, where named pipes are not available.
Antigravity now stores the command basename instead of the full command line, so secrets in flags no longer reach the on-disk session cache, matching every other provider. A Copilot OpenTelemetry skill or tool is credited once per trace instead of once per chat span, so a multi-span trace no longer multiplies it in the skill and tool breakdowns; grand totals are unchanged, and the copilot parse version and the status snapshot version are bumped so warm caches re-derive. The daily-cache carried-remainder row now also carries a token shortfall, so per-model token sums reconcile to the day total where cost and calls already did. Bedrock provisioned-model and custom-model ARNs are recognised as unpriced Bedrock rather than hidden as local inference. Verified against the real cache: report, models and menubar totals identical to the cent.
Fresh installs now default to the light theme; an explicit theme choice is still honored. Type and spacing move one step up (larger body, titles, sidebar rows and page padding), sidebar shortcut chips move to hover tooltips, and primary text gets full contrast in both themes.
The Plugins page shows the Menu bar and Teams plugins as two cards side by side with embossed background art. The Menu bar card carries the Capacity Dock switch and Settings, Quit and Uninstall as icon buttons with tooltips, an info modal describing the menu bar and the Capacity Dock with the real supported-provider list, and it can install, update, open, quit and uninstall the menu bar app, versioned so an older one shows Update, with a Settings action that opens the menu bar app's own settings. The Teams card shows what is planned in an info modal and a Register for beta testing link.
The Plans page is redesigned as provider cards in a responsive grid, each with its own logo embossed. Connection status is now stable: a single empty or slow poll no longer flips a connected provider to disconnected, a connected provider keeps its quota bars through a transient refresh, and a stuck waiting resolves to an actionable state after twenty seconds. A login-expired error offers Connect, and Gemini's expired login is now a terminal actionable state instead of an endless wait, matching the menu bar.
CLI report, models and menubar-json numbers are identical to the cent before and after; only the Gemini quota error string changes.
When an OpenCode database has the 2.x session_v2 tables and the legacy session/message/part tables, only the v2 rows were read, so sessions that never migrated during a 1.x to 2.x upgrade were silently dropped and lifetime totals fell. Discovery now also lists legacy sessions whose id is not present in session_v2, and each such session is read from the legacy tables. A session present in both is counted once and resolves to v2. Legacy-only and v2-only databases are unchanged, verified byte-identical on a real database. No cache version bump is needed.
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.
Copilot calls parsed from VS Code chatSessions and the OTel agent-traces store
never carried the structured skill name, so the skills breakdown stayed empty
for every period while the Skill tool itself showed up under tools.
Pull the skill name out of the tool call arguments on both paths, and bump the
Copilot parse version and the status snapshot render version so warm caches
re-derive once instead of serving the old empty breakdown.
Costs, token counts and daily totals are unchanged for every provider.
macOS gates ~/Library/Group Containers behind Full Disk Access, and an open() there never returns when the process has no consent and cannot prompt, so codeburn status and the resident serve child both hung with no output. Any database under that prefix is now opened first in a killable child with a three second timeout; on a timeout or refusal it is skipped with one warning naming Full Disk Access, warp is reported as failed discovery rather than empty, serve skips fs.watch on the same path, and the result is cached for a minute so access granted later is picked up. A probe spawn failure is never treated as blocked.
The test wrote 2,000 session files of 30 KB per run to keep the child holding the hydration lock, but the lock window depends on bytes while setup and teardown depend on file count, so on Windows CI the fixture alone pushed it past 30 s. The same 60 MB now lives in 100 files with the identical lock window. The readiness gate also waits for the lock to still be held after a 150 ms settle, since early index phases blink it for a few milliseconds. Test only, no product change.
Hovering or focusing a dollar amount shows the input, output, cache read and cache write tokens behind it, plus calls, in a popover portaled to the body. Amounts with no token counts behind them, such as projected month, percentages, retry tax, savings and the Spend and Plans tables, are left alone rather than shown a fabricated breakdown. periodTotals carries the four counts so the hero keeps its breakdown when the number comes from the generation window, and weekly bars sum them. No typography or layout change.
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.
The JSON report built its model rows from surviving sessions while the headline
came from the durable daily cache, so a long period's rows summed to less than
the headline. Rows now come from the same days as the headline and the daily
rows, keyed through modelRowKey. Days recorded before per-model tracking keep
their calls and cost under one explicit "Unknown (carried)" row, credited per
provider slice and per day, idempotently, remainders under half a cent ignored.
A guard test runs all 42 providers proving none writes a day without its model map.
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 period model list the Overview table reads is no longer cut at 20 rows, so
every model with usage in the period shows up instead of only the costliest
twenty - the rows that dropped out were exactly the $0 local and free models.
formatTokens gains a billions rung, so a codeburn models totals row reads 29.9B
instead of 29868.7M; the menubar row formatter gains the same rung, and compare
now reuses formatTokens instead of its own K/M copy.
The desktop Models tables stop dimming the calls column on unpriced rows. In the
shipped app those rows do not reach the table yet - codeburn models defaults to
minCost 0.01 and getModels passes no override - so the change takes effect once
that filter is addressed, tracked separately.
Also re-lands the three #1315 commits that never reached main: per-model token
counts beside per-model cost in the Overview and the macOS menubar Models rows.
Status snapshot revision goes to 9 so a warm snapshot cannot keep serving the
truncated list. Fixes#1318.
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.
Bedrock foundation-model ids end in a -[v]<major>:<minor> version. looksLikeLocalModel read that colon as an Ollama :tag, so an unpriced Bedrock model was classed as free local inference: excluded from models --unpriced, from the unpriced warning and from the pricing-coverage denominator, with metered spend showing $0. The version shape is now exempted before the tag rule. Covers ids without the v (openai.gpt-oss-120b-1:0, cohere.rerank-v3-5:0); Ollama and LM Studio tags are unaffected.
Every period headline now comes from one aggregation, so switching period no longer mixes snapshots taken minutes apart and Lifetime can no longer read below the six months inside it.
Days preserved from expired transcripts count toward the totals, and the usage streak is one machine-wide number taken from the hydrated daily cache.
A provider-filtered hero reads the scoped payload instead of the unscoped one.
Anchored menus and popovers render through a portal, so the card anatomy no longer clips them after three rows.
The Spend by-branch picker groups checkouts by git origin, one entry per repository with the checkout count beside it.
On the performance side, serve invalidation is scoped to the day a changed file can hold, -shm writes are ignored, and the optimize detectors keep one range-independent scan record with ordered version events.
Memo budgets are set by retained bytes, and every serve response carries the generation and timestamp it was built from.
A 30D stay with appends drops from 7253ms to 2387ms and peak RSS from 2904MB to 1819MB.
No cache format changed. Payloads stay byte-identical except for periodTotals, streak and liveSessions.
* 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.
* fix(codex): keep token_count events in sessions that never write total_token_usage
* fix(codex): preserve distinct requests when cumulative usage is missing
* fix(codex): narrow missing-cumulative fix - byte-identical re-emissions still collapse (#257)
Real-data answer to the review's question: Codex does re-emit byte-identical
token_count events. Scanned 53 public rollout sessions (codeset-ai/
codeset-release-evals, 1313 token_count events): 603 (46%) are byte-identical
repeats of their predecessor (identical total AND last usage), 0 cumulative
monotonicity violations, 54 events carry no total_token_usage at all.
The missing-cumulative path now collapses a record whose info payload is
byte-identical to the previous event's (#257 semantics restored); only
records with differing payloads count as distinct requests. prevInfoIdentity
is persisted in the resume state. The #1088 invariant note is restored at
the dedupKey in adapted form; docs/providers/codex.md documents the
three-layer dedup and the accepted fork-cutoff trade-off.
* fix(codex): revert cache/parse version bumps, correct null-info docs per review
Re-verified on a fresh spread sample of codeset-release-evals (21 sessions,
404 events): exactly one info:null rate-limit ping per session, zero events
with info present but total_token_usage absent — matching the reviewer's
137k-event corpus. The missing-cumulative branch is a no-op on real data:
- CODEX_CACHE_VERSION stays 15 (no cold reparse for identical numbers; v16
never shipped in a release)
- drop the -missing-cumulative-v1 parse suffix
- Quirks: null-info pings take the estimate path, not the dedup path; the
earlier 54-of-1313 figure conflated them with partial-info events
- dedup layer 3 now documents the forkedFromId/#1383 sub-agent caveat
---------
Co-authored-by: Roy tong <tongxiarui@elliwooddeMacBook-Air.local>
* Add per-day cost for both sides to the compare-periods report
* Rebuild the Compare periods screen around a lead sentence, day bars and top movers
* Update the Compare periods doc for the new layout
* Extract the duplicated chart tooltip into one ChartTip component
* Make the Compare periods sentence, tiles, day chart and movers table say what they measure
* Cover the compare lead sentence rules and the percent, date and path formats
* Update the Compare periods doc for the new day chart
* Style the Compare periods Swap button as an outlined pill instead of a solid grey block
* Fix the round-2 findings on the Compare periods chart, range label, Swap button and duration
* Fix single-day range label doubling and lone day bar width in Compare periods
rangeLabel now short-circuits from === to like terseRangeLabel already did.
pcmp-bar uses flex 0 1 50% so a lone bar in a slot takes half the slot
instead of the full slot, matching paired bars.
* Keep a lone day bar the same width as a paired one
* fix(models): price cliproxy-gateway routed ids as the models they wrap
codex-cliproxy-gateway forwards only cliproxy/* ids to a local CLIProxyAPI,
so routed Codex sessions record cliproxy/<id> model names, sometimes with a
CLIProxyAPI provider path behind the wrapper (cliproxy/zcode/glm-5.3-flash).
None of those match a pricing row, so the tokens were counted but the dollars
were not, and the models table hides zero-cost rows — routed spend looked
absent rather than unpriced.
Peel the cliproxy/ wrapper like the other routing wrappers and treat zcode
as a known vendor namespace so the provider path strips to its priced glm-*
leaf. An unknown vendor behind the wrapper still fails closed.
* Fix cliproxy-gateway README link and correct bare-glm pricing comment
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
* test: wait for the drawer close callback and give the TUI frame wait a time budget
* test(dashboard): await the indexing settle callback instead of polling frames
current.pullRequests survived both sanitizeForSharing and
redactProjectNames(include_project_names: false) verbatim, so each row's
full GitHub url and owner/repo#123 label named the repository to a paired
peer device and to a name-redacted MCP client — the same class of leak
Sharing drops the block like it drops the project, session and per-branch
rows. The MCP pass pseudonymizes url and label from the url (the
aggregation key) with the same salted, domain-separated hash as the other
pseudonyms, so rows referring to the same PR still line up and every
number stays untouched.
Closes#1346
A Capacity Dock row that vanishes while hovered fires no hover exit, so the
pushed pointing-hand cursor stuck system-wide; the row now pops it on the way
out. Claude Code writes a fresh sessions/<pid>.json on every resume, so several
files name one session and the first listed was taken; the newest is taken now,
and a newest that fails the recycled-pid guard leaves the row inert.
collectKimicodeInputs recursed the whole Kimi session store to find wire files,
listing ~18k entries for ~2k wires at ~250ms per payload build. It now walks
work dir, session and agent levels only: ~23ms, and a decoy wire.jsonl under
blobs/ or file-history/ no longer reports itself as a session.
Scan the Kimi Code stores for session dirs whose agents/*/wire.jsonl was
written inside the liveness window and fold them into the menubar
liveSessions block as provider kimicode, with the project from state.cwd,
the model from the last llm.request in the main wire tail, and sub-agent
wire mtimes as the session's sub-agent activity.