gastownhall/beads · error
UnclaimIfAssignee: %w
Error message
UnclaimIfAssignee: %w
What it means
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.
Source
Thrown at internal/storage/domain/issue.go:1901
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)
}
return nil
}
View on GitHub (pinned to 71377f2769)
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
Example fix
// before
if err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil { return err }
// after
if err := uc.UnclaimIfAssignee(ctx, id, actor, expected); err != nil {
if isAssigneeConflict(err) {
issue, _ := uc.Show(ctx, id)
expected = issue.Assignee
return uc.UnclaimIfAssignee(ctx, id, actor, expected)
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
issue, err := uc.Show(ctx, id)
if err != nil || issue == nil || issue.Assignee != expectedAssignee {
return fmt.Errorf("issue %s no longer assigned to %s; refresh before CAS unclaim", id, expectedAssignee)
} Try / catch
err := uc.UnclaimIfAssignee(ctx, id, actor, expected)
if err != nil {
if isAssigneeConflict(err) {
time.Sleep(backoff)
return uc.UnclaimIfAssignee(ctx, id, actor, refetchAssignee(ctx, id))
}
return err
} Prevention
- 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
When it happens
Trigger: Calling UnclaimIfAssignee(ctx, id, actor, expectedAssignee) when the repo CAS fails: assignee changed since read, issue deleted, concurrent writer, or database error.
Common situations: Multi-agent workflows where another actor reassigned or unclaimed the issue between read and release; stale expectedAssignee values cached from an earlier Show call.
Related errors
- Unclaim: %w
- not found
- load wisp labels: %w
- edge %d %s->%s: checking planned blocking cycle: %w
- reading existing dependencies for %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/99753bbc500e3918.
Report an issue: GitHub.