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
- Audit where node.Parent is assigned; ensure a node is never set as an ancestor of itself and detaching a subtree clears/reparents correctly.
- Rebuild the tree from scratch if it was mutated — constructing via the normal Add/child APIs prevents cycles.
- The error names the offending node (%q); trace that node's Parent chain in a debugger to find the loop.
- 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
- Never assign Parent to a node that is already a descendant; always detach subtrees before reparenting.
- Only mutate Parent through tree-building APIs that validate acyclicity.
- Add an acyclicity assertion in tests for any code that rewires the tree.
- Treat a parent-cycle error as corrupted internal state — rebuild rather than patch.
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
- decode persisted directory payload: missing lazy kind
- decode persisted mod tree: zero node ID
- decode persisted mod tree: duplicate node ID %d
- failed to load module source dependencies: %w
- failed to generate json method code for %s: %w
AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05).
Data as JSON: /api/errors/604503b50823e3fb.
Report an issue: GitHub.