{"record":{"id":"ef048726ec1c10f4","repo":"BigPizzaV3/CodexPlusPlus","slug":"long-organizer-ef0487","errorCode":null,"errorMessage":"待恢复批次与资料不匹配","messagePattern":"待恢复批次与资料不匹配","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/long-organizer.mjs","lineNumber":126,"sourceCode":"// coverage; a restart can resume its pending side-chat turn before resubmitting.\nexport async 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":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/long-organizer.mjs#L108-L144","documentation":"organizeLong resumes a possibly-persisted job. When a pending batch exists it maps pending.partIds through partIndex (current history parts); if any partId no longer resolves to a part, the saved batch no longer matches the conversation material and resuming is refused rather than producing corrupt output.","triggerScenarios":"Resuming a saved document whose job.pending.partIds reference parts that were removed/changed in history.parts (conversation edited or reloaded from a different history snapshot).","commonSituations":"Editing/deleting messages in the native thread while a long-organization run is paused; restoring an old checkpoint file against newer history; mismatched thread content between sessions.","solutions":["Restart organizeLong from the current history (drop the stale pending batch)","Verify the saved document corresponds to the same thread/history snapshot","Clear job.pending and let nextBatch recompute from done state if the parts changed","Re-run from scratch; completed batches in state.done may still be reusable"],"exampleFix":"// before\nawait organizeLong(resumedDocument) // stale pending partIds\n// after\nresumedDocument.job.pending = null; // recompute batch from current parts\nawait organizeLong(resumedDocument)","handlingStrategy":"validation","validationCode":"const idx = new Map(history.parts.map(p => [p.partId, p]));\nconst stale = (job.pending?.partIds || []).filter(id => !idx.has(id));\nif (stale.length) throw new Error('stale pending parts: ' + stale.join(','));","typeGuard":null,"tryCatchPattern":"try { await organizeLong(doc) } catch (e) { if (e.message === '待恢复批次与资料不匹配') { doc.job.pending = null; restartJob(); } else throw e; }","preventionTips":["Never edit thread messages while an organization run is paused","Persist history snapshot together with job state","Clear job.pending when parts change","Use the same thread content for resume as for the original run"],"tags":["resume","state-mismatch","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"}