{"record":{"id":"1f395f12756c5e45","repo":"BigPizzaV3/CodexPlusPlus","slug":"organize-1f395f","errorCode":null,"errorMessage":"任务树存在循环关联","messagePattern":"任务树存在循环关联","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/organize.mjs","lineNumber":28,"sourceCode":"}\n\nexport function validateOrganization(value,messages,requestId) {\n  if(value?.requestId!==requestId||!Array.isArray(value.nodes)||!value.nodes.length)throw Error('整理结果格式不完整');\n  const sources=new Set(messages.map(m=>m.id)), nodes=[], ids=new Set();\n  for(const n of value.nodes){\n    if(!n||typeof n.id!=='string'||!/^[a-zA-Z0-9_-]{1,80}$/.test(n.id)||ids.has(n.id))throw Error('整理结果节点 ID 无效');\n    if(!['main','branch'].includes(n.lane)||!Array.isArray(n.sources)||!n.sources.length||!n.sources.every(id=>sources.has(id)))throw Error('整理结果包含无效来源');\n    for(const key of ['title','summary','description','status'])if(typeof n[key]!=='string'||!n[key].trim()||n[key].length>12000)throw Error('整理结果缺少节点描述');\n    if(n.parent!==null&&typeof n.parent!=='string')throw Error('整理结果父节点无效');\n    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)]});\n  }\n  const roots=nodes.filter(n=>n.parent===null),byId=new Map(nodes.map(n=>[n.id,n]));\n  if(roots.length!==1||roots[0].lane!=='main')throw Error('任务树必须有且只有一个主目标根节点');\n  const checked=new Set();\n  for(const node of nodes){\n    const path=new Set();let current=node;\n    while(current&&!checked.has(current.id)){\n      if(path.has(current.id))throw Error('任务树存在循环关联');\n      path.add(current.id);\n      if(current.parent!==null&&!byId.has(current.parent))throw Error('任务树包含不存在的父节点');\n      current=current.parent===null?null:byId.get(current.parent);\n    }\n    for(const id of path)checked.add(id);\n  }\n  const currentNodeId=value.currentNodeId??null;\n  if(currentNodeId!==null&&!byId.has(currentNodeId))throw Error('当前推进节点不存在');\n  return {schemaVersion:2,currentNodeId,nodes};\n}\n\nexport function parseOrganization(text,messages,requestId) {\n  const blocks=[...text.matchAll(/```(?:json)?\\s*([\\s\\S]*?)```/g)].map(m=>m[1]);\n  for(const raw of [...blocks,text.trim()]){\n    let value;try{value=JSON.parse(raw);}catch{continue;}\n    if(value?.requestId===requestId)return validateOrganization(value,messages,requestId);\n  }\n  throw Error('侧边对话未返回可识别的整理结果');","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/organize.mjs#L10-L46","documentation":"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('任务树存在循环关联')`.","triggerScenarios":"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`.","commonSituations":"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.","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."],"exampleFix":"// before: cycle\n[{\"id\":\"a\",\"parent\":\"b\"},{\"id\":\"b\",\"parent\":\"a\"}]\n// after\n[{\"id\":\"root\",\"parent\":null,\"lane\":\"main\"},{\"id\":\"a\",\"parent\":\"root\"},{\"id\":\"b\",\"parent\":\"a\"}]","handlingStrategy":"validation","validationCode":"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; }","typeGuard":"function isSelfParent(n){ return typeof n?.parent === 'string' && n.parent === n.id; }","tryCatchPattern":"try { const org = parseOrganization(text, messages, requestId); } catch (e) { if (e.message === '任务树存在循环关联') { /* regenerate with no-cycle constraint in prompt */ } else throw e; }","preventionTips":["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."],"tags":["validation","circular-reference","tree-structure","llm-output"],"backgroundTag":"circular-reference-detected","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}