gastownhall/beads · error
UnclaimIfAssignee: id must not be empty
Error message
UnclaimIfAssignee: id must not be empty
What it means
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.
Source
Thrown at internal/storage/domain/issue.go:1898
return fmt.Errorf("Unclaim: id must not be empty")
}
if err := u.issueRepo.UnclaimIssue(ctx, id, actor, force); err != nil {
return fmt.Errorf("Unclaim: %w", err)
}
return nil
}
// UnclaimIfAssignee is the compare-and-swap release: it clears the claim only
// while the issue is still assigned to expectedAssignee, and otherwise returns
// storage.ErrAssigneeMismatch having written nothing. It is the conditional
// twin of Unclaim and runs the SAME transition (assignee cleared, status
// reopened, started_at cleared, lease dropped, row_lock rewritten, "unclaimed"
// event recorded) because both reach the one classic implementation in
// issueops — which is what makes `bd unclaim --if-assignee` behave identically
// on the proxied-server and embedded backends.
func (u *issueUseCaseImpl) UnclaimIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {
if id == "" {
return fmt.Errorf("UnclaimIfAssignee: id must not be empty")
}
if err := u.issueRepo.UnclaimIssueIfAssignee(ctx, id, actor, expectedAssignee); err != nil {
return fmt.Errorf("UnclaimIfAssignee: %w", err)
}
return nil
}
// Heartbeat refreshes the lease on an issue actor holds in_progress. The
// write touches ONLY the ephemeral leases table (bd-lrgn1), so the caller
// must run it under uow.RunTxEphemeral's no-Dolt-commit form — a heartbeat
// mints no Dolt commit and no history in any mode (bd-aq0ql).
func (u *issueUseCaseImpl) Heartbeat(ctx context.Context, id, actor string) error {
if id == "" {
return fmt.Errorf("Heartbeat: id must not be empty")
}
if err := u.issueRepo.HeartbeatIssue(ctx, id, actor); err != nil {
return fmt.Errorf("Heartbeat: %w", err)
}View on GitHub (pinned to 71377f2769)
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
Example fix
// before
err := uc.UnclaimIfAssignee(ctx, id, actor, assignee)
// after
if id == "" {
return fmt.Errorf("cannot unclaim: issue id is empty")
}
err := uc.UnclaimIfAssignee(ctx, id, actor, assignee) Defensive patterns
Strategy: validation
Validate before calling
if id == "" || expectedAssignee == "" {
return fmt.Errorf("CAS unclaim requires non-empty id and expectedAssignee")
} Try / catch
if err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil {
if strings.Contains(err.Error(), "id must not be empty") {
return fmt.Errorf("caller bug: empty id in CAS unclaim")
}
return err
} Prevention
- 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
When it happens
Trigger: Calling UnclaimIfAssignee(ctx, "", actor, expectedAssignee) — the id variable is empty, usually from an unparsed or missing upstream value.
Common situations: Automation scripts where the issue key resolution step silently produced an empty string; wiring bugs where the wrong variable is passed.
Related errors
- Unclaim: id must not be empty
- UnclaimIfAssignee: %w
- %w: claim cannot use expected assignee or status
- ErrNotClaimable
- %w: claim next requires an actor
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6557d804be3a426b.
Report an issue: GitHub.