ruvnet/ruflo · error · ClaimOperationError

HANDOFF_NOT_FOUND

HANDOFF_NOT_FOUND

Error message

No pending handoff found for claimant ${claimant.name}

What it means

Thrown by acceptHandoff when a claim exists but claim.handoffChain contains no entry with status 'pending' AND h.to.id === claimant.id. The handoff was never requested, targets a different claimant, or has already been accepted/rejected and is no longer pending.

Source

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

    );
    await this.eventStore.append(statusEvent);
  }

  async acceptHandoff(issueId: string, claimant: Claimant): Promise<void> {
    const claim = await this.claimRepository.findByIssueId(issueId);

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

    // Find pending handoff for this claimant
    const pendingHandoff = claim.handoffChain?.find(
      (h) => h.status === 'pending' && h.to.id === claimant.id
    );

    if (!pendingHandoff) {
      throw new ClaimOperationError(
        'HANDOFF_NOT_FOUND',
        `No pending handoff found for claimant ${claimant.name}`
      );
    }

    // Check claimant's workload
    const currentClaimCount = await this.claimRepository.countByClaimant(claimant.id);
    const maxClaims = claimant.maxConcurrentClaims ?? 5;
    if (currentClaimCount >= maxClaims) {
      throw new ClaimOperationError(
        'MAX_CLAIMS_EXCEEDED',
        `Cannot accept handoff: claimant ${claimant.name} at max capacity`
      );
    }

    // Update handoff record
    pendingHandoff.status = 'accepted';
    pendingHandoff.resolvedAt = new Date();

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Pass the exact Claimant object (matching id) that was the 'to' target of the original requestHandoff call.
  2. Inspect claim.handoffChain via getIssueStatus and confirm a pending entry exists for claimant.id before accepting.
  3. If the handoff was already accepted, treat the operation as a no-op success rather than re-accepting.
  4. If the prior handoff was rejected, request a new handoff via requestHandoff instead of accepting.

Example fix

// before
await claimService.acceptHandoff(issueId, { id: agentRef.code, ... }); // wrong id scheme

// after
const target = handoffRecord.to; // the claimant recorded at request time
await claimService.acceptHandoff(issueId, target);
Defensive patterns

Strategy: validation

Validate before calling

const status = await claimService.getIssueStatus(issueId);
const pending = status.claim?.handoffChain?.find(
  h => h.status === 'pending' && h.to.id === claimant.id
);
if (!pending) {
  throw new Error(`No pending handoff targets ${claimant.id}`);
}
await claimService.acceptHandoff(issueId, claimant);

Type guard

function hasPendingHandoffFor(claim: IssueClaim | null, claimantId: string): claim is IssueClaim {
  return !!claim && !!claim.handoffChain?.some(h => h.status === 'pending' && h.to.id === claimantId);
}

Try / catch

try {
  await claimService.acceptHandoff(issueId, claimant);
} catch (e) {
  if (e instanceof ClaimOperationError && e.code === 'HANDOFF_NOT_FOUND') {
    // already resolved or targets someone else; log and move on
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a claimant whose id differs from the handoff's to.id; calling acceptHandoff after the pending entry was already resolved (accepted/rejected); the handoff was cancelled; only a release (not a handoff) was performed on the claim.

Common situations: Agent id format mismatch between the agent registry and the Claimant object used at request time; concurrent accept by two workers that both consumed the same notification; stale notification replayed after the first accept resolved the handoff.

Related errors


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