fix(engine): prevent worktree collisions on manual task moves

Two related bugs let two in-progress tasks share a single
.worktrees/<name> directory:

1. The dashboard POST /tasks/:id/move route promoted tasks to
   in-progress without allocating a fresh worktree path, so a queued
   task carrying a stale worktree field from a prior preserveResumeState
   requeue could land in-progress on a directory already held by another
   active task.

2. moveTask({preserveResumeState:true}) kept the worktree pointer on
   requeue. When the on-disk checkout was later removed or reassigned,
   the next dispatch collided with a worktree the scheduler had handed
   to another task.

moveTask now releases the worktree pointer on every reopen-to-todo hop
(branch is kept so committed progress survives via git worktree add
<path> <branch>). A new preserveWorktree option opts internal bounces
out of the release. moveTask also accepts an allocateWorktree callback
that runs under a new cross-task allocation lock in TaskStore, so two
concurrent moves cannot pick the same name from a stale snapshot. Both
the manual-move route and the scheduler dispatch path flow through the
allocator and share the lock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 07:23:38 -07:00
parent 9f2b96e235
commit 4866f341bd
11 changed files with 308 additions and 73 deletions

View File

@@ -0,0 +1,37 @@
---
"@runfusion/fusion": patch
---
Fix worktree collisions when tasks are manually moved into in-progress.
Two related bugs caused two in-progress tasks to share a single
`.worktrees/<name>` directory:
1. The dashboard `POST /tasks/:id/move` route promoted tasks to
in-progress without allocating a fresh worktree path, so a queued
task carrying a stale `worktree` field from a prior
`preserveResumeState` requeue could land in-progress on a directory
already owned by another active task.
2. `TaskStore.moveTask({ preserveResumeState: true })` kept the
worktree pointer on requeue. When the on-disk checkout was later
removed or reassigned, the next dispatch could collide with a
worktree the scheduler had since handed to another task.
Fixes:
- `moveTask` now releases the worktree pointer on every reopen-to-todo
hop. The `branch` field is preserved so the next run reattaches via
`git worktree add <path> <branch>` and resumes any committed
progress. A new `preserveWorktree: true` option opts internal
bounces (workflow-rerun) out of the release so listeners never see
an interim `worktree=null` state.
- `moveTask` accepts an `allocateWorktree` callback that runs under a
cross-task allocation lock in `TaskStore`, building `reservedNames`
from a fresh `listTasks` snapshot so two concurrent moves cannot
pick the same name.
- The manual-move route and the scheduler dispatch path both flow
through the new allocator, sharing the lock.
- `planTaskWorktreePath` is exported from `@fusion/engine` for
consumers that need to plan worktree paths the same way the
scheduler does.