dagger/dagger · error

encode persisted mod tree node %q: parent cycle

Error message

encode persisted mod tree node %q: parent cycle

What it means

When encoding a ModTreeNode hierarchy into a persistedModTree, the encoder recursively serializes each node's Parent first, tracking nodes currently on the recursion stack (enc.visiting). If a node is encountered while still being visited, the parent chain forms a cycle and encoding aborts with this error, naming the node. The tree invariant requires parents to form a strict acyclic chain (a forest rooted at nil parents).

Source

Thrown at core/modtree.go:1154

}

func newPersistedModTreeEncoder(cache dagql.PersistedObjectCache) *persistedModTreeEncoder {
	return &persistedModTreeEncoder{
		cache:    cache,
		ids:      map[*ModTreeNode]int{},
		visiting: map[*ModTreeNode]bool{},
	}
}

func (enc *persistedModTreeEncoder) Add(node *ModTreeNode) (int, error) {
	if node == nil {
		return 0, nil
	}
	if id, ok := enc.ids[node]; ok {
		return id, nil
	}
	if enc.visiting[node] {
		return 0, fmt.Errorf("encode persisted mod tree node %q: parent cycle", node.Name)
	}
	enc.visiting[node] = true
	defer delete(enc.visiting, node)

	parentID, err := enc.Add(node.Parent)
	if err != nil {
		return 0, err
	}

	id := len(enc.tree.Nodes) + 1
	enc.ids[node] = id
	persisted := persistedModTreeNode{
		ID:          id,
		ParentID:    parentID,
		Name:        node.Name,
		Description: node.Description,
		IsCheck:     node.IsCheck,
		IsGenerator: node.IsGenerator,

View on GitHub (pinned to 82ba2681db)

Solutions

  1. Audit where node.Parent is assigned; ensure a node is never set as an ancestor of itself and detaching a subtree clears/reparents correctly.
  2. Rebuild the tree from scratch if it was mutated — constructing via the normal Add/child APIs prevents cycles.
  3. The error names the offending node (%q); trace that node's Parent chain in a debugger to find the loop.
  4. Report or fix the library path that produced the cyclic tree; this indicates corrupted internal state, not a user input problem.

Example fix

// before
child.Parent = parent
parent.Parent = child // cycle
// after
child.Parent = parent
parent.Parent = nil // keep the chain acyclic
Defensive patterns

Strategy: validation

Validate before calling

func hasParentCycle(start *ModTreeNode) bool {
    seen := map[*ModTreeNode]bool{}
    for n := start; n != nil; n = n.Parent {
        if seen[n] {
            return true
        }
        seen[n] = true
    }
    return false
}
// call before persisting: if hasParentCycle(root) { abort }

Type guard

func isAcyclicSubtree(n *ModTreeNode) bool {
    for p := n.Parent; p != nil; p = p.Parent {
        if p == n {
            return false
        }
    }
    return true
}

Try / catch

id, err := enc.Add(node)
if err != nil {
    if strings.Contains(err.Error(), "parent cycle") {
        return fmt.Errorf("corrupt mod tree: rebuild the tree before persisting: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: ModTreeNode.Parent pointers form a cycle (e.g. node A's parent is B and B's parent is A, or a node is its own parent), then the tree is persisted via persistedModTreeEncoder.Add (used by module caching/checkpointing of mod trees).

Common situations: Bugs in code that rewires node parents (e.g. moving subtrees without detaching first); deserializing/patching a persisted tree incorrectly and attaching a node as a descendant of itself; custom code mutating Parent after tree construction.

Related errors


AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05). Data as JSON: /api/errors/604503b50823e3fb. Report an issue: GitHub.