{"record":{"id":"2e982521def74b56","repo":"BigPizzaV3/CodexPlusPlus","slug":"message-without-an-id-requires-a-stable-fallback-id-model","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/model.mjs","lineNumber":12,"sourceCode":"export 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\nexport function excerpt(text, length=46) {\n  return text.replace(/```[\\s\\S]*?```/g,'').replace(/[*#`]/g,'').replace(/\\s+/g,' ').trim().slice(0,length);\n}\n\nexport function 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":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/tools/conversation-canvas/model.mjs#L1-L30","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst m = parseMessage(record);\n// after\nconst m = parseMessage(record, (text) => `msg-${threadId}-${hash(text)}`);","handlingStrategy":"validation","validationCode":"const hasId = (rec) => Boolean(rec?.payload?.id) || typeof fallbackId === 'function' || (typeof fallbackId === 'string' && fallbackId);\nif (!hasId(record)) throw new Error('Message without an ID requires a stable fallback ID');","typeGuard":"const canParse = (rec, fallbackId) => rec?.payload?.id || (typeof fallbackId==='function' && typeof fallbackId('')==='string' && fallbackId('')) || (typeof fallbackId==='string' && fallbackId);","tryCatchPattern":"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; }","preventionTips":["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"],"tags":["parser","identity","validation"],"backgroundTag":"missing-required-argument","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"}