{"record":{"id":"17437c66c91ca0dd","repo":"BigPizzaV3/CodexPlusPlus","slug":"canvas-user-17437c","errorCode":null,"errorMessage":"待恢复批次与资料不匹配","messagePattern":"待恢复批次与资料不匹配","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":276,"sourceCode":"// coverage; a restart can resume its pending side-chat turn before resubmitting.\nasync function organizeLong({messages,record,save,run,signal,progress=()=>{},onGraph=()=>{},compact=false}){\n  const history=await prepareHistory(messages,signal);\n  const partIndex=new Map(history.parts.map(part=>[part.partId,part]));\n  const previous=record?.published;\n  let job=record?.job;\n  if(!job||!compatible(job.state.manifest,history.manifest)){\n    const incremental=previous?.done&&compatible(previous.manifest,history.manifest);\n    const state=incremental?{...previous,manifest:history.manifest}:{schemaVersion:2,nodes:[],currentNodeId:null,batchCount:0,done:{},manifest:history.manifest};\n    job={kind:incremental||!previous?.nodes?.length?'incremental':'rebuild',state,session:{},pending:null};\n  }else job={...job,state:{...job.state,manifest:history.manifest}};\n  let document={version:3,published:previous??null,job};\n  const persist=async()=>{signal?.throwIfAborted();await save(structuredClone(document));};\n  await persist();\n  for(;;){\n    signal?.throwIfAborted();\n    // A resumed final batch must not absorb messages appended while paused.\n    const batch=job.pending?job.pending.partIds.map(id=>partIndex.get(id)):nextBatch(history.parts,job.state.done);\n    if(batch.some(part=>!part))throw Error('待恢复批次与资料不匹配');\n    if(!batch.length)break;\n    progress(`正在整理第 ${job.state.batchCount+1} 批 · 已处理 ${Object.keys(job.state.done).length}/${history.parts.length} 个片段`);\n    if(!job.pending){job.pending={requestId:crypto.randomUUID(),partIds:batch.map(p=>p.partId),compact};await persist();}\n    if(JSON.stringify(job.pending.partIds)!==JSON.stringify(batch.map(p=>p.partId)))throw Error('待恢复批次与资料不匹配');\n    const {requestId}=job.pending,{catalog,prefix,aliases,prompt:basePrompt}=batchPrompt(batch,job.state,requestId,job.pending.compact);\n    const repair=job.pending.repair;\n    const missing=repair?.missingPartIds?.map(id=>aliases?Object.keys(aliases).find(key=>aliases[key]===id)||id:id);\n    const prompt=basePrompt+(repair?`\\n上次回复未通过覆盖校验，未保存任何本批节点。本次为第 ${repair.attempt}/2 次补正。上次漏引的 partId：${JSON.stringify(missing)}。请重新输出整个批次的完整 JSON，使用本次 requestId，覆盖本批全部 ${batch.length} 个片段（包括上次已引用的片段），不要只输出遗漏部分。发送前逐个核对 sources；重复讨论、进度说明也要归入有依据的任务节点。`: '');\n    const response=await run(prompt,requestId,job.session,async()=>save(structuredClone(document)));\n    signal?.throwIfAborted();\n    let next;\n    try{next=mergeBatch(response,job.state,batch,requestId,catalog,prefix,aliases);}\n    catch(error){\n      job.session.turnId=null;job.session.requestId=null;\n      if(error.missingPartIds&&(repair?.attempt||0)<2){\n        job.pending={requestId:crypto.randomUUID(),partIds:batch.map(p=>p.partId),compact:job.pending.compact,repair:{attempt:(repair?.attempt||0)+1,missingPartIds:error.missingPartIds}};\n        await persist();progress(`本批遗漏 ${error.missingPartIds.length} 个片段，正在自动补正 ${job.pending.repair.attempt}/2…`);continue;\n      }","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L258-L294","documentation":"During resume of an organize job, the loop maps the pending batch's stored partIds through partIndex. If any partId no longer resolves to a part (part is undefined), the persisted pending batch is inconsistent with the current history parts. The job aborts rather than silently skipping or absorbing wrong messages.","triggerScenarios":"Resuming a saved/persisted organize job whose job.pending.partIds reference parts that were removed, re-split, or re-indexed because the conversation history changed between save and resume.","commonSituations":"User saves a paused organize job, the plugin version changes the part splitting logic or history is reloaded from a different thread snapshot, then resumes the job.","solutions":["Discard the stale pending state and restart the organize job so nextBatch recomputes batches from current parts","Verify the conversation history was not modified (messages deleted/edited) between pause and resume","Upgrade consistently: run the same plugin version that created the persisted job when resuming","Back up / re-export the thread and re-run organization from scratch if parts were re-indexed"],"exampleFix":"// before\nconst batch=job.pending?job.pending.partIds.map(id=>partIndex.get(id)):nextBatch(history.parts,job.state.done);\nif(batch.some(part=>!part))throw Error('待恢复批次与资料不匹配');\n// after\nlet batch=job.pending?job.pending.partIds.map(id=>partIndex.get(id)):nextBatch(history.parts,job.state.done);\nif(batch.some(part=>!part)){ job.pending=null; batch=nextBatch(history.parts,job.state.done); }","handlingStrategy":"try-catch","validationCode":"const allResolvable=job.pending?.partIds.every(id=>partIndex.has(id));\nif(job.pending&&!allResolvable){ job.pending=null; /* recompute batches */ }","typeGuard":"function pendingMatchesParts(pending, partIndex){ return !!pending && Array.isArray(pending.partIds) && pending.partIds.every(id=>partIndex.has(id)); }","tryCatchPattern":"try { await organizeJob(...) } catch(e){ if(e.message==='待恢复批次与资料不匹配'){ await restartOrganizeJob(); } else throw e; }","preventionTips":["Resume jobs with the same plugin version that created them","Avoid editing/deleting messages while an organize job is paused","Persist partIndex alongside pending partIds so resumption validates cheaply"],"tags":["resume","state-consistency","persistence"],"backgroundTag":"record-not-found","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"}