gastownhall/beads · error
failed to get current claim state
Error message
failed to get current claim state: %w
What it means
When the claim CAS affects 0 rows, ClaimIssueInTx reads the issue's current assignee/status inside the same transaction to decide between idempotent success and a refusal. This error means that state read (SELECT assignee, status) failed, so the claim cannot be disambiguated. The claim is abandoned and the underlying SQL error is wrapped.
Solutions
- Retry the claim; the row may just have been deleted/raced
- Inspect the wrapped driver error for connection/context issues
- If the issue no longer exists, refresh the issue list (bd ready) before claiming
Defensive patterns
Strategy: retry
Validate before calling
// confirm the issue still exists before claiming
iss, err := GetIssueInTx(ctx, tx, id)
if err != nil || iss == nil { skip } Try / catch
res, err := ClaimIssueInTx(ctx, tx, id, actor)
if err != nil {
var conflict *publicops.ClaimConflictError
if errors.As(err, &conflict) { return handleConflict(conflict) }
return retryClaim(ctx, id, actor) // transient read failure path
} Prevention
- Keep claim transactions short to shrink the race window
- Refresh issue lists (bd ready) after repeated failures — the row may be gone
- Never claim in a loop with a canceled/expiring context
When it happens
Trigger: rowsAffected == 0 and readClaimStateInTx's SELECT fails — e.g. row deleted concurrently between the read and the CAS, connection error, or context canceled mid-transaction.
Common situations: Racing agents where one closes/deletes the issue while another claims it; a canceled CLI context (Ctrl-C) hitting the follow-up SELECT; DB connectivity loss during a burst of claims.
Related errors
- get claimed issue
- ErrNotClaimable
- failed to claim issue
- failed to record claim event
- add deps: classify sources
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/9d8095d5919c7c75.
Report an issue: GitHub.
Appendix: source
Thrown at internal/storage/issueops/claim.go:141
result, err = tx.ExecContext(ctx, fmt.Sprintf(`
UPDATE %s
SET assignee = ?, status = 'in_progress', updated_at = ?, %s
WHERE id = ? AND row_lock = ? AND status IN (%s)
`, issueTable, rowLockClause, statusPlaceholders), args...)
}
if err != nil {
return nil, fmt.Errorf("failed to claim issue: %w", err)
}
rowsAffected, err = result.RowsAffected()
if err != nil {
return nil, fmt.Errorf("failed to get rows affected: %w", err)
}
}
if rowsAffected == 0 {
assignee, currentStatus, err := readClaimStateInTx(ctx, tx, issueTable, id)
if err != nil {
return nil, fmt.Errorf("failed to get current claim state: %w", err)
}
// Idempotent: if already claimed in_progress by the same actor —
// including a spelling difference across layers (ga-wzl83) — treat as
// success. This supports agent retry workflows where claim may be
// called multiple times after transient failures (GH#8).
if actorMatches(assignee, actor) && currentStatus == types.StatusInProgress {
return &ClaimResult{OldIssue: oldIssue, IsWisp: isWisp}, nil
}
// The refusal carries the state that lost the CAS, read just above in
// THIS transaction, so a caller learns who won without parsing the
// message. The typed wrapper carries the fields; the PROSE is composed
// here, because ClaimConflictError.Error() passes its wrapped refusal
// through byte-for-byte — a bare sentinel would reach the caller as
// "issue already claimed" with the holder dropped and
// beads.ParseClaimConflict unable to recover it. The fragments are the
// storage layer's exported ones, which is what keeps the parser and
// this producer in step. The sentinel stays matchable through both
// wraps, which errors.Is, ParseClaimConflict and the proxied batchView on GitHub (pinned to 71377f2769)