{"record":{"id":"4c0886a8694f5b80","repo":"BigPizzaV3/CodexPlusPlus","slug":"message-without-an-id-requires-a-stable-fallback-id","errorCode":null,"errorMessage":"Message without an ID requires a stable fallback ID","messagePattern":"Message without an ID requires a stable fallback ID","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/conversation-canvas/public/canvas.user.js","lineNumber":24,"sourceCode":"(function installCanvas() {\n  if(!document.body){window.addEventListener('DOMContentLoaded',installCanvas,{once:true});return;}\n  if(window.top!==window || !/^app:\\/\\/-/.test(location.href))return;\n  window.__conversationCanvasCleanup?.();\n  const previous=document.getElementById('conversation-canvas-host');\n  previous?.shadowRoot?.querySelector('aside')?.classList.remove('open');\n  previous?.remove();\n  function parseMessage(record, fallbackId) {\n  const p = record.payload;\n  if (record.type !== 'response_item' || p?.type !== 'message' || !['user','assistant'].includes(p.role)) return null;\n  let text = (p.content || []).map(c => c.text || '').join('\\n').trim();\n  if (!text || /^<(recommended_plugins|environment_context)/.test(text) || text.startsWith('# AGENTS.md instructions')) return null;\n  if (text.startsWith('<send_user_message_question_reply>')) {\n    try { text = JSON.parse(text.replace(/<\\/?send_user_message_question_reply>/g,'').trim()).map(a=>a.answer).join('\\n'); } catch { return null; }\n  }\n  text = text.replace(/<oai-mem-citation>[\\s\\S]*?<\\/oai-mem-citation>/g,'').trim();\n  // Callers supply a stable fallback; native Codex message IDs take precedence.\n  const id = p.id || (typeof fallbackId==='function'?fallbackId(text):fallbackId);\n  if (!id) throw new Error('Message without an ID requires a stable fallback ID');\n  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};\n}\n\nfunction excerpt(text, length=46) {\n  return text.replace(/```[\\s\\S]*?```/g,'').replace(/[*#`]/g,'').replace(/\\s+/g,' ').trim().slice(0,length);\n}\n\nfunction buildGraph(messages, annotations={nodes:[]}) {\n  const ids = new Set(messages.map(m=>m.id));\n  const nodes = (annotations.nodes || []).filter(n=>n.sources?.length && n.sources.every(id=>ids.has(id))).map(n=>({...n,summaryKind:'已整理'}));\n  const covered = new Set(nodes.flatMap(n=>n.sources));\n  for(const id of annotations.incompleteSources||[])covered.delete(id);\n  let previous = nodes.filter(n=>n.lane==='main').at(-1)?.id || null;\n  for (const m of messages) {\n    if (covered.has(m.id) || (m.role==='assistant' && m.phase!=='final_answer')) continue;\n    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]});\n    previous = `auto-${m.id}`;\n  }","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/public/canvas.user.js#L6-L42","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nconst id = p.id || (typeof fallbackId==='function'?fallbackId(text):fallbackId);\nif (!id) throw new Error('Message without an ID requires a stable fallback ID');\n// after\nconst id = p.id || p.internal_chat_message_metadata_passthrough?.turn_id || (typeof fallbackId==='function'?fallbackId(text):fallbackId) || `ordinal-${record.ordinal}`;\nif (!id) return null; // skip unidentifiable records instead of throwing","handlingStrategy":"validation","validationCode":"const id = p.id || p.internal_chat_metadata?.turn_id || record.ordinal;\nif (id == null) { console.warn('skipping message without stable id', record); return null; }","typeGuard":"function hasUsableId(p, record) {\n  return p?.id != null || p?.internal_chat_message_metadata_passthrough?.turn_id != null || record?.ordinal != null;\n}","tryCatchPattern":"try {\n  const msg = parseMessage(record, fallbackId);\n} catch (e) {\n  if (String(e).includes('stable fallback ID')) {\n    console.warn('Unidentifiable message skipped:', record.ordinal);\n    return null;\n  } throw e;\n}","preventionTips":["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."],"tags":["parser","identity","invariant","javascript"],"backgroundTag":"internal-invariant-violation","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}