{"record":{"id":"4edb4953e7727bee","repo":"BigPizzaV3/CodexPlusPlus","slug":"canvas-user-4edb49","errorCode":null,"errorMessage":"任务树存在循环关联","messagePattern":"任务树存在循环关联","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":131,"sourceCode":"}\n\nfunction 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\nfunction 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":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L113-L149","documentation":"validateOrganization throws this when following parent links from a node revisits a node already on the current path, i.e. the parent graph contains a cycle. A tree must be acyclic, so circular parent relationships are rejected.","triggerScenarios":"Two or more nodes form a parent cycle (A.parent=B, B.parent=A), or a node is its own parent (self-loop), while parseOrganization validates the model output.","commonSituations":"The model, when merging or reorganizing nodes, reparents an ancestor under its own descendant; batch merges that connect old subtrees into loops.","solutions":["Break the cycle by setting one node's parent to another valid ancestor or null","Re-run the organize request with instructions that parent links must form a tree, never a cycle","Verify no node's ancestor chain includes itself","Check nodes that were reparented in an update for accidental loops"],"exampleFix":"// before\nA.parent=B; B.parent=A  // cycle\n// after\nA.parent=null (root); B.parent=A","handlingStrategy":"validation","validationCode":"function hasCycle(nodes) {\n  const byId = new Map(nodes.map(n => [n.id, n]));\n  for (const n of nodes) {\n    const seen = new Set(); let cur = n;\n    while (cur && cur.parent !== null) { if (seen.has(cur.id)) return true; seen.add(cur.id); cur = byId.get(cur.parent); }\n  }\n  return false;\n}","typeGuard":"function isAcyclicTree(nodes) { return !hasCycle(nodes); }","tryCatchPattern":"try { org = parseOrganization(text, messages, requestId); } catch (e) { if (e.message.includes('循环关联')) retryWithAcyclicInstruction(); else throw e; }","preventionTips":["Instruct the model that parent links must always point toward the root","When reparenting nodes in updates, never attach an ancestor under its descendant","Validate incrementally after each merge"],"tags":["validation","tree-structure","cycle-detection"],"backgroundTag":"invalid-state-transition","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"}