gastownhall/beads · error

db: Claim %s: read old issue: %w

Error message

db: Claim %s: read old issue: %w

What it means

This error wraps a failure to read the current (pre-claim) issue via Get at the start of Claim. Claim needs the old issue snapshot to validate claimability and journal old/new values; if Get fails, Claim aborts with this wrapped error.

Source

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

	default:
		return ""
	}
}

func (r *issueSQLRepositoryImpl) Claim(ctx context.Context, id, actor string, opts domain.IssueTableOpts) (domain.ClaimRowResult, error) {
	if id == "" {
		return domain.ClaimRowResult{}, errors.New("db: Claim: id must not be empty")
	}
	// The CAS below writes assignee = actor. actor is user-settable (--actor /
	// BEADS_ACTOR), so bound it against the VARCHAR(255) assignee column up front
	// and return a typed ErrFieldTooLong rather than a raw backend error.
	if err := types.CheckFieldLen("actor", actor); err != nil {
		return domain.ClaimRowResult{}, err
	}

	oldIssue, err := r.Get(ctx, id, opts)
	if err != nil {
		return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: read old issue: %w", id, err)
	}

	table := pickIssueTable(opts.UseWispsTable)
	now := time.Now().UTC()
	startedWasZero := oldIssue.StartedAt == nil

	// Rewrite row_lock exactly like the primary claim path (issueops.
	// ClaimIssueInTx). Without this, a claim made through the proxied-server
	// (uow) path leaves row_lock unchanged — open to the cell-merge bug the
	// row_lock invariant guards against (see issueops/lease.go). The lease
	// itself is granted into the ephemeral leases table below, after the CAS.
	rowLockClause, rowLockArgs := issueops.RowLockClause()

	// Mirror the primary path's pool-aware predicate (bd-bguz6): aliases in
	// the claim.pools config are claimable by any actor. This dual must stay
	// in lockstep with issueops.ClaimIssueInTx — the lease comment above is
	// the scar from the last time it drifted.
	pools, err := issueops.ClaimPoolAliasesInTx(ctx, r.runner)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped cause: if ErrNotFound, verify the issue exists (bd show <id>).
  2. Fix id typos or stale references before claiming.
  3. Verify UseWispsTable routing matches where the issue lives.
  4. Retry if the cause is a transient connection/context error.

Example fix

// before
res, err := store.Claim(ctx, id, actor, opts) // fails with confusing wrapped error
// after
if _, err := store.Get(ctx, id, opts); err != nil {
    return fmt.Errorf("issue %s not claimable: %w", id, err)
}
res, err := store.Claim(ctx, id, actor, opts)
Defensive patterns

Strategy: validation

Validate before calling

issue, err := store.Get(ctx, id, opts)
if err != nil { return err } // fail fast: nonexistent id or bad routing
_ = issue

Type guard

func claimable(ctx context.Context, s Store, id string, opts Options) bool {
    _, err := s.Get(ctx, id, opts)
    return err == nil
}

Try / catch

res, err := store.Claim(ctx, id, actor, opts)
if err != nil {
    if errors.Is(err, domain.ErrNotFound) { /* refresh issue list, skip id */ }
    return err
}

Prevention

When it happens

Trigger: Calling Claim(id, actor, opts) for a nonexistent id (ErrNotFound from Get), with wrong UseWispsTable routing, or when the underlying read query fails (connection error, context canceled).

Common situations: Claiming a deleted or mistyped issue id; stale ids from another repo or cache; database unavailable; wisp vs issues table mismatch.

Related errors


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