gastownhall/beads · error
conditional unclaim of %s: expected assignee must not be emp
Error message
conditional unclaim of %s: expected assignee must not be empty (use UnclaimIssueInTx for an unconditional release)
What it means
UnclaimIssueIfAssigneeInTx performs a conditional (compare-and-swap) release, so it requires a non-empty expectedAssignee to compare against. An empty expectedAssignee would make the condition meaningless (and would 'match' the already-unclaimed state), so the library refuses it and points to UnclaimIssueInTx for an unconditional release.
Source
Thrown at internal/storage/issueops/unclaim.go:155
// still assigned to expectedAssignee — the compare-and-swap inverse of
// ClaimIssueInTx: a Go-side actorMatches precheck (ga-5ksp5) plus a conditional
// UPDATE CASed on row_lock, with RowsAffected as the verdict, so a stale
// releaser can never clobber a claim that has since moved to (or been
// re-taken by) someone else. "Still assigned to expectedAssignee" is judged
// under actorMatches, not verbatim equality, so a caller naming the current
// holder under a different layer's spelling of the same identity is a match,
// not a mismatch — see canonicalActor. On success it applies the same
// transition as UnclaimIssueInTx (assignee cleared, status reopened,
// started_at cleared, lease dropped, row_lock rewritten, "unclaimed" event
// recorded). When the current assignee does not match expectedAssignee —
// including when the issue is no longer assigned at all — it returns
// storage.ErrAssigneeMismatch naming the current holder and leaves the row
// untouched. actor is recorded as the event author.
//
//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func UnclaimIssueIfAssigneeInTx(ctx context.Context, tx DBTX, id string, actor string, expectedAssignee string) error {
if expectedAssignee == "" {
return fmt.Errorf("conditional unclaim of %s: expected assignee must not be empty (use UnclaimIssueInTx for an unconditional release)", id)
}
// Route to the correct table (issues/wisps) automatically, matching
// UnclaimIssueInTx.
isWisp := IsActiveWispInTx(ctx, tx, id)
issueTable, _, eventTable, _ := WispTableRouting(isWisp)
oldIssue, err := GetIssueInTx(ctx, tx, id)
if err != nil {
return fmt.Errorf("failed to get issue for unclaim: %w", err)
}
// Validate: cannot unclaim closed issues.
if oldIssue.Status == types.StatusClosed {
return fmt.Errorf("cannot unclaim closed issue %s", id)
}
// Compare-and-swap precheck: a mismatched holder — including anView on GitHub (pinned to 71377f2769)
Solutions
- Populate expectedAssignee with the holder you expect before calling.
- If you want an unconditional release, call UnclaimIssueInTx(ctx, tx, id, actor, force) instead.
- Guard the call site: skip the conditional unclaim when the expected assignee is unknown.
Example fix
// before
err := issueops.UnclaimIssueIfAssigneeInTx(ctx, tx, id, actor, expected) // expected == ""
// after
if expected == "" {
err = issueops.UnclaimIssueInTx(ctx, tx, id, actor, false)
} else {
err = issueops.UnclaimIssueIfAssigneeInTx(ctx, tx, id, actor, expected)
} Defensive patterns
Strategy: validation
Validate before calling
if expectedAssignee == "" {
return issueops.UnclaimIssueInTx(ctx, tx, id, actor, force) // unconditional release
} Type guard
func validExpectedAssignee(s string) bool { return strings.TrimSpace(s) != "" } Prevention
- Never call UnclaimIssueIfAssigneeInTx with an empty expectedAssignee.
- Use UnclaimIssueInTx when you do not know or care who holds the claim.
- Validate the expected holder at the call site before invoking conditional releases.
- Fail fast if the expectedAssignee source (config/state) returns an empty value.
When it happens
Trigger: Calling UnclaimIssueIfAssigneeInTx(ctx, tx, id, actor, "") — e.g. the expectedAssignee variable was never populated, or the caller actually wanted an unconditional release and used the wrong function.
Common situations: A caller copies the expected assignee from a field that is empty (already-released issue); a refactor swaps UnclaimIssueIfAssigneeInTx in where UnclaimIssueInTx was intended; config/env supplying the expected holder is missing.
Related errors
- no store is open for this workspace
- not found
- proxy.ForceStopUnverified: at most one options value is allo
- db: ChildCounterSQLRepository.NextChildID: parentID must not
- db: DependencySQLRepository.Insert: dep must not be nil
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f78d42879457f5a9.
Report an issue: GitHub.