BigPizzaV3/CodexPlusPlus · error

历史轮次分页游标重复

Error message

历史轮次分页游标重复

What it means

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.

Solutions

  1. Upgrade/restart the codex backend so thread/turns/list advances its cursor
  2. Inspect the mocked or proxied send() implementation for a fixed nextCursor
  3. Capture the responses and report a pagination-loop bug upstream
  4. Reduce page size or use a different itemsView/sortDirection to work around the loop

Example fix

// before
const page = await send('thread/turns/list',{threadId,cursor,limit:20});
// after
// ensure send() is the real backend, not a stub returning a constant nextCursor
Defensive patterns

Strategy: try-catch

Validate before calling

// no pre-call validation possible; detect via response
if (page?.nextCursor != null && seen.has(page.nextCursor)) throw new Error('cursor loop');

Type guard

const isValidPage = p => p && Array.isArray(p.data);

Try / catch

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

Prevention

When it happens

Trigger: The codex backend returns a nextCursor that repeats (including repeating null/null cycles) during thread/turns/list pagination.

Common situations: Backend bug or protocol version change in thread/turns/list; proxy/mocked send() that always returns the same page; server-side cursor serialization issue.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at tools/conversation-canvas/native-history.mjs:14

import {parseMessage} from './model.mjs';

export 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.
export 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)