gastownhall/beads · error

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

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.

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)

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.