A second implementation of session-storage reclamation was drafted
uncommitted on the control root's dev checkout (SessionDataArchive).
This folds its distinct ideas into the committed implementation, which
stays the base (per-directory locking, partial-then-rename with the
directory-wins invariant, funnel-covered transparent restore, tests):
- deleteSession is now truly permanent: it removes sessions/<id>/, the
compressed <id>.tar.gz if the sweep packed it, and any local
Carbon-mirror copy — previously every deleted chat leaked its
transcript directory forever. Project deletion purges the same per
chat; deleteEphemeralSession drops its now-redundant own removal.
- The worktree-cleanup and storage sweeps source candidates from
database.loadAllSessions() instead of persistedSessionRecords, which
only holds active projects' sessions — an archived project's chats
could otherwise never be reclaimed (a permanent worktree leak).
- New opt-in retention policy (Settings ▸ Chats ▸ "Reclaim archived
chat data", Never by default): an archived, non-favorite chat past
the window is deleted outright on an Optimized-storage Mac, but on an
All-Copies Mac it is compressed in place through the same guarded
compression path — full storage never deletes. Off by default because
discarding chat data must be an explicit opt-in.
- The archiver verifies the fresh archive is listable (tar -tzf)
before the source directory is removed, so a truncated stream is
caught at compress time, not restore time.
Co-Authored-By: Claude Fable 5 <[email protected]>
At hundreds-to-thousands of sessions Nucleic's storage balloons into the
hundreds of gigabytes across worktrees and per-session storage. Two changes:
1. "Delete archived chat worktrees" now reclaims only chats whose work is
Done (a finished run, or a last turn classified/marked .completed) — a
chat archived mid-conversation keeps its checkout. Moved-away tombstones
remain reclaimable regardless: their work lives on another Mac, and the
transfer's own discard is best-effort. The Done predicate is the new
Session.isCompleted, shared with isChatDone so the definitions can't
drift, and the Settings ▸ Chats picker documents the narrowed behavior.
2. Idle-chat session-storage compression for Macs in Carbon's "All Copies"
role — the machines carrying the mesh's full storage burden. A new
SessionStorageArchiver packs sessions/<id>/ (transcript, render
sidecars, agent home) into a sibling <id>.tar.gz via the system tar
(~10x on this JSONL-heavy data), with a partial-then-rename protocol
and a "directory wins over any archive beside it" invariant so every
crash point degrades safely. A sweep on the auto-archive loop packs at
most 4 chats per pass, and only ones with no live controller, no
hydration in flight or queued, not open, not pinned, not transferring,
and idle past the threshold. Restore is transparent at every read
funnel: prepareSession (open/hydration/dashboard repair), the open-chat
preview streamer, and the mesh transcript server. The idle window is a
new Settings ▸ Carbon dropdown (default: after 7 days), resolved
against the storage role at launch and on change; Optimized-role Macs
and headless hosts stay off.
Co-Authored-By: Claude Fable 5 <[email protected]>
582cf9eaf clamped every host child (agent CLIs, builds, tests, probes) to
.utility to protect main-thread responsiveness. Measured on a loaded M2 Max
(load avg ~30, sibling session builds running), that clamp makes an 8-way
parallel child workload 3.2x slower (~1.0s -> ~3.1s) and single-threaded
commands ~1.6x slower on average (up to 2.5x), with the child at kernel
priority 20 vs 31 — the same mechanism that pushed every GitRunner call past
its 120s wall-clock timeout. Default-tier children still lose to the
user-interactive main thread under contention, so the clamp buys no
responsiveness; .userInitiated measured no better than default, so no
per-command priority plumbing is warranted.
ProcessStallMonitor needs no threshold change: even a maximally starved
utility child still received 86% of one core (43x the 0.02 resource-quiet
threshold), so slow-but-honest work was never at risk of a false
"Command Stalled" verdict — the clamp's only real cost was throughput.
Co-Authored-By: Claude Fable 5 <[email protected]>
Every git op the app ran (worktree add, ship's add -A, the reconcile
landed-check diff) was dying as GitError.timedOut(120s) in the 2026-08-03
canary — observed live: the app's own 'git add -A' SIGTERM/KILLed at
exactly the deadline while actively working. Three compounding causes:
1. 582cf9eaf clamped every GitRunner child to .utility QoS. On Apple
silicon that confines the child to efficiency cores and deprioritizes
its I/O, so under a session build/test storm honest multi-GB git work
slowed 20-40x and blew the wall-clock bound. GitRunner children are no
longer QoS-clamped (they're bounded and usually user-blocking; see the
comment in run) — ProcessHost's agent/build clamp is unchanged.
2. The 120s default timeout policed honest work, not just pathological
hangs. Killing an honest worktree checkout fails the whole flow it
serves; the default is now 600s, which still catches the wedged
hook/credential-helper case it exists for.
3. CarbonSnapshotter seeded a throwaway GIT_INDEX_FILE every tick, so
'add -A' re-read and re-hashed every tracked+untracked byte of the
worktree per tick (~5 GB on a real checkout) — a permanent kill/retry
storm that kept the disk saturated and dragged every other git op over
the deadline too. The scratch index is now persistent per session (in
the worktree's own git dir, swept of stale locks, dropped on archive),
so each tick re-hashes only what changed.
New test pins the persistent-index lifecycle: reuse across ticks, stale
index.lock sweep, porcelain untouched (§16), removal on archive.
Co-Authored-By: Claude Fable 5 <[email protected]>
A full SwiftPM build tree (.build-host/ — 4.82 GB, 41,678 files: debug
binaries, .a archives, xctest bundles, dependency clones' git packs) was
committed to dev by 6945303ca on 2026-08-03. Every worktree created from
dev since then checks it out, and every whole-tree git operation in those
worktrees (worktree add, add -A, status, Carbon's snapshot hashing) has to
stat/hash ~5 GB it never should have seen — the data-volume half of the
2026-08-03 'all git ops time out' failure.
Untracked here (disk copy left in place, now ignored); .gitignore grows a
.build-*/ rule so no ad-hoc --build-path tree can be committed again.
Co-Authored-By: Claude Fable 5 <[email protected]>