BigPizzaV3/CodexPlusPlus · error
任务树存在循环关联
Error message
任务树存在循环关联
What it means
While walking each node's parent chain toward the root, the validator tracks the current path in a Set; if it revisits a node already on that path, the parent links form a cycle and no tree exists. This error is thrown at `if(path.has(current.id))throw Error('任务树存在循环关联')`.
Solutions
- Inspect the JSON output and trace parent links to find the cycle (look for a node whose ancestor chain returns to itself, including self-parents).
- Retry the organize call asking the model to ensure parent chains terminate at the single parent:null root.
- In the prompt, forbid `parent` equal to the node's own id and require parent ids to reference only previously defined ancestors.
- If building the result programmatically, run cycle detection before submission or reject with a targeted repair prompt.
Example fix
// before: cycle
[{"id":"a","parent":"b"},{"id":"b","parent":"a"}]
// after
[{"id":"root","parent":null,"lane":"main"},{"id":"a","parent":"root"},{"id":"b","parent":"a"}] Defensive patterns
Strategy: validation
Validate before calling
function hasNoParentCycle(org){ const byId=new Map((org?.nodes||[]).map(n=>[n.id,n])); for(const n of byId.values()){ const seen=new Set(); let c=n; while(c && c.parent!==null){ if(seen.has(c.id)) return false; seen.add(c.id); c=byId.get(c.parent); if(c && seen.has(c.id)) return false; } } return true; } Type guard
function isSelfParent(n){ return typeof n?.parent === 'string' && n.parent === n.id; } Try / catch
try { const org = parseOrganization(text, messages, requestId); } catch (e) { if (e.message === '任务树存在循环关联') { /* regenerate with no-cycle constraint in prompt */ } else throw e; } Prevention
- Forbid parent === own id and any parent chain that loops back in the prompt instructions.
- Require parent ids to reference only nodes defined earlier in the array.
- Run cheap cycle detection on model output before presenting results to users.
When it happens
Trigger: The returned nodes contain a parent cycle such as a.parent='b' and b.parent='a', or a node whose parent is itself (self-loop, n.parent='n'). Any cycle makes the while-loop revisit an id already in `path`.
Common situations: The LLM hallucinates circular references when summarizing interleaved messages; ids get swapped or reused during regeneration; a model copies a previous (valid) response then manually re-parents nodes creating an A->B->A loop; self-parenting when a node id equals its own parent.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/1f395f12756c5e45.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/organize.mjs:28
}
export function validateOrganization(value,messages,requestId) {
if(value?.requestId!==requestId||!Array.isArray(value.nodes)||!value.nodes.length)throw Error('整理结果格式不完整');
const sources=new Set(messages.map(m=>m.id)), nodes=[], ids=new Set();
for(const n of value.nodes){
if(!n||typeof n.id!=='string'||!/^[a-zA-Z0-9_-]{1,80}$/.test(n.id)||ids.has(n.id))throw Error('整理结果节点 ID 无效');
if(!['main','branch'].includes(n.lane)||!Array.isArray(n.sources)||!n.sources.length||!n.sources.every(id=>sources.has(id)))throw Error('整理结果包含无效来源');
for(const key of ['title','summary','description','status'])if(typeof n[key]!=='string'||!n[key].trim()||n[key].length>12000)throw Error('整理结果缺少节点描述');
if(n.parent!==null&&typeof n.parent!=='string')throw Error('整理结果父节点无效');
ids.add(n.id);nodes.push({id:n.id,parent:n.parent,lane:n.lane,title:n.title,summary:n.summary,description:n.description,status:n.status,sources:[...new Set(n.sources)]});
}
const roots=nodes.filter(n=>n.parent===null),byId=new Map(nodes.map(n=>[n.id,n]));
if(roots.length!==1||roots[0].lane!=='main')throw Error('任务树必须有且只有一个主目标根节点');
const checked=new Set();
for(const node of nodes){
const path=new Set();let current=node;
while(current&&!checked.has(current.id)){
if(path.has(current.id))throw Error('任务树存在循环关联');
path.add(current.id);
if(current.parent!==null&&!byId.has(current.parent))throw Error('任务树包含不存在的父节点');
current=current.parent===null?null:byId.get(current.parent);
}
for(const id of path)checked.add(id);
}
const currentNodeId=value.currentNodeId??null;
if(currentNodeId!==null&&!byId.has(currentNodeId))throw Error('当前推进节点不存在');
return {schemaVersion:2,currentNodeId,nodes};
}
export function parseOrganization(text,messages,requestId) {
const blocks=[...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map(m=>m[1]);
for(const raw of [...blocks,text.trim()]){
let value;try{value=JSON.parse(raw);}catch{continue;}
if(value?.requestId===requestId)return validateOrganization(value,messages,requestId);
}
throw Error('侧边对话未返回可识别的整理结果');View on GitHub (pinned to b1ed92e5e4)