can1357/oh-my-pi · error · Error
First kept entry has no ID - session may need migration
Error message
First kept entry has no ID - session may need migration
What it means
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.
Source
Thrown at packages/snapcompact/src/snapcompact.ts:2043
* Run a snapcompact compaction over prepared messages. Fully local: serializes
* the discarded history, appends it to the accumulated archive source text, and
* re-renders that source into an ordered history layout: plain text at the
* oldest edge, imaged middle, then plain text at the newest edge. The imaged
* middle itself foveates (HQ/LQ/HQ) when it grows large.
*
* The full kept source persists on the archive (`text`) so each later compaction
* unfolds and re-renders it coherently alongside the newly archived history.
*
* If the previous compaction was text-based, its summary is printed at the head
* of the archive as `[Summary of earlier history]` so no continuity is lost.
*/
export async function compact<TMessage = Message>(
preparation: CompactionPreparation<TMessage>,
options?: Options<TMessage>,
): Promise<CompactionResult> {
const { firstKeptEntryId, tokensBefore, previousSummary, previousPreserveData, fileOps } = preparation;
if (!firstKeptEntryId) {
throw new Error("First kept entry has no ID - session may need migration");
}
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
const serialized = serializeConversation(llmMessages, options);
const previousArchive = getPreservedArchive(previousPreserveData);
const previousTextRaw =
previousArchive?.text ??
[previousArchive?.textHead, previousArchive?.textTail]
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join(NEWLINE_GLYPH);
// Legacy archives may carry `¶think:` sections from before includeThinking
// existed; scrub them when this compaction excludes thinking so the
// re-rendered archive stops replaying reasoning (issue #6093). They may
// also carry data URLs a pre-guard slice cut at any offset; heal those in
// archive context before the text is folded into the new source.
const previousTextHealed = elideDataUrls(previousTextRaw, "archive");
const previousText =
options?.includeThinking === false && previousTextHealed.length > 0View on GitHub (pinned to 9690622007)
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
Example fix
// before
const preparation = { messagesToSummarize, messagesToKeep };
await compact(preparation);
// after
const firstKept = messagesToKeep[0];
if (!firstKept?.id) throw new Error('Run session migration first: omp session migrate <file>');
const preparation = { messagesToSummarize, messagesToKeep, firstKeptEntryId: firstKept.id };
await compact(preparation); Defensive patterns
Strategy: validation
Validate before calling
if (!preparation.firstKeptEntryId) {
throw new Error('Cannot compact: first kept entry has no ID. Run session migration before compacting.');
}
await compact(preparation); Type guard
function isCompactable<T>(p: CompactionPreparation<T>): p is CompactionPreparation<T> & { firstKeptEntryId: string } {
return typeof p.firstKeptEntryId === 'string' && p.firstKeptEntryId.length > 0;
} Try / catch
try {
await compact(preparation);
} catch (err) {
if (err.message.includes('session may need migration')) {
await migrateSession(sessionPath);
return compact(preparation);
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Compaction failed: no available model
- Session append failed and its partial bytes could not be rol
- Model ${model.id} does not support V2 streaming compaction
- V2 remote compaction failed (${response.status} ${response.s
- No response body for V2 compaction streaming
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/a42c6e0280ca1609.
Report an issue: GitHub.