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

  1. Pass a stable fallbackId (e.g. (text)=>hash(text) or `${record.ordinal}`) at every parseMessage call site.
  2. 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}`.
  3. 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

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


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)