BigPizzaV3/CodexPlusPlus · error · Error

历史轮次分页游标重复

Error message

历史轮次分页游标重复

What it means

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.

Solutions

  1. Update the plugin/Codex so the pagination protocol matches the backend implementation
  2. Inspect the raw thread/turns/list responses for a protocol change (e.g. renamed nextCursor field)
  3. Add a page-count cap in addition to cursor tracking if very large threads are expected
  4. Retry the history read once — a transient backend glitch can return a duplicated cursor

Example fix

// before
if(turnCursors.has(cursor))throw Error('历史轮次分页游标重复');
// after
if(turnCursors.has(cursor))break; // treat as end of pagination
if(turns.length>MAX_TURNS)throw Error('历史轮次超出上限');
Defensive patterns

Strategy: retry

Validate before calling

const seen=new Set(); let cursor=null, guard=0;
while(guard++<100){ if(seen.has(cursor)) return /* stop early */; seen.add(cursor); cursor=await fetchNextCursor(cursor); if(cursor==null) break; }

Type guard

function isWellFormedTurnsPage(page){ return !!page && Array.isArray(page.data) && (page.nextCursor===undefined||page.nextCursor===null||typeof page.nextCursor==='string'); }

Try / catch

try { history=await readNativeHistory(send,threadId) } catch(e){ if(e.message==='历史轮次分页游标重复'){ history=await readNativeHistory(send,threadId,new Map()); } else throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/4f2080f266e0e4be. Report an issue: GitHub.

Appendix: source

Thrown at tools/conversation-canvas/public/canvas.user.js:403

    })().catch(error=>{pending=null;throw error;});
    return pending;
  };
}
const loadNativeRuntime=createNativeLoader();


function isExportSizeError(message){
  return /超过.*(?:大小|限制)|too (?:large|big)|size.{0,30}limit|maximum.{0,20}size/i.test(String(message));
}

// Wire contracts verified in the installed Desktop's history loader:
// thread/turns/list(itemsView=notLoaded), then thread/items/list per turn.
async function readNativeHistory(send,threadId,cache=new Map(),onProgress=()=>{},signal){
  const turns=[],turnIds=new Set(),turnCursors=new Set();
  let cursor=null;
  do{
    signal?.throwIfAborted();
    if(turnCursors.has(cursor))throw Error('历史轮次分页游标重复');
    turnCursors.add(cursor);
    const page=await send('thread/turns/list',{threadId,cursor,limit:20,itemsView:'notLoaded',sortDirection:'asc'});
    if(!Array.isArray(page?.data))throw Error('历史轮次接口返回格式无效');
    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;

View on GitHub (pinned to b1ed92e5e4)