gastownhall/beads · error

cascade traversal discovered over %d issues; aborting to pre

Error message

cascade traversal discovered over %d issues; aborting to prevent runaway deletion

What it means

FindAllDependentsInTx performs a breadth-first traversal of the dependency graph to find everything that would be cascade-deleted. If the traversal discovers more than maxRecursiveResults distinct issues, it aborts with this error to prevent a runaway (possibly cyclic or hub-dependent) cascade from deleting a huge portion of the database.

Source

Thrown at internal/storage/issueops/delete.go:417

}

// findAllDependentsRecursiveInTx finds all issues that depend on the given
// issues, recursively. Uses batched IN-clause queries. Traversal is capped
// at maxRecursiveResults total discovered IDs.
//
//nolint:gosec // G201: inClause contains only ? placeholders
func FindAllDependentsInTx(ctx context.Context, tx DBTX, ids []string) (map[string]bool, error) {
	result := make(map[string]bool)
	for _, id := range ids {
		result[id] = true
	}

	toProcess := make([]string, len(ids))
	copy(toProcess, ids)

	for len(toProcess) > 0 {
		if len(result) > maxRecursiveResults {
			return nil, fmt.Errorf("cascade traversal discovered over %d issues; aborting to prevent runaway deletion", maxRecursiveResults)
		}
		batchEnd := deleteBatchSize
		if batchEnd > len(toProcess) {
			batchEnd = len(toProcess)
		}
		batch := toProcess[:batchEnd]
		toProcess = toProcess[batchEnd:]

		inClause, args := buildSQLInClause(batch)
		for _, depTable := range []string{"dependencies", "wisp_dependencies"} {
			rows, err := tx.QueryContext(ctx,
				fmt.Sprintf(`SELECT issue_id FROM %s WHERE %s`, depTable, depTargetIn("", inClause)),
				args...)
			if err != nil {
				if optionalBlockedTable(depTable) && isTableNotExistError(err) {
					continue
				}
				return nil, fmt.Errorf("query dependents for batch from %s: %w", depTable, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete in smaller pieces — delete leaf dependents first instead of one big cascade
  2. Inspect the dependency graph (bd dep tree / bd blocked) to find the hub or cycle and break it
  3. If the set is legitimately large and intended, delete in batches scoped by explicit ID lists
  4. Report/fix the cycle if one exists; cycles should not occur in a healthy DAG

Example fix

// before
bd delete bd-1 --cascade  // aborts: >maxRecursiveResults
// after — prune from the leaves
bd dep tree bd-1          # inspect scope
bd delete bd-101 bd-102 --cascade
bd delete bd-1 --cascade
Defensive patterns

Strategy: validation

Validate before calling

// pre-check cascade size before attempting delete
func cascadeSize(ctx, tx, ids) int {
	// count reachable dependents via bd dep tree or iterative queries
}
// if cascadeSize > maxRecursiveResults { delete in smaller batches instead }

Try / catch

_, err := DeleteIssuesInTx(ctx, tx, ids, WithCascade())
if err != nil && strings.Contains(err.Error(), "aborting to prevent runaway deletion") {
	// fall back to scoped deletes
	return deleteInChunks(ctx, ids)
}
return err

Prevention

When it happens

Trigger: ResolveDeletionSetInTx invoked with --cascade on an issue that transitively depends on more than maxRecursiveResults issues — e.g. deleting a root blocking hundreds of chained beads, or a dependency cycle inflating the reachable set.

Common situations: Deleting a top-level epic/milestone whose entire subtree exceeds the cap; accidental cycles in dependencies making the reachable set appear unbounded; importing a dependency graph with a hub issue everything depends on.

Related errors


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