Files
fusion/packages
gsxdsm 7fd1c7f124 P0 fix: stop reaping worktrees out from under live planners (FN-6756) (#2531)
User-reported: worktrees deleted while a planning agent was still
working in them. Small, isolated, ahead of all remaining capacity work.

## Mechanism

`clearPhantomExecutorBinding` is documented as *"the last line of
defense against pulling a worktree out from under a running agent"*. It
computed liveness from four sets — `activeSessions`,
`activeStepExecutors`, `activeWorkflowStepSessions`,
`activeCliTaskSessions` — **all TaskExecutor-owned**. A triage PLANNING
session is owned by `TriageProcessor`, lives in *its own*
`activeSessions` map, and registers in the module-level
`activeSessionRegistry`. It matched none of the four.

Worse: the method **writes** to that registry (unregistering the task’s
paths) but never **read** it as a liveness signal. It destroyed the very
evidence that proved the planner alive.

Under plan-in-place a card is specified while it sits in
`todo`/`triage`, and `reapLeakedConcurrencySlots` treats both as
reapable on a rationale written *before* planning moved there (“a task
waiting to run must not pin a worktree”). Every gate ahead of the last
one passes for a planner:

| Gate | Saves a planner? |
|---|---|
| in `listWorktreeHolders()`? | **No** — `ensureTaskWorktreeForPlanning`
→ `ensureGraphCustomNodeWorktree` → `addActiveWorktree`
(`executor.ts:8581`) |
| reapable column? | **No** — plan-in-place keeps the card in
`todo`/`triage` |
| in the executor’s `executing` set? | **No** — a planner is
triage-owned |
| 60 s `LEAKED_WORKTREE_SLOT_GRACE_MS` | **No** — keyed on
`columnMovedAt`, and planning routinely runs for minutes |

So the broken guard decided alone.

## This is FN-8600 recurring through a second sweep

That fix registered planning paths in the registry and taught the
**self-owned-branch reclaim** sweep to consult `isPathActive`. The
leaked-slot reaper never got the same signal — fixed at one surface, not
enumerated across all. Exactly what the AGENTS.md Surface Enumeration
rule exists to prevent.

## Fix

The refusal now also fires when
`activeSessionRegistry.pathsForTask(taskId)` is non-empty. Keyed on
**any** registered path rather than on kind: the point is that a
registered surface of any kind means someone is working in that
worktree.

## Enumeration — the part that stops a third recurrence

The guard is a **chokepoint**, so this covers every caller rather than
just the reported one:

- `reapLeakedConcurrencySlots` — the reported path
- `recoverPausedAbortFailures` — **had the identical executor-only
pre-gate**
- the `preserveWorktrees: true` reclaim

Audited the rest of self-healing’s liveness gates: the self-owned-branch
reclaim, worktree-metadata reconcile and PR-branch sweeps already
consult `isPathActive`/`lookupByPath`. The three that read only
`getExecutingTaskIds` — `checkStuckBudget`, `recoverCompletedTasks`,
`recoverStrandedCompletedTodoTasks` — move columns and never destroy a
worktree, so they are noted rather than changed.

## Trade-off, stated plainly

A leaked registry entry now blocks this sweep instead of a live planner
losing its worktree. That is the strictly safer failure and the one the
“last line of defense” wording already promises. The registry is
process-local and in-memory, so a leak cannot outlive the process, and
stale entries have their own reconciler. **A test pins that a genuine
phantom — no executor surface AND no registration — still clears**, so
this is not a blanket refusal that would trade this bug for a wedged
queue.

**The 60 s grace is deliberately unchanged.** Raising it would only make
the bug rarer and harder to reproduce; the liveness gate was the defect.

## Verification

Revert-proof, measured: removing the registry term turns **3 of the 4**
new tests red, including the end-to-end sweep case (card in `triage`,
past the grace, executor sets empty → asserts the slot is not reaped and
the worktree survives). The 4th stays green both ways *by design* — it
is the anti-overcorrection guard.

`pnpm lint` clean · engine `tsc` clean · `pnpm test:gate` green (309 +
10 + 71) · new suite 4/4. The 2 failures in `self-healing.test.ts` /
`-completion-fanout.test.ts` are **pre-existing** — identical with this
change stashed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented active planning worktrees from being mistakenly deleted or
reclaimed while related planning sessions are still active.
* Enhanced session liveness checks so phantom executor bindings are not
cleared when a live session is registered.
* Updated paused abort recovery to defer or abort safely when a live
planning session is detected, avoiding unintended task/worktree
mutations.
* **Tests**
* Added regression coverage for leaked-slot reaping, paused abort
recovery behavior, phantom binding refusal, and end-to-end sweep
outcomes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:04:51 -07:00
..
2026-07-26 18:11:47 -07:00