ruvnet/ruflo · error
Only the original claimant can contest the steal
Error message
Only the original claimant can contest the steal
What it means
Thrown by WorkStealingService.contestSteal(issueId, originalClaimant, reason) when originalClaimant.id does not equal claim.contestInfo.contestedBy.id. Only the agent that was displaced by the steal (the one recorded when the contest was created) may file the contest; it is the final guard after the claim-exists, contest-open, and window checks. Any other identity — the stealing agent, a coordinator, a renamed agent — is rejected.
Solutions
- Fetch the claim first (repository.findByIssueId) and pass claim.contestInfo.contestedBy as originalClaimant instead of reconstructing a Claimant by hand
- Verify the running agent's ID equals the displaced owner before calling; if not, skip or escalate to resolveContest
- Normalize agent IDs (trim, exact-string compare) everywhere so the same agent always yields byte-identical IDs
- If the original owner is permanently gone, settle ownership via resolveContest(issueId, winner, reason) rather than contestSteal
Example fix
// before
await svc.contestSteal(issueId, { id: process.env.AGENT_ID!, name: 'worker' }, 'stolen');
// after
const claim = await svc.repository.findByIssueId(issueId);
if (claim?.contestInfo && claim.contestInfo.contestedBy.id === me.id) {
await svc.contestSteal(issueId, claim.contestInfo.contestedBy, 'stolen');
} Defensive patterns
Strategy: validation
Validate before calling
const claim = await repository.findByIssueId(issueId); const eligible = !!claim?.contestInfo && !claim.contestInfo.resolution && claim.contestInfo.contestedBy.id === me.id; if (eligible) await svc.contestSteal(issueId, me, reason);
Type guard
function isEligibleContestant(claim: IssueClaim | null, me: Claimant): boolean {
return !!claim?.contestInfo
&& !claim.contestInfo.resolution
&& claim.contestInfo.contestedBy.id === me.id;
} Try / catch
try { await svc.contestSteal(issueId, me, reason); }
catch (e) { if (e instanceof Error && e.message.includes('original claimant')) { /* wrong identity: log and skip */ } else throw e; } Prevention
- Pass the claim.contestInfo.contestedBy object from the fetched claim instead of rebuilding a Claimant
- Use one stable agent ID per agent across restarts (persist it, never regenerate)
- Compare agent IDs as exact strings; normalize whitespace and case at creation time
When it happens
Trigger: Calling contestSteal with a Claimant whose id differs from claim.contestInfo.contestedBy.id: the new (stealing) owner tries to contest; the agent restarted and generated a fresh agent ID; IDs differ by case, prefix, or whitespace; the caller passes the queen/coordinator identity instead of the displaced owner.
Common situations: Agent restarts that regenerate a random agent ID; contest handlers copy-pasted from another issue's flow; mixing Claimant objects built from config vs runtime state; federated nodes recording the contest under a different node-prefixed ID.
Related errors
- No contest to resolve
- Issue is not claimed by
- Issue is not stealable or not claimed
- UNAUTHORIZED
- Unknown federation claim
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/badc839f973610be.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/claims/src/application/work-stealing-service.ts:382
// Check if there's a valid contest window
if (!claim.contestInfo) {
throw new Error('No steal to contest - issue was not recently stolen');
}
if (claim.contestInfo.resolution) {
throw new Error('Contest has already been resolved');
}
const nowMs = this.nowMs();
const windowEndsAtMs = new Date(claim.contestInfo.windowEndsAt).getTime();
if (nowMs > windowEndsAtMs) {
throw new Error('Contest window has expired');
}
// Verify the contester was the original owner
if (claim.contestInfo.contestedBy.id !== originalClaimant.id) {
throw new Error('Only the original claimant can contest the steal');
}
// Update contest info with reason
claim.contestInfo.reason = reason;
claim.contestInfo.contestedAt = new Date(nowMs);
await this.repository.update(claim);
// Emit contest event
await this.emitContestEvent(claim);
}
// ===========================================================================
// Resolve Contest
// ===========================================================================
/**
* Resolve a contest (queen or human decides the winner)View on GitHub (pinned to fa13ee4ad6)