{"record":{"id":"99753bbc500e3918","repo":"gastownhall/beads","slug":"unclaimifassignee-w","errorCode":null,"errorMessage":"UnclaimIfAssignee: %w","messagePattern":"UnclaimIfAssignee: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1901,"sourceCode":"\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}\n\treturn nil\n}\n","sourceCodeStart":1883,"sourceCodeEnd":1919,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1883-L1919","documentation":"Wrapped error from UnclaimIfAssignee: issueRepo.UnclaimIssueIfAssignee failed and is prefixed with \"UnclaimIfAssignee: \". Because this is the compare-and-swap path, a very common cause is that the issue is no longer assigned to expectedAssignee (the CAS condition failed), in addition to ordinary storage failures.","triggerScenarios":"Calling UnclaimIfAssignee(ctx, id, actor, expectedAssignee) when the repo CAS fails: assignee changed since read, issue deleted, concurrent writer, or database error.","commonSituations":"Multi-agent workflows where another actor reassigned or unclaimed the issue between read and release; stale expectedAssignee values cached from an earlier Show call.","solutions":["Unwrap the error and check for an assignee-mismatch/conflict cause","Re-fetch the issue, refresh expectedAssignee, and retry the CAS","Fall back to plain Unclaim with force if you intentionally want to override","Check DB connectivity if the wrapped error is a storage failure"],"exampleFix":"// before\nif err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil { return err }\n// after\nif err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil {\n\tif isAssigneeConflict(err) {\n\t\tissue, _ := uc.Show(ctx, id)\n\t\texpected = issue.Assignee\n\t\treturn uc.UnclaimIfAssignee(ctx, id, actor, expected)\n\t}\n\treturn err\n}","handlingStrategy":"retry","validationCode":"issue, err := uc.Show(ctx, id)\nif err != nil || issue == nil || issue.Assignee != expectedAssignee {\n\treturn fmt.Errorf(\"issue %s no longer assigned to %s; refresh before CAS unclaim\", id, expectedAssignee)\n}","typeGuard":null,"tryCatchPattern":"err := uc.UnclaimIfAssignee(ctx, id, actor, expected)\nif err != nil {\n\tif isAssigneeConflict(err) {\n\t\ttime.Sleep(backoff)\n\t\treturn uc.UnclaimIfAssignee(ctx, id, actor, refetchAssignee(ctx, id))\n\t}\n\treturn err\n}","preventionTips":["Re-fetch expectedAssignee right before the CAS call; never cache it long","Bound CAS retries with a small attempt count and backoff","Handle the 'assignee changed' outcome as a normal event, not an exceptional failure"],"tags":["storage","error-wrapping","compare-and-swap","claim"],"backgroundTag":"repo-error-wrapping","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}