{"record":{"id":"0d5a9c5a270b03b2","repo":"BigPizzaV3/CodexPlusPlus","slug":"native-history","errorCode":null,"errorMessage":"历史轮次分页游标重复","messagePattern":"历史轮次分页游标重复","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/native-history.mjs","lineNumber":14,"sourceCode":"import {parseMessage} from './model.mjs';\n\nexport function 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.\nexport async 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;","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/native-history.mjs#L1-L32","documentation":"readNativeHistory pages through thread/turns/list using nextCursor. It tracks every cursor it has already used; seeing the same cursor again means the server is looping pagination and would otherwise fetch turns forever. The function aborts with this error.","triggerScenarios":"The codex backend returns a nextCursor that repeats (including repeating null/null cycles) during thread/turns/list pagination.","commonSituations":"Backend bug or protocol version change in thread/turns/list; proxy/mocked send() that always returns the same page; server-side cursor serialization issue.","solutions":["Upgrade/restart the codex backend so thread/turns/list advances its cursor","Inspect the mocked or proxied send() implementation for a fixed nextCursor","Capture the responses and report a pagination-loop bug upstream","Reduce page size or use a different itemsView/sortDirection to work around the loop"],"exampleFix":"// before\nconst page = await send('thread/turns/list',{threadId,cursor,limit:20});\n// after\n// ensure send() is the real backend, not a stub returning a constant nextCursor","handlingStrategy":"try-catch","validationCode":"// no pre-call validation possible; detect via response\nif (page?.nextCursor != null && seen.has(page.nextCursor)) throw new Error('cursor loop');","typeGuard":"const isValidPage = p => p && Array.isArray(p.data);","tryCatchPattern":"try { await readNativeHistory(send, threadId) } catch (e) { if (e.message === '历史轮次分页游标重复') { fallbackToPartialTurns(); } else throw e; }","preventionTips":["Keep the codex backend up to date","Avoid stub send() implementations with constant cursors","Set an abort signal to cap runtime","Log raw pages for upstream bug reports"],"tags":["pagination","network","api"],"backgroundTag":"unexpected-api-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"}