gastownhall/beads · error

add deps[%d]: cycle check: %w

Error message

add deps[%d]: cycle check: %w

What it means

For each scheduling edge (when SkipPerEdgeCycleCheck is false), AddDependencies calls HasCycle to test whether the proposed edge would close a cycle. This error wraps a failure of the HasCycle query itself — the cycle detection could not be executed, so the batch is aborted rather than risking an unchecked write. Note: an actual detected cycle is a different, cycleErrorf-typed error ('would create a cycle').

Source

Thrown at internal/storage/domain/dependency.go:735

	// request. The shared repository guard can then evaluate existing + planned
	// ancestry without widening #4034 into #4035's combined-graph cycle check.
	for phase := 0; phase < 2; phase++ {
		parentPhase := phase == 0
		for i, dep := range deps {
			if (dep.Type == types.DepParentChild) != parentPhase {
				continue
			}
			if err := u.depRepo.ValidateBlockingHierarchy(ctx, dep); err != nil {
				var hierarchyConflict *DependencyHierarchyConflictError
				if errors.As(err, &hierarchyConflict) {
					return BulkAddDepsResult{}, err
				}
				return BulkAddDepsResult{}, fmt.Errorf("add deps[%d]: hierarchy check: %w", i, err)
			}
			if !opts.SkipPerEdgeCycleCheck && types.IsSchedulingEdge(dep.Type) {
				cycle, err := u.depRepo.HasCycle(ctx, dep.IssueID, dep.DependsOnID)
				if err != nil {
					return BulkAddDepsResult{}, fmt.Errorf("add deps[%d]: cycle check: %w", i, err)
				}
				if cycle {
					return BulkAddDepsResult{}, cycleErrorf("add deps[%d]: adding %s -> %s would create a cycle", i, dep.IssueID, dep.DependsOnID)
				}
			}
			// The explicit `bd dep add` / `bd link` verb on the proxied server
			// (cmd/bd/dep_proxied_server.go, link_proxied_server.go) records a
			// dependency_added event for each genuine new edge — unlike
			// create-with-deps, which calls depRepo.Insert directly without
			// EmitEvent. UseWispsTable routes both the edge and that event to
			// the source's own pair of tables.
			_, sourceIsWisp := wispSources[dep.IssueID]
			if err := u.depRepo.Insert(ctx, dep, actor, DepInsertOpts{
				UseWispsTable:      sourceIsWisp,
				HierarchyValidated: true,
				CycleValidated:     true,
				EmitEvent:          true,
			}); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped error after 'cycle check:' for the storage root cause
  2. Retry with a longer context deadline if the graph is large
  3. If cycles are verified separately, set opts.SkipPerEdgeCycleCheck=true to rely on the final CycleThroughEdges check instead
Defensive patterns

Strategy: validation

Validate before calling

// Optionally skip the per-edge probe when you pre-verified acyclicity:
opts.SkipPerEdgeCycleCheck = true // rely on final CycleThroughEdges instead

Try / catch

if err != nil && strings.Contains(err.Error(), "cycle check") {
    // HasCycle query failed (not a detected cycle); inspect wrapped cause, retry
}

Prevention

When it happens

Trigger: Calling AddDependencies with opts.SkipPerEdgeCycleCheck=false and a scheduling dep type, when depRepo.HasCycle(ctx, IssueID, DependsOnID) returns a storage/query error.

Common situations: Slow or locked Dolt database during deep graph traversal; context deadline exceeded on very large dependency graphs; transient DB connection drops mid-batch.

Related errors


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