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

  1. Unwrap the error to identify the repository-level cause (not-found vs. conflict vs. connectivity)
  2. Verify the issue exists and inspect its current assignee before unclaiming
  3. Use UnclaimIfAssignee for compare-and-swap semantics if races are the problem
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/f6fa4dbfaa491f56. Report an issue: GitHub.