Native session worktrees accumulate under the purge-eligible temp root #967

Closed
opened 2026-08-07 02:34:19 +00:00 by coilyco-ops · 1 comment
Owner

What

git worktree list in the canonical agentic-os checkout reports 119 worktrees, 118 of them under the per-user temp root ($TMPDIR, which macOS resolves to /var/folders/<bucket>/<token>/T). Each native session registers one and nothing deregisters it.

Why it matters

That root is exactly what com.apple.bsd.dirhelper purges, by atime, on roughly a 3-day horizon. Git worktree registrations live in the canonical checkout's .git/worktrees/ and know nothing about that purge. When dirhelper removes a session directory, the admin entry survives as a stale reference.

Measured state today: git worktree list --porcelain reports 0 prunable entries, so nothing has actually been purged out from under git yet. The hazard is prospective, not realized. Filing it while it is still cheap.

Two costs, in order of likely bite:

  • Registration bloat - 118 live entries make git worktree list unusable as a human surface and add work to operations that walk them.
  • Stale entries after purge - once dirhelper does trim an old session, git worktree prune becomes necessary and, until run, branches stay pinned as checked-out and cannot be deleted or checked out elsewhere.

Landed in d5ed1f62, which points the native sessions root at a /tmp/aos symlink so session paths are readable. That change does not touch worktree lifecycle, and it deliberately keeps storage under the same purge-eligible root, so this issue is unaffected by it either way.

Possible directions

Not scoped, listing so the next session does not re-derive them:

  1. Prune on session start, so each launch cleans what the purge already took.
  2. Deregister a worktree when its session ends, making creation and teardown symmetric.
  3. Cap retained sessions, evicting oldest beyond N.
  4. Leave it and document that git worktree prune is periodic hygiene.

Option 2 is the real fix. Option 1 is the cheap backstop and composes with it.

## What `git worktree list` in the canonical agentic-os checkout reports **119 worktrees, 118 of them under the per-user temp root** (`$TMPDIR`, which macOS resolves to `/var/folders/<bucket>/<token>/T`). Each native session registers one and nothing deregisters it. ## Why it matters That root is exactly what `com.apple.bsd.dirhelper` purges, by atime, on roughly a 3-day horizon. Git worktree registrations live in the canonical checkout's `.git/worktrees/` and know nothing about that purge. When dirhelper removes a session directory, the admin entry survives as a stale reference. **Measured state today:** `git worktree list --porcelain` reports **0 prunable** entries, so nothing has actually been purged out from under git yet. The hazard is prospective, not realized. Filing it while it is still cheap. Two costs, in order of likely bite: * **Registration bloat** - 118 live entries make `git worktree list` unusable as a human surface and add work to operations that walk them. * **Stale entries after purge** - once dirhelper does trim an old session, `git worktree prune` becomes necessary and, until run, branches stay pinned as checked-out and cannot be deleted or checked out elsewhere. ## Related Landed in d5ed1f62, which points the native sessions root at a `/tmp/aos` symlink so session paths are readable. That change does **not** touch worktree lifecycle, and it deliberately keeps storage under the same purge-eligible root, so this issue is unaffected by it either way. ## Possible directions Not scoped, listing so the next session does not re-derive them: 1. Prune on session start, so each launch cleans what the purge already took. 2. Deregister a worktree when its session ends, making creation and teardown symmetric. 3. Cap retained sessions, evicting oldest beyond N. 4. Leave it and document that `git worktree prune` is periodic hygiene. Option 2 is the real fix. Option 1 is the cheap backstop and composes with it.
Author
Owner

Landed on main as c216536b.

What the measurement showed

Classifying all 113 native worktrees in the canonical checkout:

  • 100 - clean, branch tip reachable from origin - nothing to rescue.
  • 13 - clean but unpushed - real local-only work.
  • 0 - dirty.
  • 0 prunable, so no purge had bitten yet, as the issue said.

Lease state matched: 115 leases, 106 already stamped dead, 9 live. So the accumulation was not a missing reclaim path. cleanDeadNativeSessions already removed clean worktrees whose tip is reachable from origin, but only after the 24-hour dead-lease grace. With sessions launching faster than the grace expires, the steady state is roughly a day of sessions times nine resident repositories.

