{"record":{"id":"604503b50823e3fb","repo":"dagger/dagger","slug":"encode-persisted-mod-tree-node-q-parent-cycle","errorCode":null,"errorMessage":"encode persisted mod tree node %q: parent cycle","messagePattern":"encode persisted mod tree node %q: parent cycle","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/modtree.go","lineNumber":1154,"sourceCode":"}\n\nfunc newPersistedModTreeEncoder(cache dagql.PersistedObjectCache) *persistedModTreeEncoder {\n\treturn &persistedModTreeEncoder{\n\t\tcache:    cache,\n\t\tids:      map[*ModTreeNode]int{},\n\t\tvisiting: map[*ModTreeNode]bool{},\n\t}\n}\n\nfunc (enc *persistedModTreeEncoder) Add(node *ModTreeNode) (int, error) {\n\tif node == nil {\n\t\treturn 0, nil\n\t}\n\tif id, ok := enc.ids[node]; ok {\n\t\treturn id, nil\n\t}\n\tif enc.visiting[node] {\n\t\treturn 0, fmt.Errorf(\"encode persisted mod tree node %q: parent cycle\", node.Name)\n\t}\n\tenc.visiting[node] = true\n\tdefer delete(enc.visiting, node)\n\n\tparentID, err := enc.Add(node.Parent)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tid := len(enc.tree.Nodes) + 1\n\tenc.ids[node] = id\n\tpersisted := persistedModTreeNode{\n\t\tID:          id,\n\t\tParentID:    parentID,\n\t\tName:        node.Name,\n\t\tDescription: node.Description,\n\t\tIsCheck:     node.IsCheck,\n\t\tIsGenerator: node.IsGenerator,","sourceCodeStart":1136,"sourceCodeEnd":1172,"githubUrl":"https://github.com/dagger/dagger/blob/82ba2681dbe30d3547a1dc50ea495900ab5b6047/core/modtree.go#L1136-L1172","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nchild.Parent = parent\nparent.Parent = child // cycle\n// after\nchild.Parent = parent\nparent.Parent = nil // keep the chain acyclic","handlingStrategy":"validation","validationCode":"func hasParentCycle(start *ModTreeNode) bool {\n    seen := map[*ModTreeNode]bool{}\n    for n := start; n != nil; n = n.Parent {\n        if seen[n] {\n            return true\n        }\n        seen[n] = true\n    }\n    return false\n}\n// call before persisting: if hasParentCycle(root) { abort }","typeGuard":"func isAcyclicSubtree(n *ModTreeNode) bool {\n    for p := n.Parent; p != nil; p = p.Parent {\n        if p == n {\n            return false\n        }\n    }\n    return true\n}","tryCatchPattern":"id, err := enc.Add(node)\nif err != nil {\n    if strings.Contains(err.Error(), \"parent cycle\") {\n        return fmt.Errorf(\"corrupt mod tree: rebuild the tree before persisting: %w\", err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["dagger-modules","serialization","cycle-detection","internal-state"],"backgroundTag":"cyclic-parent-reference","analyzedSha":"82ba2681dbe30d3547a1dc50ea495900ab5b6047","analyzedAt":"2026-09-05T07:21:37.930Z","contentChangedAt":"2026-09-05T07:21:37.930Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}