{"record":{"id":"cd8bea9fe819c0d7","repo":"ruvnet/ruflo","slug":"contest-has-already-been-resolved","errorCode":null,"errorMessage":"Contest has already been resolved","messagePattern":"Contest has already been resolved","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"v3/@claude-flow/claims/src/application/work-stealing-service.ts","lineNumber":371,"sourceCode":"  // ===========================================================================\n\n  /**\n   * Contest a steal (original owner wants the work back)\n   */\n  async contestSteal(issueId: IssueId, originalClaimant: Claimant, reason: string): Promise<void> {\n    const claim = await this.repository.findByIssueId(issueId);\n\n    if (!claim) {\n      throw new Error(`Claim not found for issue: ${issueId}`);\n    }\n\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);","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/claims/src/application/work-stealing-service.ts#L353-L389","documentation":"Plain Error thrown by contestSteal when claim.contestInfo.resolution is already set. Once resolveContest has decided a winner, the contest is closed and cannot be contested again. This is an idempotency guard against duplicate or racing contest attempts.","triggerScenarios":"Calling contestSteal after resolveContest already ran; a retry loop re-firing the contest after resolution; two contest paths racing where one resolved first.","commonSituations":"At-least-once delivery redelivers the StealStolen event after a coordinator already resolved; UI 'contest' button enabled after resolution; missing idempotency key on the contest request.","solutions":["Check claim.contestInfo.resolution before calling; if set, treat as already-decided.","De-duplicate contest requests with an idempotency key tied to the steal event id.","Disable the contest action in the UI once a resolution is observed.","Consume the StealContestResolved event to invalidate pending contest attempts."],"exampleFix":"// before\nawait workStealing.contestSteal(issueId, originalClaimant, reason); // already resolved -> throws\n\n// after\nconst claim = await repo.findByIssueId(issueId);\nif (claim?.contestInfo && !claim.contestInfo.resolution) {\n  await workStealing.contestSteal(issueId, originalClaimant, reason);\n}","handlingStrategy":"validation","validationCode":"const claim = await repository.findByIssueId(issueId);\nif (claim?.contestInfo?.resolution) {\n  // already decided; nothing to do\n  return;\n}\nawait workStealing.contestSteal(issueId, originalClaimant, reason);","typeGuard":"function contestIsOpen(claim: IssueClaimWithStealing | null): claim is IssueClaimWithStealing {\n  return !!claim && !!claim.contestInfo && !claim.contestInfo.resolution;\n}","tryCatchPattern":"try {\n  await workStealing.contestSteal(issueId, originalClaimant, reason);\n} catch (e) {\n  if (e instanceof Error && /already been resolved/.test(e.message)) {\n    return; // idempotent: already decided\n  }\n  throw e;\n}","preventionTips":["Track resolved contest ids and skip duplicate requests.","Consume StealContestResolved events to invalidate pending contest UI/actions.","Add an idempotency key tied to the steal event id on contest requests."],"tags":["work-stealing","contest","idempotency","plain-error"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}