The fix

Option 2 in the issue (deregister at session end) is closed off by design - docs/native-agent-workspaces.md commits to no timer, daemon, or exit hook. So the reclaim stays at startup, and the change is to when the grace applies rather than to add a new trigger.

The second consecutive dead reading of a lease now confirms the first and releases the recoverable worktrees at that point rather than at grace expiry. The first reading still only stamps dead_since, so a transient ps failure that misreads a live session dead never costs a worktree - the next launch reads it live again and un-stamps it.

The grace keeps the job that actually needs a day: it holds the session root and its shadow home (harness transcripts stay resumable), and it holds any worktree that is dirty, unpushed, unreadable, or a *-workdir. Nothing that could hold unrecovered work reclaims faster than before.

Both costs the issue lists are addressed. Registrations now track unfinished work rather than session count, so the expected steady state is the 13-ish worktrees holding local commits plus the live sessions. And a worktree whose directory dirhelper purged is pruned one launch after its session ends instead of a day later.

Verification

  • ward aos-test green.
  • ward aos-lint green.
  • pre-commit run --all-files green.
  • TestLegacyDeadSessionIsCleanedAfterGrace rewritten as TestConfirmedDeadSessionReleasesRecoverableWorktree, asserting the worktree and branch go on the confirming pass while the lease and session root survive until the grace expires.
  • TestExpiredDeadSessionPreservesDirtyWorktree and TestExpiredDeadSessionPreservesUnpushedWorktree unchanged and still green, which is the guard that the eager path did not widen what gets removed.

The existing 100 registrations drain on their own once the released binary reaches the host, since every one of those leases is already stamped dead and will hit the confirming pass on the next launch.

Landed on `main` as c216536b. ## What the measurement showed Classifying all 113 native worktrees in the canonical checkout: * 100 - clean, branch tip reachable from `origin` - nothing to rescue. * 13 - clean but unpushed - real local-only work. * 0 - dirty. * 0 prunable, so no purge had bitten yet, as the issue said. Lease state matched: 115 leases, 106 already stamped dead, 9 live. So the accumulation was not a missing reclaim path. `cleanDeadNativeSessions` already removed clean worktrees whose tip is reachable from `origin`, but only after the 24-hour dead-lease grace. With sessions launching faster than the grace expires, the steady state is roughly a day of sessions times nine resident repositories. ## The fix Option 2 in the issue (deregister at session end) is closed off by design - `docs/native-agent-workspaces.md` commits to no timer, daemon, or exit hook. So the reclaim stays at startup, and the change is to when the grace applies rather than to add a new trigger. The **second** consecutive dead reading of a lease now confirms the first and releases the recoverable worktrees at that point rather than at grace expiry. The first reading still only stamps `dead_since`, so a transient `ps` failure that misreads a live session dead never costs a worktree - the next launch reads it live again and un-stamps it. The grace keeps the job that actually needs a day: it holds the session root and its shadow home (harness transcripts stay resumable), and it holds any worktree that is dirty, unpushed, unreadable, or a `*-workdir`. Nothing that could hold unrecovered work reclaims faster than before. Both costs the issue lists are addressed. Registrations now track unfinished work rather than session count, so the expected steady state is the 13-ish worktrees holding local commits plus the live sessions. And a worktree whose directory dirhelper purged is pruned one launch after its session ends instead of a day later. ## Verification * `ward aos-test` green. * `ward aos-lint` green. * `pre-commit run --all-files` green. * `TestLegacyDeadSessionIsCleanedAfterGrace` rewritten as `TestConfirmedDeadSessionReleasesRecoverableWorktree`, asserting the worktree and branch go on the confirming pass while the lease and session root survive until the grace expires. * `TestExpiredDeadSessionPreservesDirtyWorktree` and `TestExpiredDeadSessionPreservesUnpushedWorktree` unchanged and still green, which is the guard that the eager path did not widen what gets removed. The existing 100 registrations drain on their own once the released binary reaches the host, since every one of those leases is already stamped dead and will hit the confirming pass on the next launch.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agentic-os#967
No description provided.