{"record":{"id":"96c0b1677c7f131c","repo":"BigPizzaV3/CodexPlusPlus","slug":"native-history-96c0b1","errorCode":null,"errorMessage":"历史消息所属轮次不匹配","messagePattern":"历史消息所属轮次不匹配","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/native-history.mjs","lineNumber":48,"sourceCode":"    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.\n    // The current directory and cache key provide the authoritative owner.\n    for(const message of parsed)if(!seen.has(message.id)){seen.add(message.id);messages.push({...message,turnId:turn.id,turnStatus:turn.status});}\n  }","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/native-history.mjs#L30-L66","documentation":"While mapping `page.data` rows from `thread/items/list` inside readNativeHistory, any row carrying a non-null `turnId` that differs from the turn currently being fetched throws this error. It is an internal-consistency check: items from another turn must never appear in this turn's page, so a mismatch means the wire contract or the cursor/pagination state is broken.","triggerScenarios":"A row returned by `thread/items/list` for `turnId: turn.id` has `row.turnId` set to a different turn's id — e.g. the backend changed `turnId` semantics, returns thread-scoped items without honoring the turnId filter, or a cached partial page (`${key}:partial`) was stored under the wrong turn and resumed with the wrong cursor.","commonSituations":"Codex Desktop update changed the items payload so `turnId` now references a parent/branch turn; corrupted or stale partial cache entries mixing turns; custom relay reordering or merging items across turns.","solutions":["Clear the plugin's conversation-history cache (partial pages keyed `${threadId}:${turnId}:partial`) and re-run the history read.","Log the offending row's `turnId` vs the expected `turn.id` to confirm whether the backend returns foreign-turn items.","Update the plugin to match the installed Codex Desktop build if `turnId` semantics changed.","If the backend legitimately returns thread-scoped rows, relax the check to trust the API's turn scoping instead of throwing."],"exampleFix":"// before\nif(row.turnId!=null&&row.turnId!==turn.id)throw Error('历史消息所属轮次不匹配');\n// after\nif(row.turnId!=null&&row.turnId!==turn.id){console.warn('skipping foreign-turn item',row.turnId,'!=',turn.id);continue;}","handlingStrategy":"validation","validationCode":"for(const row of page.data){if(row.turnId!=null&&row.turnId!==turn.id){console.warn('foreign turnId',row.turnId,'expected',turn.id);continue;}}\n","typeGuard":"function rowBelongsToTurn(row,turnId){return row?.turnId==null||row.turnId===turnId;}\n","tryCatchPattern":"try{messages=await readNativeHistory(send,threadId,cache);}catch(e){if(/轮次不匹配/.test(e.message)){await cache.clear?.();return retryOnce();}throw e;}\n","preventionTips":["Clear partial-page cache entries (${threadId}:${turnId}:partial) after any Codex update.","Validate that the turnId filter parameter is actually honored by the backend with one smoke-test thread.","Prefer warn-and-skip over throw for cross-turn contamination so one bad row cannot kill the whole history read."],"tags":["api","data-integrity","history-pagination"],"backgroundTag":"internal-invariant-violation","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"}