{"record":{"id":"78a4703ec2f8d498","repo":"BigPizzaV3/CodexPlusPlus","slug":"id-78a470","errorCode":null,"errorMessage":"历史轮次缺少 ID","messagePattern":"历史轮次缺少 ID","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":408,"sourceCode":"\n\nfunction isExportSizeError(message){\n  return /超过.*(?:大小|限制)|too (?:large|big)|size.{0,30}limit|maximum.{0,20}size/i.test(String(message));\n}\n\n// Wire contracts verified in the installed Desktop's history loader:\n// thread/turns/list(itemsView=notLoaded), then thread/items/list per turn.\nasync function readNativeHistory(send,threadId,cache=new Map(),onProgress=()=>{},signal){\n  const turns=[],turnIds=new Set(),turnCursors=new Set();\n  let cursor=null;\n  do{\n    signal?.throwIfAborted();\n    if(turnCursors.has(cursor))throw Error('历史轮次分页游标重复');\n    turnCursors.add(cursor);\n    const page=await send('thread/turns/list',{threadId,cursor,limit:20,itemsView:'notLoaded',sortDirection:'asc'});\n    if(!Array.isArray(page?.data))throw Error('历史轮次接口返回格式无效');\n    for(const turn of page.data){\n      if(typeof turn.id!=='string')throw Error('历史轮次缺少 ID');\n      if(!turnIds.has(turn.id)){turnIds.add(turn.id);turns.push(turn);}\n    }\n    cursor=page.nextCursor??null;\n    onProgress(`正在读取轮次目录 · ${turns.length} 轮…`);\n  }while(cursor!==null);\n  const messages=[],seen=new Set();\n  for(const [index,turn] of turns.entries()){\n    signal?.throwIfAborted();onProgress(`正在读取历史 ${index+1}/${turns.length}…`);\n    const key=`${threadId}:${turn.id}`,cached=await cache.get(key);\n    let parsed;\n    if(cached&&turn.status==='completed')parsed=cached;\n    else{\n      const partial=turn.status==='completed'?await cache.get(`${key}:partial`):null;\n      parsed=partial?.messages?.slice()||[];let itemCursor=partial?.cursor??null,limit=32;const cursors=new Set();\n      do{\n        signal?.throwIfAborted();\n        if(cursors.has(itemCursor))throw Error('历史消息分页游标重复');\n        let page;","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L390-L426","documentation":"canvas.user.js paginates a thread's turn history via 'thread/turns/list' and requires every returned turn to carry a string 'id' used for dedup and downstream per-turn item fetching. When the backend returns a turn entry whose id is not a string (null, number, missing), it throws this error to abort history import rather than silently dropping turns.","triggerScenarios":"A 'thread/turns/list' page response contains a data[] element with turn.id undefined/non-string — e.g. an in-progress or corrupted turn record, or a Codex backend/schema version that renamed or reshaped the turn id field.","commonSituations":"Codex app update changed the turns API payload shape; thread stored by an older/newer version with partial turn records; backend returning placeholder rows during active streaming.","solutions":["Update canvas.user.js to the version matching your installed Codex build (the internal API schema changed).","Inspect the raw page response (log page.data) to confirm which field now holds the turn id and patch the check accordingly.","Exclude/retry malformed turns instead of aborting: change the throw to a continue with a warning.","Verify the thread is not mid-write; reload the thread once streaming completes and retry history import."],"exampleFix":"// before\nif(typeof turn.id!=='string')throw Error('历史轮次缺少 ID');\n// after\nif(typeof turn.id!=='string'){console.warn('skip turn without id',turn);continue;}","handlingStrategy":"validation","validationCode":"const page=await send('thread/turns/list',{threadId,cursor,limit:20,itemsView:'notLoaded',sortDirection:'asc'});\nconst bad=(page?.data||[]).filter(t=>typeof t?.id!=='string');\nif(bad.length)console.warn('turns missing id, will be skipped',bad.length);","typeGuard":"const hasTurnId=t=>t!=null&&typeof t.id==='string';","tryCatchPattern":"try{await importHistory(threadId,{onProgress});}catch(e){if(e.message==='历史轮次缺少 ID'){await importHistory(threadId,{onProgress,skipInvalidTurns:true});}else throw e;}","preventionTips":["Keep canvas.user.js in lockstep with your Codex version","Log raw API pages once when integrating a new Codex build","Prefer skipping malformed rows over aborting whole imports","Pin Codex version when running automated history imports"],"tags":["pagination","schema-mismatch","history-import"],"backgroundTag":"unexpected-response-shape","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"}