gastownhall/beads · error

delete wisp from dependencies

Error message

delete wisp %s from dependencies: %w

What it means

Wraps a failed DELETE FROM dependencies WHERE depends_on_wisp_id = ? while DeleteWispFromDependenciesInTx removes all dependency edges that point at a deleted wisp. Called from deleteIssueRowInTx during issue deletion; a failure here aborts the deletion transaction so no dangling wisp references remain.

Solutions

  1. Inspect the wrapped cause (lock timeout vs connection loss) and address it
  2. Retry the deletion after concurrent transactions complete; the tx is atomic
  3. Check for long-running transactions holding locks on the dependencies table
  4. Batch large deletions to avoid long transactions timing out

Example fix

// before
err := issueops.DeleteWispFromDependenciesInTx(ctx, tx, wispID)
// after: retry transient failures before aborting the whole delete
err := issueops.DeleteWispFromDependenciesInTx(ctx, tx, wispID)
if err != nil && isLockWaitTimeout(err) {
	return retryWithBackoff(func() error {
		return deleteIssueWithDeps(ctx, issueID)
	})
}
Defensive patterns

Strategy: retry

Validate before calling

// check the wisp has references and DB is reachable before deleting
refs := countDepsOnWisp(ctx, db, wispID)
if err := db.PingContext(ctx); err != nil {
	return fmt.Errorf("db unreachable, defer delete of %s", wispID)
}

Type guard

func isWispCleanupFailure(err error) bool {
	return err != nil && strings.Contains(err.Error(), "delete wisp ") && strings.Contains(err.Error(), "from dependencies")
}

Try / catch

err := deleteIssueRowInTx(ctx, tx, issueID)
if err != nil && isLockWaitTimeout(err) {
	return retryWithBackoff(func() error { return deleteIssue(ctx, issueID) })
}

Prevention

When it happens

Trigger: Deleting an issue whose wisp is referenced in the dependencies table when the DELETE fails — DB connection loss, lock held by a concurrent reader/writer, or transaction timeout on a large dependencies table.

Common situations: Deleting issues during a sync/import while other connections hold locks; Dolt conflicts between the delete and concurrent dependency writes; network blips mid-transaction.

Related errors


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

Appendix: source

Thrown at internal/storage/issueops/dependencies.go:581

			SELECT ?
			UNION
			SELECT d.parent_id
			FROM ancestors a
			JOIN (%s) d ON d.issue_id = a.node
		)
		SELECT COUNT(*) FROM ancestors WHERE node = ?
	`, strings.Join(unions, " UNION "))
	var n int
	if err := tx.QueryRowContext(ctx, query, node, candidate).Scan(&n); err != nil {
		return false, err
	}
	return n > 0, nil
}

func DeleteWispFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispID string) error {
	if _, err := tx.ExecContext(ctx,
		"DELETE FROM dependencies WHERE depends_on_wisp_id = ?", wispID); err != nil {
		return fmt.Errorf("delete wisp %s from dependencies: %w", wispID, err)
	}
	return nil
}

//nolint:gosec // G201: inClause contains only ? placeholders
func DeleteWispsFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispIDs []string) error {
	if len(wispIDs) == 0 {
		return nil
	}
	inClause, args := buildSQLInClause(wispIDs)
	if _, err := tx.ExecContext(ctx,
		fmt.Sprintf("DELETE FROM dependencies WHERE depends_on_wisp_id IN (%s)", inClause),
		args...); err != nil {
		return fmt.Errorf("delete wisps from dependencies: %w", err)
	}
	return nil
}

View on GitHub (pinned to 71377f2769)