{"record":{"id":"010671755d34fc48","repo":"earendil-works/pi","slug":"invalid-entry-010671","errorCode":"invalid_entry","errorMessage":"Invalid session mutation: ${message}","messagePattern":"Invalid session mutation: (.+?)","errorType":"exception","errorClass":"SessionError","httpStatus":null,"severity":"critical","filePath":"packages/agent/src/harness/session/state.ts","lineNumber":27,"sourceCode":"\ttype LogItem,\n\ttype LogOptions,\n\ttype OperationStartedRecord,\n\ttype RecordQuery,\n\tSessionError,\n\ttype SessionStats,\n} from \"./types.ts\";\n\nexport type SessionMutation =\n\t| { kind: \"entry\"; lane?: string; entry: Entry }\n\t| { kind: \"record\"; record: LaneRecord }\n\t| { kind: \"lane\"; seq: number; lane: string; leafId: string | null }\n\t| { kind: \"fact\"; seq: number; fact: \"name\"; name: string | undefined }\n\t| { kind: \"fact\"; seq: number; fact: \"label\"; targetId: string; label: string | undefined };\n\ntype InvalidMutation = (message: string) => never;\n\nfunction invalidMutation(message: string): never {\n\tthrow new SessionError(\"invalid_entry\", `Invalid session mutation: ${message}`);\n}\n\nfunction assertValidLimit(limit: number | undefined): void {\n\tif (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {\n\t\tthrow new SessionError(\"invalid_query\", \"limit must be a positive integer\");\n\t}\n}\n\nfunction assertValidCursor(afterSeq: number | undefined): void {\n\tif (afterSeq !== undefined && (!Number.isInteger(afterSeq) || afterSeq < 0)) {\n\t\tthrow new SessionError(\"invalid_query\", \"cursor sequence must be a non-negative integer\");\n\t}\n}\n\nfunction* ordered<T>(items: readonly T[], order: EntryOrder | undefined): IterableIterator<T> {\n\tif (order === \"oldestFirst\") {\n\t\tyield* items;\n\t\treturn;","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/harness/session/state.ts#L9-L45","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Take seq from state.nextSequence at append time — never hardcode or persist-and-reuse it","Replay logs in strict ascending seq order starting at 1 with no gaps, and verify replayed counts against storage","Serialize writers so only one append reads nextSequence at a time","If you only use the public Session / InMemorySessionStorage APIs, report it as a library bug with the failing mutation"],"exampleFix":"// before (custom storage replay, order not guaranteed)\nfor (const m of loadedLog) state.applyMutation(m);\n// after\nconst seqOf = (m) => (m.kind === 'entry' ? m.entry.seq : m.kind === 'record' ? m.record.seq : m.seq);\nfor (const m of [...loadedLog].sort((a, b) => seqOf(a) - seqOf(b))) state.applyMutation(m);\n// and build new mutations with seq: state.nextSequence","handlingStrategy":"try-catch","validationCode":"// before replaying a persisted log into SessionState\nconst seqOf = (m: SessionMutation): number =>\n  m.kind === 'entry' ? m.entry.seq : m.kind === 'record' ? m.record.seq : m.seq;\nconst isContiguous = (log: SessionMutation[]): boolean =>\n  [...log].sort((a, b) => seqOf(a) - seqOf(b)).every((m, i) => seqOf(m) === i + 1);","typeGuard":null,"tryCatchPattern":"try {\n  state.applyMutation(mutation);\n} catch (error) {\n  if (error instanceof SessionError && error.code === 'invalid_entry') {\n    // non-retryable: quarantine this session's log and report, do not re-apply the mutation\n    throw new Error(`corrupt session log: ${error.message}`, { cause: error });\n  }\n  throw error;\n}","preventionTips":["Serialize writers so nextSequence reads never race","Build mutations with seq: state.nextSequence at append time","Persist logs atomically (write temp file, then rename)","Round-trip custom storage implementations through replay tests"],"tags":["internal","invariant","storage","replay","session"],"backgroundTag":"state-invariant-violation","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}