BigPizzaV3/CodexPlusPlus · error · Error
Message without an ID requires a stable fallback ID
Error message
Message without an ID requires a stable fallback ID
What it means
parseMessage() builds a message object that must carry an id; it prefers the native Codex message id (p.id) and falls back to a caller-supplied fallbackId (string or function of the text). If neither exists it throws, because downstream code (dedup via seen sets, tree assembly, ordinal mapping in canvas.user.js) depends on a stable identifier per message.
Solutions
- Pass a stable fallbackId (e.g. (text)=>hash(text) or `${record.ordinal}`) at every parseMessage call site.
- Prefer a structurally stable key like turnId or ordinal when p.id is absent: fallbackId = (t)=>p.internal_chat_message_metadata_passthrough?.turn_id ?? `ord-${record.ordinal}`.
- Handle records with no id by skipping them (return null) instead of throwing, if they carry no renderable content.
Example fix
// before
const id = p.id || (typeof fallbackId==='function'?fallbackId(text):fallbackId);
if (!id) throw new Error('Message without an ID requires a stable fallback ID');
// after
const id = p.id || p.internal_chat_message_metadata_passthrough?.turn_id || (typeof fallbackId==='function'?fallbackId(text):fallbackId) || `ordinal-${record.ordinal}`;
if (!id) return null; // skip unidentifiable records instead of throwing Defensive patterns
Strategy: validation
Validate before calling
const id = p.id || p.internal_chat_metadata?.turn_id || record.ordinal;
if (id == null) { console.warn('skipping message without stable id', record); return null; } Type guard
function hasUsableId(p, record) {
return p?.id != null || p?.internal_chat_message_metadata_passthrough?.turn_id != null || record?.ordinal != null;
} Try / catch
try {
const msg = parseMessage(record, fallbackId);
} catch (e) {
if (String(e).includes('stable fallback ID')) {
console.warn('Unidentifiable message skipped:', record.ordinal);
return null;
} throw e;
} Prevention
- Always supply a deterministic fallbackId (hash of text or ordinal-based) at call sites.
- Prefer structurally stable keys (turn_id, ordinal) over text hashes to survive content edits.
- Skip rather than throw for records lacking ids when they carry no renderable content.
- Add a unit test feeding rollout records without id fields through parseMessage.
When it happens
Trigger: Calling parseMessage on a rollout JSONL record where the parsed payload has no id field and the caller passed an empty/undefined fallbackId (or a fallbackId function returning falsy, e.g. for a message whose text is empty after stripping citations).
Common situations: New Codex rollout record kinds that omit id; callers that hardcode undefined as fallback; messages whose entire content was <oai-mem-citation> blocks so the fallbackId(text) hash of empty text collides or yields null.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Message without an ID requires a stable fallback ID
- 本批改变了共同目标根节点
- Codex command is empty
- {}
- 记录中的任务 ID 与当前任务不一致
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/4c0886a8694f5b80.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/public/canvas.user.js:24
(function installCanvas() {
if(!document.body){window.addEventListener('DOMContentLoaded',installCanvas,{once:true});return;}
if(window.top!==window || !/^app:\/\/-/.test(location.href))return;
window.__conversationCanvasCleanup?.();
const previous=document.getElementById('conversation-canvas-host');
previous?.shadowRoot?.querySelector('aside')?.classList.remove('open');
previous?.remove();
function parseMessage(record, fallbackId) {
const p = record.payload;
if (record.type !== 'response_item' || p?.type !== 'message' || !['user','assistant'].includes(p.role)) return null;
let text = (p.content || []).map(c => c.text || '').join('\n').trim();
if (!text || /^<(recommended_plugins|environment_context)/.test(text) || text.startsWith('# AGENTS.md instructions')) return null;
if (text.startsWith('<send_user_message_question_reply>')) {
try { text = JSON.parse(text.replace(/<\/?send_user_message_question_reply>/g,'').trim()).map(a=>a.answer).join('\n'); } catch { return null; }
}
text = text.replace(/<oai-mem-citation>[\s\S]*?<\/oai-mem-citation>/g,'').trim();
// Callers supply a stable fallback; native Codex message IDs take precedence.
const id = p.id || (typeof fallbackId==='function'?fallbackId(text):fallbackId);
if (!id) throw new Error('Message without an ID requires a stable fallback ID');
return {id, text, role:p.role, phase:p.phase || '', timestamp:record.timestamp, ordinal:record.ordinal, turnId:p.internal_chat_message_metadata_passthrough?.turn_id || null};
}
function excerpt(text, length=46) {
return text.replace(/```[\s\S]*?```/g,'').replace(/[*#`]/g,'').replace(/\s+/g,' ').trim().slice(0,length);
}
function buildGraph(messages, annotations={nodes:[]}) {
const ids = new Set(messages.map(m=>m.id));
const nodes = (annotations.nodes || []).filter(n=>n.sources?.length && n.sources.every(id=>ids.has(id))).map(n=>({...n,summaryKind:'已整理'}));
const covered = new Set(nodes.flatMap(n=>n.sources));
for(const id of annotations.incompleteSources||[])covered.delete(id);
let previous = nodes.filter(n=>n.lane==='main').at(-1)?.id || null;
for (const m of messages) {
if (covered.has(m.id) || (m.role==='assistant' && m.phase!=='final_answer')) continue;
nodes.push({id:`auto-${m.id}`,parent:annotations.schemaVersion===2?null:previous,lane:annotations.schemaVersion===2?'pending':'main',title:excerpt(m.text,34),summary:excerpt(m.text,160),description:m.text, status:'待整理',summaryKind:'原文摘录',sources:[m.id]});
previous = `auto-${m.id}`;
}View on GitHub (pinned to b1ed92e5e4)