ruvnet/ruflo · error · ClaimOperationError

HANDOFF_PENDING

HANDOFF_PENDING

Error message

Cannot release claim with pending handoff to ${pendingHandoff.to.name}

What it means

Thrown by ClaimService.release when claim.handoffChain contains a handoff with status 'pending'. A claim mid-handoff cannot be released — doing so would orphan the pending transfer. Code is HANDOFF_PENDING, and the message names the pending handoff's target.

Source

Thrown at v3/@claude-flow/claims/src/application/claim-service.ts:261

    const claim = await this.claimRepository.findByIssueId(issueId);

    // Validate claim exists
    if (!claim) {
      throw new ClaimOperationError('NOT_CLAIMED', `Issue ${issueId} is not claimed`);
    }

    // Validate claimant owns the claim
    if (claim.claimant.id !== claimant.id) {
      throw new ClaimOperationError(
        'UNAUTHORIZED',
        `Claimant ${claimant.name} does not own the claim on issue ${issueId}`
      );
    }

    // Check for pending handoffs
    const pendingHandoff = claim.handoffChain?.find((h) => h.status === 'pending');
    if (pendingHandoff) {
      throw new ClaimOperationError(
        'HANDOFF_PENDING',
        `Cannot release claim with pending handoff to ${pendingHandoff.to.name}`
      );
    }

    // Update claim status
    const previousStatus = claim.status;
    claim.status = 'released';
    claim.lastActivityAt = new Date();

    await this.claimRepository.save(claim);

    // Emit events
    const releaseEvent = createClaimReleasedEvent(claim.id, issueId, claimant);
    await this.eventStore.append(releaseEvent);

    if (previousStatus !== 'released') {
      const statusEvent = createClaimStatusChangedEvent(

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Resolve the pending handoff first (accept or reject via the handoff APIs) before releasing.
  2. If the handoff is stale, add a handoff cancellation/expire step (or extend the service) then release.
  3. Check claim.handoffChain for any pending entry before calling release.

Example fix

// before
await service.release(issueId, claimant); // throws HANDOFF_PENDING

// after
const pending = claim.handoffChain?.find(h => h.status === 'pending');
if (pending) {
  await service.respondHandoff(issueId, pending.id, 'rejected', /*by*/ pending.to);
}
await service.release(issueId, claimant);
Defensive patterns

Strategy: validation

Validate before calling

function hasPendingHandoff(claim) {
  return !!claim?.handoffChain?.some(h => h.status === 'pending');
}

Type guard

function hasPendingHandoff(claim) { return !!claim?.handoffChain?.some(h => h.status === 'pending'); }

Try / catch

try { await service.release(issueId, claimant); } catch (e) {
  if (e instanceof ClaimOperationError && e.code === 'HANDOFF_PENDING') { /* resolve handoff first */ } else throw e;
}

Prevention

When it happens

Trigger: Calling release after requestHandoff but before the target accepts/rejects; a handoff that was requested and never resolved.

Common situations: A handoff abandoned by the target; the caller forgetting a handoff is in flight; orchestrator releasing a worker's claim while a handoff to another worker is pending.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/10229aad006d3add. Report an issue: GitHub.