{"record":{"id":"556d97e2364411e5","repo":"ruvnet/ruflo","slug":"issue-issueid-is-not-claimed","errorCode":null,"errorMessage":"Issue ${issueId} is not claimed","messagePattern":"Issue (.+?) is not claimed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/claim-service.ts","lineNumber":302,"sourceCode":"\n    this.claims.set(issueId, claim);\n    await this.saveClaims();\n\n    this.emitEvent({\n      type: 'issue:claimed',\n      timestamp: now,\n      issueId,\n      claimant,\n      previousClaimant: existing?.claimant,\n    });\n\n    return { success: true, claim };\n  }\n\n  async release(issueId: string, claimant: Claimant): Promise<void> {\n    const claim = this.claims.get(issueId);\n    if (!claim) {\n      throw new Error(`Issue ${issueId} is not claimed`);\n    }\n\n    if (!this.isSameClaimant(claim.claimant, claimant)) {\n      throw new Error(`Issue ${issueId} is not claimed by ${this.formatClaimant(claimant)}`);\n    }\n\n    this.claims.delete(issueId);\n    this.stealableInfo.delete(issueId);\n    await this.saveClaims();\n\n    this.emitEvent({\n      type: 'issue:released',\n      timestamp: new Date(),\n      issueId,\n      claimant,\n    });\n  }\n","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/services/claim-service.ts#L284-L320","documentation":"ClaimService.release(issueId, claimant) looks up the claim in its in-memory map; if none exists for that issueId it throws 'Issue ... is not claimed' before checking the claimant. This is the first of two guards in release: existence, then ownership. There is no 'release if exists' variant — callers must know the claim is present.","triggerScenarios":"Calling release() for an issue that was never claimed, that was already released, or whose claim expired and was reaped; double-release by two code paths; releasing after a steal that already removed the old claimant's entry.","commonSituations":"Agent crash-recovery retrying release on an issue another agent already stole and released; idempotency logic that re-runs release; UI 'unclaim' button pressed twice.","solutions":["Make release idempotent: check whether the claim exists before calling.","Use the ClaimService events (issue:released, issue:stolen) to update local state so you do not call release on a stale view.","Tolerate 'not claimed' as a success in retry paths (it is already in the desired state)."],"exampleFix":"// before\nawait claims.release(issueId, claimant); // throws if already released\n\n// after: tolerate already-released as success\ntry {\n  await claims.release(issueId, claimant);\n} catch (e) {\n  if (!String(e.message).includes('is not claimed')) throw e;\n  // already in desired state — no-op\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await claims.release(issueId, claimant);\n} catch (e) {\n  if (String(e.message).includes('is not claimed')) return; // already released — desired state\n  throw e;\n}","preventionTips":["Treat 'is not claimed' on release as idempotent success in retry paths.","Drive local claim state from ClaimService events (issue:released, issue:stolen) to avoid stale releases.","Avoid double-release by tracking a released flag per issue in the caller."],"tags":["claim-service","lifecycle","state-machine","concurrency"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}