fix: scrub queued/blockedBy/overlapBlockedBy on in-review transition
A task that picked up status='queued' or overlapBlockedBy while waiting in todo (file-scope overlap with a higher-priority peer) was carrying those todo-dispatch markers into in-review, where the merge gate then permanently refused with "task is marked 'queued'". Ghost-review → todo → scheduler re-queue → stranded-completed-todo recovery → in-review formed a steady- state loop that never let the task merge. moveTaskInternal now treats queued/blockedBy/overlapBlockedBy as todo-only dispatch state and clears them on every transition into in-review. failed and awaiting-* statuses are left untouched (already covered by an existing test, plus a new regression test for the queued case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
.changeset/fix-handoff-clear-queued-state.md
Normal file
9
.changeset/fix-handoff-clear-queued-state.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
fix: clear scheduler-side `status='queued'`, `blockedBy`, and `overlapBlockedBy` when a task transitions into `in-review` so the merge gate is no longer permanently blocked by stale todo-dispatch markers.
|
||||
|
||||
Repro: a task that picked up `status='queued'` while waiting in `todo` (e.g. file-scope overlap with a higher-priority queued peer) and then completed and was handed off to `in-review` — directly via `handoffToReview` or indirectly via stranded-completed-todo recovery — would carry the queued flag into review. Every subsequent merge attempt failed with `Cannot merge <id>: task is marked 'queued'`, and the in-review stall surface kept re-firing `[no-worktree-no-merge-confirmed]` without progress. Ghost-review → todo → scheduler re-queue → stranded → in-review formed a steady-state loop.
|
||||
|
||||
Fix: `TaskStore.moveTaskInternal` now treats `queued`/`blockedBy`/`overlapBlockedBy` as todo-only dispatch state and scrubs them on every transition into `in-review`. Failed/awaiting-* statuses are unaffected.
|
||||
@@ -193,6 +193,30 @@ describe("TaskStore handoffToReview", () => {
|
||||
expect(getAuditEventsByInsertion(task.id).filter((event) => event.mutationType === "task:handoff-invariant-violation")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("clears scheduler-state queued/blockedBy/overlapBlockedBy on handoff to in-review", async () => {
|
||||
// Regression for FN-5434: a task that picked up status='queued' or
|
||||
// overlapBlockedBy while waiting in todo would carry those todo-dispatch
|
||||
// markers into in-review, where the merge gate then refuses to merge it
|
||||
// with "task is marked 'queued'". Handoff must scrub those fields.
|
||||
const task = await createInProgressTask("high");
|
||||
await store.updateTask(task.id, {
|
||||
status: "queued",
|
||||
blockedBy: "FN-OTHER",
|
||||
overlapBlockedBy: "FN-OTHER",
|
||||
});
|
||||
|
||||
const handedOff = await store.handoffToReview(task.id, {
|
||||
ownerAgentId: "agent-1",
|
||||
evidence: { reason: "fn_task_done", runId: "run-1", agentId: "agent-1" },
|
||||
now: "2026-05-19T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(handedOff.column).toBe("in-review");
|
||||
expect(handedOff.status).toBeUndefined();
|
||||
expect(handedOff.blockedBy).toBeUndefined();
|
||||
expect(handedOff.overlapBlockedBy).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves failed status and error details during handoff", async () => {
|
||||
const task = await createInProgressTask();
|
||||
await store.updateTask(task.id, {
|
||||
|
||||
@@ -4911,6 +4911,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
if (toColumn === "in-review") {
|
||||
task.recoveryRetryCount = undefined;
|
||||
task.nextRecoveryAt = undefined;
|
||||
// Clear scheduler-side dispatch state: `queued`, `blockedBy`, and
|
||||
// `overlapBlockedBy` are stamped while the task waits in `todo`. If
|
||||
// they survive the transition into `in-review` they permanently block
|
||||
// the merge gate (see getTaskMergeBlocker's BLOCKING_TASK_STATUSES).
|
||||
if (task.status === "queued") {
|
||||
task.status = undefined;
|
||||
}
|
||||
task.blockedBy = undefined;
|
||||
task.overlapBlockedBy = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user