(
taskListId: string,
taskId: string,
claimantAgentId: string,
options: ClaimTaskOptions = {},
)
| 539 | * if the agent owns any other open tasks before claiming. |
| 540 | */ |
| 541 | export async function claimTask( |
| 542 | taskListId: string, |
| 543 | taskId: string, |
| 544 | claimantAgentId: string, |
| 545 | options: ClaimTaskOptions = {}, |
| 546 | ): Promise<ClaimTaskResult> { |
| 547 | const taskPath = getTaskPath(taskListId, taskId) |
| 548 | |
| 549 | // Check existence before locking — proper-lockfile.lock throws if the |
| 550 | // target file doesn't exist, and we want a clean task_not_found result. |
| 551 | const taskBeforeLock = await getTask(taskListId, taskId) |
| 552 | if (!taskBeforeLock) { |
| 553 | return { success: false, reason: 'task_not_found' } |
| 554 | } |
| 555 | |
| 556 | // If we need to check agent busy status, use task-list-level lock |
| 557 | // to prevent TOCTOU race conditions |
| 558 | if (options.checkAgentBusy) { |
| 559 | return claimTaskWithBusyCheck(taskListId, taskId, claimantAgentId) |
| 560 | } |
| 561 | |
| 562 | // Otherwise, use task-level lock (original behavior) |
| 563 | let release: (() => Promise<void>) | undefined |
| 564 | try { |
| 565 | // Acquire exclusive lock on the task file |
| 566 | release = await lockfile.lock(taskPath, LOCK_OPTIONS) |
| 567 | |
| 568 | // Read current task state |
| 569 | const task = await getTask(taskListId, taskId) |
| 570 | if (!task) { |
| 571 | return { success: false, reason: 'task_not_found' } |
| 572 | } |
| 573 | |
| 574 | // Check if already claimed by another agent |
| 575 | if (task.owner && task.owner !== claimantAgentId) { |
| 576 | return { success: false, reason: 'already_claimed', task } |
| 577 | } |
| 578 | |
| 579 | // Check if already resolved |
| 580 | if (task.status === 'completed') { |
| 581 | return { success: false, reason: 'already_resolved', task } |
| 582 | } |
| 583 | |
| 584 | // Check for unresolved blockers (open or in_progress tasks block) |
| 585 | const allTasks = await listTasks(taskListId) |
| 586 | const unresolvedTaskIds = new Set( |
| 587 | allTasks.filter(t => t.status !== 'completed').map(t => t.id), |
| 588 | ) |
| 589 | const blockedByTasks = task.blockedBy.filter(id => |
| 590 | unresolvedTaskIds.has(id), |
| 591 | ) |
| 592 | if (blockedByTasks.length > 0) { |
| 593 | return { success: false, reason: 'blocked', task, blockedByTasks } |
| 594 | } |
| 595 | |
| 596 | // Claim the task (already holding taskPath lock — use unsafe variant) |
| 597 | const updated = await updateTaskUnsafe(taskListId, taskId, { |
| 598 | owner: claimantAgentId, |
no test coverage detected