{"record":{"id":"fdaa5992936f189b","repo":"ruvnet/ruflo","slug":"no-steal-to-contest-issue-was-not-recently-stole","errorCode":null,"errorMessage":"No steal to contest - issue was not recently stolen","messagePattern":"No steal to contest - issue was not recently stolen","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/claims/src/application/work-stealing-service.ts","lineNumber":367,"sourceCode":"  }\n\n  // ===========================================================================\n  // Contest Steal\n  // ===========================================================================\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","sourceCodeStart":349,"sourceCodeEnd":385,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/claims/src/application/work-stealing-service.ts#L349-L385","documentation":"Plain Error thrown by contestSteal when claim.contestInfo is undefined. contestInfo is only populated by the steal() operation; if it is absent, the issue was never stolen (it may have been handed off normally, or is still owned by its original claimant), so there is no contest window to open.","triggerScenarios":"Calling contestSteal after a normal handoff rather than a steal; on an issue that was never stolen; after contestInfo was cleared following resolution; confusing a HandoffRequested event with an IssueStolen event.","commonSituations":"Event routing bug delivering handoff notifications to the steal-contest handler; replay of an old notification; caller assumes any ownership change is a steal.","solutions":["Inspect the claim (findByIssueId/getStealable) and confirm contestInfo is present before contesting.","If the transfer was a handoff, use the handoff accept/reject APIs instead of the contest API.","Drop stale or misrouted notifications rather than contesting a non-stolen claim.","Disambiguate IssueStolen events from HandoffRequested events in your event handler."],"exampleFix":"// before\nawait workStealing.contestSteal(issueId, originalClaimant, reason); // not stolen -> 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) {\n  throw new Error('Cannot contest: issue was not stolen');\n}\nawait workStealing.contestSteal(issueId, originalClaimant, reason);","typeGuard":"function hasOpenContest(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 && /not recently stolen/.test(e.message)) {\n    // route to handoff accept/reject instead, or drop\n    return;\n  }\n  throw e;\n}","preventionTips":["Only invoke contestSteal in response to a verified IssueStolen event.","Route HandoffRequested events to the handoff APIs, not the contest API.","Inspect contestInfo at the boundary before attempting a contest."],"tags":["work-stealing","contest","state-validation","plain-error"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}