gastownhall/beads · error

applyGraph: edge %d %s->%s duplicates a parent-child relatio

Error message

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

What it means

applyGraph computes the set of parent-child pairs implied by the plan's node hierarchy (parentDepPairs). If an explicit edge duplicates one of those pairs (same from->to direction) but declares a non-parent-child dependency type (e.g. blocks), the apply aborts because two different relationship semantics for one pair are ambiguous and would corrupt hierarchy/scheduling invariants.

Source

Thrown at internal/storage/domain/issue.go:1190

	}
	// Preserve failure-before-write for explicit edges that conflict directly
	// with an implicit node parent relationship. Parent-first mutation below is
	// 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]

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove the redundant explicit edge and rely on node.Parent to express the relationship
  2. Change the explicit edge's Type to "parent-child" (it will be skipped as a duplicate)
  3. Drop node.Parent if the relationship really is blocks, not hierarchy
  4. Rebuild the plan from current DB state with bd to avoid stale duplicates

Example fix

// before
plan.Nodes = append(plan.Nodes, GraphNode{Key: "bd-2", Parent: "bd-1"})
plan.Edges = append(plan.Edges, GraphEdge{FromKey: "bd-1", ToKey: "bd-2", Type: "blocks"})
// after
plan.Nodes = append(plan.Nodes, GraphNode{Key: "bd-2", Parent: "bd-1"}) // parent-child only
Defensive patterns

Strategy: validation

Validate before calling

parentPairs := map[string]bool{}
for _, n := range plan.Nodes {
    if n.Parent != "" {
        parentPairs[depPairKey(n.Parent, n.Key)] = true
    }
}
for _, e := range plan.Edges {
    if parentPairs[depPairKey(e.FromKey, e.ToKey)] && e.Type != "parent-child" {
        return fmt.Errorf("edge %s->%s duplicates parent-child with type %q", e.FromKey, e.ToKey, e.Type)
    }
}

Type guard

func edgeConflictsWithParent(e GraphEdge, parentPairs map[string]bool) bool {
    return parentPairs[depPairKey(e.FromKey, e.ToKey)] && e.Type != "parent-child"
}

Prevention

When it happens

Trigger: Plan has a node with Parent=X (creating a parent-child dep X->node) plus an explicit edge X->node typed "blocks" (or any non-parent-child type), detected in the pre-write pass.

Common situations: Mixing hand-written edges with node.Parent fields generated by tooling; migrating an old beads DB where an issue was both a parent and a blocker; two clients produced conflicting plans for the same pair.

Related errors


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