gastownhall/beads · error
Unclaim: %w
Error message
Unclaim: %w
What it means
Wrapped error from Unclaim: the underlying issueRepo.UnclaimIssue call failed and is returned with an "Unclaim: " prefix. At this point the empty-id validation has passed; the failure originates in the storage layer while releasing an issue claim.
Source
Thrown at internal/storage/domain/issue.go:1883
return nil, fmt.Errorf("GetStaleIssues: %w", err)
}
return out, nil
}
func (u *issueUseCaseImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {
out, err := u.issueRepo.GetEpicsEligibleForClosure(ctx)
if err != nil {
return nil, fmt.Errorf("GetEpicsEligibleForClosure: %w", err)
}
return out, nil
}
func (u *issueUseCaseImpl) Unclaim(ctx context.Context, id, actor string, force bool) error {
if id == "" {
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)View on GitHub (pinned to 71377f2769)
Solutions
- Unwrap the error to identify the repository-level cause (not-found vs. conflict vs. connectivity)
- Verify the issue exists and inspect its current assignee before unclaiming
- Use UnclaimIfAssignee for compare-and-swap semantics if races are the problem
- Retry on transient storage errors
Example fix
// before
if err := uc.Unclaim(ctx, id, actor, true); err != nil { return err }
// after
if err := uc.Unclaim(ctx, id, actor, true); err != nil {
var nf types.ErrNotFound
if errors.As(err, &nf) { return fmt.Errorf("issue %s gone", id) }
return fmt.Errorf("unclaim %s: %w", id, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
issue, err := uc.Show(ctx, id)
if err != nil || issue == nil {
return fmt.Errorf("issue %s not found, skipping unclaim", id)
} Try / catch
if err := uc.Unclaim(ctx, id, actor, force); err != nil {
var nf types.ErrNotFound
if errors.As(err, &nf) { return nil /* already gone */ }
return fmt.Errorf("unclaim %s: %w", id, err)
} Prevention
- Check the issue's current assignee before unclaiming
- Prefer UnclaimIfAssignee when multiple actors may race
- Treat not-found as success in idempotent cleanup loops
When it happens
Trigger: Calling Unclaim(ctx, id, actor, force) where issueRepo.UnclaimIssue fails: the issue does not exist, the actor does not hold the claim, concurrent modification, or a database error.
Common situations: Two agents racing to unclaim/update the same issue; unclaiming an issue that was already reassigned or deleted; transient DB outages during CI automation.
Related errors
- UnclaimIfAssignee: %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/f6fa4dbfaa491f56.
Report an issue: GitHub.