gastownhall/beads · error

db: Claim %s: read current state: %w

Error message

db: Claim %s: read current state: %w

What it means

This error wraps a failure to re-read the issue's current assignee/status after the claim CAS matched zero rows (the lost-race path). Claim uses this SELECT to build a ClaimRowResult describing who actually holds the issue; if the read fails, Claim aborts with this wrapped error.

Source

Thrown at internal/storage/domain/db/issue.go:500

			`, 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(&currentAssignee, &currentStatus); err != nil {
			return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: read current state: %w", id, err)
		}
		assignee := ""
		if currentAssignee.Valid {
			assignee = currentAssignee.String
		}
		return domain.ClaimRowResult{
			Updated:               false,
			CurrentAssignee:       assignee,
			CurrentAssigneeIsPool: slices.Contains(pools, assignee),
			CurrentStatus:         currentStatus,
			StartedAtWasZero:      startedWasZero,
			OldIssue:              oldIssue,
		}, nil
	}

	// Grant the lease in the ephemeral leases table, mirroring
	// issueops.ClaimIssueInTx. Wisps are never leased. This dual must stay in
	// lockstep with the primary path (see the row_lock comment above).

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause: if ErrNotFound, the issue was deleted concurrently — refresh the issue list.
  2. Retry the claim against current state.
  3. Check for concurrent deletion workflows colliding with claims.
  4. Verify connection stability if the cause is a network error.

Example fix

// before
res, err := store.Claim(ctx, id, actor, opts) // lost race; opaque read failure
// after
res, err := store.Claim(ctx, id, actor, opts)
if err != nil && res.CurrentAssignee != "" {
    log.Printf("issue %s held by %s", id, res.CurrentAssignee)
}
Defensive patterns

Strategy: try-catch

Validate before calling

issue, err := store.Get(ctx, id, opts)
if err != nil { return err }
if issue.Assignee != "" && issue.Assignee != actor { return fmt.Errorf("taken by %s", issue.Assignee) }

Type guard

func claimLostRace(err error, res domain.ClaimRowResult) bool {
    return res.CurrentAssignee != "" && res.CurrentAssignee != actor
}

Try / catch

res, err := store.Claim(ctx, id, actor, opts)
if err != nil && strings.Contains(err.Error(), "read current state") {
    // lost race or concurrent delete; re-fetch and decide
    fresh, gerr := store.Get(ctx, id, opts)
    if gerr != nil { return gerr }
    _ = fresh
}

Prevention

When it happens

Trigger: Calling Claim where another actor won the race (rows == 0) and the follow-up SELECT assignee, status ... WHERE id = ? fails — connection loss, context cancellation, or the issue being deleted between the UPDATE and SELECT.

Common situations: Two agents claiming the same issue concurrently; issue deleted by another process between CAS and re-read; transient network errors to a remote database.

Related errors


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