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

  1. Resolve and validate the issue id before the CAS unclaim
  2. Check that the variable bound to the id is the one populated by your lookup step
  3. 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

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


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