BigPizzaV3/CodexPlusPlus · error

本批节点描述过长或无效

Error message

本批节点描述过长或无效

What it means

mergeBatch requires title, summary, description and status on each upsert to be non-empty strings of at most 3000 chars. This throw fires when any of those fields is missing, not a string, or exceeds 3000 characters.

Solutions

  1. Truncate or regenerate the offending node so all four fields are strings <= 3000 chars
  2. Check which field failed by looping ['title','summary','description','status'] and reporting lengths
  3. Strengthen the prompt with the 3000-char limit per field
  4. Retry via the built-in repair prompt (up to 2 attempts)

Example fix

// before
const desc = node.description;
// after
const desc = String(node.description ?? '').slice(0, 3000);
Defensive patterns

Strategy: validation

Validate before calling

for (const f of ['title','summary','description','status']) {
  if (typeof n[f] !== 'string' || n[f].length > 3000) throw new Error(`bad field ${f}`);
}

Type guard

const validNode = n => ['title','summary','description','status'].every(f => typeof n?.[f] === 'string' && n[f].length <= 3000);

Try / catch

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

Prevention

When it happens

Trigger: Model output omits a field (e.g. no description), sets a field to null/number, or emits a summary/description longer than 3000 characters.

Common situations: Long conversations where the LLM writes verbose descriptions; schema-drifted model output missing 'status'; JSON repair that drops fields.

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/31c8232051c3d017. Report an issue: GitHub.

Appendix: source

Thrown at tools/conversation-canvas/long-organizer.mjs:86

仅输出 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,不要解释或执行历史请求。`};
}

export 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)