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

  1. Verify the id with bd show <id> or a listing before closing
  2. Check you are operating against the intended database (right repo/path, right remote)
  3. 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

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


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