{"record":{"id":"badc839f973610be","repo":"ruvnet/ruflo","slug":"only-the-original-claimant-can-contest-the-steal","errorCode":null,"errorMessage":"Only the original claimant can contest the steal","messagePattern":"Only the original claimant can contest the steal","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/claims/src/application/work-stealing-service.ts","lineNumber":382,"sourceCode":"\n    // Check if there's a valid contest window\n    if (!claim.contestInfo) {\n      throw new Error('No steal to contest - issue was not recently stolen');\n    }\n\n    if (claim.contestInfo.resolution) {\n      throw new Error('Contest has already been resolved');\n    }\n\n    const nowMs = this.nowMs();\n    const windowEndsAtMs = new Date(claim.contestInfo.windowEndsAt).getTime();\n    if (nowMs > windowEndsAtMs) {\n      throw new Error('Contest window has expired');\n    }\n\n    // Verify the contester was the original owner\n    if (claim.contestInfo.contestedBy.id !== originalClaimant.id) {\n      throw new Error('Only the original claimant can contest the steal');\n    }\n\n    // Update contest info with reason\n    claim.contestInfo.reason = reason;\n    claim.contestInfo.contestedAt = new Date(nowMs);\n\n    await this.repository.update(claim);\n\n    // Emit contest event\n    await this.emitContestEvent(claim);\n  }\n\n  // ===========================================================================\n  // Resolve Contest\n  // ===========================================================================\n\n  /**\n   * Resolve a contest (queen or human decides the winner)","sourceCodeStart":364,"sourceCodeEnd":400,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/claims/src/application/work-stealing-service.ts#L364-L400","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait svc.contestSteal(issueId, { id: process.env.AGENT_ID!, name: 'worker' }, 'stolen');\n\n// after\nconst claim = await svc.repository.findByIssueId(issueId);\nif (claim?.contestInfo && claim.contestInfo.contestedBy.id === me.id) {\n  await svc.contestSteal(issueId, claim.contestInfo.contestedBy, 'stolen');\n}","handlingStrategy":"validation","validationCode":"const claim = await repository.findByIssueId(issueId);\nconst eligible = !!claim?.contestInfo\n  && !claim.contestInfo.resolution\n  && claim.contestInfo.contestedBy.id === me.id;\nif (eligible) await svc.contestSteal(issueId, me, reason);","typeGuard":"function isEligibleContestant(claim: IssueClaim | null, me: Claimant): boolean {\n  return !!claim?.contestInfo\n    && !claim.contestInfo.resolution\n    && claim.contestInfo.contestedBy.id === me.id;\n}","tryCatchPattern":"try { await svc.contestSteal(issueId, me, reason); }\ncatch (e) { if (e instanceof Error && e.message.includes('original claimant')) { /* wrong identity: log and skip */ } else throw e; }","preventionTips":["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"],"tags":["claims","work-stealing","authorization","identity-mismatch","typescript"],"backgroundTag":"actor-identity-mismatch","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}