{"record":{"id":"a42c6e0280ca1609","repo":"can1357/oh-my-pi","slug":"first-kept-entry-has-no-id-session-may-need-migr-a42c6e","errorCode":null,"errorMessage":"First kept entry has no ID - session may need migration","messagePattern":"First kept entry has no ID - session may need migration","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/snapcompact/src/snapcompact.ts","lineNumber":2043,"sourceCode":" * Run a snapcompact compaction over prepared messages. Fully local: serializes\n * the discarded history, appends it to the accumulated archive source text, and\n * re-renders that source into an ordered history layout: plain text at the\n * oldest edge, imaged middle, then plain text at the newest edge. The imaged\n * middle itself foveates (HQ/LQ/HQ) when it grows large.\n *\n * The full kept source persists on the archive (`text`) so each later compaction\n * unfolds and re-renders it coherently alongside the newly archived history.\n *\n * If the previous compaction was text-based, its summary is printed at the head\n * of the archive as `[Summary of earlier history]` so no continuity is lost.\n */\nexport async function compact<TMessage = Message>(\n\tpreparation: CompactionPreparation<TMessage>,\n\toptions?: Options<TMessage>,\n): Promise<CompactionResult> {\n\tconst { firstKeptEntryId, tokensBefore, previousSummary, previousPreserveData, fileOps } = preparation;\n\tif (!firstKeptEntryId) {\n\t\tthrow new Error(\"First kept entry has no ID - session may need migration\");\n\t}\n\tconst messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);\n\tconst llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);\n\tconst serialized = serializeConversation(llmMessages, options);\n\tconst previousArchive = getPreservedArchive(previousPreserveData);\n\tconst previousTextRaw =\n\t\tpreviousArchive?.text ??\n\t\t[previousArchive?.textHead, previousArchive?.textTail]\n\t\t\t.filter((part): part is string => typeof part === \"string\" && part.length > 0)\n\t\t\t.join(NEWLINE_GLYPH);\n\t// Legacy archives may carry `¶think:` sections from before includeThinking\n\t// existed; scrub them when this compaction excludes thinking so the\n\t// re-rendered archive stops replaying reasoning (issue #6093). They may\n\t// also carry data URLs a pre-guard slice cut at any offset; heal those in\n\t// archive context before the text is folded into the new source.\n\tconst previousTextHealed = elideDataUrls(previousTextRaw, \"archive\");\n\tconst previousText =\n\t\toptions?.includeThinking === false && previousTextHealed.length > 0","sourceCodeStart":2025,"sourceCodeEnd":2061,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/snapcompact/src/snapcompact.ts#L2025-L2061","documentation":"Thrown by compact() in @oh-my-pi/pi-snapcompact when the CompactionPreparation supplied by the caller has a missing/empty firstKeptEntryId. Compaction stores a summary plus the tail of original messages; the ID of the first kept entry is the anchor that ties the new compacted session state to the existing session log. Without it the resulting session cannot be persisted consistently, so the library refuses to proceed.","triggerScenarios":"Calling compact() with a preparation object whose firstKeptEntryId is undefined or empty string — typically because the caller built CompactionPreparation from a session whose entries lack IDs (pre-migration session format, hand-constructed preparation, or an entry lookup returned nothing).","commonSituations":"Opening a session file created by an older omp version whose entries predate ID assignment; tooling or tests that construct CompactionPreparation manually and forget to set firstKeptEntryId; a corrupted or truncated session JSONL where the first kept entry lost its id field.","solutions":["Run the session migration / reopen the session so entry IDs are assigned, then retry compaction","Verify the code building CompactionPreparation sets firstKeptEntryId from the first entry passed to messagesToKeep","Inspect the session file and ensure every entry has a non-empty id; repair or regenerate a corrupted session file","Update omp so the session loader applies the migration that backfills IDs on old session files"],"exampleFix":"// before\nconst preparation = { messagesToSummarize, messagesToKeep };\nawait compact(preparation);\n// after\nconst firstKept = messagesToKeep[0];\nif (!firstKept?.id) throw new Error('Run session migration first: omp session migrate <file>');\nconst preparation = { messagesToSummarize, messagesToKeep, firstKeptEntryId: firstKept.id };\nawait compact(preparation);","handlingStrategy":"validation","validationCode":"if (!preparation.firstKeptEntryId) {\n  throw new Error('Cannot compact: first kept entry has no ID. Run session migration before compacting.');\n}\nawait compact(preparation);","typeGuard":"function isCompactable<T>(p: CompactionPreparation<T>): p is CompactionPreparation<T> & { firstKeptEntryId: string } {\n  return typeof p.firstKeptEntryId === 'string' && p.firstKeptEntryId.length > 0;\n}","tryCatchPattern":"try {\n  await compact(preparation);\n} catch (err) {\n  if (err.message.includes('session may need migration')) {\n    await migrateSession(sessionPath);\n    return compact(preparation);\n  }\n  throw err;\n}","preventionTips":["Always derive firstKeptEntryId from the actual first kept entry instead of hardcoding","Run session-format migrations when opening sessions from older omp versions","When constructing CompactionPreparation manually, assert entry IDs exist before calling compact","Validate session files on load: every entry must carry a non-empty id"],"tags":["session","compaction","migration","data-integrity"],"backgroundTag":"missing-session-entry-id","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}