diff --git a/.changeset/plan-review-root-lease-cleanup.md b/.changeset/plan-review-root-lease-cleanup.md new file mode 100644 index 0000000000..1626c5ee4f --- /dev/null +++ b/.changeset/plan-review-root-lease-cleanup.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent Plan Review tasks from blocking each other after a missing-worktree fallback. +category: fix +dev: Releases the exact workflow-step session path after repository-root Plan Review fallback. diff --git a/packages/engine/src/__tests__/reliability-interactions/graph-node-missing-worktree-recovery.test.ts b/packages/engine/src/__tests__/reliability-interactions/graph-node-missing-worktree-recovery.test.ts index 5cbd295a26..5b3b5516b7 100644 --- a/packages/engine/src/__tests__/reliability-interactions/graph-node-missing-worktree-recovery.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/graph-node-missing-worktree-recovery.test.ts @@ -3,7 +3,14 @@ import type { TaskDetail } from "@fusion/core"; import "../executor-test-helpers.js"; import { PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE } from "../../workflow-graph-executor.js"; import { TaskExecutor } from "../../executor.js"; -import { createMockStore, mockedExecSync, mockedExistsSync, resetExecutorMocks } from "../executor-test-helpers.js"; +import { activeSessionRegistry } from "../../active-session-registry.js"; +import { + createMockStore, + mockedCreateFnAgent, + mockedExecSync, + mockedExistsSync, + resetExecutorMocks, +} from "../executor-test-helpers.js"; import { MAX_WORKTREE_SESSION_RETRIES } from "../../self-healing.js"; /* @@ -319,6 +326,59 @@ describe("Plan Review missing-worktree repo-root fallback (FN-7996)", () => { ); }); + it("releases the repo-root session lease after the fallback reviewer completes", async () => { + const store = createMockStore(); + const agentStore = { getAgent: vi.fn().mockResolvedValue(null), createAgent: vi.fn() }; + const executor = new TaskExecutor(store, "/tmp/test", { agentStore } as any); + const output = '{"verdict":"APPROVE","notes":""}'; + mockedCreateFnAgent.mockImplementation(async () => { + const listeners: Array<(event: any) => void> = []; + return { + session: { + state: {}, + subscribe: (listener: (event: any) => void) => { + listeners.push(listener); + return vi.fn(); + }, + prompt: vi.fn(async () => { + for (const listener of listeners) { + listener({ + type: "message_update", + assistantMessageEvent: { + type: "text_delta", + contentIndex: 0, + delta: output, + partial: output, + }, + }); + } + }), + dispose: vi.fn(), + }, + } as any; + }); + + const live = makeTask(); + const step = { + id: "graph:plan-review-step", + name: "Plan Review", + description: "", + mode: "prompt", + phase: "pre-merge", + gateMode: "gate", + prompt: "Review the plan.", + toolMode: "readonly", + enabled: true, + createdAt: live.createdAt, + updatedAt: live.updatedAt, + }; + + const result = await (executor as any).executeWorkflowStep(live, step, "/tmp/test", {}); + + expect(result.success).toBe(true); + expect(activeSessionRegistry.lookupByPath("/tmp/test")).toBeNull(); + }); + it("keeps other read-only nodes on the recorded path so they fail fast into recovery", async () => { const store = createMockStore(); const executor = new TaskExecutor(store, "/tmp/test"); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 5c34cd23e6..f5f0e08d27 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -16731,7 +16731,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB if (timeoutHandle) clearTimeout(timeoutHandle); const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id); if (activeWorkflowStepSession === session) { - this.deleteActiveWorkflowStepSession(task.id); + this.deleteActiveWorkflowStepSession(task.id, worktreePath); } // Suppress unused-variable warning; `timedOut` documents intent. void timedOut;