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
- Pre-check state: const c = await service.getIssueStatus(issueId); only call accept when c?.status === 'handoff-pending'
- Make acceptance single-shot per recipient: mark the notification consumed before/after accepting
- 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
- Make handoff acceptance single-shot: consume the notification before/after a successful accept
- Re-read claim status before every accept; handoffs are a two-phase state machine
- Route each issue's handoff decisions through one handler so accept and reject cannot interleave
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
- Can only complete running tasks
- Handoff not addressed to
- Issue is not claimed
- Issue is not claimed by
- Amendment already resolved
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)