gastownhall/beads · error

final cycle check failed (no edges added)

Error message

final cycle check failed (no edges added): %w

What it means

During ExecuteAddDependencies' end gate, checkAddedEdgesForCycles builds the full scheduling graph inside the transaction via AppendSchedulingGraphInTx; if that graph-load query fails, the whole add is aborted with this wrapped error. No edges have been added at this point, so the transaction is purely failed by an infrastructure/query problem, not by a real cycle. The %w preserves the underlying driver error for errors.As/errors.Is inspection.

Solutions

  1. Inspect the wrapped cause with errors.As to identify the driver error (connection lost, lock timeout, etc.) and address that root cause.
  2. Retry the whole ExecuteAddDependencies operation in a fresh transaction — the failed tx rolled back and left no edges added.
  3. Check database connectivity and server health if the error recurs; verify no competing transaction holds long locks on the dependency tables.
Defensive patterns

Strategy: retry

Try / catch

err := editor.ExecuteAddDependencies(ctx, req)
if err != nil {
    var derr error
    if errors.As(err, &derr) && strings.Contains(err.Error(), "final cycle check failed") {
        // tx rolled back with no edges added; safe to retry whole operation
        return retryOperation(ctx, req)
    }
    return err
}

Prevention

When it happens

Trigger: Any failure of the tx.QueryContext-backed AppendSchedulingGraphInTx while loading cycle-detection tables (cycleDetectionTables) mid-transaction — e.g. a database connection drop, lock timeout, or driver error during ExecuteAddDependencies or runEndGate.

Common situations: Dolt/server connection interrupted mid-transaction; database under heavy lock contention from concurrent writers; transient driver errors during long graph loads on large issue graphs.

Related errors


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

Appendix: source

Thrown at internal/storage/issueops/dependency_editor.go:227

// them off entirely, so this is the check that actually holds the invariant.
//
// The message is built through domain.NewCycleError so it errors.Is-matches
// ErrDependencyCycle while rendering byte-for-byte what the direct bulk path
// already prints.
func checkAddedEdgesForCycles(ctx context.Context, tx *sql.Tx, edges []publicops.DependencyEdge) error {
	var pairs [][2]string
	for _, edge := range edges {
		if !types.IsSchedulingEdge(edge.Type) {
			continue
		}
		pairs = append(pairs, [2]string{edge.IssueID, edge.DependsOnID})
	}
	if len(pairs) == 0 {
		return nil
	}
	graph := make(map[string][]string)
	if err := AppendSchedulingGraphInTx(ctx, tx, cycleDetectionTables(), graph); err != nil {
		return fmt.Errorf("final cycle check failed (no edges added): %w", err)
	}
	if cyclePath := CycleThroughEdgesInGraph(graph, pairs); cyclePath != "" {
		return domain.NewCycleError("dependency cycle would be created: %s (no edges added; run 'bd dep cycles' for analysis)", cyclePath)
	}
	return nil
}

// ExecuteRemoveDependency removes one edge in tx and reports the durable
// tables changed.
//
// A missing edge reports Removed false and NO changed tables, which is how the
// callers spell "commit nothing": removing an edge that was never there leaves
// the graph it already had, and a history entry for it would be a commit with
// nothing in it.
func ExecuteRemoveDependency(ctx context.Context, tx *sql.Tx, request publicops.RemoveDependencyRequest) (publicops.RemoveDependencyResult, ChangedTables, error) {
	// Routing is READ here rather than pinned, unlike the add. A removal
	// cannot put an edge anywhere, so pinning it would only mean failing to
	// remove an edge the caller named — and the staging has to name the tables

View on GitHub (pinned to 71377f2769)