fix(FN-5363): target mergeQueue lease to specific task ID instead of queue head

The merge handoff path in acquireReuseHandoff called acquireMergeQueueLease
without a targetTaskId, which grabbed the priority/FIFO-sorted queue head
instead of the specific in-review task. When the queue head was a different
task (e.g. a polluted non-review entry), the returned lease had a mismatched
taskId and triggered 'no-lease' for every in-review task.

Fix:
- Pass targetTaskId: input.task.id to acquireMergeQueueLease so the merge
  lane targets its specific task instead of grabbing the queue head.
  MergeQueueAcquireOptions.targetTaskId is already wired in store.ts.
- Replace the broken typeof check (typeof object !== 'object' is always false
  for plain JSON objects returned by RETURNING *) with a proper 'taskId' in
  lease guard.
- Add acquiredTaskId to the no-lease error payload for better observability.

Reliability backstop: merge-reuse-task-worktree.test.ts guards this path.

Fixes: FN-5363
This commit is contained in:
gsxdsm
2026-05-20 12:02:05 -07:00
parent 3e0ffa19a7
commit b8918b83b0

View File

@@ -395,9 +395,10 @@ export async function acquireReuseHandoff(input: ReuseHandoffInput): Promise<Han
// Non-atomic fallback: executor lease checks above race with mergeQueue lease acquisition.
// TaskStore does not yet expose an atomic executor-lease absence check inside mergeQueue leasing.
lease = (input.store as TaskStore & {
acquireMergeQueueLease(workerId: string, opts: { leaseDurationMs: number; now?: string }): unknown;
acquireMergeQueueLease(workerId: string, opts: { leaseDurationMs: number; now?: string; targetTaskId?: string }): unknown;
}).acquireMergeQueueLease(MERGE_HANDOFF_WORKER_ID, {
leaseDurationMs: 15 * 60 * 1000,
targetTaskId: input.task.id,
});
} catch (error) {
if (error instanceof PoolDoubleLeaseError) {
@@ -412,10 +413,11 @@ export async function acquireReuseHandoff(input: ReuseHandoffInput): Promise<Han
}
throw error;
}
if (!lease || typeof lease !== "object" || (lease as { taskId?: string }).taskId !== input.task.id) {
if (!lease || !("taskId" in lease) || lease.taskId !== input.task.id) {
throw new MergeHandoffRefusedError("lease-handoff-failed", "no-lease", {
taskId: input.task.id,
worktreePath,
acquiredTaskId: lease && "taskId" in lease ? lease.taskId : null,
});
}