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 derives a message id from the record's payload id or, when absent, from a caller-supplied fallbackId (string or function of the text). If neither yields a truthy id, it throws 'Message without an ID requires a stable fallback ID', because graph nodes need stable, deduplicatable ids across refreshes.
Solutions
- Always pass a stable fallbackId when calling parseMessage, e.g. `message-${threadId}-line-${i}` as graphFromExport does
- Pass a function fallback keyed on text (or text hash) so ids are stable across refreshes
- Skip records without ids instead of parsing them when ids aren't needed
- Check the codex client version if payload ids disappeared from rollout records
Example fix
// before
const m = parseMessage(record);
// after
const m = parseMessage(record, (text) => `msg-${threadId}-${hash(text)}`); Defensive patterns
Strategy: validation
Validate before calling
const hasId = (rec) => Boolean(rec?.payload?.id) || typeof fallbackId === 'function' || (typeof fallbackId === 'string' && fallbackId);
if (!hasId(record)) throw new Error('Message without an ID requires a stable fallback ID'); Type guard
const canParse = (rec, fallbackId) => rec?.payload?.id || (typeof fallbackId==='function' && typeof fallbackId('')==='string' && fallbackId('')) || (typeof fallbackId==='string' && fallbackId); Try / catch
try { const m = parseMessage(record, fallbackId); } catch (e) { if (String(e.message).includes('fallback ID')) { console.warn('skipping id-less record'); return null; } throw e; } Prevention
- Always pass a deterministic fallbackId (line index or text hash)
- Prefer function fallbacks keyed on text for stability across refreshes
- Update fallbacks when codex client changes rollout record shapes
When it happens
Trigger: Parsing a rollout record whose payload has no id field while the caller passed fallbackId=null/undefined/'' — e.g. parseMessage(r) called without the second argument, or a fallbackId function returning empty for empty text.
Common situations: New codex rollout record types lacking payload ids after a client update; hand-written tests parsing raw records without a fallback; fallback function returning '' for messages that reduce to empty text after citation stripping.
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
- 记录中的任务 ID 与当前任务不一致
- 记录中的任务 ID 与当前任务不一致
- Message without an ID requires a stable fallback ID
- 主题包必须是 32 MiB 以内的普通 ZIP 文件
- 请填写 API 的模型名称
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/2e982521def74b56.
Report an issue: GitHub.
Appendix: source
Thrown at tools/conversation-canvas/model.mjs:12
export 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};
}
export function excerpt(text, length=46) {
return text.replace(/```[\s\S]*?```/g,'').replace(/[*#`]/g,'').replace(/\s+/g,' ').trim().slice(0,length);
}
export 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)