BigPizzaV3/CodexPlusPlus · error · Error
历史消息分页游标重复
Error message
历史消息分页游标重复
What it means
While fetching all items of a historical turn, the script loops 'thread/items/list' with a cursor and tracks every cursor seen in a Set; if the server returns a nextCursor equal to one already processed, the loop would be infinite, so it throws this guard error.
Solutions
- Update canvas.user.js to match the installed Codex version.
- Retry after reloading the thread — if the cursor cycle was transient, a fresh pagination run may succeed.
- Clear/rebuild the local thread cache entry (`${key}:partial`) so pagination restarts from a null cursor.
- Patch the loop to reset itemCursor to null (or bail gracefully) instead of throwing when a cycle is detected.
Example fix
// before
if(cursors.has(itemCursor))throw Error('历史消息分页游标重复');
// after
if(cursors.has(itemCursor)){console.warn('cursor cycle detected, stopping');itemCursor=null;continue;} Defensive patterns
Strategy: retry
Validate before calling
// no pre-call check possible; guard is on server-returned cursor cursors=new Set();
Type guard
const isFreshCursor=(c,seen)=>c==null||!seen.has(c);
Try / catch
try{await importTurnItems(turn);}catch(e){if(e.message==='历史消息分页游标重复'){cache.delete(`${key}:partial`);return importTurnItems(turn);}throw e;} Prevention
- Clear partial cache after Codex upgrades
- Avoid importing history while the thread is being written
- Cap retry attempts when pagination cycles
- Track cursor sets across retries to detect persistent server bugs
When it happens
Trigger: A 'thread/items/list' response whose nextCursor points back to an already-used cursor value (including a repeated non-null cursor), producing a pagination cycle for that turn.
Common situations: Codex backend pagination bug after version upgrade; corrupted thread store returning stale cursors; transient server state during concurrent writes to the same thread.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/04a65ab291e0aa95.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/public/canvas.user.js:425
for(const turn of page.data){
if(typeof turn.id!=='string')throw Error('历史轮次缺少 ID');
if(!turnIds.has(turn.id)){turnIds.add(turn.id);turns.push(turn);}
}
cursor=page.nextCursor??null;
onProgress(`正在读取轮次目录 · ${turns.length} 轮…`);
}while(cursor!==null);
const messages=[],seen=new Set();
for(const [index,turn] of turns.entries()){
signal?.throwIfAborted();onProgress(`正在读取历史 ${index+1}/${turns.length}…`);
const key=`${threadId}:${turn.id}`,cached=await cache.get(key);
let parsed;
if(cached&&turn.status==='completed')parsed=cached;
else{
const partial=turn.status==='completed'?await cache.get(`${key}:partial`):null;
parsed=partial?.messages?.slice()||[];let itemCursor=partial?.cursor??null,limit=32;const cursors=new Set();
do{
signal?.throwIfAborted();
if(cursors.has(itemCursor))throw Error('历史消息分页游标重复');
let page;
for(;;){
try{page=await send('thread/items/list',{threadId,turnId:turn.id,cursor:itemCursor,limit,sortDirection:'asc'});break;}
catch(error){
if(limit>1&&/decoded message length too large/i.test(error.message)){limit=Math.max(1,Math.floor(limit/2));continue;}
throw error;
}
}
if(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');
cursors.add(itemCursor);
for(const row of page.data){
if(row.turnId!=null&&row.turnId!==turn.id)throw Error('历史消息所属轮次不匹配');
const item=row.item??row;
if(!['userMessage','agentMessage'].includes(item.type))continue;
if(typeof item.id!=='string')throw Error('历史消息缺少稳定 ID');
const role=item.type==='userMessage'?'user':'assistant';
const content=role==='user'?(item.content??[]):[{text:item.text??''}];
const message=parseMessage({type:'response_item',payload:{type:'message',id:item.id,role,phase:role==='assistant'?(item.phase??'final_answer'):'',content}},item.id);View on GitHub (pinned to b1ed92e5e4)