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

  1. Swap FromKey/ToKey so the edge points parent->child, or remove it
  2. Delete the stale blocker edge between the pair (bd dep remove) before re-parenting
  3. Change the type to a non-scheduling edge (e.g. related) if blocking semantics are not intended
  4. 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

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


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