fix(engine): reap leaked fn-verify verification worktrees in the temp-dir sweep

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 15:14:30 -07:00
parent a20ddf6ed6
commit 1e50b71255
3 changed files with 42 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Leaked verification worktrees are now reaped, so planning no longer queues behind exhausted slots.
category: fix
dev: mission-verification's fn-verify-* checkouts leaked on process death (dispose is in-process best-effort); the self-healing temp-dir sweep now includes the fn-verify- prefix under tmpdir() with the same age gates and active-session refusal.

View File

@@ -193,6 +193,28 @@ describe("SelfHealingManager worktrees-dir sweeps", () => {
}); });
describe("SelfHealingManager temp-dir AI merge worktree sweep", () => { describe("SelfHealingManager temp-dir AI merge worktree sweep", () => {
it("removes stale fn-verify verification checkouts from tmpdir (leak found on the live board)", async () => {
/*
FNXC:TempWorktreeSweep 2026-07-31-22:55:
GitCheckoutMaterializer's dispose() is in-process best-effort; a killed process leaks the
fn-verify-* checkout AND its git worktree registration forever, because no sweep knew the
prefix. Reverting the sweep-prefix change makes this test fail (the dir survives).
*/
const stale = tempMergeDir(`fn-verify-${Math.random().toString(36).slice(2)}`);
makeStale(stale);
const staleApp = tempMergeDir(`fn-verify-app-${Math.random().toString(36).slice(2)}`);
makeStale(staleApp);
const fresh = tempMergeDir(`fn-verify-fresh-${Math.random().toString(36).slice(2)}`);
const { manager } = makeManager();
await expect(sweep(manager)).resolves.toBe(2);
expect(existsSync(stale)).toBe(false);
expect(existsSync(staleApp)).toBe(false);
// Young checkouts may belong to a live verification run — the age gate must hold.
expect(existsSync(fresh)).toBe(true);
});
it("removes stale fusion-ai-merge directories and emits success audits", async () => { it("removes stale fusion-ai-merge directories and emits success audits", async () => {
const stale = tempMergeDir(); const stale = tempMergeDir();
makeStale(stale); makeStale(stale);

View File

@@ -14905,7 +14905,19 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
for (const tempRoot of roots) { for (const tempRoot of roots) {
let entries: string[]; let entries: string[];
try { try {
entries = readdirSync(tempRoot).filter((entry) => entry.startsWith("fusion-ai-merge-")); /*
FNXC:TempWorktreeSweep 2026-07-31-22:55:
`fn-verify-` (and `fn-verify-app-`) checkouts from mission-verification's
GitCheckoutMaterializer leak when the process dies between materialize and dispose — seven
registered worktrees aged Jul 24-27 were found holding registrations, and on the live board
the visible symptom was planning admission queueing behind exhausted worktree slots
("queued to plan" for ~6 minutes). Their dispose() is best-effort in-process only; this
sweep is the only out-of-process reaper, so it must know the prefix. Verification checkouts
are detached throwaways that only ever live under tmpdir(), so the extra prefixes apply to
that root alone; the same age gates, active-session refusal, and audit rows govern them.
*/
const sweepPrefixes = tempRoot === tmpdir() ? ["fusion-ai-merge-", "fn-verify-"] : ["fusion-ai-merge-"];
entries = readdirSync(tempRoot).filter((entry) => sweepPrefixes.some((prefix) => entry.startsWith(prefix)));
} catch (err: unknown) { } catch (err: unknown) {
if (!existsSync(tempRoot)) continue; if (!existsSync(tempRoot)) continue;
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);