gastownhall/beads · error
applyGraph: edge %d %s->%s creates a blocking reverse of a p
Error message
applyGraph: edge %d %s->%s creates a blocking reverse of a parent-child relationship
What it means
applyGraph rejects any explicit edge that runs in the blocking direction opposite to an existing/planned parent-child relationship (child blocks parent) when the edge type is cycle-relevant. Such an edge would make a child gate its own ancestor, so the apply fails before any write.
Source
Thrown at internal/storage/domain/issue.go:1193
// for transitive hierarchy visibility, not for deferring structural errors.
for i, edge := range plan.Edges {
fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
if fromID == "" {
return GraphApplyResult{}, fmt.Errorf("applyGraph: edge %d references undefined from_key %q", i, edge.FromKey)
}
toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
if toID == "" {
return GraphApplyResult{}, fmt.Errorf("applyGraph: edge %d references undefined to_key %q", i, edge.ToKey)
}
depType := edge.Type
if depType == "" {
depType = types.DepBlocks
}
if parentDepPairs[depPairKey(fromID, toID)] && depType != types.DepParentChild {
return GraphApplyResult{}, fmt.Errorf("applyGraph: edge %d %s->%s duplicates a parent-child relationship with dependency type %q", i, fromID, toID, depType)
}
if parentDepPairs[depPairKey(toID, fromID)] && cycleRelevantDepType(depType) {
return GraphApplyResult{}, fmt.Errorf("applyGraph: edge %d %s->%s creates a blocking reverse of a parent-child relationship", i, fromID, toID)
}
}
// Pass 3 — insert node parent-child deps now that all IDs are known. These
// must be visible before any blocking edge in the same plan so the storage
// hierarchy guard evaluates existing + planned ancestry.
for _, node := range plan.Nodes {
parentID := node.ParentID
if node.ParentKey != "" {
parentID = keyToID[node.ParentKey]
}
if parentID == "" {
continue
}
childID := keyToID[node.Key]
dep := &types.Dependency{
IssueID: childID,
DependsOnID: parentID,View on GitHub (pinned to 71377f2769)
Solutions
- Swap FromKey/ToKey so the edge points parent->child, or remove it
- Delete the stale blocker edge between the pair (bd dep remove) before re-parenting
- Change the type to a non-scheduling edge (e.g. related) if blocking semantics are not intended
- Re-run bd doctor / recompute the plan to detect the inverted relationship
Example fix
// before
GraphEdge{FromKey: "bd-2", ToKey: "bd-1", Type: "blocks"} // bd-1 is parent of bd-2
// after
GraphEdge{FromKey: "bd-1", ToKey: "bd-2", Type: "blocks"} // or remove the edge Defensive patterns
Strategy: validation
Validate before calling
for _, e := range plan.Edges {
if parentPairs[depPairKey(e.ToKey, e.FromKey)] && cycleRelevant(e.Type) {
return fmt.Errorf("edge %s->%s blocks its own ancestor", e.FromKey, e.ToKey)
}
} Type guard
func isBlockingReverse(e GraphEdge, parentPairs map[string]bool) bool {
return parentPairs[depPairKey(e.ToKey, e.FromKey)] && cycleRelevantDepType(e.Type)
} Prevention
- Double-check From/To ordering when an endpoint is a parent
- Strip stale blocker edges before re-parenting issues
- Track ancestor sets client-side and reject child->parent scheduling edges
- Run bd doctor to detect inverted relationships early
When it happens
Trigger: Plan includes edge child->parent (the reverse of a parent-child pair in parentDepPairs) with a scheduling type like "blocks" (or empty, which defaults to blocks).
Common situations: Swapped From/To fields when constructing the edge; re-parenting an issue without cleaning up old blocker edges; two agents concurrently creating parent and blocker edges.
Related errors
- edge %d %s->%s creates a blocking reverse of a parent-child
- applyGraph: edge %d %s->%s duplicates a parent-child relatio
- resolving formula: %w
- parent issue %s not found
- failed to check parent issue: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d2f0e38d1c94080a.
Report an issue: GitHub.