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

  1. 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).
  2. Retry the organize call asking the model to ensure parent chains terminate at the single parent:null root.
  3. In the prompt, forbid `parent` equal to the node's own id and require parent ids to reference only previously defined ancestors.
  4. 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

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)