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