@averyyy/pi-server 0.82.1-piclient.2 → 0.82.1-piclient.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +25 -0
- package/bin/update.js +156 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pi-server-protocol.d.ts +16 -0
- package/dist/pi-server-protocol.d.ts.map +1 -1
- package/dist/pi-server-protocol.js +30 -1
- package/dist/pi-server-protocol.js.map +1 -1
- package/dist/receive-upload.d.ts +5 -0
- package/dist/receive-upload.d.ts.map +1 -1
- package/dist/receive-upload.js +88 -6
- package/dist/receive-upload.js.map +1 -1
- package/dist/request-chunks.d.ts +33 -0
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +540 -48
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +41 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3088 -366
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts +39 -1
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +1132 -116
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +112 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +695 -120
- package/dist/session-store.js.map +1 -1
- package/dist/stream-run-persistence.d.ts +163 -0
- package/dist/stream-run-persistence.d.ts.map +1 -0
- package/dist/stream-run-persistence.js +1824 -0
- package/dist/stream-run-persistence.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { type SessionState } from "./session-store.ts";
|
|
2
|
+
export type SessionPersistenceFaultPoint = "wal_after_sync_before_head" | "snapshot_after_sync_before_head";
|
|
3
|
+
export interface SessionPersistenceOptions {
|
|
4
|
+
faultInjector?: (point: SessionPersistenceFaultPoint) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionPersistenceArtifactLimits {
|
|
7
|
+
maxSnapshotBytes: number;
|
|
8
|
+
maxWalBytes: number;
|
|
9
|
+
maxHeadBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SessionPersistencePreflightResult {
|
|
12
|
+
strategy: "none" | "snapshot" | "wal";
|
|
13
|
+
snapshotArtifactBytes?: number;
|
|
14
|
+
walArtifactBytes?: number;
|
|
15
|
+
headArtifactBytes?: number;
|
|
16
|
+
}
|
|
17
|
+
export type SessionPersistenceCapacityResource = "snapshot_artifact_bytes" | "wal_artifact_bytes" | "head_artifact_bytes" | "session_entries" | "session_logical_bytes" | "aggregate_entries" | "aggregate_logical_bytes" | "loaded_sessions";
|
|
18
|
+
export declare class SessionPersistenceCapacityError extends Error {
|
|
19
|
+
readonly code = "PI_SERVER_SESSION_PERSISTENCE_CAPACITY_EXCEEDED";
|
|
20
|
+
readonly retryable: false;
|
|
21
|
+
readonly resource: SessionPersistenceCapacityResource;
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly current: number;
|
|
25
|
+
readonly requested: number;
|
|
26
|
+
readonly limit: number;
|
|
27
|
+
constructor(input: {
|
|
28
|
+
resource: SessionPersistenceCapacityResource;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
path: string;
|
|
31
|
+
current: number;
|
|
32
|
+
requested: number;
|
|
33
|
+
limit: number;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export declare function getSessionPersistenceArtifactLimits(): SessionPersistenceArtifactLimits;
|
|
37
|
+
export declare function configureSessionPersistenceArtifactLimits(overrides: Partial<SessionPersistenceArtifactLimits>): SessionPersistenceArtifactLimits;
|
|
38
|
+
export declare function resetSessionPersistenceArtifactLimits(): void;
|
|
2
39
|
export declare function loadPersistedSessions(sessionStoreDir: string): void;
|
|
3
|
-
export declare function
|
|
40
|
+
export declare function preflightPersistedSession(sessionStoreDir: string, session: SessionState): SessionPersistencePreflightResult;
|
|
41
|
+
export declare function savePersistedSession(sessionStoreDir: string, session: SessionState, options?: SessionPersistenceOptions): void;
|
|
4
42
|
export declare function deletePersistedSession(sessionStoreDir: string, sessionId: string): void;
|
|
5
43
|
//# sourceMappingURL=session-persistence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-persistence.d.ts","sourceRoot":"","sources":["../src/session-persistence.ts"],"names":[],"mappings":"AAaA,OAAO,EAKN,KAAK,YAAY,EAEjB,MAAM,oBAAoB,CAAC;AA8N5B,wBAAgB,qBAAqB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAkBnE;AAkCD,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAazF;AAED,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvF","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport {\n\tappendFileSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport {\n\texportSessionState,\n\tmarkSessionPersisted,\n\ttype PersistedSessionState,\n\trestoreSessionState,\n\ttype SessionState,\n\ttype SessionStaticContext,\n} from \"./session-store.ts\";\n\nconst PERSISTED_SESSION_VERSION = 1;\nconst WAL_SNAPSHOT_INTERVAL = 32;\nconst REPLACE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];\n\ninterface PersistedSessionFile {\n\tversion: 1;\n\tsession: PersistedSessionState;\n}\n\ninterface PersistedSessionWalRecord {\n\tversion: 1;\n\tsessionId: string;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n\tupdatedAt: number;\n\tstaticContext: SessionStaticContext | undefined;\n}\n\ninterface PersistedSessionMeta {\n\tentryCount: number;\n\twalRecords: number;\n}\n\nconst persistedSessions = new Map<string, PersistedSessionMeta>();\n\nfunction sessionFileName(sessionId: string): string {\n\treturn `${createHash(\"sha256\").update(sessionId).digest(\"hex\")}.json`;\n}\n\nfunction sessionPath(sessionStoreDir: string, sessionId: string): string {\n\treturn join(sessionStoreDir, sessionFileName(sessionId));\n}\n\nfunction walPath(sessionStoreDir: string, sessionId: string): string {\n\treturn `${sessionPath(sessionStoreDir, sessionId)}.wal`;\n}\n\nfunction persistedSessionKey(sessionStoreDir: string, sessionId: string): string {\n\treturn `${sessionStoreDir}\\0${sessionId}`;\n}\n\nfunction isRetryableRenameError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn process.platform === \"win32\" && code === \"EPERM\";\n}\n\nfunction sleepSync(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\nfunction replaceFileSync(sourcePath: string, targetPath: string): void {\n\tfor (let attempt = 0; attempt <= REPLACE_RETRY_DELAYS_MS.length; attempt++) {\n\t\ttry {\n\t\t\trenameSync(sourcePath, targetPath);\n\t\t\treturn;\n\t\t} catch (error) {\n\t\t\tif (!isRetryableRenameError(error) || attempt === REPLACE_RETRY_DELAYS_MS.length) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tsleepSync(REPLACE_RETRY_DELAYS_MS[attempt]);\n\t\t}\n\t}\n}\n\nfunction assertRecord(value: unknown, path: string): asserts value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${path} must be an object`);\n\t}\n}\n\nfunction assertString(value: unknown, path: string): asserts value is string {\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`${path} must be a string`);\n\t}\n}\n\nfunction assertNumber(value: unknown, path: string): asserts value is number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) {\n\t\tthrow new Error(`${path} must be a finite number`);\n\t}\n}\n\nfunction assertNullableString(value: unknown, path: string): asserts value is string | null {\n\tif (value !== null && typeof value !== \"string\") {\n\t\tthrow new Error(`${path} must be a string or null`);\n\t}\n}\n\nfunction assertStaticContext(value: unknown): asserts value is SessionStaticContext | undefined {\n\tif (value === undefined) return;\n\tassertRecord(value, \"session.staticContext\");\n\tconst systemPrompt = value.systemPrompt;\n\tif (systemPrompt !== undefined && typeof systemPrompt !== \"string\") {\n\t\tthrow new Error(\"session.staticContext.systemPrompt must be a string\");\n\t}\n\tconst tools = value.tools;\n\tif (tools !== undefined) {\n\t\tif (!Array.isArray(tools)) {\n\t\t\tthrow new Error(\"session.staticContext.tools must be an array\");\n\t\t}\n\t\tfor (const [index, tool] of tools.entries()) {\n\t\t\tassertRecord(tool, `session.staticContext.tools[${index}]`);\n\t\t\tassertString(tool.name, `session.staticContext.tools[${index}].name`);\n\t\t\tassertString(tool.description, `session.staticContext.tools[${index}].description`);\n\t\t\tif (tool.parameters === undefined) {\n\t\t\t\tthrow new Error(`session.staticContext.tools[${index}].parameters is required`);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction assertSessionTreeEntries(value: unknown): asserts value is SessionTreeEntry[] {\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(\"session.entries must be an array\");\n\t}\n\tfor (const [index, entry] of value.entries()) {\n\t\tassertRecord(entry, `session.entries[${index}]`);\n\t\tassertString(entry.type, `session.entries[${index}].type`);\n\t\tassertString(entry.id, `session.entries[${index}].id`);\n\t\tassertNullableString(entry.parentId, `session.entries[${index}].parentId`);\n\t\tassertString(entry.timestamp, `session.entries[${index}].timestamp`);\n\t\tif (entry.type === \"message\") {\n\t\t\tassertRecord(entry.message, `session.entries[${index}].message`);\n\t\t}\n\t}\n}\n\nfunction assertPersistedSessionState(value: unknown): asserts value is PersistedSessionState {\n\tassertRecord(value, \"session\");\n\tassertString(value.sessionId, \"session.sessionId\");\n\tassertStaticContext(value.staticContext);\n\tassertSessionTreeEntries(value.entries);\n\tassertNullableString(value.leafId, \"session.leafId\");\n\tassertNumber(value.revision, \"session.revision\");\n\tassertNumber(value.createdAt, \"session.createdAt\");\n\tassertNumber(value.updatedAt, \"session.updatedAt\");\n}\n\nfunction assertPersistedWalRecord(\n\tvalue: unknown,\n\tsourcePath: string,\n\tlineNumber: number,\n): asserts value is PersistedSessionWalRecord {\n\tconst path = `${sourcePath}:${lineNumber}`;\n\tassertRecord(value, path);\n\tif (value.version !== PERSISTED_SESSION_VERSION) {\n\t\tthrow new Error(`Unsupported persisted session WAL version in ${path}`);\n\t}\n\tassertString(value.sessionId, `${path}.sessionId`);\n\tassertNumber(value.baseEntryCount, `${path}.baseEntryCount`);\n\tassertSessionTreeEntries(value.entries);\n\tassertNullableString(value.leafId, `${path}.leafId`);\n\tassertNumber(value.revision, `${path}.revision`);\n\tassertNumber(value.updatedAt, `${path}.updatedAt`);\n\tassertStaticContext(value.staticContext);\n}\n\nfunction parsePersistedSessionFile(raw: string, sourcePath: string): PersistedSessionFile {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow new Error(`Persisted session file contains invalid JSON: ${sourcePath}`, { cause: error });\n\t}\n\tassertRecord(parsed, \"persisted session file\");\n\tif (parsed.version !== PERSISTED_SESSION_VERSION) {\n\t\tthrow new Error(`Unsupported persisted session version in ${sourcePath}`);\n\t}\n\tassertPersistedSessionState(parsed.session);\n\treturn {\n\t\tversion: PERSISTED_SESSION_VERSION,\n\t\tsession: parsed.session,\n\t};\n}\n\nfunction parseWalLine(raw: string, sourcePath: string, lineNumber: number): PersistedSessionWalRecord {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow new Error(`Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`, { cause: error });\n\t}\n\tassertPersistedWalRecord(parsed, sourcePath, lineNumber);\n\treturn parsed;\n}\n\nfunction applyWalRecord(session: PersistedSessionState, record: PersistedSessionWalRecord): void {\n\tif (record.sessionId !== session.sessionId) {\n\t\tthrow new Error(`Persisted session WAL sessionId does not match snapshot: ${record.sessionId}`);\n\t}\n\tif (record.baseEntryCount < session.entries.length) {\n\t\tif (record.baseEntryCount + record.entries.length <= session.entries.length) return;\n\t\tthrow new Error(`Persisted session WAL overlaps snapshot for ${record.sessionId}`);\n\t}\n\tif (record.baseEntryCount !== session.entries.length) {\n\t\tthrow new Error(`Persisted session WAL has a gap for ${record.sessionId}`);\n\t}\n\tsession.entries.push(...record.entries.map((entry) => ({ ...entry })));\n\tsession.leafId = record.leafId;\n\tsession.revision = record.revision;\n\tsession.updatedAt = record.updatedAt;\n\tsession.staticContext = record.staticContext;\n}\n\nfunction applyPersistedWal(sessionStoreDir: string, session: PersistedSessionState): number {\n\tconst filePath = walPath(sessionStoreDir, session.sessionId);\n\tif (!existsSync(filePath)) return 0;\n\tconst lines = readFileSync(filePath, \"utf-8\").split(\"\\n\");\n\tlet applied = 0;\n\tfor (const [index, line] of lines.entries()) {\n\t\tif (!line) continue;\n\t\tapplyWalRecord(session, parseWalLine(line, filePath, index + 1));\n\t\tapplied++;\n\t}\n\treturn applied;\n}\n\nexport function loadPersistedSessions(sessionStoreDir: string): void {\n\tif (!existsSync(sessionStoreDir)) return;\n\tconst entries = readdirSync(sessionStoreDir, { withFileTypes: true });\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !entry.name.endsWith(\".json\")) continue;\n\t\tconst filePath = join(sessionStoreDir, entry.name);\n\t\tconst persisted = parsePersistedSessionFile(readFileSync(filePath, \"utf-8\"), filePath);\n\t\tconst expectedFileName = sessionFileName(persisted.session.sessionId);\n\t\tif (entry.name !== expectedFileName) {\n\t\t\tthrow new Error(`Persisted session file name does not match sessionId: ${filePath}`);\n\t\t}\n\t\tconst walRecords = applyPersistedWal(sessionStoreDir, persisted.session);\n\t\trestoreSessionState(persisted.session);\n\t\tpersistedSessions.set(persistedSessionKey(sessionStoreDir, persisted.session.sessionId), {\n\t\t\tentryCount: persisted.session.entries.length,\n\t\t\twalRecords,\n\t\t});\n\t}\n}\n\nfunction writeSnapshot(sessionStoreDir: string, session: SessionState): void {\n\tmkdirSync(sessionStoreDir, { recursive: true });\n\tconst filePath = sessionPath(sessionStoreDir, session.sessionId);\n\tconst tempPath = `${filePath}.${randomUUID()}.tmp`;\n\tconst body: PersistedSessionFile = {\n\t\tversion: PERSISTED_SESSION_VERSION,\n\t\tsession: exportSessionState(session),\n\t};\n\ttry {\n\t\twriteFileSync(tempPath, JSON.stringify(body), \"utf-8\");\n\t\treplaceFileSync(tempPath, filePath);\n\t\trmSync(walPath(sessionStoreDir, session.sessionId), { force: true });\n\t} catch (error) {\n\t\trmSync(tempPath, { force: true });\n\t\tthrow error;\n\t}\n}\n\nfunction appendWalRecord(sessionStoreDir: string, session: SessionState, baseEntryCount: number): void {\n\tconst record: PersistedSessionWalRecord = {\n\t\tversion: PERSISTED_SESSION_VERSION,\n\t\tsessionId: session.sessionId,\n\t\tbaseEntryCount,\n\t\tentries: session.persistenceChange?.kind === \"wal\" ? session.persistenceChange.entries : [],\n\t\tleafId: session.leafId,\n\t\trevision: session.revision,\n\t\tupdatedAt: session.updatedAt,\n\t\tstaticContext: session.staticContext,\n\t};\n\tappendFileSync(walPath(sessionStoreDir, session.sessionId), `${JSON.stringify(record)}\\n`, \"utf-8\");\n}\n\nexport function savePersistedSession(sessionStoreDir: string, session: SessionState): void {\n\tconst key = persistedSessionKey(sessionStoreDir, session.sessionId);\n\tconst meta = persistedSessions.get(key);\n\tif (meta && !session.persistenceChange) return;\n\tif (!meta || session.persistenceChange?.kind === \"snapshot\" || meta.walRecords >= WAL_SNAPSHOT_INTERVAL) {\n\t\twriteSnapshot(sessionStoreDir, session);\n\t\tpersistedSessions.set(key, { entryCount: session.entries.length, walRecords: 0 });\n\t\tmarkSessionPersisted(session);\n\t\treturn;\n\t}\n\tappendWalRecord(sessionStoreDir, session, meta.entryCount);\n\tpersistedSessions.set(key, { entryCount: session.entries.length, walRecords: meta.walRecords + 1 });\n\tmarkSessionPersisted(session);\n}\n\nexport function deletePersistedSession(sessionStoreDir: string, sessionId: string): void {\n\trmSync(sessionPath(sessionStoreDir, sessionId), { force: true });\n\trmSync(walPath(sessionStoreDir, sessionId), { force: true });\n\tpersistedSessions.delete(persistedSessionKey(sessionStoreDir, sessionId));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-persistence.d.ts","sourceRoot":"","sources":["../src/session-persistence.ts"],"names":[],"mappings":"AAmBA,OAAO,EASN,KAAK,YAAY,EAGjB,MAAM,oBAAoB,CAAC;AAgB5B,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG,iCAAiC,CAAC;AAE5G,MAAM,WAAW,yBAAyB;IACzC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,4BAA4B,KAAK,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,gCAAgC;IAChD,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iCAAiC;IACjD,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,KAAK,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,kCAAkC,GAC3C,yBAAyB,GACzB,oBAAoB,GACpB,qBAAqB,GACrB,iBAAiB,GACjB,uBAAuB,GACvB,mBAAmB,GACnB,yBAAyB,GACzB,iBAAiB,CAAC;AAErB,qBAAa,+BAAgC,SAAQ,KAAK;IACzD,QAAQ,CAAC,IAAI,qDAAqD;IAClE,QAAQ,CAAC,SAAS,QAAkB;IACpC,QAAQ,CAAC,QAAQ,EAAE,kCAAkC,CAAC;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,YAAY,KAAK,EAAE;QAClB,QAAQ,EAAE,kCAAkC,CAAC;QAC7C,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACd,EAWA;CACD;AAyBD,wBAAgB,mCAAmC,IAAI,gCAAgC,CAEtF;AAED,wBAAgB,yCAAyC,CACxD,SAAS,EAAE,OAAO,CAAC,gCAAgC,CAAC,GAClD,gCAAgC,CAQlC;AAED,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D;AAqmCD,wBAAgB,qBAAqB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CA+DnE;AAkWD,wBAAgB,yBAAyB,CACxC,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,YAAY,GACnB,iCAAiC,CAsCnC;AAED,wBAAgB,oBAAoB,CACnC,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,yBAA8B,GACrC,IAAI,CAwCN;AAED,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAgBvF","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport type { Dirent } from \"node:fs\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tfsyncSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\ttruncateSync,\n\twriteSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport {\n\tcalculateSessionLogicalBytes,\n\texportSessionState,\n\tgetSession,\n\tgetSessionCapacityLimits,\n\tgetSessionCapacityUsage,\n\tmarkSessionPersisted,\n\ttype PersistedSessionState,\n\trestoreSessionState,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\tvalidatePersistedSessionState,\n} from \"./session-store.ts\";\n\nconst LEGACY_SESSION_VERSION = 1;\nconst DURABLE_SESSION_VERSION = 2;\nconst LEGACY_DURABLE_WAL_VERSION = 4;\nconst DURABLE_WAL_VERSION = 5;\nconst HEAD_VERSION = 1;\nconst MIN_WAL_RECORDS_PER_SNAPSHOT = 32;\nconst MIN_WAL_BYTES_PER_SNAPSHOT = 1024 * 1024;\nconst REPLACE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];\nconst SHA256_PATTERN = /^[a-f0-9]{64}$/u;\nconst SESSION_FILE_PATTERN = /^[a-f0-9]{64}\\.json$/u;\nconst SESSION_ARTIFACT_PATTERN = /^([a-f0-9]{64}\\.json)\\.(?:head|snapshot|wal)\\.[01]$/u;\nconst LEGACY_WAL_PATTERN = /^([a-f0-9]{64}\\.json)\\.wal$/u;\nconst ZERO_DIGEST = \"0\".repeat(64);\n\nexport type SessionPersistenceFaultPoint = \"wal_after_sync_before_head\" | \"snapshot_after_sync_before_head\";\n\nexport interface SessionPersistenceOptions {\n\tfaultInjector?: (point: SessionPersistenceFaultPoint) => void;\n}\n\nexport interface SessionPersistenceArtifactLimits {\n\tmaxSnapshotBytes: number;\n\tmaxWalBytes: number;\n\tmaxHeadBytes: number;\n}\n\nexport interface SessionPersistencePreflightResult {\n\tstrategy: \"none\" | \"snapshot\" | \"wal\";\n\tsnapshotArtifactBytes?: number;\n\twalArtifactBytes?: number;\n\theadArtifactBytes?: number;\n}\n\nexport type SessionPersistenceCapacityResource =\n\t| \"snapshot_artifact_bytes\"\n\t| \"wal_artifact_bytes\"\n\t| \"head_artifact_bytes\"\n\t| \"session_entries\"\n\t| \"session_logical_bytes\"\n\t| \"aggregate_entries\"\n\t| \"aggregate_logical_bytes\"\n\t| \"loaded_sessions\";\n\nexport class SessionPersistenceCapacityError extends Error {\n\treadonly code = \"PI_SERVER_SESSION_PERSISTENCE_CAPACITY_EXCEEDED\";\n\treadonly retryable = false as const;\n\treadonly resource: SessionPersistenceCapacityResource;\n\treadonly sessionId: string;\n\treadonly path: string;\n\treadonly current: number;\n\treadonly requested: number;\n\treadonly limit: number;\n\n\tconstructor(input: {\n\t\tresource: SessionPersistenceCapacityResource;\n\t\tsessionId: string;\n\t\tpath: string;\n\t\tcurrent: number;\n\t\trequested: number;\n\t\tlimit: number;\n\t}) {\n\t\tsuper(\n\t\t\t`Persisted session capacity exceeded: resource=${input.resource}, session=${input.sessionId}, path=${input.path}, requested=${input.requested}, limit=${input.limit}`,\n\t\t);\n\t\tthis.name = \"SessionPersistenceCapacityError\";\n\t\tthis.resource = input.resource;\n\t\tthis.sessionId = input.sessionId;\n\t\tthis.path = input.path;\n\t\tthis.current = input.current;\n\t\tthis.requested = input.requested;\n\t\tthis.limit = input.limit;\n\t}\n}\n\nconst SESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES = 1024 * 1024;\nconst WAL_ARTIFACT_ENCODING_MULTIPLIER = 2;\nconst DEFAULT_MAX_HEAD_BYTES = 64 * 1024;\nlet artifactLimitOverrides: Partial<SessionPersistenceArtifactLimits> = {};\n\nfunction defaultArtifactLimits(): SessionPersistenceArtifactLimits {\n\tconst capacity = getSessionCapacityLimits();\n\tconst maxSnapshotBytes =\n\t\tcapacity.maxLogicalBytesPerSession + capacity.maxEntriesPerSession + SESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES;\n\tconst maxWalBytes =\n\t\tcapacity.maxLogicalBytesPerSession * WAL_ARTIFACT_ENCODING_MULTIPLIER +\n\t\tcapacity.maxEntriesPerSession * WAL_ARTIFACT_ENCODING_MULTIPLIER +\n\t\tSESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES * WAL_ARTIFACT_ENCODING_MULTIPLIER;\n\tif (!Number.isSafeInteger(maxSnapshotBytes) || !Number.isSafeInteger(maxWalBytes)) {\n\t\tthrow new RangeError(\"Session capacity limits are too large to derive safe persistence artifact limits\");\n\t}\n\treturn {\n\t\tmaxSnapshotBytes,\n\t\tmaxWalBytes,\n\t\tmaxHeadBytes: DEFAULT_MAX_HEAD_BYTES,\n\t};\n}\n\nexport function getSessionPersistenceArtifactLimits(): SessionPersistenceArtifactLimits {\n\treturn { ...defaultArtifactLimits(), ...artifactLimitOverrides };\n}\n\nexport function configureSessionPersistenceArtifactLimits(\n\toverrides: Partial<SessionPersistenceArtifactLimits>,\n): SessionPersistenceArtifactLimits {\n\tfor (const [name, value] of Object.entries(overrides)) {\n\t\tif (!Number.isSafeInteger(value) || (value ?? 0) <= 0) {\n\t\t\tthrow new RangeError(`${name} must be a positive safe integer`);\n\t\t}\n\t}\n\tartifactLimitOverrides = { ...artifactLimitOverrides, ...overrides };\n\treturn getSessionPersistenceArtifactLimits();\n}\n\nexport function resetSessionPersistenceArtifactLimits(): void {\n\tartifactLimitOverrides = {};\n}\n\ninterface PersistedSessionFileV1 {\n\tversion: 1;\n\tsession: PersistedSessionState;\n}\n\ninterface PersistedSessionFileV2 {\n\tversion: 2;\n\tgeneration: number;\n\tstateDigest: string;\n\tsession: PersistedSessionState;\n}\n\ntype PersistedSessionFile = PersistedSessionFileV1 | PersistedSessionFileV2;\n\ninterface PersistedSessionWalRecordV1 {\n\tversion: 1;\n\tsessionId: string;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n\tupdatedAt: number;\n\tstaticContext: SessionStaticContext | undefined;\n}\n\ninterface PersistedSessionWalRecordV2 {\n\tversion: 2;\n\tsessionId: string;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n\tupdatedAt: number;\n\tstaticContext?: SessionStaticContext;\n}\n\ninterface PersistedSessionWalRecordV3 {\n\tversion: 3;\n\tsessionId: string;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n\tupdatedAt: number;\n\tstaticContext?: SessionStaticContext;\n}\n\ntype LegacyWalRecord = PersistedSessionWalRecordV1 | PersistedSessionWalRecordV2 | PersistedSessionWalRecordV3;\n\ninterface PersistedSessionWalRecordV4 {\n\tversion: 4;\n\tsessionId: string;\n\tgeneration: number;\n\tsequence: number;\n\tbaseRevision: number;\n\trevision: number;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tupdatedAt: number;\n\tpreviousStateDigest: string;\n\tstateDigest: string;\n\tpreviousWalDigest: string;\n\tstaticContextChanged: boolean;\n\tstaticContext?: SessionStaticContext | null;\n}\n\ninterface PersistedSessionWalRecordV5 {\n\tversion: 5;\n\tsessionId: string;\n\tgeneration: number;\n\tsequence: number;\n\tbaseRevision: number;\n\trevision: number;\n\tbaseEntryCount: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tupdatedAt: number;\n\tpreviousStateDigest: string;\n\tstateDigest: string;\n\tpreviousWalDigest: string;\n\tstaticContextChanged: boolean;\n\tstaticContext?: SessionStaticContext | null;\n}\n\ntype DurableWalRecord = PersistedSessionWalRecordV4 | PersistedSessionWalRecordV5;\n\ninterface PersistedWalEnvelope {\n\tversion: 3 | 4 | 5;\n\tpayload: string;\n\tsha256: string;\n}\n\ninterface PersistedSessionHeadRecord {\n\tversion: 1;\n\tsessionId: string;\n\tsequence: number;\n\tgeneration: number;\n\tpreviousHeadDigest: string;\n\tsnapshotSha256: string;\n\tsnapshotStateDigest: string;\n\tsnapshotRevision: number;\n\tsnapshotEntryCount: number;\n\twalRecordCount: number;\n\twalByteLength: number;\n\twalTailDigest: string;\n\tstateDigest: string;\n\trevision: number;\n\tentryCount: number;\n}\n\ninterface PersistedSessionHeadEnvelope {\n\tversion: 1;\n\tpayload: string;\n\tsha256: string;\n}\n\ninterface ParsedHeadRecord {\n\trecord: PersistedSessionHeadRecord;\n\tdigest: string;\n}\n\ninterface LegacySessionMeta {\n\tformat: \"legacy\";\n\tentryCount: number;\n\twalRecords: number;\n\trevision: number;\n}\n\ninterface DurableSessionMeta {\n\tformat: \"durable\";\n\tgeneration: number;\n\theadSequence: number;\n\theadDigest: string;\n\tsnapshotSha256: string;\n\tsnapshotStateDigest: string;\n\tsnapshotRevision: number;\n\tsnapshotEntryCount: number;\n\tsnapshotByteLength: number;\n\tentryCount: number;\n\twalRecords: number;\n\twalByteLength: number;\n\twalTailDigest: string;\n\tstateDigest: string;\n\trevision: number;\n}\n\ntype PersistedSessionMeta = LegacySessionMeta | DurableSessionMeta;\n\nconst persistedSessions = new Map<string, PersistedSessionMeta>();\nconst sessionsNeedingRecovery = new Set<string>();\n\ntype SessionArtifactKind = \"snapshot\" | \"wal\" | \"head\";\n\ninterface StagedSessionCapacity {\n\tentryCount: number;\n\tlogicalBytes: number;\n}\n\nfunction persistenceCapacityError(\n\tresource: SessionPersistenceCapacityResource,\n\tsessionId: string,\n\tpath: string,\n\tcurrent: number,\n\trequested: number,\n\tlimit: number,\n): SessionPersistenceCapacityError {\n\treturn new SessionPersistenceCapacityError({ resource, sessionId, path, current, requested, limit });\n}\n\nfunction artifactLimit(kind: SessionArtifactKind): {\n\tresource: Extract<\n\t\tSessionPersistenceCapacityResource,\n\t\t\"snapshot_artifact_bytes\" | \"wal_artifact_bytes\" | \"head_artifact_bytes\"\n\t>;\n\tlimit: number;\n} {\n\tconst limits = getSessionPersistenceArtifactLimits();\n\tswitch (kind) {\n\t\tcase \"snapshot\":\n\t\t\treturn { resource: \"snapshot_artifact_bytes\", limit: limits.maxSnapshotBytes };\n\t\tcase \"wal\":\n\t\t\treturn { resource: \"wal_artifact_bytes\", limit: limits.maxWalBytes };\n\t\tcase \"head\":\n\t\t\treturn { resource: \"head_artifact_bytes\", limit: limits.maxHeadBytes };\n\t}\n}\n\nfunction readBoundedArtifactSync(path: string, kind: SessionArtifactKind, sessionId: string): Buffer<ArrayBufferLike> {\n\tconst { resource, limit } = artifactLimit(kind);\n\tconst size = statSync(path).size;\n\tif (size > limit) {\n\t\tthrow persistenceCapacityError(resource, sessionId, path, 0, size, limit);\n\t}\n\tconst encoded = readFileSync(path);\n\tif (encoded.byteLength > limit) {\n\t\tthrow persistenceCapacityError(resource, sessionId, path, 0, encoded.byteLength, limit);\n\t}\n\treturn encoded;\n}\n\nfunction assertArtifactWriteSize(\n\tpath: string,\n\tkind: SessionArtifactKind,\n\tsessionId: string,\n\tcurrent: number,\n\trequested: number,\n): void {\n\tconst { resource, limit } = artifactLimit(kind);\n\tif (requested > limit) {\n\t\tthrow persistenceCapacityError(resource, sessionId, path, current, requested, limit);\n\t}\n}\n\nfunction assertStagedSessionCapacity(\n\tsessionId: string,\n\tpath: string,\n\tcurrent: StagedSessionCapacity,\n\tnext: StagedSessionCapacity,\n): void {\n\tconst limits = getSessionCapacityLimits();\n\tconst usage = getSessionCapacityUsage();\n\tconst existing = getSession(sessionId);\n\tif (!existing && usage.loadedSessions + 1 > limits.maxLoadedSessions) {\n\t\tthrow persistenceCapacityError(\n\t\t\t\"loaded_sessions\",\n\t\t\tsessionId,\n\t\t\tpath,\n\t\t\tusage.loadedSessions,\n\t\t\tusage.loadedSessions + 1,\n\t\t\tlimits.maxLoadedSessions,\n\t\t);\n\t}\n\tif (next.entryCount > limits.maxEntriesPerSession) {\n\t\tthrow persistenceCapacityError(\n\t\t\t\"session_entries\",\n\t\t\tsessionId,\n\t\t\tpath,\n\t\t\tcurrent.entryCount,\n\t\t\tnext.entryCount,\n\t\t\tlimits.maxEntriesPerSession,\n\t\t);\n\t}\n\tif (next.logicalBytes > limits.maxLogicalBytesPerSession) {\n\t\tthrow persistenceCapacityError(\n\t\t\t\"session_logical_bytes\",\n\t\t\tsessionId,\n\t\t\tpath,\n\t\t\tcurrent.logicalBytes,\n\t\t\tnext.logicalBytes,\n\t\t\tlimits.maxLogicalBytesPerSession,\n\t\t);\n\t}\n\tconst requestedAggregateEntries = usage.entryCount - (existing?.entries.length ?? 0) + next.entryCount;\n\tif (requestedAggregateEntries > limits.maxAggregateEntries) {\n\t\tthrow persistenceCapacityError(\n\t\t\t\"aggregate_entries\",\n\t\t\tsessionId,\n\t\t\tpath,\n\t\t\tusage.entryCount,\n\t\t\trequestedAggregateEntries,\n\t\t\tlimits.maxAggregateEntries,\n\t\t);\n\t}\n\tconst requestedAggregateLogicalBytes = usage.logicalBytes - (existing?.logicalBytes ?? 0) + next.logicalBytes;\n\tif (requestedAggregateLogicalBytes > limits.maxAggregateLogicalBytes) {\n\t\tthrow persistenceCapacityError(\n\t\t\t\"aggregate_logical_bytes\",\n\t\t\tsessionId,\n\t\t\tpath,\n\t\t\tusage.logicalBytes,\n\t\t\trequestedAggregateLogicalBytes,\n\t\t\tlimits.maxAggregateLogicalBytes,\n\t\t);\n\t}\n}\n\nfunction createStagedSessionCapacity(session: PersistedSessionState, path: string): StagedSessionCapacity {\n\tconst staged = {\n\t\tentryCount: session.entries.length,\n\t\tlogicalBytes: calculateSessionLogicalBytes(session.staticContext, session.entries),\n\t};\n\tassertStagedSessionCapacity(session.sessionId, path, { entryCount: 0, logicalBytes: 0 }, staged);\n\treturn staged;\n}\n\nfunction nextWalCapacity(\n\tsession: PersistedSessionState,\n\trecord: LegacyWalRecord | DurableWalRecord,\n\tcurrent: StagedSessionCapacity,\n): StagedSessionCapacity {\n\tlet nextStaticContext = session.staticContext;\n\tif (record.version === 1) {\n\t\tnextStaticContext = record.staticContext;\n\t} else if (record.version === 2 || record.version === 3) {\n\t\tif (Object.hasOwn(record, \"staticContext\")) nextStaticContext = record.staticContext;\n\t} else if (record.staticContextChanged) {\n\t\tnextStaticContext = record.staticContext === null ? undefined : record.staticContext;\n\t}\n\treturn {\n\t\tentryCount: current.entryCount + record.entries.length,\n\t\tlogicalBytes:\n\t\t\tcurrent.logicalBytes -\n\t\t\tcalculateSessionLogicalBytes(session.staticContext, []) +\n\t\t\tcalculateSessionLogicalBytes(nextStaticContext, []) +\n\t\t\tcalculateSessionLogicalBytes(undefined, record.entries),\n\t};\n}\n\nfunction sha256(value: string | Buffer): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction stateDigest(session: PersistedSessionState): string {\n\treturn sha256(JSON.stringify(session));\n}\n\nfunction walTransitionStateDigest(\n\trecord: Omit<PersistedSessionWalRecordV5, \"stateDigest\"> | PersistedSessionWalRecordV5,\n): string {\n\treturn sha256(\n\t\tJSON.stringify({\n\t\t\tpreviousStateDigest: record.previousStateDigest,\n\t\t\tsessionId: record.sessionId,\n\t\t\tbaseRevision: record.baseRevision,\n\t\t\trevision: record.revision,\n\t\t\tbaseEntryCount: record.baseEntryCount,\n\t\t\tentries: record.entries,\n\t\t\tleafId: record.leafId,\n\t\t\tupdatedAt: record.updatedAt,\n\t\t\tstaticContextChanged: record.staticContextChanged,\n\t\t\t...(record.staticContextChanged ? { staticContext: record.staticContext ?? null } : {}),\n\t\t}),\n\t);\n}\n\nfunction sessionFileName(sessionId: string): string {\n\treturn `${sha256(sessionId)}.json`;\n}\n\nfunction sessionPath(sessionStoreDir: string, sessionId: string): string {\n\treturn join(sessionStoreDir, sessionFileName(sessionId));\n}\n\nfunction snapshotPath(sessionStoreDir: string, sessionId: string, generation: number): string {\n\tconst basePath = sessionPath(sessionStoreDir, sessionId);\n\treturn generation === 0 ? basePath : `${basePath}.snapshot.${generation % 2}`;\n}\n\nfunction walPath(sessionStoreDir: string, sessionId: string, generation: number): string {\n\tconst basePath = sessionPath(sessionStoreDir, sessionId);\n\treturn generation === 0 ? `${basePath}.wal` : `${basePath}.wal.${generation % 2}`;\n}\n\nfunction headPath(sessionStoreDir: string, sessionId: string, sequence: number): string {\n\treturn `${sessionPath(sessionStoreDir, sessionId)}.head.${sequence % 2}`;\n}\n\nfunction persistedSessionKey(sessionStoreDir: string, sessionId: string): string {\n\treturn `${sessionStoreDir}\\0${sessionId}`;\n}\n\nfunction isRetryableRenameError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn process.platform === \"win32\" && code === \"EPERM\";\n}\n\nfunction sleepSync(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\nfunction replaceFileSync(sourcePath: string, targetPath: string): void {\n\tfor (let attempt = 0; attempt <= REPLACE_RETRY_DELAYS_MS.length; attempt++) {\n\t\ttry {\n\t\t\trenameSync(sourcePath, targetPath);\n\t\t\treturn;\n\t\t} catch (error) {\n\t\t\tif (!isRetryableRenameError(error) || attempt === REPLACE_RETRY_DELAYS_MS.length) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tsleepSync(REPLACE_RETRY_DELAYS_MS[attempt]);\n\t\t}\n\t}\n}\n\nfunction writeAllSync(handle: number, encoded: Buffer): void {\n\tlet offset = 0;\n\twhile (offset < encoded.byteLength) {\n\t\tconst written = writeSync(handle, encoded, offset, encoded.byteLength - offset);\n\t\tif (written === 0) {\n\t\t\tthrow new Error(\"Filesystem write made no progress\");\n\t\t}\n\t\toffset += written;\n\t}\n}\n\nfunction syncDirectorySync(path: string): void {\n\tif (process.platform === \"win32\") return;\n\tconst handle = openSync(path, \"r\");\n\ttry {\n\t\tfsyncSync(handle);\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n}\n\nfunction ensureDurableDirectorySync(path: string): void {\n\tif (existsSync(path)) return;\n\tconst missingDirectories: string[] = [];\n\tlet current = path;\n\twhile (!existsSync(current)) {\n\t\tmissingDirectories.push(current);\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) break;\n\t\tcurrent = parent;\n\t}\n\tmkdirSync(path, { recursive: true });\n\tfor (const directory of missingDirectories.reverse()) {\n\t\tsyncDirectorySync(dirname(directory));\n\t}\n}\n\nfunction writeDurableFileSync(path: string, encoded: Buffer): void {\n\tconst handle = openSync(path, \"wx\", 0o600);\n\ttry {\n\t\twriteAllSync(handle, encoded);\n\t\tfsyncSync(handle);\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n}\n\nfunction replaceDurableFileSync(path: string, encoded: Buffer): void {\n\tconst tempPath = `${path}.${randomUUID()}.tmp`;\n\ttry {\n\t\twriteDurableFileSync(tempPath, encoded);\n\t\treplaceFileSync(tempPath, path);\n\t\tsyncDirectorySync(dirname(path));\n\t} catch (error) {\n\t\trmSync(tempPath, { force: true });\n\t\tthrow error;\n\t}\n}\n\nfunction appendDurableFileSync(path: string, encoded: Buffer): void {\n\tconst existed = existsSync(path);\n\tconst handle = openSync(path, \"a\", 0o600);\n\ttry {\n\t\twriteAllSync(handle, encoded);\n\t\tfsyncSync(handle);\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n\tif (!existed) syncDirectorySync(dirname(path));\n}\n\nfunction truncateDurableFileSync(path: string, length: number): void {\n\ttruncateSync(path, length);\n\tconst handle = openSync(path, \"r+\");\n\ttry {\n\t\tfsyncSync(handle);\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n}\n\ntype SessionRecoveryAction = () => void;\n\nfunction performOrDeferRecovery(action: SessionRecoveryAction, recoveryActions?: SessionRecoveryAction[]): void {\n\tif (recoveryActions) {\n\t\trecoveryActions.push(action);\n\t} else {\n\t\taction();\n\t}\n}\n\nfunction assertRecord(value: unknown, path: string): asserts value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${path} must be an object`);\n\t}\n}\n\nfunction assertString(value: unknown, path: string): asserts value is string {\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`${path} must be a string`);\n\t}\n}\n\nfunction assertBoolean(value: unknown, path: string): asserts value is boolean {\n\tif (typeof value !== \"boolean\") {\n\t\tthrow new Error(`${path} must be a boolean`);\n\t}\n}\n\nfunction assertDigest(value: unknown, path: string): asserts value is string {\n\tif (typeof value !== \"string\" || !SHA256_PATTERN.test(value)) {\n\t\tthrow new Error(`${path} must be a 64-character lowercase hex digest`);\n\t}\n}\n\nfunction assertNonNegativeSafeInteger(value: unknown, path: string): asserts value is number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0) {\n\t\tthrow new Error(`${path} must be a non-negative safe integer`);\n\t}\n}\n\nfunction assertNullableString(value: unknown, path: string): asserts value is string | null {\n\tif (value !== null && typeof value !== \"string\") {\n\t\tthrow new Error(`${path} must be a string or null`);\n\t}\n}\n\nfunction assertStaticContext(\n\tvalue: unknown,\n\tpath = \"session.staticContext\",\n): asserts value is SessionStaticContext | undefined {\n\tif (value === undefined) return;\n\tassertRecord(value, path);\n\tconst systemPrompt = value.systemPrompt;\n\tif (systemPrompt !== undefined && typeof systemPrompt !== \"string\") {\n\t\tthrow new Error(`${path}.systemPrompt must be a string`);\n\t}\n\tconst tools = value.tools;\n\tif (tools !== undefined) {\n\t\tif (!Array.isArray(tools)) {\n\t\t\tthrow new Error(`${path}.tools must be an array`);\n\t\t}\n\t\tfor (const [index, tool] of tools.entries()) {\n\t\t\tassertRecord(tool, `${path}.tools[${index}]`);\n\t\t\tassertString(tool.name, `${path}.tools[${index}].name`);\n\t\t\tassertString(tool.description, `${path}.tools[${index}].description`);\n\t\t\tif (tool.parameters === undefined) {\n\t\t\t\tthrow new Error(`${path}.tools[${index}].parameters is required`);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction assertSessionTreeEntries(value: unknown, path = \"session.entries\"): asserts value is SessionTreeEntry[] {\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(`${path} must be an array`);\n\t}\n\tfor (const [index, entry] of value.entries()) {\n\t\tassertRecord(entry, `${path}[${index}]`);\n\t\tassertString(entry.type, `${path}[${index}].type`);\n\t\tassertString(entry.id, `${path}[${index}].id`);\n\t\tassertNullableString(entry.parentId, `${path}[${index}].parentId`);\n\t\tassertString(entry.timestamp, `${path}[${index}].timestamp`);\n\t\tif (entry.type === \"message\") {\n\t\t\tassertRecord(entry.message, `${path}[${index}].message`);\n\t\t}\n\t}\n}\n\nfunction assertPersistedSessionState(value: unknown, path = \"session\"): asserts value is PersistedSessionState {\n\tassertRecord(value, path);\n\tassertString(value.sessionId, `${path}.sessionId`);\n\tassertStaticContext(value.staticContext, `${path}.staticContext`);\n\tassertSessionTreeEntries(value.entries, `${path}.entries`);\n\tassertNullableString(value.leafId, `${path}.leafId`);\n\tassertNonNegativeSafeInteger(value.revision, `${path}.revision`);\n\tassertNonNegativeSafeInteger(value.createdAt, `${path}.createdAt`);\n\tassertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);\n}\n\nfunction parseJson(raw: string, message: string): unknown {\n\ttry {\n\t\treturn JSON.parse(raw) as unknown;\n\t} catch (error) {\n\t\tthrow new Error(message, { cause: error });\n\t}\n}\n\nfunction parsePersistedSessionFile(raw: string, sourcePath: string): PersistedSessionFile {\n\tconst parsed = parseJson(raw, `Persisted session file contains invalid JSON: ${sourcePath}`);\n\tassertRecord(parsed, \"persisted session file\");\n\tif (parsed.version !== LEGACY_SESSION_VERSION && parsed.version !== DURABLE_SESSION_VERSION) {\n\t\tthrow new Error(`Unsupported persisted session version in ${sourcePath}`);\n\t}\n\tassertPersistedSessionState(parsed.session);\n\tif (parsed.version === LEGACY_SESSION_VERSION) {\n\t\treturn { version: LEGACY_SESSION_VERSION, session: parsed.session };\n\t}\n\tassertNonNegativeSafeInteger(parsed.generation, `${sourcePath}.generation`);\n\tassertDigest(parsed.stateDigest, `${sourcePath}.stateDigest`);\n\tconst actualStateDigest = stateDigest(parsed.session);\n\tif (parsed.stateDigest !== actualStateDigest) {\n\t\tthrow new Error(`Persisted session snapshot state digest mismatch: ${sourcePath}`);\n\t}\n\treturn {\n\t\tversion: DURABLE_SESSION_VERSION,\n\t\tgeneration: parsed.generation,\n\t\tstateDigest: parsed.stateDigest,\n\t\tsession: parsed.session,\n\t};\n}\n\nfunction assertLegacyWalRecord(\n\tvalue: unknown,\n\tsourcePath: string,\n\tlineNumber: number,\n): asserts value is LegacyWalRecord {\n\tconst path = `${sourcePath}:${lineNumber}`;\n\tassertRecord(value, path);\n\tif (value.version !== 1 && value.version !== 2 && value.version !== 3) {\n\t\tthrow new Error(`Unsupported persisted session WAL version in ${path}`);\n\t}\n\tassertString(value.sessionId, `${path}.sessionId`);\n\tassertNonNegativeSafeInteger(value.baseEntryCount, `${path}.baseEntryCount`);\n\tassertSessionTreeEntries(value.entries, `${path}.entries`);\n\tassertNullableString(value.leafId, `${path}.leafId`);\n\tassertNonNegativeSafeInteger(value.revision, `${path}.revision`);\n\tassertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);\n\tif (value.version === 1 || Object.hasOwn(value, \"staticContext\")) {\n\t\tassertStaticContext(value.staticContext, `${path}.staticContext`);\n\t}\n}\n\nfunction parseLegacyWalLine(raw: string, sourcePath: string, lineNumber: number): LegacyWalRecord {\n\tconst parsed = parseJson(raw, `Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`);\n\tassertRecord(parsed, `${sourcePath}:${lineNumber}`);\n\tif (parsed.version !== 3 || typeof parsed.payload !== \"string\" || typeof parsed.sha256 !== \"string\") {\n\t\tassertLegacyWalRecord(parsed, sourcePath, lineNumber);\n\t\treturn parsed;\n\t}\n\tassertDigest(parsed.sha256, `${sourcePath}:${lineNumber}.sha256`);\n\tif (sha256(parsed.payload) !== parsed.sha256) {\n\t\tthrow new Error(`Persisted session WAL checksum mismatch: ${sourcePath}:${lineNumber}`);\n\t}\n\tconst payload = parseJson(\n\t\tparsed.payload,\n\t\t`Persisted session WAL payload contains invalid JSON: ${sourcePath}:${lineNumber}`,\n\t);\n\tassertLegacyWalRecord(payload, sourcePath, lineNumber);\n\tif (payload.version !== 3) {\n\t\tthrow new Error(`Persisted session WAL envelope payload version mismatch: ${sourcePath}:${lineNumber}`);\n\t}\n\treturn payload;\n}\n\nfunction assertDurableWalRecord(\n\tvalue: unknown,\n\tsourcePath: string,\n\tlineNumber: number,\n): asserts value is DurableWalRecord {\n\tconst path = `${sourcePath}:${lineNumber}`;\n\tassertRecord(value, path);\n\tif (value.version !== LEGACY_DURABLE_WAL_VERSION && value.version !== DURABLE_WAL_VERSION) {\n\t\tthrow new Error(`Unsupported durable session WAL version in ${path}`);\n\t}\n\tassertString(value.sessionId, `${path}.sessionId`);\n\tassertNonNegativeSafeInteger(value.generation, `${path}.generation`);\n\tassertNonNegativeSafeInteger(value.sequence, `${path}.sequence`);\n\tassertNonNegativeSafeInteger(value.baseRevision, `${path}.baseRevision`);\n\tassertNonNegativeSafeInteger(value.revision, `${path}.revision`);\n\tassertNonNegativeSafeInteger(value.baseEntryCount, `${path}.baseEntryCount`);\n\tassertSessionTreeEntries(value.entries, `${path}.entries`);\n\tassertNullableString(value.leafId, `${path}.leafId`);\n\tassertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);\n\tassertDigest(value.previousStateDigest, `${path}.previousStateDigest`);\n\tassertDigest(value.stateDigest, `${path}.stateDigest`);\n\tassertDigest(value.previousWalDigest, `${path}.previousWalDigest`);\n\tassertBoolean(value.staticContextChanged, `${path}.staticContextChanged`);\n\tif (value.staticContextChanged) {\n\t\tif (!Object.hasOwn(value, \"staticContext\")) {\n\t\t\tthrow new Error(`${path}.staticContext is required when staticContextChanged is true`);\n\t\t}\n\t\tif (value.staticContext !== null) {\n\t\t\tassertStaticContext(value.staticContext, `${path}.staticContext`);\n\t\t}\n\t} else if (Object.hasOwn(value, \"staticContext\")) {\n\t\tthrow new Error(`${path}.staticContext must be omitted when staticContextChanged is false`);\n\t}\n}\n\nfunction parseDurableWalLine(\n\traw: string,\n\tsourcePath: string,\n\tlineNumber: number,\n): { record: DurableWalRecord; digest: string } {\n\tconst parsed = parseJson(raw, `Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`);\n\tassertRecord(parsed, `${sourcePath}:${lineNumber}`);\n\tif (parsed.version !== LEGACY_DURABLE_WAL_VERSION && parsed.version !== DURABLE_WAL_VERSION) {\n\t\tthrow new Error(`Expected durable session WAL envelope in ${sourcePath}:${lineNumber}`);\n\t}\n\tassertString(parsed.payload, `${sourcePath}:${lineNumber}.payload`);\n\tassertDigest(parsed.sha256, `${sourcePath}:${lineNumber}.sha256`);\n\tconst digest = sha256(parsed.payload);\n\tif (digest !== parsed.sha256) {\n\t\tthrow new Error(`Persisted session WAL checksum mismatch: ${sourcePath}:${lineNumber}`);\n\t}\n\tconst payload = parseJson(\n\t\tparsed.payload,\n\t\t`Persisted session WAL payload contains invalid JSON: ${sourcePath}:${lineNumber}`,\n\t);\n\tassertDurableWalRecord(payload, sourcePath, lineNumber);\n\tif (payload.version !== parsed.version) {\n\t\tthrow new Error(`Persisted session WAL envelope payload version mismatch: ${sourcePath}:${lineNumber}`);\n\t}\n\treturn { record: payload, digest };\n}\n\nfunction assertHeadRecord(value: unknown, sourcePath: string): asserts value is PersistedSessionHeadRecord {\n\tassertRecord(value, sourcePath);\n\tif (value.version !== HEAD_VERSION) {\n\t\tthrow new Error(`Unsupported persisted session head version in ${sourcePath}`);\n\t}\n\tassertString(value.sessionId, `${sourcePath}.sessionId`);\n\tassertNonNegativeSafeInteger(value.sequence, `${sourcePath}.sequence`);\n\tassertNonNegativeSafeInteger(value.generation, `${sourcePath}.generation`);\n\tassertDigest(value.previousHeadDigest, `${sourcePath}.previousHeadDigest`);\n\tassertDigest(value.snapshotSha256, `${sourcePath}.snapshotSha256`);\n\tassertDigest(value.snapshotStateDigest, `${sourcePath}.snapshotStateDigest`);\n\tassertNonNegativeSafeInteger(value.snapshotRevision, `${sourcePath}.snapshotRevision`);\n\tassertNonNegativeSafeInteger(value.snapshotEntryCount, `${sourcePath}.snapshotEntryCount`);\n\tassertNonNegativeSafeInteger(value.walRecordCount, `${sourcePath}.walRecordCount`);\n\tassertNonNegativeSafeInteger(value.walByteLength, `${sourcePath}.walByteLength`);\n\tassertDigest(value.walTailDigest, `${sourcePath}.walTailDigest`);\n\tassertDigest(value.stateDigest, `${sourcePath}.stateDigest`);\n\tassertNonNegativeSafeInteger(value.revision, `${sourcePath}.revision`);\n\tassertNonNegativeSafeInteger(value.entryCount, `${sourcePath}.entryCount`);\n}\n\nfunction parseHeadFile(path: string, slot: number, sessionId: string): ParsedHeadRecord {\n\tconst parsed = parseJson(\n\t\treadBoundedArtifactSync(path, \"head\", sessionId).toString(\"utf-8\"),\n\t\t`Persisted session head contains invalid JSON: ${path}`,\n\t);\n\tassertRecord(parsed, path);\n\tif (parsed.version !== HEAD_VERSION) {\n\t\tthrow new Error(`Unsupported persisted session head envelope version in ${path}`);\n\t}\n\tassertString(parsed.payload, `${path}.payload`);\n\tassertDigest(parsed.sha256, `${path}.sha256`);\n\tconst digest = sha256(parsed.payload);\n\tif (digest !== parsed.sha256) {\n\t\tthrow new Error(`Persisted session head checksum mismatch: ${path}`);\n\t}\n\tconst payload = parseJson(parsed.payload, `Persisted session head payload contains invalid JSON: ${path}`);\n\tassertHeadRecord(payload, path);\n\tif (payload.sequence % 2 !== slot) {\n\t\tthrow new Error(`Persisted session head is stored in the wrong slot: ${path}`);\n\t}\n\treturn { record: payload, digest };\n}\n\nfunction readLatestHead(sessionStoreDir: string, sessionId: string): ParsedHeadRecord | undefined {\n\tconst heads: ParsedHeadRecord[] = [];\n\tfor (const slot of [0, 1]) {\n\t\tconst path = headPath(sessionStoreDir, sessionId, slot);\n\t\tif (existsSync(path)) heads.push(parseHeadFile(path, slot, sessionId));\n\t}\n\tif (heads.length === 0) return undefined;\n\theads.sort((left, right) => left.record.sequence - right.record.sequence);\n\tif (heads.length === 1) {\n\t\tconst only = heads[0];\n\t\tif (only.record.sequence !== 0 || only.record.previousHeadDigest !== ZERO_DIGEST) {\n\t\t\tthrow new Error(`Persisted session head chain is incomplete for ${sessionId}`);\n\t\t}\n\t\treturn only;\n\t}\n\tconst [previous, latest] = heads;\n\tif (latest.record.sequence !== previous.record.sequence + 1) {\n\t\tthrow new Error(`Persisted session head sequence is not contiguous for ${sessionId}`);\n\t}\n\tif (latest.record.previousHeadDigest !== previous.digest) {\n\t\tthrow new Error(`Persisted session head digest chain mismatch for ${sessionId}`);\n\t}\n\tif (latest.record.sessionId !== previous.record.sessionId) {\n\t\tthrow new Error(`Persisted session head sessionId chain mismatch for ${sessionId}`);\n\t}\n\treturn latest;\n}\n\nfunction applyLegacyWalRecord(\n\tsession: PersistedSessionState,\n\trecord: LegacyWalRecord,\n\tcapacity: StagedSessionCapacity,\n\tsourcePath: string,\n): void {\n\tif (record.sessionId !== session.sessionId) {\n\t\tthrow new Error(`Persisted session WAL sessionId does not match snapshot: ${record.sessionId}`);\n\t}\n\tif (record.baseEntryCount < session.entries.length) {\n\t\tif (record.baseEntryCount + record.entries.length <= session.entries.length) {\n\t\t\tconst existingEntries = session.entries.slice(\n\t\t\t\trecord.baseEntryCount,\n\t\t\t\trecord.baseEntryCount + record.entries.length,\n\t\t\t);\n\t\t\tif (!isDeepStrictEqual(existingEntries, record.entries)) {\n\t\t\t\tthrow new Error(`Persisted session WAL diverges from snapshot for ${record.sessionId}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(`Persisted session WAL overlaps snapshot for ${record.sessionId}`);\n\t}\n\tif (record.baseEntryCount !== session.entries.length) {\n\t\tthrow new Error(`Persisted session WAL has a gap for ${record.sessionId}`);\n\t}\n\tif (record.revision < session.revision) {\n\t\tthrow new Error(`Persisted session WAL revision regresses for ${record.sessionId}`);\n\t}\n\tif (record.updatedAt < session.updatedAt) {\n\t\tthrow new Error(`Persisted session WAL updatedAt regresses for ${record.sessionId}`);\n\t}\n\tconst nextCapacity = nextWalCapacity(session, record, capacity);\n\tassertStagedSessionCapacity(session.sessionId, sourcePath, capacity, nextCapacity);\n\tsession.entries = session.entries.concat(structuredClone(record.entries));\n\tsession.leafId = record.leafId;\n\tsession.revision = record.revision;\n\tsession.updatedAt = record.updatedAt;\n\tif (record.version === 1 || Object.hasOwn(record, \"staticContext\")) {\n\t\tsession.staticContext = record.staticContext === undefined ? undefined : structuredClone(record.staticContext);\n\t}\n\tcapacity.entryCount = nextCapacity.entryCount;\n\tcapacity.logicalBytes = nextCapacity.logicalBytes;\n}\n\nfunction readCommittedLegacyWal(\n\tpath: string,\n\tsessionId: string,\n\trecoveryActions?: SessionRecoveryAction[],\n): { encoded: Buffer; lines: string[] } {\n\tconst encoded = readBoundedArtifactSync(path, \"wal\", sessionId);\n\tconst lastNewline = encoded.lastIndexOf(0x0a);\n\tconst committedBytes = lastNewline + 1;\n\tif (committedBytes !== encoded.byteLength) {\n\t\tperformOrDeferRecovery(() => truncateDurableFileSync(path, committedBytes), recoveryActions);\n\t}\n\tconst lines = encoded.subarray(0, committedBytes).toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\treturn { encoded: encoded.subarray(0, committedBytes), lines };\n}\n\nfunction applyLegacyWal(\n\tsessionStoreDir: string,\n\tsession: PersistedSessionState,\n\tcapacity: StagedSessionCapacity,\n\trecoveryActions?: SessionRecoveryAction[],\n): number {\n\tconst path = walPath(sessionStoreDir, session.sessionId, 0);\n\tif (!existsSync(path)) return 0;\n\tconst { lines } = readCommittedLegacyWal(path, session.sessionId, recoveryActions);\n\tfor (const [index, line] of lines.entries()) {\n\t\tapplyLegacyWalRecord(session, parseLegacyWalLine(line, path, index + 1), capacity, path);\n\t}\n\treturn lines.length;\n}\n\nfunction applyDurableWalRecord(\n\tsession: PersistedSessionState,\n\trecord: DurableWalRecord,\n\trecordDigest: string,\n\texpectedGeneration: number,\n\texpectedSequence: number,\n\texpectedStateDigest: string,\n\texpectedWalDigest: string,\n\tcapacity: StagedSessionCapacity,\n\tsourcePath: string,\n): { stateDigest: string; walDigest: string } {\n\tif (record.sessionId !== session.sessionId) {\n\t\tthrow new Error(`Persisted session WAL sessionId does not match snapshot: ${record.sessionId}`);\n\t}\n\tif (record.generation !== expectedGeneration) {\n\t\tthrow new Error(`Persisted session WAL generation mismatch for ${record.sessionId}`);\n\t}\n\tif (record.sequence !== expectedSequence) {\n\t\tthrow new Error(`Persisted session WAL sequence mismatch for ${record.sessionId}`);\n\t}\n\tif (record.baseRevision !== session.revision || record.revision <= record.baseRevision) {\n\t\tthrow new Error(`Persisted session WAL revision chain mismatch for ${record.sessionId}`);\n\t}\n\tif (record.baseEntryCount !== session.entries.length) {\n\t\tthrow new Error(`Persisted session WAL entry chain mismatch for ${record.sessionId}`);\n\t}\n\tif (record.previousStateDigest !== expectedStateDigest) {\n\t\tthrow new Error(`Persisted session WAL previous state digest mismatch for ${record.sessionId}`);\n\t}\n\tif (record.previousWalDigest !== expectedWalDigest) {\n\t\tthrow new Error(`Persisted session WAL digest chain mismatch for ${record.sessionId}`);\n\t}\n\tif (record.updatedAt < session.updatedAt) {\n\t\tthrow new Error(`Persisted session WAL updatedAt regresses for ${record.sessionId}`);\n\t}\n\tconst nextCapacity = nextWalCapacity(session, record, capacity);\n\tassertStagedSessionCapacity(session.sessionId, sourcePath, capacity, nextCapacity);\n\tsession.entries = session.entries.concat(structuredClone(record.entries));\n\tsession.leafId = record.leafId;\n\tsession.revision = record.revision;\n\tsession.updatedAt = record.updatedAt;\n\tif (record.staticContextChanged) {\n\t\tsession.staticContext =\n\t\t\trecord.staticContext === null || record.staticContext === undefined\n\t\t\t\t? undefined\n\t\t\t\t: structuredClone(record.staticContext);\n\t}\n\tconst actualStateDigest =\n\t\trecord.version === LEGACY_DURABLE_WAL_VERSION ? stateDigest(session) : walTransitionStateDigest(record);\n\tif (record.stateDigest !== actualStateDigest) {\n\t\tthrow new Error(`Persisted session WAL post state digest mismatch for ${record.sessionId}`);\n\t}\n\tcapacity.entryCount = nextCapacity.entryCount;\n\tcapacity.logicalBytes = nextCapacity.logicalBytes;\n\treturn { stateDigest: actualStateDigest, walDigest: recordDigest };\n}\n\nfunction readDurableWal(\n\tsessionStoreDir: string,\n\tsession: PersistedSessionState,\n\thead: PersistedSessionHeadRecord,\n\tcapacity: StagedSessionCapacity,\n\trecoveryActions?: SessionRecoveryAction[],\n): void {\n\tconst path = walPath(sessionStoreDir, session.sessionId, head.generation);\n\tif (!existsSync(path)) {\n\t\tthrow new Error(`Persisted session WAL is missing: ${path}`);\n\t}\n\tconst encoded = readBoundedArtifactSync(path, \"wal\", session.sessionId);\n\tif (encoded.byteLength < head.walByteLength) {\n\t\tthrow new Error(`Persisted session WAL is shorter than its durable head: ${path}`);\n\t}\n\tif (encoded.byteLength > head.walByteLength) {\n\t\tperformOrDeferRecovery(() => truncateDurableFileSync(path, head.walByteLength), recoveryActions);\n\t}\n\tconst committed = encoded.subarray(0, head.walByteLength);\n\tif (committed.byteLength > 0 && committed.at(-1) !== 0x0a) {\n\t\tthrow new Error(`Persisted session WAL durable head does not end at a record boundary: ${path}`);\n\t}\n\tconst lines = committed.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\tif (lines.length !== head.walRecordCount) {\n\t\tthrow new Error(`Persisted session WAL record count does not match its durable head: ${path}`);\n\t}\n\tlet expectedStateDigest = head.snapshotStateDigest;\n\tlet expectedWalDigest = head.snapshotSha256;\n\tfor (const [index, line] of lines.entries()) {\n\t\tconst parsed = parseDurableWalLine(line, path, index + 1);\n\t\tconst applied = applyDurableWalRecord(\n\t\t\tsession,\n\t\t\tparsed.record,\n\t\t\tparsed.digest,\n\t\t\thead.generation,\n\t\t\tindex,\n\t\t\texpectedStateDigest,\n\t\t\texpectedWalDigest,\n\t\t\tcapacity,\n\t\t\tpath,\n\t\t);\n\t\texpectedStateDigest = applied.stateDigest;\n\t\texpectedWalDigest = applied.walDigest;\n\t}\n\tif (\n\t\texpectedStateDigest !== head.stateDigest ||\n\t\texpectedWalDigest !== head.walTailDigest ||\n\t\tsession.revision !== head.revision ||\n\t\tsession.entries.length !== head.entryCount\n\t) {\n\t\tthrow new Error(`Persisted session WAL final state does not match its durable head: ${path}`);\n\t}\n}\n\nfunction rollbackUncommittedFutureGeneration(\n\tsessionStoreDir: string,\n\tsessionId: string,\n\theadGeneration: number,\n\trecoveryActions?: SessionRecoveryAction[],\n): void {\n\tconst basePath = sessionPath(sessionStoreDir, sessionId);\n\tlet removed = false;\n\tlet deferredRemoval = false;\n\tfor (const slot of [0, 1]) {\n\t\tconst path = `${basePath}.snapshot.${slot}`;\n\t\tif (!existsSync(path)) continue;\n\t\tconst parsed = parsePersistedSessionFile(\n\t\t\treadBoundedArtifactSync(path, \"snapshot\", sessionId).toString(\"utf-8\"),\n\t\t\tpath,\n\t\t);\n\t\tif (parsed.version !== DURABLE_SESSION_VERSION) {\n\t\t\tthrow new Error(`Persisted session snapshot slot uses a legacy format: ${path}`);\n\t\t}\n\t\tcreateStagedSessionCapacity(parsed.session, path);\n\t\tif (parsed.generation % 2 !== slot) {\n\t\t\tthrow new Error(`Persisted session snapshot is stored in the wrong slot: ${path}`);\n\t\t}\n\t\tif (parsed.generation <= headGeneration) continue;\n\t\tif (parsed.generation !== headGeneration + 1) {\n\t\t\tthrow new Error(`Persisted session snapshot generation is ahead of its durable head: ${path}`);\n\t\t}\n\t\tperformOrDeferRecovery(() => {\n\t\t\trmSync(path);\n\t\t\trmSync(`${basePath}.wal.${slot}`, { force: true });\n\t\t}, recoveryActions);\n\t\tremoved = recoveryActions === undefined;\n\t\tdeferredRemoval = recoveryActions !== undefined;\n\t}\n\tif (removed) {\n\t\tsyncDirectorySync(sessionStoreDir);\n\t} else if (recoveryActions && deferredRemoval) {\n\t\trecoveryActions.push(() => syncDirectorySync(sessionStoreDir));\n\t}\n}\n\nfunction loadDurableSession(\n\tsessionStoreDir: string,\n\tsessionId: string,\n\thead: ParsedHeadRecord,\n\trecoveryActions?: SessionRecoveryAction[],\n): { session: PersistedSessionState; meta: DurableSessionMeta } {\n\tconst record = head.record;\n\tif (record.sessionId !== sessionId) {\n\t\tthrow new Error(`Persisted session head sessionId does not match snapshot: ${record.sessionId}`);\n\t}\n\trollbackUncommittedFutureGeneration(sessionStoreDir, sessionId, record.generation, recoveryActions);\n\tconst path = snapshotPath(sessionStoreDir, sessionId, record.generation);\n\tif (!existsSync(path)) {\n\t\tthrow new Error(`Persisted session snapshot referenced by durable head is missing: ${path}`);\n\t}\n\tconst encoded = readBoundedArtifactSync(path, \"snapshot\", sessionId);\n\tif (sha256(encoded) !== record.snapshotSha256) {\n\t\tthrow new Error(`Persisted session snapshot checksum does not match its durable head: ${path}`);\n\t}\n\tconst persisted = parsePersistedSessionFile(encoded.toString(\"utf-8\"), path);\n\tif (persisted.version !== DURABLE_SESSION_VERSION || persisted.generation !== record.generation) {\n\t\tthrow new Error(`Persisted session snapshot generation does not match its durable head: ${path}`);\n\t}\n\tif (\n\t\tpersisted.stateDigest !== record.snapshotStateDigest ||\n\t\tpersisted.session.revision !== record.snapshotRevision ||\n\t\tpersisted.session.entries.length !== record.snapshotEntryCount\n\t) {\n\t\tthrow new Error(`Persisted session snapshot state does not match its durable head: ${path}`);\n\t}\n\tconst capacity = createStagedSessionCapacity(persisted.session, path);\n\treadDurableWal(sessionStoreDir, persisted.session, record, capacity, recoveryActions);\n\treturn {\n\t\tsession: persisted.session,\n\t\tmeta: {\n\t\t\tformat: \"durable\",\n\t\t\tgeneration: record.generation,\n\t\t\theadSequence: record.sequence,\n\t\t\theadDigest: head.digest,\n\t\t\tsnapshotSha256: record.snapshotSha256,\n\t\t\tsnapshotStateDigest: record.snapshotStateDigest,\n\t\t\tsnapshotRevision: record.snapshotRevision,\n\t\t\tsnapshotEntryCount: record.snapshotEntryCount,\n\t\t\tsnapshotByteLength: encoded.byteLength,\n\t\t\tentryCount: record.entryCount,\n\t\t\twalRecords: record.walRecordCount,\n\t\t\twalByteLength: record.walByteLength,\n\t\t\twalTailDigest: record.walTailDigest,\n\t\t\tstateDigest: record.stateDigest,\n\t\t\trevision: record.revision,\n\t\t},\n\t};\n}\n\nfunction assertAnchoredArtifacts(sessionStoreDir: string, entries: Dirent<string>[]): void {\n\tconst names = new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name));\n\tfor (const name of names) {\n\t\tconst match = SESSION_ARTIFACT_PATTERN.exec(name) ?? LEGACY_WAL_PATTERN.exec(name);\n\t\tif (match && !names.has(match[1])) {\n\t\t\tthrow new Error(`Persisted session artifact has no anchor snapshot: ${join(sessionStoreDir, name)}`);\n\t\t}\n\t}\n}\n\ninterface StagedPersistedSession {\n\tsession: PersistedSessionState;\n\tmeta: PersistedSessionMeta;\n\tkey: string;\n\tpath: string;\n}\n\nfunction assertStagedSessionBatchCapacity(stagedSessions: readonly StagedPersistedSession[]): void {\n\tconst limits = getSessionCapacityLimits();\n\tconst usage = getSessionCapacityUsage();\n\tlet requestedLoadedSessions = usage.loadedSessions;\n\tlet requestedEntries = usage.entryCount;\n\tlet requestedLogicalBytes = usage.logicalBytes;\n\tconst stagedIds = new Set<string>();\n\tfor (const staged of stagedSessions) {\n\t\tif (stagedIds.has(staged.session.sessionId)) {\n\t\t\tthrow new Error(`Persisted session ${staged.session.sessionId} is staged more than once`);\n\t\t}\n\t\tstagedIds.add(staged.session.sessionId);\n\t\tconst existing = getSession(staged.session.sessionId);\n\t\tif (!existing) requestedLoadedSessions++;\n\t\trequestedEntries += staged.session.entries.length - (existing?.entries.length ?? 0);\n\t\trequestedLogicalBytes +=\n\t\t\tcalculateSessionLogicalBytes(staged.session.staticContext, staged.session.entries) -\n\t\t\t(existing?.logicalBytes ?? 0);\n\t\tif (requestedLoadedSessions > limits.maxLoadedSessions) {\n\t\t\tthrow persistenceCapacityError(\n\t\t\t\t\"loaded_sessions\",\n\t\t\t\tstaged.session.sessionId,\n\t\t\t\tstaged.path,\n\t\t\t\tusage.loadedSessions,\n\t\t\t\trequestedLoadedSessions,\n\t\t\t\tlimits.maxLoadedSessions,\n\t\t\t);\n\t\t}\n\t\tif (requestedEntries > limits.maxAggregateEntries) {\n\t\t\tthrow persistenceCapacityError(\n\t\t\t\t\"aggregate_entries\",\n\t\t\t\tstaged.session.sessionId,\n\t\t\t\tstaged.path,\n\t\t\t\tusage.entryCount,\n\t\t\t\trequestedEntries,\n\t\t\t\tlimits.maxAggregateEntries,\n\t\t\t);\n\t\t}\n\t\tif (requestedLogicalBytes > limits.maxAggregateLogicalBytes) {\n\t\t\tthrow persistenceCapacityError(\n\t\t\t\t\"aggregate_logical_bytes\",\n\t\t\t\tstaged.session.sessionId,\n\t\t\t\tstaged.path,\n\t\t\t\tusage.logicalBytes,\n\t\t\t\trequestedLogicalBytes,\n\t\t\t\tlimits.maxAggregateLogicalBytes,\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function loadPersistedSessions(sessionStoreDir: string): void {\n\tif (!existsSync(sessionStoreDir)) return;\n\tconst entries = readdirSync(sessionStoreDir, { withFileTypes: true });\n\tassertAnchoredArtifacts(sessionStoreDir, entries);\n\tconst stagedSessions: StagedPersistedSession[] = [];\n\tconst recoveryActions: SessionRecoveryAction[] = [];\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !SESSION_FILE_PATTERN.test(entry.name)) continue;\n\t\tconst anchorPath = join(sessionStoreDir, entry.name);\n\t\tconst unresolvedSessionId = `<unresolved:${entry.name}>`;\n\t\tconst anchor = parsePersistedSessionFile(\n\t\t\treadBoundedArtifactSync(anchorPath, \"snapshot\", unresolvedSessionId).toString(\"utf-8\"),\n\t\t\tanchorPath,\n\t\t);\n\t\tconst expectedFileName = sessionFileName(anchor.session.sessionId);\n\t\tif (entry.name !== expectedFileName) {\n\t\t\tthrow new Error(`Persisted session file name does not match sessionId: ${anchorPath}`);\n\t\t}\n\t\tconst sessionId = anchor.session.sessionId;\n\t\tconst capacity = createStagedSessionCapacity(anchor.session, anchorPath);\n\t\tconst head = readLatestHead(sessionStoreDir, sessionId);\n\t\tif (head) {\n\t\t\tconst loaded = loadDurableSession(sessionStoreDir, sessionId, head, recoveryActions);\n\t\t\tconst key = persistedSessionKey(sessionStoreDir, sessionId);\n\t\t\tstagedSessions.push({ session: loaded.session, meta: loaded.meta, key, path: anchorPath });\n\t\t\tcontinue;\n\t\t}\n\t\tconst basePath = sessionPath(sessionStoreDir, sessionId);\n\t\tif (\n\t\t\tanchor.version === DURABLE_SESSION_VERSION ||\n\t\t\texistsSync(`${basePath}.snapshot.0`) ||\n\t\t\texistsSync(`${basePath}.snapshot.1`) ||\n\t\t\texistsSync(`${basePath}.wal.0`) ||\n\t\t\texistsSync(`${basePath}.wal.1`)\n\t\t) {\n\t\t\tthrow new Error(`Persisted session durable artifacts are missing their head for ${sessionId}`);\n\t\t}\n\t\tconst walRecords = applyLegacyWal(sessionStoreDir, anchor.session, capacity, recoveryActions);\n\t\tconst key = persistedSessionKey(sessionStoreDir, sessionId);\n\t\tstagedSessions.push({\n\t\t\tsession: anchor.session,\n\t\t\tmeta: {\n\t\t\t\tformat: \"legacy\",\n\t\t\t\tentryCount: anchor.session.entries.length,\n\t\t\t\twalRecords,\n\t\t\t\trevision: anchor.session.revision,\n\t\t\t},\n\t\t\tkey,\n\t\t\tpath: anchorPath,\n\t\t});\n\t}\n\tassertStagedSessionBatchCapacity(stagedSessions);\n\tfor (const staged of stagedSessions) {\n\t\tvalidatePersistedSessionState(staged.session);\n\t}\n\tfor (const recoveryAction of recoveryActions) recoveryAction();\n\tfor (const staged of stagedSessions) {\n\t\trestoreSessionState(staged.session);\n\t}\n\tfor (const staged of stagedSessions) {\n\t\tpersistedSessions.set(staged.key, staged.meta);\n\t\tsessionsNeedingRecovery.delete(staged.key);\n\t}\n}\n\nfunction encodeSnapshot(\n\tsession: SessionState,\n\tgeneration: number,\n): {\n\tencoded: Buffer;\n\tstate: PersistedSessionState;\n\tstateDigest: string;\n\tsnapshotDigest: string;\n} {\n\tconst state = exportSessionState(session);\n\tconst digest = stateDigest(state);\n\tconst body: PersistedSessionFileV2 = {\n\t\tversion: DURABLE_SESSION_VERSION,\n\t\tgeneration,\n\t\tstateDigest: digest,\n\t\tsession: state,\n\t};\n\tconst encoded = Buffer.from(JSON.stringify(body), \"utf-8\");\n\treturn { encoded, state, stateDigest: digest, snapshotDigest: sha256(encoded) };\n}\n\nfunction encodeHead(record: PersistedSessionHeadRecord): Buffer {\n\tconst payload = JSON.stringify(record);\n\tconst envelope: PersistedSessionHeadEnvelope = {\n\t\tversion: HEAD_VERSION,\n\t\tpayload,\n\t\tsha256: sha256(payload),\n\t};\n\treturn Buffer.from(JSON.stringify(envelope), \"utf-8\");\n}\n\nfunction prepareHeadWrite(\n\tsessionStoreDir: string,\n\tsessionId: string,\n\trecord: PersistedSessionHeadRecord,\n): { path: string; encoded: Buffer; digest: string } {\n\tconst path = headPath(sessionStoreDir, sessionId, record.sequence);\n\tconst encoded = encodeHead(record);\n\tassertArtifactWriteSize(path, \"head\", sessionId, existsSync(path) ? statSync(path).size : 0, encoded.byteLength);\n\tconst latest = readLatestHead(sessionStoreDir, sessionId);\n\tif (record.sequence === 0) {\n\t\tif (latest) {\n\t\t\tthrow new Error(`Persisted session already has a durable head for ${sessionId}`);\n\t\t}\n\t} else if (\n\t\t!latest ||\n\t\tlatest.record.sequence !== record.sequence - 1 ||\n\t\tlatest.digest !== record.previousHeadDigest\n\t) {\n\t\tthrow new Error(`Persisted session head changed before commit for ${sessionId}`);\n\t}\n\tif (existsSync(path)) {\n\t\tconst existing = parseHeadFile(path, record.sequence % 2, sessionId);\n\t\tif (record.sequence < 2 || existing.record.sequence !== record.sequence - 2) {\n\t\t\tthrow new Error(`Persisted session head slot contains an unexpected generation: ${path}`);\n\t\t}\n\t}\n\treturn { path, encoded, digest: sha256(JSON.stringify(record)) };\n}\n\nfunction writeHeadRecord(sessionStoreDir: string, sessionId: string, record: PersistedSessionHeadRecord): string {\n\tconst prepared = prepareHeadWrite(sessionStoreDir, sessionId, record);\n\treplaceDurableFileSync(prepared.path, prepared.encoded);\n\treturn prepared.digest;\n}\n\nfunction assertSnapshotSlotCanBeReused(sessionStoreDir: string, sessionId: string, generation: number): void {\n\tconst path = snapshotPath(sessionStoreDir, sessionId, generation);\n\tconst wal = walPath(sessionStoreDir, sessionId, generation);\n\tif (generation === 0) {\n\t\tif (existsSync(path) || existsSync(wal)) {\n\t\t\tthrow new Error(`Persisted session initial durable slot already exists for ${sessionId}`);\n\t\t}\n\t\treturn;\n\t}\n\tif (!existsSync(path)) {\n\t\tif (existsSync(wal)) {\n\t\t\tthrow new Error(`Persisted session WAL slot exists without its snapshot: ${wal}`);\n\t\t}\n\t\treturn;\n\t}\n\tconst existing = parsePersistedSessionFile(\n\t\treadBoundedArtifactSync(path, \"snapshot\", sessionId).toString(\"utf-8\"),\n\t\tpath,\n\t);\n\tif (existing.version !== DURABLE_SESSION_VERSION || existing.generation !== generation - 2) {\n\t\tthrow new Error(`Persisted session snapshot slot contains an unexpected generation: ${path}`);\n\t}\n}\n\ninterface SnapshotWritePlan {\n\tgeneration: number;\n\theadSequence: number;\n\tsnapshot: ReturnType<typeof encodeSnapshot>;\n\theadRecord: PersistedSessionHeadRecord;\n\tnextSnapshotPath: string;\n\tnextWalPath: string;\n\theadArtifactBytes: number;\n}\n\nfunction prepareSnapshotWrite(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n\tmeta: PersistedSessionMeta | undefined,\n): SnapshotWritePlan {\n\tconst generation = meta?.format === \"durable\" ? meta.generation + 1 : meta?.format === \"legacy\" ? 1 : 0;\n\tconst headSequence = meta?.format === \"durable\" ? meta.headSequence + 1 : 0;\n\tconst previousHeadDigest = meta?.format === \"durable\" ? meta.headDigest : ZERO_DIGEST;\n\tassertSnapshotSlotCanBeReused(sessionStoreDir, session.sessionId, generation);\n\tconst snapshot = encodeSnapshot(session, generation);\n\tconst headRecord: PersistedSessionHeadRecord = {\n\t\tversion: HEAD_VERSION,\n\t\tsessionId: session.sessionId,\n\t\tsequence: headSequence,\n\t\tgeneration,\n\t\tpreviousHeadDigest,\n\t\tsnapshotSha256: snapshot.snapshotDigest,\n\t\tsnapshotStateDigest: snapshot.stateDigest,\n\t\tsnapshotRevision: snapshot.state.revision,\n\t\tsnapshotEntryCount: snapshot.state.entries.length,\n\t\twalRecordCount: 0,\n\t\twalByteLength: 0,\n\t\twalTailDigest: snapshot.snapshotDigest,\n\t\tstateDigest: snapshot.stateDigest,\n\t\trevision: snapshot.state.revision,\n\t\tentryCount: snapshot.state.entries.length,\n\t};\n\tconst nextSnapshotPath = snapshotPath(sessionStoreDir, session.sessionId, generation);\n\tconst nextWalPath = walPath(sessionStoreDir, session.sessionId, generation);\n\tassertArtifactWriteSize(\n\t\tnextSnapshotPath,\n\t\t\"snapshot\",\n\t\tsession.sessionId,\n\t\texistsSync(nextSnapshotPath) ? statSync(nextSnapshotPath).size : 0,\n\t\tsnapshot.encoded.byteLength,\n\t);\n\tassertArtifactWriteSize(nextWalPath, \"wal\", session.sessionId, 0, 0);\n\tconst preparedHead = prepareHeadWrite(sessionStoreDir, session.sessionId, headRecord);\n\treturn {\n\t\tgeneration,\n\t\theadSequence,\n\t\tsnapshot,\n\t\theadRecord,\n\t\tnextSnapshotPath,\n\t\tnextWalPath,\n\t\theadArtifactBytes: preparedHead.encoded.byteLength,\n\t};\n}\n\nfunction writeSnapshot(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n\tmeta: PersistedSessionMeta | undefined,\n\tfaultInjector?: (point: SessionPersistenceFaultPoint) => void,\n): DurableSessionMeta {\n\tensureDurableDirectorySync(sessionStoreDir);\n\tconst plan = prepareSnapshotWrite(sessionStoreDir, session, meta);\n\treplaceDurableFileSync(plan.nextSnapshotPath, plan.snapshot.encoded);\n\treplaceDurableFileSync(plan.nextWalPath, Buffer.alloc(0));\n\tfaultInjector?.(\"snapshot_after_sync_before_head\");\n\tconst headDigest = writeHeadRecord(sessionStoreDir, session.sessionId, plan.headRecord);\n\treturn {\n\t\tformat: \"durable\",\n\t\tgeneration: plan.generation,\n\t\theadSequence: plan.headSequence,\n\t\theadDigest,\n\t\tsnapshotSha256: plan.snapshot.snapshotDigest,\n\t\tsnapshotStateDigest: plan.snapshot.stateDigest,\n\t\tsnapshotRevision: plan.snapshot.state.revision,\n\t\tsnapshotEntryCount: plan.snapshot.state.entries.length,\n\t\tsnapshotByteLength: plan.snapshot.encoded.byteLength,\n\t\tentryCount: plan.snapshot.state.entries.length,\n\t\twalRecords: 0,\n\t\twalByteLength: 0,\n\t\twalTailDigest: plan.snapshot.snapshotDigest,\n\t\tstateDigest: plan.snapshot.stateDigest,\n\t\trevision: plan.snapshot.state.revision,\n\t};\n}\n\nfunction encodeWalRecord(record: PersistedSessionWalRecordV5): { encoded: Buffer; digest: string } {\n\tconst payload = JSON.stringify(record);\n\tconst digest = sha256(payload);\n\tconst envelope: PersistedWalEnvelope = {\n\t\tversion: DURABLE_WAL_VERSION,\n\t\tpayload,\n\t\tsha256: digest,\n\t};\n\treturn { encoded: Buffer.from(`${JSON.stringify(envelope)}\\n`, \"utf-8\"), digest };\n}\n\ninterface WalWritePlan {\n\tpath: string;\n\tencodedRecord: ReturnType<typeof encodeWalRecord>;\n\theadRecord: PersistedSessionHeadRecord;\n\theadArtifactBytes: number;\n}\n\nfunction prepareWalWrite(sessionStoreDir: string, session: SessionState, meta: DurableSessionMeta): WalWritePlan {\n\tif (session.persistenceChange?.kind !== \"wal\") {\n\t\tthrow new Error(`Session ${session.sessionId} does not have a WAL persistence change`);\n\t}\n\tif (session.revision <= meta.revision) {\n\t\tthrow new Error(`Session ${session.sessionId} revision did not advance before WAL persistence`);\n\t}\n\tconst path = walPath(sessionStoreDir, session.sessionId, meta.generation);\n\tif (!existsSync(path) || statSync(path).size !== meta.walByteLength) {\n\t\tthrow new Error(`Session ${session.sessionId} WAL does not match its in-memory durable head`);\n\t}\n\tconst recordWithoutStateDigest: Omit<PersistedSessionWalRecordV5, \"stateDigest\"> = {\n\t\tversion: DURABLE_WAL_VERSION,\n\t\tsessionId: session.sessionId,\n\t\tgeneration: meta.generation,\n\t\tsequence: meta.walRecords,\n\t\tbaseRevision: meta.revision,\n\t\trevision: session.revision,\n\t\tbaseEntryCount: meta.entryCount,\n\t\tentries: session.entries.slice(meta.entryCount),\n\t\tleafId: session.leafId,\n\t\tupdatedAt: session.updatedAt,\n\t\tpreviousStateDigest: meta.stateDigest,\n\t\tpreviousWalDigest: meta.walTailDigest,\n\t\tstaticContextChanged: session.persistenceChange.staticContextChanged,\n\t};\n\tif (session.persistenceChange.staticContextChanged) {\n\t\trecordWithoutStateDigest.staticContext = session.staticContext ?? null;\n\t}\n\tconst record: PersistedSessionWalRecordV5 = {\n\t\t...recordWithoutStateDigest,\n\t\tstateDigest: walTransitionStateDigest(recordWithoutStateDigest),\n\t};\n\tconst encodedRecord = encodeWalRecord(record);\n\tconst headRecord: PersistedSessionHeadRecord = {\n\t\tversion: HEAD_VERSION,\n\t\tsessionId: session.sessionId,\n\t\tsequence: meta.headSequence + 1,\n\t\tgeneration: meta.generation,\n\t\tpreviousHeadDigest: meta.headDigest,\n\t\tsnapshotSha256: meta.snapshotSha256,\n\t\tsnapshotStateDigest: meta.snapshotStateDigest,\n\t\tsnapshotRevision: meta.snapshotRevision,\n\t\tsnapshotEntryCount: meta.snapshotEntryCount,\n\t\twalRecordCount: meta.walRecords + 1,\n\t\twalByteLength: meta.walByteLength + encodedRecord.encoded.byteLength,\n\t\twalTailDigest: encodedRecord.digest,\n\t\tstateDigest: record.stateDigest,\n\t\trevision: session.revision,\n\t\tentryCount: session.entries.length,\n\t};\n\tassertArtifactWriteSize(path, \"wal\", session.sessionId, meta.walByteLength, headRecord.walByteLength);\n\tconst preparedHead = prepareHeadWrite(sessionStoreDir, session.sessionId, headRecord);\n\treturn {\n\t\tpath,\n\t\tencodedRecord,\n\t\theadRecord,\n\t\theadArtifactBytes: preparedHead.encoded.byteLength,\n\t};\n}\n\nfunction appendWalRecord(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n\tmeta: DurableSessionMeta,\n\tfaultInjector?: (point: SessionPersistenceFaultPoint) => void,\n): DurableSessionMeta {\n\tconst plan = prepareWalWrite(sessionStoreDir, session, meta);\n\tappendDurableFileSync(plan.path, plan.encodedRecord.encoded);\n\tfaultInjector?.(\"wal_after_sync_before_head\");\n\tconst headDigest = writeHeadRecord(sessionStoreDir, session.sessionId, plan.headRecord);\n\treturn {\n\t\t...meta,\n\t\theadSequence: plan.headRecord.sequence,\n\t\theadDigest,\n\t\tentryCount: plan.headRecord.entryCount,\n\t\twalRecords: plan.headRecord.walRecordCount,\n\t\twalByteLength: plan.headRecord.walByteLength,\n\t\twalTailDigest: plan.headRecord.walTailDigest,\n\t\tstateDigest: plan.headRecord.stateDigest,\n\t\trevision: plan.headRecord.revision,\n\t};\n}\n\nfunction recoverFailedCommit(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n\tkey: string,\n\tmeta: PersistedSessionMeta | undefined,\n): void {\n\tif (!existsSync(sessionStoreDir)) {\n\t\tif (meta) {\n\t\t\tpersistedSessions.set(key, meta);\n\t\t} else {\n\t\t\tpersistedSessions.delete(key);\n\t\t}\n\t\tsessionsNeedingRecovery.delete(key);\n\t\treturn;\n\t}\n\tsyncDirectorySync(sessionStoreDir);\n\tconst head = readLatestHead(sessionStoreDir, session.sessionId);\n\tif (!head) {\n\t\tif (meta?.format === \"durable\") {\n\t\t\tthrow new Error(`Persisted session durable head disappeared during recovery for ${session.sessionId}`);\n\t\t}\n\t\tconst uncommittedGeneration = meta?.format === \"legacy\" ? 1 : 0;\n\t\trmSync(snapshotPath(sessionStoreDir, session.sessionId, uncommittedGeneration), { force: true });\n\t\trmSync(walPath(sessionStoreDir, session.sessionId, uncommittedGeneration), { force: true });\n\t\tsyncDirectorySync(sessionStoreDir);\n\t\tif (meta) {\n\t\t\tpersistedSessions.set(key, meta);\n\t\t} else {\n\t\t\tpersistedSessions.delete(key);\n\t\t}\n\t\tsessionsNeedingRecovery.delete(key);\n\t\treturn;\n\t}\n\n\tconst loaded = loadDurableSession(sessionStoreDir, session.sessionId, head);\n\tpersistedSessions.set(key, loaded.meta);\n\tconst current = exportSessionState(session);\n\tif (isDeepStrictEqual(current, loaded.session)) {\n\t\tmarkSessionPersisted(session);\n\t\tsessionsNeedingRecovery.delete(key);\n\t\treturn;\n\t}\n\tif (session.persistenceChange?.kind === \"wal\") {\n\t\tif (\n\t\t\tloaded.session.entries.length > current.entries.length ||\n\t\t\t!isDeepStrictEqual(current.entries.slice(0, loaded.session.entries.length), loaded.session.entries)\n\t\t) {\n\t\t\tthrow new Error(`Session ${session.sessionId} diverged from its recovered durable tree`);\n\t\t}\n\t\tsession.persistenceChange = {\n\t\t\tkind: \"wal\",\n\t\t\tentries: current.entries.slice(loaded.session.entries.length),\n\t\t\tstaticContextChanged: !isDeepStrictEqual(current.staticContext, loaded.session.staticContext),\n\t\t};\n\t}\n\tsessionsNeedingRecovery.delete(key);\n}\n\nfunction shouldWriteSnapshot(session: SessionState, meta: PersistedSessionMeta | undefined): boolean {\n\treturn (\n\t\t!meta ||\n\t\tmeta.format === \"legacy\" ||\n\t\tsession.persistenceChange?.kind === \"snapshot\" ||\n\t\t// Keep replay proportional to the checkpoint while amortizing full serialization over equivalent growth.\n\t\t(meta.format === \"durable\" &&\n\t\t\t(meta.walRecords >= Math.max(MIN_WAL_RECORDS_PER_SNAPSHOT, meta.snapshotEntryCount) ||\n\t\t\t\tmeta.walByteLength >= Math.max(MIN_WAL_BYTES_PER_SNAPSHOT, meta.snapshotByteLength)))\n\t);\n}\n\nexport function preflightPersistedSession(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n): SessionPersistencePreflightResult {\n\tconst key = persistedSessionKey(sessionStoreDir, session.sessionId);\n\tif (sessionsNeedingRecovery.has(key)) {\n\t\tthrow new Error(`Session ${session.sessionId} requires persistence recovery before it can be preflighted`);\n\t}\n\tconst meta = persistedSessions.get(key);\n\tif (meta && !session.persistenceChange) return { strategy: \"none\" };\n\tif (shouldWriteSnapshot(session, meta)) {\n\t\tconst plan = prepareSnapshotWrite(sessionStoreDir, session, meta);\n\t\treturn {\n\t\t\tstrategy: \"snapshot\",\n\t\t\tsnapshotArtifactBytes: plan.snapshot.encoded.byteLength,\n\t\t\twalArtifactBytes: 0,\n\t\t\theadArtifactBytes: plan.headArtifactBytes,\n\t\t};\n\t}\n\tif (meta?.format !== \"durable\") {\n\t\tthrow new Error(`Session ${session.sessionId} does not have durable persistence metadata`);\n\t}\n\ttry {\n\t\tconst plan = prepareWalWrite(sessionStoreDir, session, meta);\n\t\treturn {\n\t\t\tstrategy: \"wal\",\n\t\t\twalArtifactBytes: plan.headRecord.walByteLength,\n\t\t\theadArtifactBytes: plan.headArtifactBytes,\n\t\t};\n\t} catch (error) {\n\t\tif (!(error instanceof SessionPersistenceCapacityError) || error.resource !== \"wal_artifact_bytes\") {\n\t\t\tthrow error;\n\t\t}\n\t\tconst plan = prepareSnapshotWrite(sessionStoreDir, session, meta);\n\t\treturn {\n\t\t\tstrategy: \"snapshot\",\n\t\t\tsnapshotArtifactBytes: plan.snapshot.encoded.byteLength,\n\t\t\twalArtifactBytes: 0,\n\t\t\theadArtifactBytes: plan.headArtifactBytes,\n\t\t};\n\t}\n}\n\nexport function savePersistedSession(\n\tsessionStoreDir: string,\n\tsession: SessionState,\n\toptions: SessionPersistenceOptions = {},\n): void {\n\tconst key = persistedSessionKey(sessionStoreDir, session.sessionId);\n\tif (sessionsNeedingRecovery.has(key)) {\n\t\trecoverFailedCommit(sessionStoreDir, session, key, persistedSessions.get(key));\n\t}\n\tconst meta = persistedSessions.get(key);\n\tif (meta && !session.persistenceChange) return;\n\tif (shouldWriteSnapshot(session, meta)) {\n\t\ttry {\n\t\t\tconst nextMeta = writeSnapshot(sessionStoreDir, session, meta, options.faultInjector);\n\t\t\tpersistedSessions.set(key, nextMeta);\n\t\t\tmarkSessionPersisted(session);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof SessionPersistenceCapacityError)) sessionsNeedingRecovery.add(key);\n\t\t\tthrow error;\n\t\t}\n\t\treturn;\n\t}\n\tif (meta?.format !== \"durable\") {\n\t\tthrow new Error(`Session ${session.sessionId} does not have durable persistence metadata`);\n\t}\n\ttry {\n\t\tconst nextMeta = appendWalRecord(sessionStoreDir, session, meta, options.faultInjector);\n\t\tpersistedSessions.set(key, nextMeta);\n\t\tmarkSessionPersisted(session);\n\t} catch (error) {\n\t\tif (error instanceof SessionPersistenceCapacityError && error.resource === \"wal_artifact_bytes\") {\n\t\t\ttry {\n\t\t\t\tconst nextMeta = writeSnapshot(sessionStoreDir, session, meta, options.faultInjector);\n\t\t\t\tpersistedSessions.set(key, nextMeta);\n\t\t\t\tmarkSessionPersisted(session);\n\t\t\t\treturn;\n\t\t\t} catch (snapshotError) {\n\t\t\t\tif (!(snapshotError instanceof SessionPersistenceCapacityError)) sessionsNeedingRecovery.add(key);\n\t\t\t\tthrow snapshotError;\n\t\t\t}\n\t\t}\n\t\tsessionsNeedingRecovery.add(key);\n\t\tthrow error;\n\t}\n}\n\nexport function deletePersistedSession(sessionStoreDir: string, sessionId: string): void {\n\tconst paths = [\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.head.0`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.head.1`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.snapshot.0`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.snapshot.1`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.wal.0`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.wal.1`,\n\t\t`${sessionPath(sessionStoreDir, sessionId)}.wal`,\n\t\tsessionPath(sessionStoreDir, sessionId),\n\t];\n\tfor (const path of paths) rmSync(path, { force: true });\n\tif (existsSync(sessionStoreDir)) syncDirectorySync(sessionStoreDir);\n\tconst key = persistedSessionKey(sessionStoreDir, sessionId);\n\tpersistedSessions.delete(key);\n\tsessionsNeedingRecovery.delete(key);\n}\n"]}
|