gastownhall/beads · error
%w: issue %s
Error message
%w: issue %s
What it means
CloseIssueCheckedInTx wraps storage.ErrNotFound when the target id exists in neither the issues nor wisps table (isClosedInTx probes both and found=false). The checked close cannot apply policy to a nonexistent target, so it refuses before any write.
Source
Thrown at internal/storage/issueops/close.go:80
// transaction rolls back with the issue unchanged (a true compare-and-swap).
// row_lock only tracks lifecycle/ownership writes (status, assignee, started_at),
// so this guards against a concurrent lifecycle change — not against concurrent
// label, dependency, rename, or is_blocked writes that leave row_lock untouched
// (see the freshRowLock invariant in lease.go).
func CloseIssueCheckedInTx(ctx context.Context, tx DBTX, id, reason, actor, session string, force bool, expectedVersion *int64) (*CloseResult, error) {
if expectedVersion != nil {
if err := CheckVersionInTx(ctx, tx, id, *expectedVersion); err != nil {
return nil, err
}
}
// Read the target before policy checks. A closed target can still have open
// children, which refuses without Force just like an open target.
closed, targetColumn, found, err := isClosedInTx(ctx, tx, id)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
}
// domain/db also supplies a *sql.DB Runner, whose independently pooled
// statements cannot retain a savepoint. The shared UOW uses a pinned
// *sql.Conn after START TRANSACTION, while embedded callers use *sql.Tx.
if !closeCheckedSavepointEligible(tx) {
return closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)
}
savepoint, err := createCloseCheckedSavepoint(ctx, tx)
if err != nil {
return nil, err
}
result, scopedErr := closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)
if scopedErr != nil {
if cleanupErr := rollbackAndReleaseCloseCheckedSavepoint(ctx, tx, savepoint); cleanupErr != nil {
return nil, fmt.Errorf("discard checked close savepoint after %v: %w", scopedErr, cleanupErr)
}
return nil, scopedErr
}View on GitHub (pinned to 71377f2769)
Solutions
- Verify the id with bd show <id> or a listing before closing
- Check you are operating against the intended database (right repo/path, right remote)
- Handle errors.Is(err, storage.ErrNotFound) in the caller and skip or report the id rather than retrying
Example fix
err := store.CloseIssue(ctx, id, reason, actor)
if errors.Is(err, storage.ErrNotFound) {
log.Printf("skipping %s: not found in this database", id)
return nil // do not retry
} Defensive patterns
Strategy: try-catch
Validate before calling
func issueExists(ctx context.Context, r storage.Reader, id string) bool {
_, err := r.GetIssue(ctx, id)
return !errors.Is(err, storage.ErrNotFound)
} Try / catch
err := store.CloseIssue(ctx, id, reason, actor)
switch {
case errors.Is(err, storage.ErrNotFound):
log.Printf("%s not found; skipping", id)
return nil
case err != nil:
return err
} Prevention
- Fetch ids from the same database you close against
- Use errors.Is(err, storage.ErrNotFound) rather than string matching
- Skip-not-fail in batch close scripts so one stale id does not abort the run
When it happens
Trigger: Calling CloseIssueCheckedInTx (via ExecuteClose / bd close) with an id that is misspelled, already fully deleted, or present only in a table this database does not contain.
Common situations: Stale ids from an old export or another database; typo in a shell script; an issue deleted by a concurrent actor between listing and closing; compacted/purged wisps referenced by id.
Related errors
- no store is open for this workspace
- gate not found: %s
- close: id must not be empty
- close: actor must not be empty
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a03a3f0466a1c54e.
Report an issue: GitHub.