gastownhall/beads · error

legacy SQLite dependency graph has a scheduling cycle

Error message

legacy SQLite dependency graph has a scheduling cycle

What it means

The reader builds a scheduling edge map from blocking-type dependencies (blocks and conditional-blocks) and runs a directed-cycle detection. A scheduling cycle means a group of issues each block each other transitively, so no valid execution order exists; migration aborts rather than importing an unsatisfiable graph.

Source

Thrown at internal/migration/legacysqlite/reader.go:1007

	hierarchy := make(map[string][]string)
	var blocking []*types.Dependency
	for _, issue := range issues {
		for _, dep := range issue.Dependencies {
			if dep.IssueID == dep.DependsOnID {
				return fmt.Errorf("dependency %s -> %s is a self-dependency", dep.IssueID, dep.DependsOnID)
			}
			switch dep.Type {
			case types.DepBlocks, types.DepConditionalBlocks:
				blocking = append(blocking, dep)
				scheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)
			case types.DepParentChild:
				hierarchy[dep.IssueID] = append(hierarchy[dep.IssueID], dep.DependsOnID)
				scheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)
			}
		}
	}
	if hasDirectedCycle(scheduling) {
		return fmt.Errorf("legacy SQLite dependency graph has a scheduling cycle")
	}
	for _, dep := range blocking {
		if types.ExtractPrefix(dep.IssueID) == types.ExtractPrefix(dep.DependsOnID) &&
			(reachable(hierarchy, dep.IssueID, dep.DependsOnID) ||
				reachable(hierarchy, dep.DependsOnID, dep.IssueID)) {
			return fmt.Errorf("blocking dependency %s -> %s conflicts with parent-child hierarchy", dep.IssueID, dep.DependsOnID)
		}
	}
	return nil
}

func hasDirectedCycle(graph map[string][]string) bool {
	indegree := make(map[string]int, len(graph))
	for from, targets := range graph {
		if _, ok := indegree[from]; !ok {
			indegree[from] = 0
		}
		for _, target := range targets {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Identify the cycle members from the migration log (they appear in the scheduling edges) and remove or retype one dependency to break the loop: DELETE FROM dependencies WHERE issue_id='bd-c' AND depends_on_id='bd-a'
  2. Convert non-essential blocking edges to a non-scheduling type (e.g. related) in the legacy DB
  3. If the cycle reflects reality, merge the issues or re-plan dependencies so a topological order exists, then re-run migration

Example fix

-- before
-- bd-a -> bd-b -> bd-c -> bd-a (blocks)
DELETE FROM dependencies WHERE issue_id = 'bd-c' AND depends_on_id = 'bd-a';
-- after: scheduling graph is acyclic
Defensive patterns

Strategy: validation

Validate before calling

function hasCycle(edges) {
  const state = {};
  function dfs(n) {
    if (state[n] === 1) return true;
    if (state[n] === 2) return false;
    state[n] = 1;
    for (const m of edges[n] || []) if (dfs(m)) return true;
    state[n] = 2;
    return false;
  }
  return Object.keys(edges).some(dfs);
}
const scheduling = {};
for (const d of legacyDeps.filter(d => ['blocks','conditional-blocks'].includes(d.type)))
  (scheduling[d.issue_id] ||= []).push(d.depends_on_id);
if (hasCycle(scheduling)) throw new Error('scheduling cycle in legacy blocking deps');

Type guard

function schedulingGraphAcyclic(deps) {
  const edges = {};
  for (const d of deps.filter(d => d.type === 'blocks' || d.type === 'conditional-blocks'))
    (edges[d.issue_id] ||= []).push(d.depends_on_id);
  return !hasCycle(edges);
}

Try / catch

try { migrateLegacySQLite(dbPath) } catch (e) {
  if (e.message.includes('scheduling cycle')) {
    const cycle = findSchedulingCycle(dbPath); // DFS over blocking edges
    breakOrRetypeCycleEdge(dbPath, cycle);
    retry();
  } else throw e;
}

Prevention

When it happens

Trigger: Legacy SQLite database whose blocking dependencies form a directed cycle, e.g. bd-a blocks bd-b, bd-b blocks bd-c, bd-c blocks bd-a (any length loop through DepBlocks/DepConditionalBlocks edges).

Common situations: Long-lived legacy databases where overlapping 'blocks' relations accumulated into loops; bulk imports or scripts that added blocking deps without cycle checks; merges of several legacy DBs.

Related errors


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