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