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

  1. Fetch the claim first (repository.findByIssueId) and pass claim.contestInfo.contestedBy as originalClaimant instead of reconstructing a Claimant by hand
  2. Verify the running agent's ID equals the displaced owner before calling; if not, skip or escalate to resolveContest
  3. Normalize agent IDs (trim, exact-string compare) everywhere so the same agent always yields byte-identical IDs
  4. 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

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


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)