* feat(desktop): one-click updates through electron-updater on the update-feeds release
* feat(tray): signed one-click updates through tauri-plugin-updater; feed publishing and release docs
* ci(tray): build without the updater key until it is configured
* chore(windows): set the tray updater public key
* ci(windows): skip updater signing in the release build smoke
* ci(windows): build without updater artifacts when no signing key is available
* ci(windows): turn off updater artifacts through TAURI_CONFIG, not CLI flags pwsh mangles
* ci(windows): run tauri build under bash so --no-sign reaches the CLI (pwsh drops the -- separator)
npm redacts UUID-shaped path segments to `***` in `npm ls --parseable`, so matching each line against the checkout's absolute <root>/node_modules/ prefix found nothing on any path containing a UUID and packaging aborted with the misleading "is the root npm installed?".
The top-level package name is now read by segment position: after the N-th /node_modules segment, N counted off the real root path, which the redaction renames around but cannot move.
Closure verified identical: 160 packages, same names and versions, same 55M, staged from a UUID worktree and from a UUID-free checkout of main.
Reproduced before and after: main aborts from the UUID path, the branch stages 160 packages from that same path.
A non-zero `npm ls` exit still yields its stdout, so an extraneous package in node_modules does not abort staging.
A missing npm_execpath now fails with its own message instead of degrading into the empty-closure one.
Review patch on the branch: the two hand-rolled path scanners collapse to one split('/node_modules/'), and the npm attribution is corrected from 12 to 11 and later, reproduced on npm 11.19.0.
Fixes#1466.
writeDockEnabled still did its own read, mutate and atomic write of
windows-dock.json, outside the lock the rest of the module takes. It is
the Node side of the dock's enabled switch, reached from the desktop
app's Sidebar toggle, so a toggle there could still land between the
tray app's read and its rename and erase whatever it had just stored,
which in practice is the rail's placement. It now merges through
patchTrayFile, which takes the lock for the whole cycle; enabled is
already an accepted dock key, so nothing is dropped on the way.
Stale-lock recovery could hand out two owners at once. Both sides did
"if abandoned, unlink and retry", and nothing stood between the check
and the unlink: contender A could already have removed the stale lock
and created a fresh one of its own, and B's unlink then deleted A's
fresh lock, leaving both of them holding the file. Windows' share mode
hides this most of the time, because a lock a live holder still has open
will not unlink at all, but the same protocol runs on Linux and macOS,
where the unlink always succeeds. Removing an abandoned lock is now
itself arbitrated, by the same exclusive create the lock uses, on a
sibling <lock>.takeover file. Only its winner may unlink, and only after
re-checking staleness under that right, so a rival that reclaimed first
keeps the lock it now holds. A takeover file is dropped on every path
out, never held across the wait, and one left behind by a reclaimer that
died is freed by the same staleness rule as the lock itself.
The cross-process test asserted only each side's final value, so it
noticed a lost update only when the last writes happened to collide: on
a deliberately broken lock it passed about one run in three, which is
weak evidence for the property it exists to prove. Each side now writes
a counter that only rises, and a reader thread watches the file for the
whole run. A separate reader is the point: neither writer can see its
own counter rolled back, because in the unlocked cycle each carries the
other side's value forward itself, so what it reads back is never older
than what it last read. With the lock both counters only ever rise in
the file; without it one falls. It now fails on every unlocked run.
Also: a placement that cannot be saved is logged instead of discarded
silently, since losing the lock under contention is a new way for the
rail to revert at the next launch; and the Codex sameLogin comment now
states outright that a replacement document carrying none of the three
identifying fields reads as the same login and is written over, which is
the deliberate side to err on.
The desktop app and the tray app both read, merge and rename the same
preference files. The atomic rename stopped a torn file but not a lost
update: both read state A, each changed a different field, and the last
rename erased the other's change, so dock placement, enabled state,
provider choice or scale silently reverted.
One cross-process lock now covers the whole read-modify-write cycle,
designed once and implemented in both Rust and Node against the same
protocol: a sibling .lock file taken by exclusive create, carrying the
holder's pid and timestamp, taken over only when abandoned by a dead pid
or a stale mtime, and failing loudly rather than writing behind a live
holder. It follows the CLI's existing cache-refresh lock. All three
sites use it, including the dock's enabled, provider and placement
writes. A test drives real Node and Rust processes at the same file and
fails without the lock.
Tauri names the bundle after productName, so `tauri build` leaves
`CodeBurn Menubar_<version>_x64_en-US.msi` with a space. GitHub rewrites
that space to a dot on release upload, which is the form the CLI
installer, the Electron side and the tray updater all match. The staging
script only knew the dot form, so the installer workflow built the tray
and then failed with "no CodeBurn.Menubar_<version>_x64_en-US.msi".
It now accepts both and stages the dot form, so a local build and a
downloaded release produce the same shape.
stage-menubar.mjs is stage-cli.mjs for the Tauri tray app: it copies rather than
downloads, taking the local `cargo tauri build` output by default and whatever
CI fetched with --from. The two routes need different things staged, so it has
two shapes. The NSIS build gets the .msi and its .sha256, computing the digest
when a local build has none, so the installer's verification means the same
thing there as it does for a release. The Store build gets the executable, since
a packaged app cannot run msiexec at all.
build/menubar rides into both packages as win.extraResources. Unlike the CLI it
carries no node_modules, so electron-builder's production-dependency filter has
nothing to strip and the afterPack hook is not needed.
installer.nsh removes the tray app on uninstall only when the marker credits
this app with installing it, which is what keeps a manual `codeburn menubar`
install alive through a desktop uninstall. PowerShell reads the marker and pulls
the product code out of the uninstall string, because NSIS has no JSON and the
alternative was hand-parsing one in a macro.
appx-extensions.xml declares the tray app's launch at login as the package's own
windows.startupTask, so it appears in Settings > Apps > Startup like anything
else the Store installed. No Run value is written on that route.
Snap Store review (forum topic 52615) asked for both: the credential
file is sensitive and only powers the optional quota tile, so it moves
to its own claude-quota-credentials plug with no auto-connect; the
cline parser reads only ~/.cline/data/tasks/<id> transcripts, so the
grant names that directory instead of the whole data root.
The ported declaration was written against main as of c9e6e2ec. Main has since
added dsh and carries five entries that commit never saw, all of them still bare
tool roots, so the port dropped them rather than reintroduce what the store
rejected. Each is restored at the path its provider opens:
.dsh/sessions dsh.ts reads <DSH_HOME>/sessions only
.kiro/sessions CLI store at sessions/cli, v2 IDE store
at sessions/<hash> — siblings
.quickwork/{profiles.json,sessions,metrics}
profiles.json names the profile bases;
the legacy layout is sessions/sessions.db
plus metrics/
.config/Claude/local-agent-mode-sessions
Claude Desktop's local-agent-mode store
.config/Open Design/{runs,data/runs,namespaces}
the three discovery roots open-design.ts
probes under its data dir
.lingtai and .lingtai-tui stay dropped. A LingTai ledger lives at
.lingtai/<agent>/logs/token_ledger.jsonl, and personal-files has no wildcard for
the agent segment; .lingtai-tui only exists to enumerate project homes that could
not be read anyway. Goose is narrowed to .local/share/goose/sessions, which holds
the only file it opens.
Four entries stay whole roots because the provider reads a file sitting directly
in the root: .config/github-copilot (JetBrains stores nest under a variable
<ide>/<kind>/<storeId>) and .local/share/{opencode,crush,kilo}. The new
app/scripts/snap-grants.test.ts asserts every other entry is at least one level
below its tool root, so a bare root cannot come back unnoticed.
Reading, decoding and line-parsing a Claude session JSONL is per-file work that
touches nothing shared, so it moves onto worker_threads for a large cold parse.
parseClaudeFileFull() is the extracted unit both sides run; a worker runs it
against an empty dedup set and returns the result as a JSON string, and the
parent installs results in the order the serial loop would. Everything with
cross-file state stays on the main thread, and a file whose message ids were
already claimed (or whose worker failed) re-parses in-process, so the output is
identical to the serial path.
Thread count is decided per parse: never with <=2 cores, under 2 GB free memory,
fewer than 200 pending whole-file re-parses or under 200 MB behind them, so warm
and incremental runs spawn nothing. CODEBURN_PARSE_WORKERS overrides it. The pool
is terminated when the parse ends, so the resident serve child accumulates no
threads.
- build stamp ignores the pricing-snapshot files the build regenerates,
so a release checkout no longer shows a false -dirty
- splash shows just the flame, CodeBurn, and the version (dropped the
commit-hash line); the clean build id stays in About only
- About 'Check for updates' uses the in-app checker and reports inline
('You're on the latest' / 'Update available: X · Download') instead
of blindly opening the releases page
392/392.
The packaged app resolved whatever codeburn was on PATH — for real
users the published npm release, which predates every JSON surface the
app calls. The app now ships its own CLI copy under resources/cli
(staged production node_modules tree; tsup output is not
self-contained) and spawns it with Electron's own binary via
ELECTRON_RUN_AS_NODE. No install prerequisite remains.
- resolution order: CODEBURN_BIN, dev repo CLI, bundled, persisted
path, PATH search
- launch.js shim strips the extra argv element commander mis-slices
under packaged Electron-as-node (process.versions.electron is set),
which reproduced the 'too many arguments' class of error
- afterPack hook copies the staged tree (extraResources runs
node_modules through the production-dep filter and ships it empty);
lands before signing, signature verified intact
- packaging always restages from src via root build:cli (tsup only,
no network); ~12MB compressed per artifact
Verified on the built app: Electron-as-node CLI emits current JSON
(providerDetails, currency), and a minimal-PATH GUI launch spawns the
bundled CLI with zero external dependencies. 290/290.