{"record":{"id":"6557d804be3a426b","repo":"gastownhall/beads","slug":"unclaimifassignee-id-must-not-be-empty","errorCode":null,"errorMessage":"UnclaimIfAssignee: id must not be empty","messagePattern":"UnclaimIfAssignee: id must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1898,"sourceCode":"\t\treturn fmt.Errorf(\"Unclaim: id must not be empty\")\n\t}\n\tif err := u.issueRepo.UnclaimIssue(ctx, id, actor, force); err != nil {\n\t\treturn fmt.Errorf(\"Unclaim: %w\", err)\n\t}\n\treturn nil\n}\n\n// UnclaimIfAssignee is the compare-and-swap release: it clears the claim only\n// while the issue is still assigned to expectedAssignee, and otherwise returns\n// storage.ErrAssigneeMismatch having written nothing. It is the conditional\n// twin of Unclaim and runs the SAME transition (assignee cleared, status\n// reopened, started_at cleared, lease dropped, row_lock rewritten, \"unclaimed\"\n// event recorded) because both reach the one classic implementation in\n// issueops — which is what makes `bd unclaim --if-assignee` behave identically\n// on the proxied-server and embedded backends.\nfunc (u *issueUseCaseImpl) UnclaimIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"UnclaimIfAssignee: id must not be empty\")\n\t}\n\tif err := u.issueRepo.UnclaimIssueIfAssignee(ctx, id, actor, expectedAssignee); err != nil {\n\t\treturn fmt.Errorf(\"UnclaimIfAssignee: %w\", err)\n\t}\n\treturn nil\n}\n\n// Heartbeat refreshes the lease on an issue actor holds in_progress. The\n// write touches ONLY the ephemeral leases table (bd-lrgn1), so the caller\n// must run it under uow.RunTxEphemeral's no-Dolt-commit form — a heartbeat\n// mints no Dolt commit and no history in any mode (bd-aq0ql).\nfunc (u *issueUseCaseImpl) Heartbeat(ctx context.Context, id, actor string) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"Heartbeat: id must not be empty\")\n\t}\n\tif err := u.issueRepo.HeartbeatIssue(ctx, id, actor); err != nil {\n\t\treturn fmt.Errorf(\"Heartbeat: %w\", err)\n\t}","sourceCodeStart":1880,"sourceCodeEnd":1916,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1880-L1916","documentation":"Validation error from UnclaimIfAssignee: the compare-and-swap unclaim was called with an empty issue id. Thrown before any repository access; no state changed. The CAS release only proceeds when the issue is still assigned to expectedAssignee.","triggerScenarios":"Calling UnclaimIfAssignee(ctx, \"\", actor, expectedAssignee) — the id variable is empty, usually from an unparsed or missing upstream value.","commonSituations":"Automation scripts where the issue key resolution step silently produced an empty string; wiring bugs where the wrong variable is passed.","solutions":["Resolve and validate the issue id before the CAS unclaim","Check that the variable bound to the id is the one populated by your lookup step","Return a clear user-facing error for missing issue keys"],"exampleFix":"// before\nerr := uc.UnclaimIfAssignee(ctx, id, actor, assignee)\n// after\nif id == \"\" {\n\treturn fmt.Errorf(\"cannot unclaim: issue id is empty\")\n}\nerr := uc.UnclaimIfAssignee(ctx, id, actor, assignee)","handlingStrategy":"validation","validationCode":"if id == \"\" || expectedAssignee == \"\" {\n\treturn fmt.Errorf(\"CAS unclaim requires non-empty id and expectedAssignee\")\n}","typeGuard":null,"tryCatchPattern":"if err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil {\n\tif strings.Contains(err.Error(), \"id must not be empty\") {\n\t\treturn fmt.Errorf(\"caller bug: empty id in CAS unclaim\")\n\t}\n\treturn err\n}","preventionTips":["Resolve the id and expected assignee from a fresh Show call immediately before the CAS","Assert non-empty inputs in wrapper functions around the use case","Keep id plumbing explicit — avoid defaulting to empty strings"],"tags":["validation","claim","compare-and-swap"],"backgroundTag":"empty-required-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}