ruvnet/ruflo · error · Error
Only the original claimant can contest the steal
Error message
Only the original claimant can contest the steal
What it means
Plain Error thrown by contestSteal when originalClaimant.id does not equal claim.contestInfo.contestedBy.id. contestInfo.contestedBy records the claimant from whom the work was stolen; only that exact claimant (by id) is permitted to contest. This is an authorization guard, not a state problem.
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 6b01dc5a68)
Solutions
- Pass the exact Claimant whose id matches claim.contestInfo.contestedBy.id.
- If you are a queen/coordinator adjudicating rather than contesting, use resolveContest instead.
- Verify the id source and formatting (trim, casing, prefixes) against the recorded contestedBy.
- Read contestedBy off the claim and reuse that object rather than reconstructing it.
Example fix
// before
await workStealing.contestSteal(issueId, coordinator, reason); // not the victim -> throws
// after
const claim = await repo.findByIssueId(issueId);
const victim = claim?.contestInfo?.contestedBy;
if (victim) {
await workStealing.contestSteal(issueId, victim, reason);
} else {
// coordinator adjudicates instead
await workStealing.resolveContest(issueId, winner, reason);
} Defensive patterns
Strategy: validation
Validate before calling
const claim = await repository.findByIssueId(issueId);
const victim = claim?.contestInfo?.contestedBy;
if (!victim || victim.id !== originalClaimant.id) {
throw new Error('Only the original claimant may contest');
}
await workStealing.contestSteal(issueId, victim, reason); Type guard
function isContestVictim(claimant: Claimant, claim: IssueClaimWithStealing | null): boolean {
return !!claim?.contestInfo?.contestedBy && claim.contestInfo.contestedBy.id === claimant.id;
} Try / catch
try {
await workStealing.contestSteal(issueId, originalClaimant, reason);
} catch (e) {
if (e instanceof Error && /Only the original claimant/.test(e.message)) {
// caller is not the victim; coordinators should use resolveContest instead
return;
}
throw e;
} Prevention
- Read contestedBy off the claim and reuse that exact object as the contesting claimant.
- Coordinators adjudicating a contest should call resolveContest, not contestSteal.
- Normalize claimant ids (casing/prefix) at all boundaries to avoid spurious mismatches.
When it happens
Trigger: A different agent/user attempting to contest; a coordinator passing its own id instead of the original owner's; id format mismatch (e.g. 'agent-7' vs 'agent-007'); wrong Claimant object passed from a stale cache.
Common situations: Coordinator attempts to contest on behalf of an agent but uses its own identity; id source differs between steal-time recording and contest-time lookup; case or prefix differences in ids.
Related errors
- No steal to contest - issue was not recently stolen
- Contest has already been resolved
- Contest window has expired
- Claim not found for issue: ${issueId}
- Claim is still in grace period
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/badc839f973610be.
Report an issue: GitHub.