ruvnet/ruflo · error · ClaimOperationError
HANDOFF_PENDING
HANDOFF_PENDING
Error message
Cannot release claim with pending handoff to ${pendingHandoff.to.name} What it means
ClaimOperationError with code HANDOFF_PENDING, thrown by ClaimService.release when the claim's handoffChain contains a record with status 'pending'. A claim that is mid-handoff to another claimant cannot be released until that handoff is completed or cancelled, preventing the target claimant from receiving a dead claim.
Solutions
- Complete the pending handoff first: have the target claimant accept (or reject) it, then release or proceed
- Cancel the pending handoff via the service's handoff cancellation path if the target is unreachable, then release
- Order shutdown flows as: resolve handoffs -> release claims, and skip release for claims with pending handoffs
Example fix
// before await claimService.requestHandoff(issueId, from, to, 'shift end'); await claimService.release(issueId, from); // throws HANDOFF_PENDING // after await claimService.requestHandoff(issueId, from, to, 'shift end'); await claimService.acceptHandoff(issueId, to); // or rejectHandoff to cancel await claimService.release(issueId, from); // handoff no longer pending
Defensive patterns
Strategy: try-catch
Validate before calling
const claim = await claimRepository.findByIssueId(issueId);
const pending = claim?.handoffChain?.find(h => h.status === 'pending');
if (pending) {
await claimService.cancelHandoff(issueId, pending); // or wait for accept/reject
}
await claimService.release(issueId, claimant); Try / catch
try { await claimService.release(issueId, claimant); } catch (e) { if (e instanceof ClaimOperationError && e.code === 'HANDOFF_PENDING') { await resolveOrCancelPendingHandoff(issueId); return claimService.release(issueId, claimant); } throw e; } Prevention
- Order teardown: resolve handoffs before releasing claims
- Add timeouts to handoff acceptance so pending records cannot wedge releases
When it happens
Trigger: Calling release() after requestHandoff() but before the target accepts/rejects the handoff; orchestrators running release-all sweeps that hit claims in transfer; crashed handoff flows leaving a permanently pending record that then blocks release.
Common situations: Agent handoff at end of shift followed by immediate cleanup release; handoff target never responding so the pending record wedges the claim; automated teardown during a redeploy catching in-flight handoffs.
Related errors
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/10229aad006d3add.
Report an issue: GitHub.
Appendix: 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 fa13ee4ad6)