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 > 0

View on GitHub (pinned to 9690622007)

Solutions

  1. Run the session migration / reopen the session so entry IDs are assigned, then retry compaction
  2. Verify the code building CompactionPreparation sets firstKeptEntryId from the first entry passed to messagesToKeep
  3. Inspect the session file and ensure every entry has a non-empty id; repair or regenerate a corrupted session file
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a42c6e0280ca1609. Report an issue: GitHub.