fix(FN-5256): keep live task worktrees through pause/resume races
Three independent reliability fixes that all surfaced as the same bug:
live tasks losing their worktrees mid-execution and emitting
`wrong_toplevel` errors.
Fix A — executor stale-self-owned classifier:
`reconcileSelfOwnedActiveSessionForRemoval` now takes a process-active
probe (`executingTaskLock.has`) and a minimum-idle window (default 5s)
in addition to the existing in-memory `activeWorktrees` binding probe.
Recently-registered or still-running entries are refused with
`process-active-refuses` / `too-recent-refuses`, with audit-grade
log lines. Both the pre-remove path
(`reconcileSelfOwnedBeforeRemove`), the post-throw retry in
`removeOwnWorktreeWithReconcile`, and the defensive reconcile in
`removeWorktree` route through the same hardened gates.
Fix B — pause synchronously reaps the agent session:
New `awaitAbortInFlightTaskWork` mirrors the existing fire-and-forget
abort but awaits each `session.abort()` /
`stepExecutor.terminateAllSessions()` /
`workflowSession.abort()`. `parkTaskAfterWorkflowStepPause` calls it
before `moveTask("todo")`, and the `task:updated` user-pause handler
routes through it, so a fast re-dispatch can no longer race a still-
live shell.
Fix C — self-healing realpath + active-task skip:
`reconcileTaskWorktreeMetadata` now realpath-normalizes both sides of
the registry comparison (handling macOS `/private/var/...`) and
refuses to clear `worktree`/`branch` on in-progress or in-review
tasks. The skip emits a new
`task:auto-recover-worktree-metadata-skipped-active` audit event;
executor-level recovery paths remain in charge of active tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ describe("FN-4973: executor worktree conflict cleanup", () => {
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
store.listTasks.mockResolvedValue([]);
|
||||
activeSessionRegistry.registerPath(CONFLICT_PATH, { taskId: "FN-4973", kind: "executor", ownerKey: "FN-4973" });
|
||||
// FN-5256: backdate so the new min-idle window doesn't refuse the reconcile.
|
||||
(activeSessionRegistry.lookupByPath(CONFLICT_PATH) as any).registeredAt = 0;
|
||||
|
||||
const removeSpy = vi.spyOn(worktreePoolModule, "removeWorktree").mockResolvedValue(undefined);
|
||||
const result = await (executor as any).cleanupConflictingWorktree(CONFLICT_PATH, "fusion/fn-4973", "FN-4973");
|
||||
@@ -85,6 +87,8 @@ describe("FN-4973: executor worktree conflict cleanup", () => {
|
||||
removeSpy
|
||||
.mockImplementationOnce(async () => {
|
||||
activeSessionRegistry.registerPath(CONFLICT_PATH, { taskId: "FN-4973", kind: "executor", ownerKey: "FN-4973" });
|
||||
// FN-5256: backdate so the post-throw reconcile is not refused by min-idle.
|
||||
(activeSessionRegistry.lookupByPath(CONFLICT_PATH) as any).registeredAt = 0;
|
||||
throw new ActiveSessionWorktreeRemovalError({
|
||||
worktreePath: CONFLICT_PATH,
|
||||
taskId: "FN-4973",
|
||||
|
||||
Reference in New Issue
Block a user