BigPizzaV3/CodexPlusPlus · error · Error
历史消息缺少稳定 ID
Error message
历史消息缺少稳定 ID
What it means
Only userMessage/agentMessage items are imported, and each must have a stable string 'id' because it is used as the dedup/message key passed to parseMessage. An item lacking a string id cannot be anchored in the canvas, so the script throws.
Solutions
- Update canvas.user.js to the current Codex-compatible version.
- Log the offending row to identify the new id field name and adjust the extraction (e.g. row.id ?? item.id).
- Skip id-less items with a warning instead of aborting the import.
- Rebuild the thread's cached pages and retry after an app reload.
Example fix
// before
if(typeof item.id!=='string')throw Error('历史消息缺少稳定 ID');
// after
const id=item.id??row.id;
if(typeof id!=='string'){console.warn('skip item without id',item);continue;} Defensive patterns
Strategy: type-guard
Validate before calling
const badItems=page.data.filter(r=>['userMessage','agentMessage'].includes((r.item??r)?.type)&&typeof (r.item??r)?.id!=='string');
if(badItems.length)console.warn('items without stable id will be skipped',badItems.length); Type guard
const hasStableId=item=>item!=null&&typeof item.id==='string';
Try / catch
try{await importTurnItems(turn);}catch(e){if(e.message==='历史消息缺少稳定 ID'){return importTurnItems(turn,{skipIdless:true});}throw e;} Prevention
- Fall back to row.id when item.id is absent
- Deduplicate by multiple key candidates (id/itemId/messageId)
- Skip-and-log instead of abort for display-only imports
- Diff item schema after each Codex update
When it happens
Trigger: A page row of type userMessage or agentMessage whose item.id is missing or not a string — schema drift in the items payload, or a synthesized/placeholder item from a Codex version change.
Common situations: Codex update renamed id to itemId or moved it; items created by a different client version without ids; corrupted thread store entries.
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/3199a66f59f7d1e4.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/public/canvas.user.js:440
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;
for(;;){
try{page=await send('thread/items/list',{threadId,turnId:turn.id,cursor:itemCursor,limit,sortDirection:'asc'});break;}
catch(error){
if(limit>1&&/decoded message length too large/i.test(error.message)){limit=Math.max(1,Math.floor(limit/2));continue;}
throw error;
}
}
if(!Array.isArray(page?.data))throw Error('历史消息接口返回格式无效');
cursors.add(itemCursor);
for(const row of page.data){
if(row.turnId!=null&&row.turnId!==turn.id)throw Error('历史消息所属轮次不匹配');
const item=row.item??row;
if(!['userMessage','agentMessage'].includes(item.type))continue;
if(typeof item.id!=='string')throw Error('历史消息缺少稳定 ID');
const role=item.type==='userMessage'?'user':'assistant';
const content=role==='user'?(item.content??[]):[{text:item.text??''}];
const message=parseMessage({type:'response_item',payload:{type:'message',id:item.id,role,phase:role==='assistant'?(item.phase??'final_answer'):'',content}},item.id);
if(message)parsed.push({...message,turnId:turn.id,turnStatus:turn.status});
}
itemCursor=page.nextCursor??null;
onProgress(`正在读取历史 ${index+1}/${turns.length} · 已读 ${parsed.length} 条消息…`);
if(turn.status==='completed'&&itemCursor!==null)await cache.set(`${key}:partial`,{messages:parsed,cursor:itemCursor});
}while(itemCursor!==null);
if(turn.status==='completed'){await cache.set(key,parsed);await cache.delete(`${key}:partial`);}
}
// Older completed-turn caches contain text and IDs but no turn metadata.
// The current directory and cache key provide the authoritative owner.
for(const message of parsed)if(!seen.has(message.id)){seen.add(message.id);messages.push({...message,turnId:turn.id,turnStatus:turn.status});}
}
// Bound cross-task retention, without truncating the returned conversation.
while(cache.size>500)cache.delete(cache.keys().next().value);
return messages;View on GitHub (pinned to b1ed92e5e4)