gastownhall/beads · warning · storage.ErrAssigneeMismatch
%w: %s is held by %q, expected %q
Error message
%w: %s is held by %q, expected %q
What it means
This is the assignee-mismatch branch of the CAS check: the caller declared an expected assignee, but the row's current assignee does not match (per actorMatches). The library returns storage.ErrAssigneeMismatch wrapped with actual vs expected values so callers can detect a lost update race and re-acquire.
Source
Thrown at internal/storage/issueops/update_cas.go:53
if expectedAssignee == nil && expectedStatus == nil {
return nil
}
isWisp := IsActiveWispInTx(ctx, tx, id)
issueTable, _, _, _ := WispTableRouting(isWisp)
var assignee sql.NullString
var status string
err := tx.QueryRowContext(ctx,
fmt.Sprintf("SELECT assignee, status FROM %s WHERE id = ?", issueTable), id,
).Scan(&assignee, &status)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
}
if err != nil {
return fmt.Errorf("failed to read assignee/status for %s: %w", id, err)
}
if expectedAssignee != nil && !actorMatches(assignee.String, *expectedAssignee) {
return fmt.Errorf("%w: %s is held by %q, expected %q", storage.ErrAssigneeMismatch, id, assignee.String, *expectedAssignee)
}
if expectedStatus != nil && status != *expectedStatus {
return fmt.Errorf("%w: %s has status %q, expected %q", storage.ErrStatusMismatch, id, status, *expectedStatus)
}
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Treat errors.Is(err, storage.ErrAssigneeMismatch) as 'someone else owns it' — skip or re-fetch instead of retrying blindly
- Re-read the issue, and only retry if the new assignee is acceptable (e.g. claim after release)
- Use CAS on a less contended field or rely on row-version checks when assignee semantics are not required
- Surface the actual holder (in the error text) to the user/agent so it can coordinate
Example fix
// before
if err := ExecuteUpdate(...); err != nil { return err } // treats contention as fatal
// after
var amErr *storage.AssigneeMismatch
if errors.As(err, &amErr) || errors.Is(err, storage.ErrAssigneeMismatch) {
log.Printf("issue %s already claimed: %v", id, err)
return nil // pick another issue
} Defensive patterns
Strategy: fallback
Validate before calling
iss, _ := store.GetIssue(ctx, id)
if expAssignee != nil && iss.Assignee != *expAssignee { return fmt.Errorf("pre-check: held by %q", iss.Assignee) } Try / catch
err := ExecuteUpdate(ctx, tx, id, updates, exp)
if errors.Is(err, storage.ErrAssigneeMismatch) {
return pickAnotherIssue(ctx) // someone else holds it
} Prevention
- Design claim flows to skip on ErrAssigneeMismatch rather than retry-spam
- Re-read the issue after mismatch and coordinate with the listed holder
- Prefer short claim-release cycles to reduce contention windows
When it happens
Trigger: ExecuteUpdate with ExpectedFields.Assignee set while another actor holds the issue — e.g. two agents both claim the same issue, or a human assigned it between fetch and write.
Common situations: Concurrent agents racing to claim issues from a shared queue; a stale local view after another session reassigned the issue; automation expecting 'unassigned' on an already-claimed issue.
Related errors
- %w: %s has status %q, expected %q
- %w: issue %s is assigned to %q
- %w: issue %s
- ErrLockHeld
- server: ExternalDoltServer.Start: server already started
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a21a83515a089f75.
Report an issue: GitHub.