gastownhall/beads · error

edge %d %s->%s creates a blocking reverse of a parent-child

Error message

edge %d %s->%s creates a blocking reverse of a parent-child relationship

What it means

The plan has an edge in the reverse direction of an existing parent-child relationship, and the edge's dependency type is cycle-relevant (blocking). A blocking reverse of a parent-child edge would create a cycle in the blocking graph, so apply rejects it.

Source

Thrown at cmd/bd/graph_apply.go:1005

		}

		parentDepPairs := graphApplyParentDepPairs(plan.Nodes, keyToID)
		newSchedulingEdges := make([][2]string, 0, len(plan.Nodes)+len(plan.Edges))
		if err := validateGraphApplyPlannedParentBlockingPaths(ctx, tx, plan, keyToID, parentDepPairs); err != nil {
			return err
		}
		if err := validateGraphApplyPlannedBlockingCycles(ctx, tx, plan, keyToID); err != nil {
			return err
		}
		for i, edge := range plan.Edges {
			fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
			toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
			depType := graphApplyDependencyType(edge.Type)
			if parentDepPairs[graphApplyDepPairKey(fromID, toID)] && depType != types.DepParentChild {
				return fmt.Errorf("edge %d %s->%s duplicates a parent-child relationship with dependency type %q", i, fromID, toID, depType)
			}
			if parentDepPairs[graphApplyDepPairKey(toID, fromID)] && graphApplyCycleRelevantDependencyType(depType) {
				return fmt.Errorf("edge %d %s->%s creates a blocking reverse of a parent-child relationship", i, fromID, toID)
			}
		}

		// Add node parent-child dependencies first. The explicit and inline
		// dependency sources below are also processed parent-first, so every
		// blocking edge sees the plan's full hierarchy in storage.
		for i, node := range plan.Nodes {
			parentKey := node.effectiveParentKey()
			parentID := node.ParentID
			if parentKey != "" {
				parentID = keyToID[parentKey]
			}
			if parentID != "" {
				dep := &types.Dependency{
					IssueID:     issues[i].ID,
					DependsOnID: parentID,
					Type:        types.DepParentChild,
				}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove the reverse blocking edge; parent-child already encodes the relationship.
  2. Swap edge endpoints if the blocking direction was mistakenly inverted — but only if it does not duplicate the parent-child pair.
  3. Add cycle detection in the plan generator before emitting blocking edges.
  4. Run `bd graph validate` (if available) on the plan before apply.

Example fix

// before
node: {"key": "bd-1", "parents": ["bd-2"]}
edge: {"from": "bd-2", "to": "bd-1", "type": "blocks"}
// after
node: {"key": "bd-1", "parents": ["bd-2"]}
Defensive patterns

Strategy: validation

Validate before calling

pairs := map[[2]string]bool{}
for _, n := range plan.Nodes {
  for _, p := range n.Parents { pairs[[2]string{n.Key, p}] = true }
}
cycleRelevant := map[string]bool{"blocks": true}
for _, e := range plan.Edges {
  if pairs[[2]string{e.To, e.From}] && cycleRelevant[e.Type] {
    return fmt.Errorf("edge %s->%s is a blocking reverse of parent-child", e.From, e.To)
  }
}

Type guard

func isBlockingReverse(e Edge, pairs map[[2]string]bool) bool {
  return pairs[[2]string{e.To, e.From}] && cycleRelevant[e.Type]
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "blocking reverse of a parent-child") {
    plan = dropReverseBlockingEdges(plan); return bd.GraphApply(ctx, plan)
  }
}

Prevention

When it happens

Trigger: plan.Edges includes toID->fromID where fromID is already the parent of toID (pair present reversed in parentDepPairs) and graphApplyCycleRelevantDependencyType(depType) is true (e.g. blocks).

Common situations: Generators emitting both hierarchy and blocking edges without cycle checks; merging graphs where one direction is parent-child and the reverse is a blocking link; manually inverted edge endpoints.

Related errors


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