gastownhall/beads · error

edge %d %s->%s duplicates a parent-child relationship with d

Error message

edge %d %s->%s duplicates a parent-child relationship with dependency type %q

What it means

The plan contains an explicit parent-child dependency (via node parents) and an additional edge over the same pair with a different dependency type. Since the pair is already parent-child, any other dep type would duplicate or contradict it, so validation rejects the edge during apply.

Source

Thrown at cmd/bd/graph_apply.go:1002

			if err := tx.UpdateIssue(ctx, issues[i].ID, updates, actor); err != nil {
				return fmt.Errorf("node %q: updating metadata refs: %w", node.Key, err)
			}
		}

		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,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove the redundant edge; the parent-child dependency already covers it.
  2. Change edge.Type to the parent-child type if that relationship is intended.
  3. Fix the plan generator to skip edges duplicating inline parent relationships.

Example fix

// before
node: {"key": "bd-1", "parents": ["bd-2"]}
edge: {"from": "bd-1", "to": "bd-2", "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 }
}
for _, e := range plan.Edges {
  if pairs[[2]string{e.From, e.To}] && e.Type != "parent-child" {
    return fmt.Errorf("edge %s->%s duplicates a parent-child pair", e.From, e.To)
  }
}

Type guard

func duplicatesParentChild(e Edge, pairs map[[2]string]bool) bool {
  return pairs[[2]string{e.From, e.To}] && e.Type != "parent-child"
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "duplicates a parent-child relationship") {
    plan = dropRedundantEdges(plan); return bd.GraphApply(ctx, plan)
  }
}

Prevention

When it happens

Trigger: plan.Edges includes an edge fromID->toID where the pair was already established as parent-child (present in parentDepPairs) but edge.Type maps to something other than DepParentChild.

Common situations: Generated plans that emit both inline parents and a separate generic edge for the same pair; merging two plans that model the same relationship differently; hand-edited edges with a wrong type field.

Related errors


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