{"record":{"id":"4f2080f266e0e4be","repo":"BigPizzaV3/CodexPlusPlus","slug":"canvas-user-4f2080","errorCode":null,"errorMessage":"历史轮次分页游标重复","messagePattern":"历史轮次分页游标重复","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":403,"sourceCode":"    })().catch(error=>{pending=null;throw error;});\n    return pending;\n  };\n}\nconst loadNativeRuntime=createNativeLoader();\n\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;","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L385-L421","documentation":"readNativeHistory paginates thread/turns/list with a cursor, tracking every cursor value in turnCursors to detect server-side pagination loops. Because the first cursor is null, a repeated null means the server keeps returning a page without nextCursor; a repeated non-null cursor means the server loops. The read aborts to prevent an infinite loop and duplicated history.","triggerScenarios":"thread/turns/list repeatedly returns the same nextCursor (including null/undefined while more data exists), causing the do/while loop to see a cursor it has already visited.","commonSituations":"Native Codex backend bug or protocol change where page.nextCursor is undefined despite more turns existing, or a server returning a stable cursor for the last page repeatedly.","solutions":["Update the plugin/Codex so the pagination protocol matches the backend implementation","Inspect the raw thread/turns/list responses for a protocol change (e.g. renamed nextCursor field)","Add a page-count cap in addition to cursor tracking if very large threads are expected","Retry the history read once — a transient backend glitch can return a duplicated cursor"],"exampleFix":"// before\nif(turnCursors.has(cursor))throw Error('历史轮次分页游标重复');\n// after\nif(turnCursors.has(cursor))break; // treat as end of pagination\nif(turns.length>MAX_TURNS)throw Error('历史轮次超出上限');","handlingStrategy":"retry","validationCode":"const seen=new Set(); let cursor=null, guard=0;\nwhile(guard++<100){ if(seen.has(cursor)) return /* stop early */; seen.add(cursor); cursor=await fetchNextCursor(cursor); if(cursor==null) break; }","typeGuard":"function isWellFormedTurnsPage(page){ return !!page && Array.isArray(page.data) && (page.nextCursor===undefined||page.nextCursor===null||typeof page.nextCursor==='string'); }","tryCatchPattern":"try { history=await readNativeHistory(send,threadId) } catch(e){ if(e.message==='历史轮次分页游标重复'){ history=await readNativeHistory(send,threadId,new Map()); } else throw e; }","preventionTips":["Cap pagination iterations in addition to cursor tracking","Verify nextCursor handling against the backend's actual protocol","Retry once on transient pagination glitches before failing the read"],"tags":["pagination","api","infinite-loop"],"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"}