The daily-cache re-derivation test seeded v18, a version that only ever
existed as an unreleased draft of this change. Seed the shipped v17 so the
test models the 17 -> 19 upgrade path users actually hit, and rename it: the
bump re-derives every day for every provider, not just Grok, because the
daily cache has no per-provider invalidation. The Grok day stays as the
fixture since Grok is what the bump exists to correct.
The changelog entry now says outright that Grok totals change materially on
upgrade (150K -> 96.3M cache-read tokens on a 568-session corpus), that a
turn without a turn_completed record inside an otherwise-covered session is
dropped rather than estimated, and that the one-time daily re-derivation
reads the warm session cache and keeps the superseded file. The
context-bloat denominator fix moves to Fixed and names the providers it
corrects.
docs/providers/grok.md gets the same undercount warning in the token model
and a matching entry under Quirks.
The unpriced-models pointer told every user to run `codeburn model-alias`. A
subscription or flat-rate model is correctly $0, and mapping it onto another
model's per-token rate invents spend that was never billed (#968), so the hint
now states the condition instead of the instruction.
`sanitizeModelForDisplay` guarded only the `--unpriced` path, leaving every
other command and format rendering provider-supplied IDs unfiltered. It moves
to the `formatModel` closures in models-report and audit-report, the two sites
every renderer routes through, covering the raw-ID fallback as well. The
`--unpriced` override stays: it bypasses that path deliberately, because
`model-alias` keys on the raw ID, not the friendly name. README says so.
Below 45 columns of panel the dashboard warning dropped its marker and count
and printed a bare command, so a narrow terminal gave no signal that anything
was wrong. `! N: codeburn models --unpriced` is 31 characters and fits.
Only duplicate-reads has a structural reason to skip them: a subagent
starts on a fresh context, so re-reading what its parent read is a
necessary read, not a repeat. Reading node_modules or editing without
reading is the same waste whoever does it, and the CLAUDE.md rule both
findings suggest binds subagents too - filtering them there discarded
most of the evidence on a subagent-heavy corpus.
The prefix alone could not tell a claude.ai connector from a local MCP
server that happens to be named claude_ai_*, so the latter lost its
removal command and its apply plan. A namespace is now a connector only
when no readable local config claims that exact name; localMcpServerNames
supplies the set from the same files the remove plan edits (settings.json,
.mcp.json, and ~/.claude.json top-level plus per-project mcpServers).
A prefixed name that local config does own stays local: normal removal
command, apply plan, class fix, full savings attribution. Because the
transcript still cannot rule out a same-name connector, the finding adds a
manual note about it instead of asserting the server is one. Config that
cannot be read contributes no names, which leaves every prefixed namespace
on the conservative connector path.
The rewritten cost pass charges each session only for the schemas that
session loaded, which changes local-only estimates too (on a real corpus
the mcp-low-coverage estimate roughly halves). Pin it so the change is
deliberate rather than a side effect of the connector split.
A mixed local + claude.ai connector finding is class `fix`, but `--apply`
only mutates the local servers. classTotals now credits the `fix` group
with `applyTokensSaved` when present, so the "Fix now (apply-able)"
subtotal, the "apply-able: ~$X" headline and `summary.byClass.fix` (CLI,
TUI and desktop all read these) describe what apply can actually recover.
The finding keeps the whole opportunity in its own `tokensSaved`.
Also fixes the desktop connector fixture, which predated the class/basis
fields, and adds class-level coverage: connector-only findings resolve to
`nudge` (no apply payload), a local server named like a connector stays
manual-only, and local-only findings keep their full subtotal.
The menubar told System Events to make its login item, so macOS asked for
Automation access on first launch. SMAppService.mainApp does it in-process
with no Automation grant. No AppleScript fallback: a failure must not bring
the prompt back. Package floor is macOS 14, so the 13+ API needs no
availability guard.
Fixes#1026
Shares the mac release resolution behind a per-platform spec (tag prefix, asset name, error text), so the Windows path reuses the pinned-version URL, the release-API fallback scan, the retrying download and the sha256 verify unchanged. Windows then runs msiexec out of %SystemRoot%\System32 with /i /passive /norestart, treats 3010 and 1602 as non-failures, and launches the exe named by the product's Uninstall registry key.
The spend badge is a second tray icon carrying the number as its bitmap, which
only the Tauri tray backend provides; Linux runs its own SNI tray and has no
equivalent. The module was compiled there anyway, so every item in it - and the
two tray ids in lib.rs - tripped dead_code under clippy -D warnings on the
ubuntu leg.
- `mod tray_badge` and TRAY_ID / BADGE_TRAY_ID are now cfg(not(linux)), so the
code is absent on Linux rather than present and unused.
- `set_tray_badge` reports the badge as unsupported on Linux instead of
returning a success that never happened.
- The frontend hides the control wherever it is unsupported, behind
TRAY_BADGE_SUPPORTED in lib/platform.ts - one constant, three call sites
(the settings row, the footer menu item, and the effect that would otherwise
invoke the command).
- AppState's `linux_tray` field is dropped: it was written and never read, which
is the same lint one file over. init_tray_linux already owns the handle.
Checked rather than reasoned: `cargo clippy --all-targets -- -D warnings` passes
on macOS, and a scratch copy of the crate with the linux/macos cfg arms swapped
(so a normal macOS clippy selects everything a Linux build would keep, and drops
everything it would drop) also passes. Reinstating the ungated `mod tray_badge`
in that copy reproduces the exact CI failure, so the check is real. It stubs
tray_linux.rs, whose ksni and png deps do not build here - that file is still
only covered by CI's ubuntu leg.
The 5s default failed three unrelated real-I/O tests on the CI Node-22 runner
in two days (dashboard optimize scan, codex resume differential, OTel DB
prune) purely from runner load. Hung tests still fail at 30s.
README gains a Windows section next to the macOS and GNOME ones, a download
badge on the menubar card, and an honest note that the Tauri tray builds on
Linux but is unreleased there. docs/architecture.md picks up windows/ in the
surfaces diagram and gets a section covering the crate layout, the PATH and
System32 spawn rules, and the Claude quota parity.
`codeburn menubar` on Windows now points at the windows-v release page instead
of failing with "macOS only". The generalized installer from the source branch
is not brought over: it is 1182 commits behind this file and would drop the
proxy support, retry/backoff, checksum and bundle verification, and persistent
CLI path handling that landed since.
windows-menubar-ci.yml runs on windows/** changes: tsc, clippy with -D warnings,
and cargo test on both windows-latest and ubuntu-latest, plus a release-profile
`tauri build --no-bundle` on Windows. The Linux leg exists because most of the
crate's cfg(windows) code cannot be compiled anywhere else, so the reverse -
keeping the ksni paths and every shared helper clean off Windows - has to be
checked somewhere too.
release-menubar-windows.yml mirrors release-menubar.yml: a `windows-v*` tag (or
a manual dispatch) builds the MSI and publishes it, with a sha256, to a
"Windows Menubar vX" release.
The two release-desktop-*.yml files on the source branch are deliberately not
imported: their tag names and "Desktop" release titles would collide with the
Electron app in app/, which already owns `desktop-v*`.
Process spawning (Windows searches the current directory before PATH):
- reg.exe and cmd.exe are now spawned by absolute System32 path via
cli::system_command, which also carries the CREATE_NO_WINDOW flag the three
previous copies each set by hand. The tray badge re-ran `reg query` on every
refresh, so this was the most reliably reachable planted-binary path.
- "Connect Claude" resolves the claude binary itself instead of handing a bare
name to the console shell.
- CLI lookup ignores empty and relative PATH entries. `;;` or a trailing `;`
used to yield PathBuf::from("").join("codeburn.cmd"), a current-directory
lookup, at startup. The filter lives in one place (find_in_dirs) that every
search - codeburn, claude, and the Linux terminal probe - goes through.
- The Linux terminal path re-validates the whole command against the argument
allowlist before joining it into the string `bash -lc` parses; anything that
fails falls through to the argv-only detached spawn.
CLI version gate:
- MIN_CLI_VERSION moves from 0.7.0 to 0.9.9, the first release accepting
`status --format menubar-json --no-optimize` (every quiet refresh passes it)
and emitting all the payload fields the popover reads.
- The gate is probed on mount, before the first fetch. It previously only ran
when a fetch failed with the literal "CLI not found", so an old CLI produced
a payload whose missing history.daily threw and blanked the popover.
- A successful fetch no longer flips an incompatible CLI to compatible, and the
settings panel no longer probes on its own - App owns the verdict, so a
transient probe failure there cannot drop a working app onto the setup screen.
- The payload reads App makes are optional now, so a surprising payload lands on
an empty state rather than a blank window.
Refresh cadence, mirroring mac RefreshCadence.swift: 60s with optimize findings
while the popover is visible, 120s today/all without them while it is hidden,
and an immediate refresh on show when the visible key is stale. Every hide path
in lib.rs now goes through mark_hidden so the debounce stamp and the frontend
signal cannot drift. Previously it was 60s with optimize regardless of
visibility - about 2880 CLI spawns a day.
Claude quota (plan.rs) stops calling the token refresh endpoint. Claude's
refresh token is single-use and rotates, so spending it invalidated the token
Claude Code itself holds and broke the user's login. Parity with
ClaudeCredentialStore.refreshAfter401: re-read Claude's own store for a token it
has already rotated, and report a transient failure when there is not one yet.
Smaller:
- Snapshot writes refuse a symlinked target and are 0600 on unix, mirroring
mac SafeFile; no home directory now means no snapshots rather than a file
dropped in whatever directory the tray was launched from.
- The Windows config lock keeps its file handle open, so the stale sweep can
only ever unlink a lock whose owner is gone. The doc comment now says what the
lock actually buys (the CLI never takes it).
- "updated Xs ago" is only stamped by a fetch of the key on screen.
- External links go through tauri-plugin-opener instead of target=_blank.
- The tray badge font is loaded once into a OnceLock instead of read and parsed
on every render.
- autostart's shared import and constant are cfg-gated so clippy is clean on
every OS.
Adds unit tests for the PATH filter and the version gate.
Brings the Tauri 2.x tray popover from #1022 onto main as windows/, mirroring
mac/. Product name, bundle identifier, and version line up with the macOS
menubar (org.agentseal.codeburn-menubar, 0.9.20); the crate is renamed off
"desktop" so it no longer collides with the Electron app in app/.
Linux (ksni) stays compiled and dev-usable but is documented as experimental:
gnome/ is the shipping Linux surface.
The five src/ CLI commits on that branch are dropped - they re-implement a
daily-bucketing fix main already carries.
An SDK session's opening prompt and a subagent's task prompt are written by
a program: they repeat by design and have no home in CLAUDE.md. Both are
flagged on the entry, but a user entry over the parser's large-line
threshold comes back without its root flags - routine for generated prompts,
which are exactly the long ones - so the markers are read off the ends of
the raw line, where the fields sit either side of the oversized message.
Same section, compact: one line per still-applied fix with the verdict
glyph, and the undo command for the ones that measured nothing. The app
reads appliedFixes[] off the optimize JSON, tolerating its absence from
an older CLI.
Every still-applied journal entry now comes back with a verdict on the
next optimize run: worked (>=70% of its window-scaled estimate
realized), partial, no-effect (printed with its undo command), or
measuring while it is younger than the 3-day window. The verdicts come
off the rows act report already computes, so there is one
reconciliation, not two; the AppliedFix type and its formatter live in
act/types.ts so the optimize renderer can use them without importing
report.ts back into optimize.ts.
--auto-revert undoes the no-effect entries through the same code path as
codeburn act undo. It never touches partial or measuring entries, and
never a claude-md-rule - those land in whatever directory the user
happened to be in, the same reason --yes skips them.
--apply now names when the re-measure happens, and --format json carries
appliedFixes[] (add-only).
Groups sessions by their opening block (whitespace/ANSI-normalized, hashed
over the first 2 KB) and flags a block of at least 1.5 KB that opens five or
more sessions. Class nudge: CodeBurn will not move the user's own text into
CLAUDE.md, so the fix asks Claude to give the block a permanent home. Only
the repeats count as savings, sized from the block's bytes because provider
usage is per API call and cannot isolate the paste. The opener comes from
the session scan that already runs, so nothing extra is read.
Each class header now carries its own token/dollar subtotal and finding
count, so the apply-able slice is never mistaken for the whole board; the
headline savings line names that slice explicitly. CLI and TUI share one
classHeaderLine helper, the desktop app reads the same numbers from the
new summary.byClass in --format json (add-only; the three subtotals sum to
findingCount and potentialSavingsTokens).
Also scopes the SHELL_PROFILE_SCOPE comment to what is actually true: the
MCP deferral plans refuse to rewrite a shell profile, but bash-output-cap
appends its own marker block to one.