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

  1. Pass the exact Claimant whose id matches claim.contestInfo.contestedBy.id.
  2. If you are a queen/coordinator adjudicating rather than contesting, use resolveContest instead.
  3. Verify the id source and formatting (trim, casing, prefixes) against the recorded contestedBy.
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/badc839f973610be. Report an issue: GitHub.