gastownhall/beads · error
failed to unclaim issue: %w
Error message
failed to unclaim issue: %w
What it means
UnclaimIssueInTx wraps the driver's UPDATE error with this message. The UPDATE atomically clears assignee, resets status to open, and bumps the row lock guarded by the previously-read row version; a failure means the database refused or failed the write, and the transaction should roll back leaving the claim intact.
Source
Thrown at internal/storage/issueops/unclaim.go:86
// and rewrite row_lock. The predicate CASes on row_lock rather than
// assignee (ga-5ksp5): ownership was already authorized above (or bypassed
// by force) against the row read into oldIssue, and row_lock is rewritten
// by every path that mutates status/assignee/started_at (see the
// freshRowLock invariant in lease.go) — so requiring it to still equal
// oldIssue.RowVersion detects a claim that changed hands (or was released,
// or closed) between that read and this write exactly as precisely as the
// old `assignee = <actor>` predicate did, without embedding a
// spelling-sensitive string comparison in SQL. force does not exempt this
// check: force only widens WHO may unclaim, not whether the row is still
// the one we read.
result, err := tx.ExecContext(ctx, fmt.Sprintf(`
UPDATE %s
SET assignee = '', status = 'open', updated_at = ?,
started_at = NULL, row_lock = ?
WHERE id = ? AND status IN ('open', 'in_progress') AND row_lock = ?
`, issueTable), now, freshRowLock(), id, oldIssue.RowVersion)
if err != nil {
return fmt.Errorf("failed to unclaim issue: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
// The pre-checks passed, so a 0-row result means the row changed
// underneath us: re-read to disambiguate an ownership change from a
// status change. actorMatches, not verbatim, mirrors the precheck above.
current, gerr := GetIssueInTx(ctx, tx, id)
if gerr != nil {
return fmt.Errorf("failed to unclaim issue %s: no matching row", id)
}
if !force && !actorMatches(current.Assignee, actor) {
return fmt.Errorf("%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)",
storage.ErrNotOwner, id, current.Assignee)View on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped cause (errors.Unwrap/errors.Is) to identify the driver error class.
- Retry the whole unclaim operation with a fresh transaction on transient errors.
- Check database health/connectivity and storage backend resources.
- If persistent, inspect driver logs; never partially retry inside the same transaction.
Example fix
// before
err := store.ReleaseIssue(ctx, id) // one-shot, fails hard on transient DB error
// after
for i := 0; i < 3; i++ {
err = store.ReleaseIssue(ctx, id)
if err == nil || !isTransientDBErr(err) { break }
time.Sleep(backoff(i))
} Defensive patterns
Strategy: retry
Try / catch
if err := store.ReleaseIssue(ctx, id); err != nil {
if isTransientDBErr(err) { /* retry whole op with fresh tx */ }
return fmt.Errorf("release %s: %w", id, err)
} Prevention
- Keep transactions short to reduce deadlock/connection windows.
- Use bounded retry with backoff around whole operations, not partial statements.
- Monitor Dolt/database health and connection pool limits.
When it happens
Trigger: The UPDATE ... WHERE id = ? AND status IN ('open','in_progress') AND row_lock = ? statement fails: DB connection loss, aborted transaction, driver/constraint error, deadlock, or storage backend failure.
Common situations: Dolt/server connection dropped mid-transaction; lock contention causing deadlock aborts; disk-full or driver-level storage failures.
Related errors
- failed to get issue for unclaim: %w
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b0d229da7d4a8fe3.
Report an issue: GitHub.