ruvnet/ruflo · error

No pending handoff for issue

Error message

No pending handoff for issue ${issueId}

What it means

ClaimService.acceptHandoff(issueId, claimant) requires the issue's claim to exist AND be in status 'handoff-pending'. This error is thrown when either precondition fails: the issue has no claim at all, or its status is something else ('active', 'stealable', 'review-requested', ...) — i.e. no handoff is waiting to be accepted. Handoffs are a two-phase transfer: requestHandoff sets 'handoff-pending', acceptHandoff completes it.

Solutions

  1. Pre-check state: const c = await service.getIssueStatus(issueId); only call accept when c?.status === 'handoff-pending'
  2. Make acceptance single-shot per recipient: mark the notification consumed before/after accepting
  3. If the status moved on, re-read the claim (getIssueStatus) and follow the current state instead of retrying blindly

Example fix

// before
await claims.acceptHandoff(issueId, workerB); // retried notification -> status already 'active'

// after
const claim = await claims.getIssueStatus(issueId);
if (claim?.status === 'handoff-pending' && sameClaimant(claim.handoffTo, workerB)) {
  await claims.acceptHandoff(issueId, workerB);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const claim = await claims.getIssueStatus(issueId);
if (claim?.status === 'handoff-pending' && claim.handoffTo && isSameClaimant(claim.handoffTo, me)) {
  await claims.acceptHandoff(issueId, me);
}

Try / catch

try {
  await claims.acceptHandoff(issueId, me);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('No pending handoff')) {
    const c = await claims.getIssueStatus(issueId);
    if (!c || c.status !== 'handoff-pending') return; // already resolved by someone — accept as done
    throw e;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling acceptHandoff before requestHandoff was called; accepting twice (status already flipped back to 'active' by the first accept); accepting after the handoff was rejected (rejectHandoff restores 'active'); accepting after the claim was released or stolen in between.

Common situations: Both target agents processing the handoff notification concurrently (first wins, second throws), message redelivery/retries re-invoking accept, UI polling that re-fires the accept action.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/407e5f63e94b4269. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/services/claim-service.ts:358

    claim.status = 'handoff-pending';
    claim.statusChangedAt = new Date();
    claim.handoffTo = to;
    claim.handoffReason = reason;
    await this.saveClaims();

    this.emitEvent({
      type: 'issue:handoff:requested',
      timestamp: new Date(),
      issueId,
      claimant: from,
      data: { to, reason },
    });
  }

  async acceptHandoff(issueId: string, claimant: Claimant): Promise<void> {
    const claim = this.claims.get(issueId);
    if (!claim || claim.status !== 'handoff-pending') {
      throw new Error(`No pending handoff for issue ${issueId}`);
    }

    if (!claim.handoffTo || !this.isSameClaimant(claim.handoffTo, claimant)) {
      throw new Error(`Handoff not addressed to ${this.formatClaimant(claimant)}`);
    }

    const previousClaimant = claim.claimant;
    claim.claimant = claimant;
    claim.status = 'active';
    claim.statusChangedAt = new Date();
    delete claim.handoffTo;
    delete claim.handoffReason;
    await this.saveClaims();

    this.emitEvent({
      type: 'issue:handoff:accepted',
      timestamp: new Date(),
      issueId,

View on GitHub (pinned to fa13ee4ad6)