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
- Check the wrapped cause: if ErrNotFound, verify the issue exists (bd show <id>).
- Fix id typos or stale references before claiming.
- Verify UseWispsTable routing matches where the issue lives.
- 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
- Always verify the id exists before claiming (bd show <id>).
- Pass the same opts (UseWispsTable) used when the issue was created.
- Refresh local caches of issue ids before claim loops.
- Handle ErrNotFound explicitly rather than retrying blindly.
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
- %w: issue %s
- db: Update %s: %w
- db: Claim %s: resolve claim pools: %w
- db: Claim %s: resolve claimable statuses: %w
- db: Claim %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/285a793d86487950.
Report an issue: GitHub.