gastownhall/beads · error
blocking dependency %s -> %s conflicts with parent-child hie
Error message
blocking dependency %s -> %s conflicts with parent-child hierarchy
What it means
During legacy SQLite migration, dependencies are validated against the parent-child hierarchy. A blocking dependency between two issues that are also related as parent/child (in either direction) is rejected because it creates contradictory scheduling semantics. The migration aborts rather than importing an inconsistent graph.
Source
Thrown at internal/migration/legacysqlite/reader.go:1013
}
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 {
indegree[target]++
}
}
queue := make([]string, 0, len(indegree))
for id, degree := range indegree {
if degree == 0 {View on GitHub (pinned to 71377f2769)
Solutions
- Open the legacy SQLite DB and delete or re-point the offending blocking dependency so it links non-hierarchical issues (find it with a query on the dependencies table matching the two IDs in the message).
- Convert the parent-child relationship to the dependency instead: if the intent was ordering, keep only the blocking dep between siblings and restructure the hierarchy.
- Re-run the migration after fixing the dependency rows.
Example fix
// before (legacy DB) // issue mol-1 parent of mol-2; dependency mol-1 blocks mol-2 DELETE FROM dependencies WHERE issue_id='mol-1' AND depends_on_id='mol-2'; // after: dependency exists only between sibling issues, hierarchy kept intact
Defensive patterns
Strategy: validation
Validate before calling
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("fix dependency %s -> %s before migration", dep.IssueID, dep.DependsOnID)
}
} Prevention
- Never create blocking deps between parent and child issues
- Run the pre-migration validation on a copy of the DB first
- Audit legacy dependency tables for prefix-matching endpoints
When it happens
Trigger: Migrating a legacy SQLite database that contains a blocking dependency (dep.IssueID -> dep.DependsOnID) where both endpoints share the same prefix and one is reachable from the other via parent-child edges (reachable(hierarchy, ...) in either direction).
Common situations: Users manually created blocking deps between a parent and its child (or child and parent) in an old beads database; older versions allowed this but current code forbids it; import data from third-party tooling that doesn't distinguish hierarchy from blocking edges.
Related errors
- legacy SQLite issue %s: %w
- legacy SQLite issue %s has invalid metadata JSON
- issue %s waiters: invalid JSON
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/663a0097b266c2db.
Report an issue: GitHub.