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
- Update canvas.user.js to the version matching your installed Codex build (the internal API schema changed).
- Inspect the raw page response (log page.data) to confirm which field now holds the turn id and patch the check accordingly.
- Exclude/retry malformed turns instead of aborting: change the throw to a continue with a warning.
- 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
- Keep canvas.user.js in lockstep with your Codex version
- Log raw API pages once when integrating a new Codex build
- Prefer skipping malformed rows over aborting whole imports
- Pin Codex version when running automated history imports
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)