{"record":{"id":"e4b7c96c382e0cd1","repo":"gastownhall/beads","slug":"issue-s-is-not-assigned","errorCode":null,"errorMessage":"issue %s is not assigned","messagePattern":"issue (.+?) is not assigned","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/unclaim.go","lineNumber":53,"sourceCode":"\t// Route to the correct table (issues/wisps) automatically, matching\n\t// ClaimIssueInTx — a wisp claim lives in the wisp tables, so its release\n\t// must update them too rather than no-op against the permanent issues table.\n\tisWisp := IsActiveWispInTx(ctx, tx, id)\n\tissueTable, _, eventTable, _ := WispTableRouting(isWisp)\n\n\toldIssue, err := GetIssueInTx(ctx, tx, id)\n\tif err != nil {\n\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","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/unclaim.go#L35-L71","documentation":"UnclaimIssueInTx requires an existing assignee to unclaim; if oldIssue.Assignee is empty it returns this error. The library throws it because there is no claim to release on an unassigned issue, and silently succeeding would mask a caller bug or stale information.","triggerScenarios":"Calling ReleaseIssueInTx/UnclaimIssueInTx on an issue whose Assignee field is empty (never claimed, or already released by another process).","commonSituations":"Racing agents where one already released the claim; retry logic re-running a release that already succeeded; releasing an issue this process never claimed.","solutions":["Check Assignee before calling and treat an empty assignee as already-released (no-op).","Make release logic idempotent: catch this error and continue cleanup.","Verify which agent actually holds the claim via GetIssue before releasing.","Fix retry loops so a previously-succeeded release isn't retried."],"exampleFix":"// before\nerr := store.ReleaseIssue(ctx, id)\nif err != nil { return err }\n// after\niss, _ := store.GetIssue(ctx, id)\nif iss.Assignee == \"\" { return nil } // already unclaimed\nerr := store.ReleaseIssue(ctx, id)","handlingStrategy":"validation","validationCode":"iss, err := store.GetIssue(ctx, id)\nif err != nil { return err }\nif iss.Assignee == \"\" {\n\treturn nil // nothing to release\n}","typeGuard":null,"tryCatchPattern":"if err := store.ReleaseIssue(ctx, id); err != nil {\n\tif strings.Contains(err.Error(), \"is not assigned\") {\n\t\treturn nil // idempotent release\n\t}\n\treturn err\n}","preventionTips":["Design claim/release flows to be idempotent.","Re-read issue state immediately before releasing in multi-agent setups.","Only release claims your own process created."],"tags":["state-conflict","claim-management","idempotency"],"backgroundTag":"precondition-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}