gastownhall/beads · error

failed to get current claim state: %w

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.

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 batch

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the claim; the row may just have been deleted/raced
  2. Inspect the wrapped driver error for connection/context issues
  3. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/9d8095d5919c7c75. Report an issue: GitHub.