{"record":{"id":"3177e450d4d87e4a","repo":"BigPizzaV3/CodexPlusPlus","slug":"native-history-3177e4","errorCode":null,"errorMessage":"历史消息接口返回格式无效","messagePattern":"历史消息接口返回格式无效","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/native-history.mjs","lineNumber":45,"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":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/native-history.mjs#L27-L63","documentation":"readNativeHistory pages through a thread's items via the native `thread/items/list` RPC. After the call returns, it requires `page.data` to be an array; if the response is null, missing `data`, or `data` is not an array, it throws this error to fail fast on a malformed/unrecognized wire contract instead of producing silently corrupted history.","triggerScenarios":"The `send('thread/items/list', {threadId, turnId, cursor, limit, sortDirection:'asc'})` call resolves with an object whose `.data` is absent or not an Array — e.g. the native Desktop API returns `{items:[...]}` or `{error:...}` instead of `{data:[...]}` after a Codex Desktop update changed the response shape, or a failed/incomplete response object is passed through without rejecting.","commonSituations":"Codex Desktop upgraded to a build whose thread/items/list payload no longer uses `data`; a relay/proxy in front of the API returns an error JSON body that still resolves; the plugin's wire-contract comments (verified against a specific installed Desktop version) no longer match the installed version.","solutions":["Update the plugin to a version matching the installed Codex Desktop build (the history wire contract is version-pinned).","Log the raw `page` object at the throw site to inspect the actual response shape and confirm which field replaced `data`.","If the response shape changed, adapt native-history.mjs to read the new field (e.g. `page.items`) or normalize it into `data`.","Retry after fully reloading/restarting the Codex Desktop app to rule out a transient truncated response."],"exampleFix":"// before\nif(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');\n// after\nconst rows=page?.data??page?.items;\nif(!Array.isArray(rows))throw Error('历史消息接口返回格式无效');","handlingStrategy":"try-catch","validationCode":"if(!resp||!Array.isArray(resp.data))return [];\n","typeGuard":"function isValidPage(p){return p!=null&&Array.isArray(p.data);}\n","tryCatchPattern":"try{page=await send('thread/items/list',{threadId,turnId,cursor,limit,sortDirection:'asc'});}catch(e){console.warn('items/list response shape:',page);return cachedMessages;}\n","preventionTips":["Type-guard every paginated RPC response before consuming `.data`.","Pin plugin version to the tested Codex Desktop build and update them together.","Log raw API responses once when upgrading Codex to detect wire-contract drift early."],"tags":["api","schema-validation","history-pagination"],"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"}