BigPizzaV3/CodexPlusPlus · error

历史轮次缺少 ID

Error message

历史轮次缺少 ID

What it means

readNativeHistory paginates thread/turns/list with a cursor; this throw fires when a returned page's turn(s) lack an id. Turn ids are the keys for the per-turn thread/items/list follow-ups and the dedupe sets (turnIds/turnCursors), so an id-less turn cannot be processed and the whole history read aborts.

Solutions

  1. Skip or log-and-skip malformed turns instead of aborting, if acceptable
  2. Upgrade the codex backend to a version emitting string turn ids
  3. Inspect the raw page.data to identify the malformed turn
  4. Validate threadId points at a healthy thread; try a different thread

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 bad = page.data.filter(t => typeof t?.id !== 'string');
if (bad.length) console.warn('turns without id', bad);

Type guard

const hasTurnId = t => typeof t?.id === 'string' && t.id.length > 0;

Try / catch

try { await readNativeHistory(send, threadId) } catch (e) { if (e.message === '历史轮次缺少 ID') { skipBadTurnsAndRetry(); } else throw e; }

Prevention

When it happens

Trigger: Backend returns a turn object without id, or with a numeric/null id; partial/corrupted turn records in a page.

Common situations: Protocol version drift where turn records changed shape; corrupted session storage in the codex app; custom backend implementations omitting id.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

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

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