{"record":{"id":"833479714a399e83","repo":"gastownhall/beads","slug":"w-s-is-held-by-s-coordinate-with-the-holder","errorCode":null,"errorMessage":"%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)","messagePattern":"%w: (.+?) is held by (.+?); coordinate with the holder — pass --force only if their claim is abandoned \\(crashed agent, expired lease\\)","errorType":"error_code","errorClass":"storage.ErrNotOwner","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/unclaim.go","lineNumber":61,"sourceCode":"\t\treturn fmt.Errorf(\"failed to get issue for unclaim: %w\", err)\n\t}\n\n\t// Validate: cannot unclaim closed issues\n\tif oldIssue.Status == types.StatusClosed {\n\t\treturn fmt.Errorf(\"cannot unclaim closed issue %s\", id)\n\t}\n\n\t// Validate: must have an assignee to unclaim\n\tif oldIssue.Assignee == \"\" {\n\t\treturn fmt.Errorf(\"issue %s is not assigned\", id)\n\t}\n\n\t// Validate ownership unless the caller forced the release. Without force, a\n\t// process may only release its own claim. Compared under actorMatches, not\n\t// verbatim, so a caller naming its own identity under a different layer's\n\t// spelling (ga-5ksp5) is not refused as a stranger.\n\tif !force && !actorMatches(oldIssue.Assignee, actor) {\n\t\treturn fmt.Errorf(\"%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)\",\n\t\t\tstorage.ErrNotOwner, id, oldIssue.Assignee)\n\t}\n\n\tnow := time.Now().UTC()\n\n\t// Atomic UPDATE: clear assignee, reset status to open, clear started_at,\n\t// and rewrite row_lock. The predicate CASes on row_lock rather than\n\t// assignee (ga-5ksp5): ownership was already authorized above (or bypassed\n\t// by force) against the row read into oldIssue, and row_lock is rewritten\n\t// by every path that mutates status/assignee/started_at (see the\n\t// freshRowLock invariant in lease.go) — so requiring it to still equal\n\t// oldIssue.RowVersion detects a claim that changed hands (or was released,\n\t// or closed) between that read and this write exactly as precisely as the\n\t// old `assignee = <actor>` predicate did, without embedding a\n\t// spelling-sensitive string comparison in SQL. force does not exempt this\n\t// check: force only widens WHO may unclaim, not whether the row is still\n\t// the one we read.\n\tresult, err := tx.ExecContext(ctx, fmt.Sprintf(`","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/unclaim.go#L43-L79","documentation":"UnclaimIssueInTx enforces ownership: without force, only a caller whose actor matches the current assignee may release the claim. The error wraps storage.ErrNotOwner and tells the caller the claim is held by someone else, advising --force only when the holder is genuinely gone (crashed agent, expired lease). Comparison uses actorMatches, so equivalent identity spellings are accepted.","triggerScenarios":"Calling ReleaseIssueInTx/UnclaimIssueInTx with force=false where the actor does not match the issue's current Assignee, e.g. agent B releasing agent A's claim, or an inconsistently configured actor identity.","commonSituations":"An operator cleaning up after a crashed agent; a supervisor releasing workers' claims without force; misconfigured actor name so a process doesn't recognize its own claim.","solutions":["Pass the same actor identity that originally claimed the issue (check your actor config/env).","If the holder is confirmed dead (crashed/expired lease), call with force=true to take over.","Coordinate with the holder to have them release their own claim.","Fix actor naming inconsistencies so actorMatches recognizes your own claim."],"exampleFix":"// before\nerr := store.ReleaseIssue(ctx, id) // wrong actor identity -> ErrNotOwner\n// after\n// confirmed other agent crashed:\nerr := store.ReleaseIssue(ctx, id, storage.WithForce())\n// or for your own claim, run with the correct actor identity","handlingStrategy":"validation","validationCode":"iss, err := store.GetIssue(ctx, id)\nif err != nil { return err }\nif iss.Assignee != \"\" && iss.Assignee != myActor {\n\t// holder is someone else — do not force without confirming they are gone\n\treturn fmt.Errorf(\"claim held by %s\", iss.Assignee)\n}","typeGuard":null,"tryCatchPattern":"if err := store.ReleaseIssue(ctx, id); err != nil {\n\tif errors.Is(err, storage.ErrNotOwner) {\n\t\t// prompt for --force or abort; never auto-force\n\t}\n}","preventionTips":["Configure a stable, consistent actor identity for every process.","Only use --force after verifying the holder is actually gone.","Use leases/heartbeats so abandoned claims are detectable before forcing.","Check assignee ownership before calling release in admin tooling."],"tags":["ownership","claim-management","concurrency","locking"],"backgroundTag":"not-claim-owner","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}