BigPizzaV3/CodexPlusPlus · error · Error

历史轮次缺少 ID

Error message

历史轮次缺少 ID

What it means

canvas.user.js paginates a thread's turn history via 'thread/turns/list' and requires every returned turn to carry a string 'id' used for dedup and downstream per-turn item fetching. When the backend returns a turn entry whose id is not a string (null, number, missing), it throws this error to abort history import rather than silently dropping turns.

Solutions

  1. Update canvas.user.js to the version matching your installed Codex build (the internal API schema changed).
  2. Inspect the raw page response (log page.data) to confirm which field now holds the turn id and patch the check accordingly.
  3. Exclude/retry malformed turns instead of aborting: change the throw to a continue with a warning.
  4. Verify the thread is not mid-write; reload the thread once streaming completes and retry history import.

Example fix

// before
if(typeof turn.id!=='string')throw Error('历史轮次缺少 ID');
// after
if(typeof turn.id!=='string'){console.warn('skip turn without id',turn);continue;}
Defensive patterns

Strategy: validation

Validate before calling

const page=await send('thread/turns/list',{threadId,cursor,limit:20,itemsView:'notLoaded',sortDirection:'asc'});
const bad=(page?.data||[]).filter(t=>typeof t?.id!=='string');
if(bad.length)console.warn('turns missing id, will be skipped',bad.length);

Type guard

const hasTurnId=t=>t!=null&&typeof t.id==='string';

Try / catch

try{await importHistory(threadId,{onProgress});}catch(e){if(e.message==='历史轮次缺少 ID'){await importHistory(threadId,{onProgress,skipInvalidTurns:true});}else throw e;}

Prevention

When it happens

Trigger: A 'thread/turns/list' page response contains a data[] element with turn.id undefined/non-string — e.g. an in-progress or corrupted turn record, or a Codex backend/schema version that renamed or reshaped the turn id field.

Common situations: Codex app update changed the turns API payload shape; thread stored by an older/newer version with partial turn records; backend returning placeholder rows during active streaming.

Related errors


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

Appendix: source

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


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

View on GitHub (pinned to b1ed92e5e4)