{"record":{"id":"4365f7ddf61ca6a0","repo":"BigPizzaV3/CodexPlusPlus","slug":"canvas-user-4365f7","errorCode":null,"errorMessage":"历史消息接口返回格式无效","messagePattern":"历史消息接口返回格式无效","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":434,"sourceCode":"    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;\n        for(;;){\n          try{page=await send('thread/items/list',{threadId,turnId:turn.id,cursor:itemCursor,limit,sortDirection:'asc'});break;}\n          catch(error){\n            if(limit>1&&/decoded message length too large/i.test(error.message)){limit=Math.max(1,Math.floor(limit/2));continue;}\n            throw error;\n          }\n        }\n        if(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');\n        cursors.add(itemCursor);\n        for(const row of page.data){\n          if(row.turnId!=null&&row.turnId!==turn.id)throw Error('历史消息所属轮次不匹配');\n          const item=row.item??row;\n          if(!['userMessage','agentMessage'].includes(item.type))continue;\n          if(typeof item.id!=='string')throw Error('历史消息缺少稳定 ID');\n          const role=item.type==='userMessage'?'user':'assistant';\n          const content=role==='user'?(item.content??[]):[{text:item.text??''}];\n          const message=parseMessage({type:'response_item',payload:{type:'message',id:item.id,role,phase:role==='assistant'?(item.phase??'final_answer'):'',content}},item.id);\n          if(message)parsed.push({...message,turnId:turn.id,turnStatus:turn.status});\n        }\n        itemCursor=page.nextCursor??null;\n        onProgress(`正在读取历史 ${index+1}/${turns.length} · 已读 ${parsed.length} 条消息…`);\n        if(turn.status==='completed'&&itemCursor!==null)await cache.set(`${key}:partial`,{messages:parsed,cursor:itemCursor});\n      }while(itemCursor!==null);\n      if(turn.status==='completed'){await cache.set(key,parsed);await cache.delete(`${key}:partial`);}\n    }\n    // Older completed-turn caches contain text and IDs but no turn metadata.","sourceCodeStart":416,"sourceCodeEnd":452,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L416-L452","documentation":"Each 'thread/items/list' page must contain an array in 'data'; the script validates Array.isArray(page?.data) before iterating rows. A null page or non-array data means the response shape is unrecognized, so it throws rather than processing garbage.","triggerScenarios":"'thread/items/list' returns null/undefined page, or data missing/not an array — typically after a Codex API schema change, an error swallowed upstream by send(), or a malformed RPC envelope.","commonSituations":"Codex client update renamed the items-list response wrapper; transient RPC failure surfaced as empty payload; hitting the internal API with wrong parameter shape after version drift.","solutions":["Update canvas.user.js for the current Codex version (response schema likely changed).","Log the raw page object to see the new response shape and adapt parsing.","Check that the 'decoded message length too large' retry loop exhausted limits correctly — an overly large page may come back malformed; retry with a smaller limit.","Retry history import after fully reloading the Codex app."],"exampleFix":"// before\nif(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');\n// after\nif(!Array.isArray(page?.data)){console.warn('unexpected items page',page);page={data:[],nextCursor:null};}","handlingStrategy":"type-guard","validationCode":"const page=await send('thread/items/list',...);\nif(!page||!Array.isArray(page.data))throw new Error('malformed items page');","typeGuard":"const isItemsPage=p=>p!=null&&Array.isArray(p.data);","tryCatchPattern":"try{await importTurnItems(turn);}catch(e){if(e.message==='历史消息接口返回格式无效'){await sleep(500);return importTurnItems(turn,{smallerLimit:true});}throw e;}","preventionTips":["Validate response shape at one choke point before iterating","Reduce page limit when payloads are large","Reload the app after Codex updates before importing history","Keep a schema snapshot per Codex version in tests"],"tags":["schema-mismatch","api-response","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"}