earendil-works/pi · critical · SessionError

invalid_entry

invalid_entry

Error message

Invalid session mutation: ${message}

What it means

SessionState.applyMutation is the replay engine behind durable sessions and enforces the invariants that make the log reconstructible: seq must be exactly previous + 1, entry/record ids must be unique, referenced lanes/parents/label targets must exist, and entries must chain to the lane's current leaf. This error reports the first breached invariant and the message names it (for example 'has non-consecutive seq 5', 'does not chain to the lane leaf', 'references missing parent <id>'). The built-in InMemorySessionStorage always builds conforming mutations, so hitting this means a custom SessionStorage implementation, a hand-rolled log replay, or corrupted persisted log data.

Source

Thrown at packages/agent/src/harness/session/state.ts:27

	type LogItem,
	type LogOptions,
	type OperationStartedRecord,
	type RecordQuery,
	SessionError,
	type SessionStats,
} from "./types.ts";

export type SessionMutation =
	| { kind: "entry"; lane?: string; entry: Entry }
	| { kind: "record"; record: LaneRecord }
	| { kind: "lane"; seq: number; lane: string; leafId: string | null }
	| { kind: "fact"; seq: number; fact: "name"; name: string | undefined }
	| { kind: "fact"; seq: number; fact: "label"; targetId: string; label: string | undefined };

type InvalidMutation = (message: string) => never;

function invalidMutation(message: string): never {
	throw new SessionError("invalid_entry", `Invalid session mutation: ${message}`);
}

function assertValidLimit(limit: number | undefined): void {
	if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
		throw new SessionError("invalid_query", "limit must be a positive integer");
	}
}

function assertValidCursor(afterSeq: number | undefined): void {
	if (afterSeq !== undefined && (!Number.isInteger(afterSeq) || afterSeq < 0)) {
		throw new SessionError("invalid_query", "cursor sequence must be a non-negative integer");
	}
}

function* ordered<T>(items: readonly T[], order: EntryOrder | undefined): IterableIterator<T> {
	if (order === "oldestFirst") {
		yield* items;
		return;

View on GitHub (pinned to 4af9d21d3b)

Solutions

  1. Take seq from state.nextSequence at append time — never hardcode or persist-and-reuse it
  2. Replay logs in strict ascending seq order starting at 1 with no gaps, and verify replayed counts against storage
  3. Serialize writers so only one append reads nextSequence at a time
  4. If you only use the public Session / InMemorySessionStorage APIs, report it as a library bug with the failing mutation

Example fix

// before (custom storage replay, order not guaranteed)
for (const m of loadedLog) state.applyMutation(m);
// after
const seqOf = (m) => (m.kind === 'entry' ? m.entry.seq : m.kind === 'record' ? m.record.seq : m.seq);
for (const m of [...loadedLog].sort((a, b) => seqOf(a) - seqOf(b))) state.applyMutation(m);
// and build new mutations with seq: state.nextSequence
Defensive patterns

Strategy: try-catch

Validate before calling

// before replaying a persisted log into SessionState
const seqOf = (m: SessionMutation): number =>
  m.kind === 'entry' ? m.entry.seq : m.kind === 'record' ? m.record.seq : m.seq;
const isContiguous = (log: SessionMutation[]): boolean =>
  [...log].sort((a, b) => seqOf(a) - seqOf(b)).every((m, i) => seqOf(m) === i + 1);

Try / catch

try {
  state.applyMutation(mutation);
} catch (error) {
  if (error instanceof SessionError && error.code === 'invalid_entry') {
    // non-retryable: quarantine this session's log and report, do not re-apply the mutation
    throw new Error(`corrupt session log: ${error.message}`, { cause: error });
  }
  throw error;
}

Prevention

When it happens

Trigger: A custom storage backend calling state.applyMutation with mutations built out of order or with hardcoded seq values; replaying a persisted log that was truncated or has duplicated items; two concurrent writers both reading nextSequence and appending; appending an entry whose parentId is not the lane's current leaf.

Common situations: Writing alternative persistence (SQLite, flat files) on top of SessionState and forwarding mutations without preserving order; a crash mid-write leaving a partial log; parallel test writers sharing one session state.

Related errors


AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24). Data as JSON: /api/errors/010671755d34fc48. Report an issue: GitHub.