gastownhall/beads · error
db: Claim %s: %w
Error message
db: Claim %s: %w
What it means
This error wraps a failure of the claim UPDATE statement itself — a conditional CAS update (UPDATE ... SET assignee=?, status='in_progress' WHERE id=? AND row_lock=? AND (<status predicate>)). Unlike a zero-row match (handled separately), this is an execution error of the statement, which aborts Claim.
Source
Thrown at internal/storage/domain/db/issue.go:485
//nolint:gosec // G201: table is one of two hardcoded constants
res, err = r.runner.ExecContext(ctx, fmt.Sprintf(`
UPDATE %s
SET assignee = ?, status = 'in_progress', updated_at = ?, started_at = ?, %s
WHERE id = ? AND row_lock = ? AND (%s)
`, table, rowLockClause, statusPredicate), args...)
} else {
args := append([]any{actor, now}, rowLockArgs...)
args = append(args, id, oldIssue.RowVersion)
args = append(args, statusArgs...)
//nolint:gosec // G201: table is one of two hardcoded constants
res, err = r.runner.ExecContext(ctx, fmt.Sprintf(`
UPDATE %s
SET assignee = ?, status = 'in_progress', updated_at = ?, %s
WHERE id = ? AND row_lock = ? AND (%s)
`, table, rowLockClause, statusPredicate), args...)
}
if err != nil {
return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: %w", id, err)
}
rows, err = res.RowsAffected()
if err != nil {
return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: rows affected: %w", id, err)
}
}
if rows == 0 {
var currentAssignee sql.NullString
var currentStatus types.Status
//nolint:gosec // G201: table is one of two hardcoded constants
if err := r.runner.QueryRowContext(ctx,
fmt.Sprintf("SELECT assignee, status FROM %s WHERE id = ?", table), id,
).Scan(¤tAssignee, ¤tStatus); err != nil {
return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: read current state: %w", id, err)
}
assignee := ""
if currentAssignee.Valid {View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause (%w): deadlock/timeout suggests retrying the claim.
- Retry the claim — it is a CAS and safe to re-attempt after transient failures.
- Check for another process holding a lease/lock on the issue row; wait or clear it.
- Validate custom status configuration so the generated status predicate is well-formed.
Example fix
// before
res, err := store.Claim(ctx, id, actor, opts) // fails on lock timeout
// after
for i := 0; i < 3; i++ {
res, err = store.Claim(ctx, id, actor, opts)
if err == nil || !isTransient(err) { break }
time.Sleep(100 * time.Millisecond << i)
} Defensive patterns
Strategy: retry
Validate before calling
issue, _ := store.Get(ctx, id, opts)
if issue.Assignee != "" && issue.Assignee != actor {
return fmt.Errorf("issue %s already assigned to %s", id, issue.Assignee)
} Try / catch
res, err := store.Claim(ctx, id, actor, opts)
if err != nil && isDeadlockOrTimeout(err) {
time.Sleep(200 * time.Millisecond)
res, err = store.Claim(ctx, id, actor, opts) // CAS is safe to retry
} Prevention
- Use exponential backoff for claim retries under contention.
- Check current assignee before claiming to reduce lost races.
- Respect existing leases; don't fight another active agent's claim.
- Serialize claims per issue within your own tooling.
When it happens
Trigger: Calling Claim when the UPDATE execution fails at the driver/SQL level: connection loss, deadlock/lock-wait timeout on the issue row, malformed generated status predicate, or context cancellation mid-statement.
Common situations: Heavy contention on the same issue row causing lock waits; database failover mid-claim; SQL errors from malformed custom status configuration; stale pooled connections.
Related errors
- db: Claim %s: read old issue: %w
- db: Claim %s: resolve claim pools: %w
- db: Claim %s: resolve claimable statuses: %w
- db: Claim %s: read current state: %w
- db: Claim %s: record event: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f4883c134430caaf.
Report an issue: GitHub.