BigPizzaV3/CodexPlusPlus · error
本批父节点未提供或不存在
Error message
本批父节点未提供或不存在
What it means
After all upserts are patched, mergeBatch validates each node's parent reference: a non-null parent must exist in the provided catalog (allowed), in this batch's patch set, or match the node's previous parent in existing state. This throw means a node points at a parent that was never provided or does not exist.
Solutions
- Ensure every non-root parent id is either in the catalog or included as an upsert in the same batch
- Have the model re-emit the parent node's full upsert alongside the child
- If the intent is to keep the old parent, echo the previous parent id exactly
- Fall back to parent:null only for the single shared root
Example fix
// before
{ id: 'n3', parent: 'n9' } // n9 never provided
// after
{ id: 'n3', parent: 'n2' }, { id: 'n2', parent: 'root', ... } // include parent upsert Defensive patterns
Strategy: validation
Validate before calling
const known = new Set([...catalog.map(n => n.id), ...upserts.map(n => n.id)]);
for (const n of upserts) if (n.parent !== null && !known.has(n.parent)) throw new Error('unknown parent ' + n.parent); Type guard
const parentKnown = (n, known) => n.parent === null || known.has(n.parent);
Try / catch
try { mergeBatch(...) } catch (e) { if (e.message === '本批父节点未提供或不存在') { reemitWithParents(); } else throw e; } Prevention
- Emit parent nodes before or together with children in the same batch
- Only use parent ids from the catalog or your own batch
- Keep parent:null exclusively for the single root
- Verify the tree is connected before submitting
When it happens
Trigger: Upsert sets n.parent to an id not in catalog, not created in the same batch, and not the node's previously persisted parent.
Common situations: Model invents a parent id; model re-parents to a node it forgot to upsert; parent node belongs to a different batch.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/bc01e70a2f2d3d9b.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/long-organizer.mjs:98
if(aliases)for(const n of value.upserts)if(Array.isArray(n?.sources))n.sources=n.sources.map(id=>Object.hasOwn(aliases,id)?aliases[id]:id);
const parts=new Map(batch.map(p=>[p.partId,p])),allowed=new Set(catalog.map(n=>n.id));
const old=new Map(state.nodes.map(n=>[n.id,n])),patch=new Map(),covered=new Set();
for(const n of value.upserts){
if(!n||typeof n.id!=='string'||patch.has(n.id)||(!allowed.has(n.id)&&(!n.id.startsWith(prefix)||old.has(n.id))))throw Error('本批节点 ID 无效或覆盖了未提供的节点');
if(!Array.isArray(n.sources)||!n.sources.length||!n.sources.every(id=>parts.has(id)))throw Error('本批引用了不存在的消息片段');
for(const field of ['title','summary','description','status'])if(typeof n[field]!=='string'||n[field].length>3000)throw Error('本批节点描述过长或无效');
const previous=old.get(n.id),evidence=[...(previous?.evidence||[])];
for(const id of new Set(n.sources)){covered.add(id);const {messageId,start,end,digest}=parts.get(id);evidence.push({messageId,start,end,digest});}
const revisions=[...(previous?.revisions||[])];
if(previous&&['summary','description','status'].some(k=>previous[k]!==n[k]))revisions.push({batch:state.batchCount,summary:previous.summary,description:previous.description,status:previous.status});
patch.set(n.id,{...n,sources:[...new Set([...(previous?.sources||[]),...n.sources.map(id=>parts.get(id).messageId)])],evidence,revisions});
}
if(covered.size!==batch.length){
const error=new Error('本批仍有未归入任务树的消息片段,进度未提交');
error.missingPartIds=batch.filter(part=>!covered.has(part.partId)).map(part=>part.partId);
throw error;
}
for(const n of patch.values())if(n.parent!==null&&!allowed.has(n.parent)&&!patch.has(n.parent)&&old.get(n.id)?.parent!==n.parent)throw Error('本批父节点未提供或不存在');
const nodes=state.nodes.map(n=>patch.get(n.id)||n);for(const [id,n] of patch)if(!old.has(id))nodes.push(n);
const oldRoot=state.nodes.find(n=>n.parent===null);
if(oldRoot&&nodes.find(n=>n.parent===null)?.id!==oldRoot.id)throw Error('本批改变了共同目标根节点');
const currentNodeId=value.currentNodeId??state.currentNodeId??null;
validateOrganization({requestId,nodes,currentNodeId},[...new Set(nodes.flatMap(n=>n.sources))].map(id=>({id})),requestId);
return {...state,schemaVersion:2,nodes,currentNodeId,batchCount:state.batchCount+1,done:{...state.done,...Object.fromEntries(batch.map(p=>[p.partId,true]))}};
}
// Save is an atomic durable checkpoint. A failed model call never commits its
// coverage; a restart can resume its pending side-chat turn before resubmitting.
export async function organizeLong({messages,record,save,run,signal,progress=()=>{},onGraph=()=>{},compact=false}){
const history=await prepareHistory(messages,signal);
const partIndex=new Map(history.parts.map(part=>[part.partId,part]));
const previous=record?.published;
let job=record?.job;
if(!job||!compatible(job.state.manifest,history.manifest)){
const incremental=previous?.done&&compatible(previous.manifest,history.manifest);
const state=incremental?{...previous,manifest:history.manifest}:{schemaVersion:2,nodes:[],currentNodeId:null,batchCount:0,done:{},manifest:history.manifest};View on GitHub (pinned to b1ed92e5e4)