gastownhall/beads · error

%w: issue %s

Error message

%w: issue %s

What it means

resolveCommentPlaneInTx refuses to add a comment when the IssueID names neither an existing issue nor an active wisp in the same transaction. The refusal is deliberately typed — storage.ErrNotFound wrapped as "%w: issue %s" — so callers can classify with errors.Is instead of parsing prose from the insert itself. The existence probe runs in the caller's transaction to prevent a TOCTOU race where the anchor is deleted between the check and the insert.

Source

Thrown at internal/storage/issueops/commenter.go:80

// refusing an id that names neither an issue nor a wisp.
//
// The existence probe is here rather than left to the insert's own so the
// refusal is TYPED: AddIssueCommentInTx reports a missing anchor as prose, and
// a caller of this role classifies with errors.Is. It resolves the plane in
// the same transaction the insert runs in, so a comment cannot land on a row
// an earlier read saw and this one did not.
//
//nolint:gosec // G201: issueTable comes from WispTableRouting ("issues" or "wisps")
func resolveCommentPlaneInTx(ctx context.Context, tx *sql.Tx, issueID string) (string, error) {
	isWisp := IsActiveWispInTx(ctx, tx, issueID)
	issueTable, _, _, _ := WispTableRouting(isWisp)
	var exists bool
	if err := tx.QueryRowContext(ctx,
		fmt.Sprintf(`SELECT EXISTS(SELECT 1 FROM %s WHERE id = ?)`, issueTable), issueID).Scan(&exists); err != nil {
		return "", fmt.Errorf("check issue existence: %w", err)
	}
	if !exists {
		return "", fmt.Errorf("%w: issue %s", storage.ErrNotFound, issueID)
	}
	if isWisp {
		return "wisp_comments", nil
	}
	return "comments", nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the issue ID with bd show <id> (or an equivalent lookup) before commenting; fix typos in the ID.
  2. Handle errors.Is(err, storage.ErrNotFound) explicitly and skip/report the missing anchor rather than retrying.
  3. Re-sync or re-export your local data if the issue legitimately exists upstream but not locally.

Example fix

// before
if err := store.AddComment(ctx, req); err != nil {
	return err // blind failure
}

// after
if err := store.AddComment(ctx, req); err != nil {
	if errors.Is(err, storage.ErrNotFound) {
		log.Warnf("issue %s gone, skipping comment", req.IssueID)
		return nil
	}
	return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check (best effort; the typed error is authoritative)
if _, err := store.GetIssue(ctx, issueID); err != nil {
	return fmt.Errorf("cannot comment: %w", err)
}

Type guard

func isMissingIssue(err error) bool {
	return errors.Is(err, storage.ErrNotFound)
}

Try / catch

if err := store.AddComment(ctx, req); err != nil {
	if errors.Is(err, storage.ErrNotFound) {
		log.Warnf("issue %s not found; skipping comment", req.IssueID)
		return nil
	}
	return err
}

Prevention

When it happens

Trigger: Calling AddComment with a typo'd or deleted issue ID (bd comment bd-9999 "x"); commenting on an issue another process closed/deleted moments earlier; using an ID from a stale local export after a sync removed the issue.

Common situations: Automation following issue IDs scraped from logs or old tickets; scripts with hardcoded IDs after a database reset (bd init in a fresh clone); race between a cleanup job deleting wisps and a bot adding comments.

Related errors


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