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
- 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'
- Convert non-essential blocking edges to a non-scheduling type (e.g. related) in the legacy DB
- 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
- Run cycle detection on blocking edges before every migration
- Retype non-essential blocking deps to a non-scheduling type in the legacy DB
- Prevent cycle-creating inserts in tooling by checking reachability before adding a blocks edge
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
- dependency %s -> %s is a self-dependency
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
- legacy SQLite source changed while sealing
- legacy SQLite source %q must not be a symlink
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c8de0b475d1e5108.
Report an issue: GitHub.