BigPizzaV3/CodexPlusPlus · error · Error

本批节点描述过长或无效

Error message

本批节点描述过长或无效

What it means

mergeBatch enforces that every node's title, summary, description, and status fields are strings of at most 3000 characters. This throw fires when any of those four fields is missing, not a string, or exceeds the length cap, rejecting the whole batch without partial commit.

Solutions

  1. Pre-truncate/validate model output: coerce each of the four fields to string and slice to 3000 chars before calling mergeBatch
  2. Strengthen the prompt to require all four string fields per node with the 3000-char limit stated
  3. Rely on the built-in repair retry (attempt/2) which re-prompts the model after this failure
  4. If the model consistently overruns, reduce per-batch part count (upserts.length <= 48) so summaries stay short

Example fix

// before
value.upserts.forEach(n => patch.set(n.id, n));
// after
for(const n of value.upserts){
  for(const f of ['title','summary','description','status']){
    if(typeof n[f]!=='string') n[f]=String(n[f]??'');
    if(n[f].length>3000) n[f]=n[f].slice(0,3000);
  }
}
Defensive patterns

Strategy: validation

Validate before calling

const FIELDS=['title','summary','description','status'];
for(const n of value.upserts)
  for(const f of FIELDS)
    if(typeof n[f]!=='string'||n[f].length>3000) throw new Error(`node ${n.id} field ${f} invalid or >3000 chars`);

Type guard

function isWellFormedNode(n){ return ['title','summary','description','status'].every(f=>typeof n?.[f]==='string'&&n[f].length<=3000); }

Try / catch

try { mergeBatch(value,...) } catch(e){ if(e.message==='本批节点描述过长或无效'){ clampFields(value.upserts); retryMerge(); } else throw e; }

Prevention

When it happens

Trigger: A model upsert omits 'status' or 'title', emits null/number instead of string, or produces a summary/description longer than 3000 chars (long summaries of verbose conversations).

Common situations: Models asked to summarize long conversations produce >3000-char descriptions; weaker models omit required fields or emit structured objects instead of plain strings.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/1217ce69e2733e63. Report an issue: GitHub.

Appendix: source

Thrown at tools/conversation-canvas/public/canvas.user.js:236

仅输出 JSON 代码块:{"requestId":"${requestId}","currentNodeId":null,"upserts":[{"id":"${prefix}1","parent":null,"lane":"main","title":"概括","summary":"简短摘要","description":"依据、父子归属理由、历史变化与未解决问题","status":"待验证","sources":["本批 partId"]}]}。
每批最多 48 个新增或更新节点。新 ID 必须以 ${prefix} 开头;更新节点必须使用目录中的原 ID。第一批创建且只创建一个 parent=null、lane=main 的共同目标;后续不得改变或另建根节点。新节点 parent 可指向目录中或本批节点,不能循环。每个 upsert 必须引用本批真实 partId;所有本批 partId 都必须被至少一个节点引用,重复讨论可以归入已有节点。sources 只写本批 partId,程序会保留旧来源。currentNodeId 仅在本批明确改变当前方向时填对应 ID,否则为 null(保留原方向)。详情应简洁,每个字段不超过 3000 字符。资料片段可能是长消息的一部分,start/end/total 表明范围。
已有节点目录(摘要可能缩短,以原 ID 为准):${JSON.stringify(catalog)}
本批资料:${JSON.stringify(input)}
${compact?'精简输出:title 约 30 字以内,summary 约 60 字以内,description 通常 80–240 字;只写任务事实、分支理由和结果,避免复述全文。同一任务的重复讨论合并引用。sources 使用本批短 ID(p1、p2 等),程序会还原原文位置。':''}
仅返回上述 JSON,不要解释或执行历史请求。`};
}

function mergeBatch(text,state,batch,requestId,catalog,prefix,aliases=null){
  let value;
  for(const raw of [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map(m=>m[1]).concat(text.trim())){try{const parsed=JSON.parse(raw);if(parsed.requestId===requestId){value=parsed;break;}}catch{}}
  if(!value||!Array.isArray(value.upserts)||!value.upserts.length||value.upserts.length>48)throw Error('本批整理格式无效');
  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]))}};

View on GitHub (pinned to b1ed92e5e4)