@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.
@@ -1,19 +1,195 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
- import { join } from "node:path";
4
- import { exportSessionState, markSessionPersisted, restoreSessionState, } from "./session-store.js";
5
- const PERSISTED_SESSION_VERSION = 1;
6
- const WAL_SNAPSHOT_INTERVAL = 32;
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, truncateSync, writeSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { calculateSessionLogicalBytes, exportSessionState, getSession, getSessionCapacityLimits, getSessionCapacityUsage, markSessionPersisted, restoreSessionState, validatePersistedSessionState, } from "./session-store.js";
6
+ const LEGACY_SESSION_VERSION = 1;
7
+ const DURABLE_SESSION_VERSION = 2;
8
+ const LEGACY_DURABLE_WAL_VERSION = 4;
9
+ const DURABLE_WAL_VERSION = 5;
10
+ const HEAD_VERSION = 1;
11
+ const MIN_WAL_RECORDS_PER_SNAPSHOT = 32;
12
+ const MIN_WAL_BYTES_PER_SNAPSHOT = 1024 * 1024;
7
13
  const REPLACE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
14
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
15
+ const SESSION_FILE_PATTERN = /^[a-f0-9]{64}\.json$/u;
16
+ const SESSION_ARTIFACT_PATTERN = /^([a-f0-9]{64}\.json)\.(?:head|snapshot|wal)\.[01]$/u;
17
+ const LEGACY_WAL_PATTERN = /^([a-f0-9]{64}\.json)\.wal$/u;
18
+ const ZERO_DIGEST = "0".repeat(64);
19
+ export class SessionPersistenceCapacityError extends Error {
20
+ code = "PI_SERVER_SESSION_PERSISTENCE_CAPACITY_EXCEEDED";
21
+ retryable = false;
22
+ resource;
23
+ sessionId;
24
+ path;
25
+ current;
26
+ requested;
27
+ limit;
28
+ constructor(input) {
29
+ super(`Persisted session capacity exceeded: resource=${input.resource}, session=${input.sessionId}, path=${input.path}, requested=${input.requested}, limit=${input.limit}`);
30
+ this.name = "SessionPersistenceCapacityError";
31
+ this.resource = input.resource;
32
+ this.sessionId = input.sessionId;
33
+ this.path = input.path;
34
+ this.current = input.current;
35
+ this.requested = input.requested;
36
+ this.limit = input.limit;
37
+ }
38
+ }
39
+ const SESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES = 1024 * 1024;
40
+ const WAL_ARTIFACT_ENCODING_MULTIPLIER = 2;
41
+ const DEFAULT_MAX_HEAD_BYTES = 64 * 1024;
42
+ let artifactLimitOverrides = {};
43
+ function defaultArtifactLimits() {
44
+ const capacity = getSessionCapacityLimits();
45
+ const maxSnapshotBytes = capacity.maxLogicalBytesPerSession + capacity.maxEntriesPerSession + SESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES;
46
+ const maxWalBytes = capacity.maxLogicalBytesPerSession * WAL_ARTIFACT_ENCODING_MULTIPLIER +
47
+ capacity.maxEntriesPerSession * WAL_ARTIFACT_ENCODING_MULTIPLIER +
48
+ SESSION_ARTIFACT_ENVELOPE_OVERHEAD_BYTES * WAL_ARTIFACT_ENCODING_MULTIPLIER;
49
+ if (!Number.isSafeInteger(maxSnapshotBytes) || !Number.isSafeInteger(maxWalBytes)) {
50
+ throw new RangeError("Session capacity limits are too large to derive safe persistence artifact limits");
51
+ }
52
+ return {
53
+ maxSnapshotBytes,
54
+ maxWalBytes,
55
+ maxHeadBytes: DEFAULT_MAX_HEAD_BYTES,
56
+ };
57
+ }
58
+ export function getSessionPersistenceArtifactLimits() {
59
+ return { ...defaultArtifactLimits(), ...artifactLimitOverrides };
60
+ }
61
+ export function configureSessionPersistenceArtifactLimits(overrides) {
62
+ for (const [name, value] of Object.entries(overrides)) {
63
+ if (!Number.isSafeInteger(value) || (value ?? 0) <= 0) {
64
+ throw new RangeError(`${name} must be a positive safe integer`);
65
+ }
66
+ }
67
+ artifactLimitOverrides = { ...artifactLimitOverrides, ...overrides };
68
+ return getSessionPersistenceArtifactLimits();
69
+ }
70
+ export function resetSessionPersistenceArtifactLimits() {
71
+ artifactLimitOverrides = {};
72
+ }
8
73
  const persistedSessions = new Map();
74
+ const sessionsNeedingRecovery = new Set();
75
+ function persistenceCapacityError(resource, sessionId, path, current, requested, limit) {
76
+ return new SessionPersistenceCapacityError({ resource, sessionId, path, current, requested, limit });
77
+ }
78
+ function artifactLimit(kind) {
79
+ const limits = getSessionPersistenceArtifactLimits();
80
+ switch (kind) {
81
+ case "snapshot":
82
+ return { resource: "snapshot_artifact_bytes", limit: limits.maxSnapshotBytes };
83
+ case "wal":
84
+ return { resource: "wal_artifact_bytes", limit: limits.maxWalBytes };
85
+ case "head":
86
+ return { resource: "head_artifact_bytes", limit: limits.maxHeadBytes };
87
+ }
88
+ }
89
+ function readBoundedArtifactSync(path, kind, sessionId) {
90
+ const { resource, limit } = artifactLimit(kind);
91
+ const size = statSync(path).size;
92
+ if (size > limit) {
93
+ throw persistenceCapacityError(resource, sessionId, path, 0, size, limit);
94
+ }
95
+ const encoded = readFileSync(path);
96
+ if (encoded.byteLength > limit) {
97
+ throw persistenceCapacityError(resource, sessionId, path, 0, encoded.byteLength, limit);
98
+ }
99
+ return encoded;
100
+ }
101
+ function assertArtifactWriteSize(path, kind, sessionId, current, requested) {
102
+ const { resource, limit } = artifactLimit(kind);
103
+ if (requested > limit) {
104
+ throw persistenceCapacityError(resource, sessionId, path, current, requested, limit);
105
+ }
106
+ }
107
+ function assertStagedSessionCapacity(sessionId, path, current, next) {
108
+ const limits = getSessionCapacityLimits();
109
+ const usage = getSessionCapacityUsage();
110
+ const existing = getSession(sessionId);
111
+ if (!existing && usage.loadedSessions + 1 > limits.maxLoadedSessions) {
112
+ throw persistenceCapacityError("loaded_sessions", sessionId, path, usage.loadedSessions, usage.loadedSessions + 1, limits.maxLoadedSessions);
113
+ }
114
+ if (next.entryCount > limits.maxEntriesPerSession) {
115
+ throw persistenceCapacityError("session_entries", sessionId, path, current.entryCount, next.entryCount, limits.maxEntriesPerSession);
116
+ }
117
+ if (next.logicalBytes > limits.maxLogicalBytesPerSession) {
118
+ throw persistenceCapacityError("session_logical_bytes", sessionId, path, current.logicalBytes, next.logicalBytes, limits.maxLogicalBytesPerSession);
119
+ }
120
+ const requestedAggregateEntries = usage.entryCount - (existing?.entries.length ?? 0) + next.entryCount;
121
+ if (requestedAggregateEntries > limits.maxAggregateEntries) {
122
+ throw persistenceCapacityError("aggregate_entries", sessionId, path, usage.entryCount, requestedAggregateEntries, limits.maxAggregateEntries);
123
+ }
124
+ const requestedAggregateLogicalBytes = usage.logicalBytes - (existing?.logicalBytes ?? 0) + next.logicalBytes;
125
+ if (requestedAggregateLogicalBytes > limits.maxAggregateLogicalBytes) {
126
+ throw persistenceCapacityError("aggregate_logical_bytes", sessionId, path, usage.logicalBytes, requestedAggregateLogicalBytes, limits.maxAggregateLogicalBytes);
127
+ }
128
+ }
129
+ function createStagedSessionCapacity(session, path) {
130
+ const staged = {
131
+ entryCount: session.entries.length,
132
+ logicalBytes: calculateSessionLogicalBytes(session.staticContext, session.entries),
133
+ };
134
+ assertStagedSessionCapacity(session.sessionId, path, { entryCount: 0, logicalBytes: 0 }, staged);
135
+ return staged;
136
+ }
137
+ function nextWalCapacity(session, record, current) {
138
+ let nextStaticContext = session.staticContext;
139
+ if (record.version === 1) {
140
+ nextStaticContext = record.staticContext;
141
+ }
142
+ else if (record.version === 2 || record.version === 3) {
143
+ if (Object.hasOwn(record, "staticContext"))
144
+ nextStaticContext = record.staticContext;
145
+ }
146
+ else if (record.staticContextChanged) {
147
+ nextStaticContext = record.staticContext === null ? undefined : record.staticContext;
148
+ }
149
+ return {
150
+ entryCount: current.entryCount + record.entries.length,
151
+ logicalBytes: current.logicalBytes -
152
+ calculateSessionLogicalBytes(session.staticContext, []) +
153
+ calculateSessionLogicalBytes(nextStaticContext, []) +
154
+ calculateSessionLogicalBytes(undefined, record.entries),
155
+ };
156
+ }
157
+ function sha256(value) {
158
+ return createHash("sha256").update(value).digest("hex");
159
+ }
160
+ function stateDigest(session) {
161
+ return sha256(JSON.stringify(session));
162
+ }
163
+ function walTransitionStateDigest(record) {
164
+ return sha256(JSON.stringify({
165
+ previousStateDigest: record.previousStateDigest,
166
+ sessionId: record.sessionId,
167
+ baseRevision: record.baseRevision,
168
+ revision: record.revision,
169
+ baseEntryCount: record.baseEntryCount,
170
+ entries: record.entries,
171
+ leafId: record.leafId,
172
+ updatedAt: record.updatedAt,
173
+ staticContextChanged: record.staticContextChanged,
174
+ ...(record.staticContextChanged ? { staticContext: record.staticContext ?? null } : {}),
175
+ }));
176
+ }
9
177
  function sessionFileName(sessionId) {
10
- return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
178
+ return `${sha256(sessionId)}.json`;
11
179
  }
12
180
  function sessionPath(sessionStoreDir, sessionId) {
13
181
  return join(sessionStoreDir, sessionFileName(sessionId));
14
182
  }
15
- function walPath(sessionStoreDir, sessionId) {
16
- return `${sessionPath(sessionStoreDir, sessionId)}.wal`;
183
+ function snapshotPath(sessionStoreDir, sessionId, generation) {
184
+ const basePath = sessionPath(sessionStoreDir, sessionId);
185
+ return generation === 0 ? basePath : `${basePath}.snapshot.${generation % 2}`;
186
+ }
187
+ function walPath(sessionStoreDir, sessionId, generation) {
188
+ const basePath = sessionPath(sessionStoreDir, sessionId);
189
+ return generation === 0 ? `${basePath}.wal` : `${basePath}.wal.${generation % 2}`;
190
+ }
191
+ function headPath(sessionStoreDir, sessionId, sequence) {
192
+ return `${sessionPath(sessionStoreDir, sessionId)}.head.${sequence % 2}`;
17
193
  }
18
194
  function persistedSessionKey(sessionStoreDir, sessionId) {
19
195
  return `${sessionStoreDir}\0${sessionId}`;
@@ -41,6 +217,97 @@ function replaceFileSync(sourcePath, targetPath) {
41
217
  }
42
218
  }
43
219
  }
220
+ function writeAllSync(handle, encoded) {
221
+ let offset = 0;
222
+ while (offset < encoded.byteLength) {
223
+ const written = writeSync(handle, encoded, offset, encoded.byteLength - offset);
224
+ if (written === 0) {
225
+ throw new Error("Filesystem write made no progress");
226
+ }
227
+ offset += written;
228
+ }
229
+ }
230
+ function syncDirectorySync(path) {
231
+ if (process.platform === "win32")
232
+ return;
233
+ const handle = openSync(path, "r");
234
+ try {
235
+ fsyncSync(handle);
236
+ }
237
+ finally {
238
+ closeSync(handle);
239
+ }
240
+ }
241
+ function ensureDurableDirectorySync(path) {
242
+ if (existsSync(path))
243
+ return;
244
+ const missingDirectories = [];
245
+ let current = path;
246
+ while (!existsSync(current)) {
247
+ missingDirectories.push(current);
248
+ const parent = dirname(current);
249
+ if (parent === current)
250
+ break;
251
+ current = parent;
252
+ }
253
+ mkdirSync(path, { recursive: true });
254
+ for (const directory of missingDirectories.reverse()) {
255
+ syncDirectorySync(dirname(directory));
256
+ }
257
+ }
258
+ function writeDurableFileSync(path, encoded) {
259
+ const handle = openSync(path, "wx", 0o600);
260
+ try {
261
+ writeAllSync(handle, encoded);
262
+ fsyncSync(handle);
263
+ }
264
+ finally {
265
+ closeSync(handle);
266
+ }
267
+ }
268
+ function replaceDurableFileSync(path, encoded) {
269
+ const tempPath = `${path}.${randomUUID()}.tmp`;
270
+ try {
271
+ writeDurableFileSync(tempPath, encoded);
272
+ replaceFileSync(tempPath, path);
273
+ syncDirectorySync(dirname(path));
274
+ }
275
+ catch (error) {
276
+ rmSync(tempPath, { force: true });
277
+ throw error;
278
+ }
279
+ }
280
+ function appendDurableFileSync(path, encoded) {
281
+ const existed = existsSync(path);
282
+ const handle = openSync(path, "a", 0o600);
283
+ try {
284
+ writeAllSync(handle, encoded);
285
+ fsyncSync(handle);
286
+ }
287
+ finally {
288
+ closeSync(handle);
289
+ }
290
+ if (!existed)
291
+ syncDirectorySync(dirname(path));
292
+ }
293
+ function truncateDurableFileSync(path, length) {
294
+ truncateSync(path, length);
295
+ const handle = openSync(path, "r+");
296
+ try {
297
+ fsyncSync(handle);
298
+ }
299
+ finally {
300
+ closeSync(handle);
301
+ }
302
+ }
303
+ function performOrDeferRecovery(action, recoveryActions) {
304
+ if (recoveryActions) {
305
+ recoveryActions.push(action);
306
+ }
307
+ else {
308
+ action();
309
+ }
310
+ }
44
311
  function assertRecord(value, path) {
45
312
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
46
313
  throw new Error(`${path} must be an object`);
@@ -51,9 +318,19 @@ function assertString(value, path) {
51
318
  throw new Error(`${path} must be a string`);
52
319
  }
53
320
  }
54
- function assertNumber(value, path) {
55
- if (typeof value !== "number" || !Number.isFinite(value)) {
56
- throw new Error(`${path} must be a finite number`);
321
+ function assertBoolean(value, path) {
322
+ if (typeof value !== "boolean") {
323
+ throw new Error(`${path} must be a boolean`);
324
+ }
325
+ }
326
+ function assertDigest(value, path) {
327
+ if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
328
+ throw new Error(`${path} must be a 64-character lowercase hex digest`);
329
+ }
330
+ }
331
+ function assertNonNegativeSafeInteger(value, path) {
332
+ if (!Number.isSafeInteger(value) || value < 0) {
333
+ throw new Error(`${path} must be a non-negative safe integer`);
57
334
  }
58
335
  }
59
336
  function assertNullableString(value, path) {
@@ -61,199 +338,938 @@ function assertNullableString(value, path) {
61
338
  throw new Error(`${path} must be a string or null`);
62
339
  }
63
340
  }
64
- function assertStaticContext(value) {
341
+ function assertStaticContext(value, path = "session.staticContext") {
65
342
  if (value === undefined)
66
343
  return;
67
- assertRecord(value, "session.staticContext");
344
+ assertRecord(value, path);
68
345
  const systemPrompt = value.systemPrompt;
69
346
  if (systemPrompt !== undefined && typeof systemPrompt !== "string") {
70
- throw new Error("session.staticContext.systemPrompt must be a string");
347
+ throw new Error(`${path}.systemPrompt must be a string`);
71
348
  }
72
349
  const tools = value.tools;
73
350
  if (tools !== undefined) {
74
351
  if (!Array.isArray(tools)) {
75
- throw new Error("session.staticContext.tools must be an array");
352
+ throw new Error(`${path}.tools must be an array`);
76
353
  }
77
354
  for (const [index, tool] of tools.entries()) {
78
- assertRecord(tool, `session.staticContext.tools[${index}]`);
79
- assertString(tool.name, `session.staticContext.tools[${index}].name`);
80
- assertString(tool.description, `session.staticContext.tools[${index}].description`);
355
+ assertRecord(tool, `${path}.tools[${index}]`);
356
+ assertString(tool.name, `${path}.tools[${index}].name`);
357
+ assertString(tool.description, `${path}.tools[${index}].description`);
81
358
  if (tool.parameters === undefined) {
82
- throw new Error(`session.staticContext.tools[${index}].parameters is required`);
359
+ throw new Error(`${path}.tools[${index}].parameters is required`);
83
360
  }
84
361
  }
85
362
  }
86
363
  }
87
- function assertSessionTreeEntries(value) {
364
+ function assertSessionTreeEntries(value, path = "session.entries") {
88
365
  if (!Array.isArray(value)) {
89
- throw new Error("session.entries must be an array");
366
+ throw new Error(`${path} must be an array`);
90
367
  }
91
368
  for (const [index, entry] of value.entries()) {
92
- assertRecord(entry, `session.entries[${index}]`);
93
- assertString(entry.type, `session.entries[${index}].type`);
94
- assertString(entry.id, `session.entries[${index}].id`);
95
- assertNullableString(entry.parentId, `session.entries[${index}].parentId`);
96
- assertString(entry.timestamp, `session.entries[${index}].timestamp`);
369
+ assertRecord(entry, `${path}[${index}]`);
370
+ assertString(entry.type, `${path}[${index}].type`);
371
+ assertString(entry.id, `${path}[${index}].id`);
372
+ assertNullableString(entry.parentId, `${path}[${index}].parentId`);
373
+ assertString(entry.timestamp, `${path}[${index}].timestamp`);
97
374
  if (entry.type === "message") {
98
- assertRecord(entry.message, `session.entries[${index}].message`);
375
+ assertRecord(entry.message, `${path}[${index}].message`);
99
376
  }
100
377
  }
101
378
  }
102
- function assertPersistedSessionState(value) {
103
- assertRecord(value, "session");
104
- assertString(value.sessionId, "session.sessionId");
105
- assertStaticContext(value.staticContext);
106
- assertSessionTreeEntries(value.entries);
107
- assertNullableString(value.leafId, "session.leafId");
108
- assertNumber(value.revision, "session.revision");
109
- assertNumber(value.createdAt, "session.createdAt");
110
- assertNumber(value.updatedAt, "session.updatedAt");
111
- }
112
- function assertPersistedWalRecord(value, sourcePath, lineNumber) {
113
- const path = `${sourcePath}:${lineNumber}`;
379
+ function assertPersistedSessionState(value, path = "session") {
114
380
  assertRecord(value, path);
115
- if (value.version !== PERSISTED_SESSION_VERSION) {
116
- throw new Error(`Unsupported persisted session WAL version in ${path}`);
117
- }
118
381
  assertString(value.sessionId, `${path}.sessionId`);
119
- assertNumber(value.baseEntryCount, `${path}.baseEntryCount`);
120
- assertSessionTreeEntries(value.entries);
382
+ assertStaticContext(value.staticContext, `${path}.staticContext`);
383
+ assertSessionTreeEntries(value.entries, `${path}.entries`);
121
384
  assertNullableString(value.leafId, `${path}.leafId`);
122
- assertNumber(value.revision, `${path}.revision`);
123
- assertNumber(value.updatedAt, `${path}.updatedAt`);
124
- assertStaticContext(value.staticContext);
385
+ assertNonNegativeSafeInteger(value.revision, `${path}.revision`);
386
+ assertNonNegativeSafeInteger(value.createdAt, `${path}.createdAt`);
387
+ assertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);
125
388
  }
126
- function parsePersistedSessionFile(raw, sourcePath) {
127
- let parsed;
389
+ function parseJson(raw, message) {
128
390
  try {
129
- parsed = JSON.parse(raw);
391
+ return JSON.parse(raw);
130
392
  }
131
393
  catch (error) {
132
- throw new Error(`Persisted session file contains invalid JSON: ${sourcePath}`, { cause: error });
394
+ throw new Error(message, { cause: error });
133
395
  }
396
+ }
397
+ function parsePersistedSessionFile(raw, sourcePath) {
398
+ const parsed = parseJson(raw, `Persisted session file contains invalid JSON: ${sourcePath}`);
134
399
  assertRecord(parsed, "persisted session file");
135
- if (parsed.version !== PERSISTED_SESSION_VERSION) {
400
+ if (parsed.version !== LEGACY_SESSION_VERSION && parsed.version !== DURABLE_SESSION_VERSION) {
136
401
  throw new Error(`Unsupported persisted session version in ${sourcePath}`);
137
402
  }
138
403
  assertPersistedSessionState(parsed.session);
404
+ if (parsed.version === LEGACY_SESSION_VERSION) {
405
+ return { version: LEGACY_SESSION_VERSION, session: parsed.session };
406
+ }
407
+ assertNonNegativeSafeInteger(parsed.generation, `${sourcePath}.generation`);
408
+ assertDigest(parsed.stateDigest, `${sourcePath}.stateDigest`);
409
+ const actualStateDigest = stateDigest(parsed.session);
410
+ if (parsed.stateDigest !== actualStateDigest) {
411
+ throw new Error(`Persisted session snapshot state digest mismatch: ${sourcePath}`);
412
+ }
139
413
  return {
140
- version: PERSISTED_SESSION_VERSION,
414
+ version: DURABLE_SESSION_VERSION,
415
+ generation: parsed.generation,
416
+ stateDigest: parsed.stateDigest,
141
417
  session: parsed.session,
142
418
  };
143
419
  }
144
- function parseWalLine(raw, sourcePath, lineNumber) {
145
- let parsed;
146
- try {
147
- parsed = JSON.parse(raw);
420
+ function assertLegacyWalRecord(value, sourcePath, lineNumber) {
421
+ const path = `${sourcePath}:${lineNumber}`;
422
+ assertRecord(value, path);
423
+ if (value.version !== 1 && value.version !== 2 && value.version !== 3) {
424
+ throw new Error(`Unsupported persisted session WAL version in ${path}`);
148
425
  }
149
- catch (error) {
150
- throw new Error(`Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`, { cause: error });
426
+ assertString(value.sessionId, `${path}.sessionId`);
427
+ assertNonNegativeSafeInteger(value.baseEntryCount, `${path}.baseEntryCount`);
428
+ assertSessionTreeEntries(value.entries, `${path}.entries`);
429
+ assertNullableString(value.leafId, `${path}.leafId`);
430
+ assertNonNegativeSafeInteger(value.revision, `${path}.revision`);
431
+ assertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);
432
+ if (value.version === 1 || Object.hasOwn(value, "staticContext")) {
433
+ assertStaticContext(value.staticContext, `${path}.staticContext`);
434
+ }
435
+ }
436
+ function parseLegacyWalLine(raw, sourcePath, lineNumber) {
437
+ const parsed = parseJson(raw, `Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`);
438
+ assertRecord(parsed, `${sourcePath}:${lineNumber}`);
439
+ if (parsed.version !== 3 || typeof parsed.payload !== "string" || typeof parsed.sha256 !== "string") {
440
+ assertLegacyWalRecord(parsed, sourcePath, lineNumber);
441
+ return parsed;
442
+ }
443
+ assertDigest(parsed.sha256, `${sourcePath}:${lineNumber}.sha256`);
444
+ if (sha256(parsed.payload) !== parsed.sha256) {
445
+ throw new Error(`Persisted session WAL checksum mismatch: ${sourcePath}:${lineNumber}`);
446
+ }
447
+ const payload = parseJson(parsed.payload, `Persisted session WAL payload contains invalid JSON: ${sourcePath}:${lineNumber}`);
448
+ assertLegacyWalRecord(payload, sourcePath, lineNumber);
449
+ if (payload.version !== 3) {
450
+ throw new Error(`Persisted session WAL envelope payload version mismatch: ${sourcePath}:${lineNumber}`);
151
451
  }
152
- assertPersistedWalRecord(parsed, sourcePath, lineNumber);
153
- return parsed;
452
+ return payload;
154
453
  }
155
- function applyWalRecord(session, record) {
454
+ function assertDurableWalRecord(value, sourcePath, lineNumber) {
455
+ const path = `${sourcePath}:${lineNumber}`;
456
+ assertRecord(value, path);
457
+ if (value.version !== LEGACY_DURABLE_WAL_VERSION && value.version !== DURABLE_WAL_VERSION) {
458
+ throw new Error(`Unsupported durable session WAL version in ${path}`);
459
+ }
460
+ assertString(value.sessionId, `${path}.sessionId`);
461
+ assertNonNegativeSafeInteger(value.generation, `${path}.generation`);
462
+ assertNonNegativeSafeInteger(value.sequence, `${path}.sequence`);
463
+ assertNonNegativeSafeInteger(value.baseRevision, `${path}.baseRevision`);
464
+ assertNonNegativeSafeInteger(value.revision, `${path}.revision`);
465
+ assertNonNegativeSafeInteger(value.baseEntryCount, `${path}.baseEntryCount`);
466
+ assertSessionTreeEntries(value.entries, `${path}.entries`);
467
+ assertNullableString(value.leafId, `${path}.leafId`);
468
+ assertNonNegativeSafeInteger(value.updatedAt, `${path}.updatedAt`);
469
+ assertDigest(value.previousStateDigest, `${path}.previousStateDigest`);
470
+ assertDigest(value.stateDigest, `${path}.stateDigest`);
471
+ assertDigest(value.previousWalDigest, `${path}.previousWalDigest`);
472
+ assertBoolean(value.staticContextChanged, `${path}.staticContextChanged`);
473
+ if (value.staticContextChanged) {
474
+ if (!Object.hasOwn(value, "staticContext")) {
475
+ throw new Error(`${path}.staticContext is required when staticContextChanged is true`);
476
+ }
477
+ if (value.staticContext !== null) {
478
+ assertStaticContext(value.staticContext, `${path}.staticContext`);
479
+ }
480
+ }
481
+ else if (Object.hasOwn(value, "staticContext")) {
482
+ throw new Error(`${path}.staticContext must be omitted when staticContextChanged is false`);
483
+ }
484
+ }
485
+ function parseDurableWalLine(raw, sourcePath, lineNumber) {
486
+ const parsed = parseJson(raw, `Persisted session WAL contains invalid JSON: ${sourcePath}:${lineNumber}`);
487
+ assertRecord(parsed, `${sourcePath}:${lineNumber}`);
488
+ if (parsed.version !== LEGACY_DURABLE_WAL_VERSION && parsed.version !== DURABLE_WAL_VERSION) {
489
+ throw new Error(`Expected durable session WAL envelope in ${sourcePath}:${lineNumber}`);
490
+ }
491
+ assertString(parsed.payload, `${sourcePath}:${lineNumber}.payload`);
492
+ assertDigest(parsed.sha256, `${sourcePath}:${lineNumber}.sha256`);
493
+ const digest = sha256(parsed.payload);
494
+ if (digest !== parsed.sha256) {
495
+ throw new Error(`Persisted session WAL checksum mismatch: ${sourcePath}:${lineNumber}`);
496
+ }
497
+ const payload = parseJson(parsed.payload, `Persisted session WAL payload contains invalid JSON: ${sourcePath}:${lineNumber}`);
498
+ assertDurableWalRecord(payload, sourcePath, lineNumber);
499
+ if (payload.version !== parsed.version) {
500
+ throw new Error(`Persisted session WAL envelope payload version mismatch: ${sourcePath}:${lineNumber}`);
501
+ }
502
+ return { record: payload, digest };
503
+ }
504
+ function assertHeadRecord(value, sourcePath) {
505
+ assertRecord(value, sourcePath);
506
+ if (value.version !== HEAD_VERSION) {
507
+ throw new Error(`Unsupported persisted session head version in ${sourcePath}`);
508
+ }
509
+ assertString(value.sessionId, `${sourcePath}.sessionId`);
510
+ assertNonNegativeSafeInteger(value.sequence, `${sourcePath}.sequence`);
511
+ assertNonNegativeSafeInteger(value.generation, `${sourcePath}.generation`);
512
+ assertDigest(value.previousHeadDigest, `${sourcePath}.previousHeadDigest`);
513
+ assertDigest(value.snapshotSha256, `${sourcePath}.snapshotSha256`);
514
+ assertDigest(value.snapshotStateDigest, `${sourcePath}.snapshotStateDigest`);
515
+ assertNonNegativeSafeInteger(value.snapshotRevision, `${sourcePath}.snapshotRevision`);
516
+ assertNonNegativeSafeInteger(value.snapshotEntryCount, `${sourcePath}.snapshotEntryCount`);
517
+ assertNonNegativeSafeInteger(value.walRecordCount, `${sourcePath}.walRecordCount`);
518
+ assertNonNegativeSafeInteger(value.walByteLength, `${sourcePath}.walByteLength`);
519
+ assertDigest(value.walTailDigest, `${sourcePath}.walTailDigest`);
520
+ assertDigest(value.stateDigest, `${sourcePath}.stateDigest`);
521
+ assertNonNegativeSafeInteger(value.revision, `${sourcePath}.revision`);
522
+ assertNonNegativeSafeInteger(value.entryCount, `${sourcePath}.entryCount`);
523
+ }
524
+ function parseHeadFile(path, slot, sessionId) {
525
+ const parsed = parseJson(readBoundedArtifactSync(path, "head", sessionId).toString("utf-8"), `Persisted session head contains invalid JSON: ${path}`);
526
+ assertRecord(parsed, path);
527
+ if (parsed.version !== HEAD_VERSION) {
528
+ throw new Error(`Unsupported persisted session head envelope version in ${path}`);
529
+ }
530
+ assertString(parsed.payload, `${path}.payload`);
531
+ assertDigest(parsed.sha256, `${path}.sha256`);
532
+ const digest = sha256(parsed.payload);
533
+ if (digest !== parsed.sha256) {
534
+ throw new Error(`Persisted session head checksum mismatch: ${path}`);
535
+ }
536
+ const payload = parseJson(parsed.payload, `Persisted session head payload contains invalid JSON: ${path}`);
537
+ assertHeadRecord(payload, path);
538
+ if (payload.sequence % 2 !== slot) {
539
+ throw new Error(`Persisted session head is stored in the wrong slot: ${path}`);
540
+ }
541
+ return { record: payload, digest };
542
+ }
543
+ function readLatestHead(sessionStoreDir, sessionId) {
544
+ const heads = [];
545
+ for (const slot of [0, 1]) {
546
+ const path = headPath(sessionStoreDir, sessionId, slot);
547
+ if (existsSync(path))
548
+ heads.push(parseHeadFile(path, slot, sessionId));
549
+ }
550
+ if (heads.length === 0)
551
+ return undefined;
552
+ heads.sort((left, right) => left.record.sequence - right.record.sequence);
553
+ if (heads.length === 1) {
554
+ const only = heads[0];
555
+ if (only.record.sequence !== 0 || only.record.previousHeadDigest !== ZERO_DIGEST) {
556
+ throw new Error(`Persisted session head chain is incomplete for ${sessionId}`);
557
+ }
558
+ return only;
559
+ }
560
+ const [previous, latest] = heads;
561
+ if (latest.record.sequence !== previous.record.sequence + 1) {
562
+ throw new Error(`Persisted session head sequence is not contiguous for ${sessionId}`);
563
+ }
564
+ if (latest.record.previousHeadDigest !== previous.digest) {
565
+ throw new Error(`Persisted session head digest chain mismatch for ${sessionId}`);
566
+ }
567
+ if (latest.record.sessionId !== previous.record.sessionId) {
568
+ throw new Error(`Persisted session head sessionId chain mismatch for ${sessionId}`);
569
+ }
570
+ return latest;
571
+ }
572
+ function applyLegacyWalRecord(session, record, capacity, sourcePath) {
156
573
  if (record.sessionId !== session.sessionId) {
157
574
  throw new Error(`Persisted session WAL sessionId does not match snapshot: ${record.sessionId}`);
158
575
  }
159
576
  if (record.baseEntryCount < session.entries.length) {
160
- if (record.baseEntryCount + record.entries.length <= session.entries.length)
577
+ if (record.baseEntryCount + record.entries.length <= session.entries.length) {
578
+ const existingEntries = session.entries.slice(record.baseEntryCount, record.baseEntryCount + record.entries.length);
579
+ if (!isDeepStrictEqual(existingEntries, record.entries)) {
580
+ throw new Error(`Persisted session WAL diverges from snapshot for ${record.sessionId}`);
581
+ }
161
582
  return;
583
+ }
162
584
  throw new Error(`Persisted session WAL overlaps snapshot for ${record.sessionId}`);
163
585
  }
164
586
  if (record.baseEntryCount !== session.entries.length) {
165
587
  throw new Error(`Persisted session WAL has a gap for ${record.sessionId}`);
166
588
  }
167
- session.entries.push(...record.entries.map((entry) => ({ ...entry })));
589
+ if (record.revision < session.revision) {
590
+ throw new Error(`Persisted session WAL revision regresses for ${record.sessionId}`);
591
+ }
592
+ if (record.updatedAt < session.updatedAt) {
593
+ throw new Error(`Persisted session WAL updatedAt regresses for ${record.sessionId}`);
594
+ }
595
+ const nextCapacity = nextWalCapacity(session, record, capacity);
596
+ assertStagedSessionCapacity(session.sessionId, sourcePath, capacity, nextCapacity);
597
+ session.entries = session.entries.concat(structuredClone(record.entries));
168
598
  session.leafId = record.leafId;
169
599
  session.revision = record.revision;
170
600
  session.updatedAt = record.updatedAt;
171
- session.staticContext = record.staticContext;
601
+ if (record.version === 1 || Object.hasOwn(record, "staticContext")) {
602
+ session.staticContext = record.staticContext === undefined ? undefined : structuredClone(record.staticContext);
603
+ }
604
+ capacity.entryCount = nextCapacity.entryCount;
605
+ capacity.logicalBytes = nextCapacity.logicalBytes;
606
+ }
607
+ function readCommittedLegacyWal(path, sessionId, recoveryActions) {
608
+ const encoded = readBoundedArtifactSync(path, "wal", sessionId);
609
+ const lastNewline = encoded.lastIndexOf(0x0a);
610
+ const committedBytes = lastNewline + 1;
611
+ if (committedBytes !== encoded.byteLength) {
612
+ performOrDeferRecovery(() => truncateDurableFileSync(path, committedBytes), recoveryActions);
613
+ }
614
+ const lines = encoded.subarray(0, committedBytes).toString("utf-8").split("\n").filter(Boolean);
615
+ return { encoded: encoded.subarray(0, committedBytes), lines };
172
616
  }
173
- function applyPersistedWal(sessionStoreDir, session) {
174
- const filePath = walPath(sessionStoreDir, session.sessionId);
175
- if (!existsSync(filePath))
617
+ function applyLegacyWal(sessionStoreDir, session, capacity, recoveryActions) {
618
+ const path = walPath(sessionStoreDir, session.sessionId, 0);
619
+ if (!existsSync(path))
176
620
  return 0;
177
- const lines = readFileSync(filePath, "utf-8").split("\n");
178
- let applied = 0;
621
+ const { lines } = readCommittedLegacyWal(path, session.sessionId, recoveryActions);
622
+ for (const [index, line] of lines.entries()) {
623
+ applyLegacyWalRecord(session, parseLegacyWalLine(line, path, index + 1), capacity, path);
624
+ }
625
+ return lines.length;
626
+ }
627
+ function applyDurableWalRecord(session, record, recordDigest, expectedGeneration, expectedSequence, expectedStateDigest, expectedWalDigest, capacity, sourcePath) {
628
+ if (record.sessionId !== session.sessionId) {
629
+ throw new Error(`Persisted session WAL sessionId does not match snapshot: ${record.sessionId}`);
630
+ }
631
+ if (record.generation !== expectedGeneration) {
632
+ throw new Error(`Persisted session WAL generation mismatch for ${record.sessionId}`);
633
+ }
634
+ if (record.sequence !== expectedSequence) {
635
+ throw new Error(`Persisted session WAL sequence mismatch for ${record.sessionId}`);
636
+ }
637
+ if (record.baseRevision !== session.revision || record.revision <= record.baseRevision) {
638
+ throw new Error(`Persisted session WAL revision chain mismatch for ${record.sessionId}`);
639
+ }
640
+ if (record.baseEntryCount !== session.entries.length) {
641
+ throw new Error(`Persisted session WAL entry chain mismatch for ${record.sessionId}`);
642
+ }
643
+ if (record.previousStateDigest !== expectedStateDigest) {
644
+ throw new Error(`Persisted session WAL previous state digest mismatch for ${record.sessionId}`);
645
+ }
646
+ if (record.previousWalDigest !== expectedWalDigest) {
647
+ throw new Error(`Persisted session WAL digest chain mismatch for ${record.sessionId}`);
648
+ }
649
+ if (record.updatedAt < session.updatedAt) {
650
+ throw new Error(`Persisted session WAL updatedAt regresses for ${record.sessionId}`);
651
+ }
652
+ const nextCapacity = nextWalCapacity(session, record, capacity);
653
+ assertStagedSessionCapacity(session.sessionId, sourcePath, capacity, nextCapacity);
654
+ session.entries = session.entries.concat(structuredClone(record.entries));
655
+ session.leafId = record.leafId;
656
+ session.revision = record.revision;
657
+ session.updatedAt = record.updatedAt;
658
+ if (record.staticContextChanged) {
659
+ session.staticContext =
660
+ record.staticContext === null || record.staticContext === undefined
661
+ ? undefined
662
+ : structuredClone(record.staticContext);
663
+ }
664
+ const actualStateDigest = record.version === LEGACY_DURABLE_WAL_VERSION ? stateDigest(session) : walTransitionStateDigest(record);
665
+ if (record.stateDigest !== actualStateDigest) {
666
+ throw new Error(`Persisted session WAL post state digest mismatch for ${record.sessionId}`);
667
+ }
668
+ capacity.entryCount = nextCapacity.entryCount;
669
+ capacity.logicalBytes = nextCapacity.logicalBytes;
670
+ return { stateDigest: actualStateDigest, walDigest: recordDigest };
671
+ }
672
+ function readDurableWal(sessionStoreDir, session, head, capacity, recoveryActions) {
673
+ const path = walPath(sessionStoreDir, session.sessionId, head.generation);
674
+ if (!existsSync(path)) {
675
+ throw new Error(`Persisted session WAL is missing: ${path}`);
676
+ }
677
+ const encoded = readBoundedArtifactSync(path, "wal", session.sessionId);
678
+ if (encoded.byteLength < head.walByteLength) {
679
+ throw new Error(`Persisted session WAL is shorter than its durable head: ${path}`);
680
+ }
681
+ if (encoded.byteLength > head.walByteLength) {
682
+ performOrDeferRecovery(() => truncateDurableFileSync(path, head.walByteLength), recoveryActions);
683
+ }
684
+ const committed = encoded.subarray(0, head.walByteLength);
685
+ if (committed.byteLength > 0 && committed.at(-1) !== 0x0a) {
686
+ throw new Error(`Persisted session WAL durable head does not end at a record boundary: ${path}`);
687
+ }
688
+ const lines = committed.toString("utf-8").split("\n").filter(Boolean);
689
+ if (lines.length !== head.walRecordCount) {
690
+ throw new Error(`Persisted session WAL record count does not match its durable head: ${path}`);
691
+ }
692
+ let expectedStateDigest = head.snapshotStateDigest;
693
+ let expectedWalDigest = head.snapshotSha256;
179
694
  for (const [index, line] of lines.entries()) {
180
- if (!line)
695
+ const parsed = parseDurableWalLine(line, path, index + 1);
696
+ const applied = applyDurableWalRecord(session, parsed.record, parsed.digest, head.generation, index, expectedStateDigest, expectedWalDigest, capacity, path);
697
+ expectedStateDigest = applied.stateDigest;
698
+ expectedWalDigest = applied.walDigest;
699
+ }
700
+ if (expectedStateDigest !== head.stateDigest ||
701
+ expectedWalDigest !== head.walTailDigest ||
702
+ session.revision !== head.revision ||
703
+ session.entries.length !== head.entryCount) {
704
+ throw new Error(`Persisted session WAL final state does not match its durable head: ${path}`);
705
+ }
706
+ }
707
+ function rollbackUncommittedFutureGeneration(sessionStoreDir, sessionId, headGeneration, recoveryActions) {
708
+ const basePath = sessionPath(sessionStoreDir, sessionId);
709
+ let removed = false;
710
+ let deferredRemoval = false;
711
+ for (const slot of [0, 1]) {
712
+ const path = `${basePath}.snapshot.${slot}`;
713
+ if (!existsSync(path))
714
+ continue;
715
+ const parsed = parsePersistedSessionFile(readBoundedArtifactSync(path, "snapshot", sessionId).toString("utf-8"), path);
716
+ if (parsed.version !== DURABLE_SESSION_VERSION) {
717
+ throw new Error(`Persisted session snapshot slot uses a legacy format: ${path}`);
718
+ }
719
+ createStagedSessionCapacity(parsed.session, path);
720
+ if (parsed.generation % 2 !== slot) {
721
+ throw new Error(`Persisted session snapshot is stored in the wrong slot: ${path}`);
722
+ }
723
+ if (parsed.generation <= headGeneration)
181
724
  continue;
182
- applyWalRecord(session, parseWalLine(line, filePath, index + 1));
183
- applied++;
725
+ if (parsed.generation !== headGeneration + 1) {
726
+ throw new Error(`Persisted session snapshot generation is ahead of its durable head: ${path}`);
727
+ }
728
+ performOrDeferRecovery(() => {
729
+ rmSync(path);
730
+ rmSync(`${basePath}.wal.${slot}`, { force: true });
731
+ }, recoveryActions);
732
+ removed = recoveryActions === undefined;
733
+ deferredRemoval = recoveryActions !== undefined;
734
+ }
735
+ if (removed) {
736
+ syncDirectorySync(sessionStoreDir);
737
+ }
738
+ else if (recoveryActions && deferredRemoval) {
739
+ recoveryActions.push(() => syncDirectorySync(sessionStoreDir));
740
+ }
741
+ }
742
+ function loadDurableSession(sessionStoreDir, sessionId, head, recoveryActions) {
743
+ const record = head.record;
744
+ if (record.sessionId !== sessionId) {
745
+ throw new Error(`Persisted session head sessionId does not match snapshot: ${record.sessionId}`);
746
+ }
747
+ rollbackUncommittedFutureGeneration(sessionStoreDir, sessionId, record.generation, recoveryActions);
748
+ const path = snapshotPath(sessionStoreDir, sessionId, record.generation);
749
+ if (!existsSync(path)) {
750
+ throw new Error(`Persisted session snapshot referenced by durable head is missing: ${path}`);
751
+ }
752
+ const encoded = readBoundedArtifactSync(path, "snapshot", sessionId);
753
+ if (sha256(encoded) !== record.snapshotSha256) {
754
+ throw new Error(`Persisted session snapshot checksum does not match its durable head: ${path}`);
755
+ }
756
+ const persisted = parsePersistedSessionFile(encoded.toString("utf-8"), path);
757
+ if (persisted.version !== DURABLE_SESSION_VERSION || persisted.generation !== record.generation) {
758
+ throw new Error(`Persisted session snapshot generation does not match its durable head: ${path}`);
759
+ }
760
+ if (persisted.stateDigest !== record.snapshotStateDigest ||
761
+ persisted.session.revision !== record.snapshotRevision ||
762
+ persisted.session.entries.length !== record.snapshotEntryCount) {
763
+ throw new Error(`Persisted session snapshot state does not match its durable head: ${path}`);
764
+ }
765
+ const capacity = createStagedSessionCapacity(persisted.session, path);
766
+ readDurableWal(sessionStoreDir, persisted.session, record, capacity, recoveryActions);
767
+ return {
768
+ session: persisted.session,
769
+ meta: {
770
+ format: "durable",
771
+ generation: record.generation,
772
+ headSequence: record.sequence,
773
+ headDigest: head.digest,
774
+ snapshotSha256: record.snapshotSha256,
775
+ snapshotStateDigest: record.snapshotStateDigest,
776
+ snapshotRevision: record.snapshotRevision,
777
+ snapshotEntryCount: record.snapshotEntryCount,
778
+ snapshotByteLength: encoded.byteLength,
779
+ entryCount: record.entryCount,
780
+ walRecords: record.walRecordCount,
781
+ walByteLength: record.walByteLength,
782
+ walTailDigest: record.walTailDigest,
783
+ stateDigest: record.stateDigest,
784
+ revision: record.revision,
785
+ },
786
+ };
787
+ }
788
+ function assertAnchoredArtifacts(sessionStoreDir, entries) {
789
+ const names = new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name));
790
+ for (const name of names) {
791
+ const match = SESSION_ARTIFACT_PATTERN.exec(name) ?? LEGACY_WAL_PATTERN.exec(name);
792
+ if (match && !names.has(match[1])) {
793
+ throw new Error(`Persisted session artifact has no anchor snapshot: ${join(sessionStoreDir, name)}`);
794
+ }
795
+ }
796
+ }
797
+ function assertStagedSessionBatchCapacity(stagedSessions) {
798
+ const limits = getSessionCapacityLimits();
799
+ const usage = getSessionCapacityUsage();
800
+ let requestedLoadedSessions = usage.loadedSessions;
801
+ let requestedEntries = usage.entryCount;
802
+ let requestedLogicalBytes = usage.logicalBytes;
803
+ const stagedIds = new Set();
804
+ for (const staged of stagedSessions) {
805
+ if (stagedIds.has(staged.session.sessionId)) {
806
+ throw new Error(`Persisted session ${staged.session.sessionId} is staged more than once`);
807
+ }
808
+ stagedIds.add(staged.session.sessionId);
809
+ const existing = getSession(staged.session.sessionId);
810
+ if (!existing)
811
+ requestedLoadedSessions++;
812
+ requestedEntries += staged.session.entries.length - (existing?.entries.length ?? 0);
813
+ requestedLogicalBytes +=
814
+ calculateSessionLogicalBytes(staged.session.staticContext, staged.session.entries) -
815
+ (existing?.logicalBytes ?? 0);
816
+ if (requestedLoadedSessions > limits.maxLoadedSessions) {
817
+ throw persistenceCapacityError("loaded_sessions", staged.session.sessionId, staged.path, usage.loadedSessions, requestedLoadedSessions, limits.maxLoadedSessions);
818
+ }
819
+ if (requestedEntries > limits.maxAggregateEntries) {
820
+ throw persistenceCapacityError("aggregate_entries", staged.session.sessionId, staged.path, usage.entryCount, requestedEntries, limits.maxAggregateEntries);
821
+ }
822
+ if (requestedLogicalBytes > limits.maxAggregateLogicalBytes) {
823
+ throw persistenceCapacityError("aggregate_logical_bytes", staged.session.sessionId, staged.path, usage.logicalBytes, requestedLogicalBytes, limits.maxAggregateLogicalBytes);
824
+ }
184
825
  }
185
- return applied;
186
826
  }
187
827
  export function loadPersistedSessions(sessionStoreDir) {
188
828
  if (!existsSync(sessionStoreDir))
189
829
  return;
190
830
  const entries = readdirSync(sessionStoreDir, { withFileTypes: true });
831
+ assertAnchoredArtifacts(sessionStoreDir, entries);
832
+ const stagedSessions = [];
833
+ const recoveryActions = [];
191
834
  for (const entry of entries) {
192
- if (!entry.isFile() || !entry.name.endsWith(".json"))
835
+ if (!entry.isFile() || !SESSION_FILE_PATTERN.test(entry.name))
193
836
  continue;
194
- const filePath = join(sessionStoreDir, entry.name);
195
- const persisted = parsePersistedSessionFile(readFileSync(filePath, "utf-8"), filePath);
196
- const expectedFileName = sessionFileName(persisted.session.sessionId);
837
+ const anchorPath = join(sessionStoreDir, entry.name);
838
+ const unresolvedSessionId = `<unresolved:${entry.name}>`;
839
+ const anchor = parsePersistedSessionFile(readBoundedArtifactSync(anchorPath, "snapshot", unresolvedSessionId).toString("utf-8"), anchorPath);
840
+ const expectedFileName = sessionFileName(anchor.session.sessionId);
197
841
  if (entry.name !== expectedFileName) {
198
- throw new Error(`Persisted session file name does not match sessionId: ${filePath}`);
842
+ throw new Error(`Persisted session file name does not match sessionId: ${anchorPath}`);
843
+ }
844
+ const sessionId = anchor.session.sessionId;
845
+ const capacity = createStagedSessionCapacity(anchor.session, anchorPath);
846
+ const head = readLatestHead(sessionStoreDir, sessionId);
847
+ if (head) {
848
+ const loaded = loadDurableSession(sessionStoreDir, sessionId, head, recoveryActions);
849
+ const key = persistedSessionKey(sessionStoreDir, sessionId);
850
+ stagedSessions.push({ session: loaded.session, meta: loaded.meta, key, path: anchorPath });
851
+ continue;
852
+ }
853
+ const basePath = sessionPath(sessionStoreDir, sessionId);
854
+ if (anchor.version === DURABLE_SESSION_VERSION ||
855
+ existsSync(`${basePath}.snapshot.0`) ||
856
+ existsSync(`${basePath}.snapshot.1`) ||
857
+ existsSync(`${basePath}.wal.0`) ||
858
+ existsSync(`${basePath}.wal.1`)) {
859
+ throw new Error(`Persisted session durable artifacts are missing their head for ${sessionId}`);
199
860
  }
200
- const walRecords = applyPersistedWal(sessionStoreDir, persisted.session);
201
- restoreSessionState(persisted.session);
202
- persistedSessions.set(persistedSessionKey(sessionStoreDir, persisted.session.sessionId), {
203
- entryCount: persisted.session.entries.length,
204
- walRecords,
861
+ const walRecords = applyLegacyWal(sessionStoreDir, anchor.session, capacity, recoveryActions);
862
+ const key = persistedSessionKey(sessionStoreDir, sessionId);
863
+ stagedSessions.push({
864
+ session: anchor.session,
865
+ meta: {
866
+ format: "legacy",
867
+ entryCount: anchor.session.entries.length,
868
+ walRecords,
869
+ revision: anchor.session.revision,
870
+ },
871
+ key,
872
+ path: anchorPath,
205
873
  });
206
874
  }
875
+ assertStagedSessionBatchCapacity(stagedSessions);
876
+ for (const staged of stagedSessions) {
877
+ validatePersistedSessionState(staged.session);
878
+ }
879
+ for (const recoveryAction of recoveryActions)
880
+ recoveryAction();
881
+ for (const staged of stagedSessions) {
882
+ restoreSessionState(staged.session);
883
+ }
884
+ for (const staged of stagedSessions) {
885
+ persistedSessions.set(staged.key, staged.meta);
886
+ sessionsNeedingRecovery.delete(staged.key);
887
+ }
207
888
  }
208
- function writeSnapshot(sessionStoreDir, session) {
209
- mkdirSync(sessionStoreDir, { recursive: true });
210
- const filePath = sessionPath(sessionStoreDir, session.sessionId);
211
- const tempPath = `${filePath}.${randomUUID()}.tmp`;
889
+ function encodeSnapshot(session, generation) {
890
+ const state = exportSessionState(session);
891
+ const digest = stateDigest(state);
212
892
  const body = {
213
- version: PERSISTED_SESSION_VERSION,
214
- session: exportSessionState(session),
893
+ version: DURABLE_SESSION_VERSION,
894
+ generation,
895
+ stateDigest: digest,
896
+ session: state,
215
897
  };
216
- try {
217
- writeFileSync(tempPath, JSON.stringify(body), "utf-8");
218
- replaceFileSync(tempPath, filePath);
219
- rmSync(walPath(sessionStoreDir, session.sessionId), { force: true });
898
+ const encoded = Buffer.from(JSON.stringify(body), "utf-8");
899
+ return { encoded, state, stateDigest: digest, snapshotDigest: sha256(encoded) };
900
+ }
901
+ function encodeHead(record) {
902
+ const payload = JSON.stringify(record);
903
+ const envelope = {
904
+ version: HEAD_VERSION,
905
+ payload,
906
+ sha256: sha256(payload),
907
+ };
908
+ return Buffer.from(JSON.stringify(envelope), "utf-8");
909
+ }
910
+ function prepareHeadWrite(sessionStoreDir, sessionId, record) {
911
+ const path = headPath(sessionStoreDir, sessionId, record.sequence);
912
+ const encoded = encodeHead(record);
913
+ assertArtifactWriteSize(path, "head", sessionId, existsSync(path) ? statSync(path).size : 0, encoded.byteLength);
914
+ const latest = readLatestHead(sessionStoreDir, sessionId);
915
+ if (record.sequence === 0) {
916
+ if (latest) {
917
+ throw new Error(`Persisted session already has a durable head for ${sessionId}`);
918
+ }
220
919
  }
221
- catch (error) {
222
- rmSync(tempPath, { force: true });
223
- throw error;
920
+ else if (!latest ||
921
+ latest.record.sequence !== record.sequence - 1 ||
922
+ latest.digest !== record.previousHeadDigest) {
923
+ throw new Error(`Persisted session head changed before commit for ${sessionId}`);
224
924
  }
925
+ if (existsSync(path)) {
926
+ const existing = parseHeadFile(path, record.sequence % 2, sessionId);
927
+ if (record.sequence < 2 || existing.record.sequence !== record.sequence - 2) {
928
+ throw new Error(`Persisted session head slot contains an unexpected generation: ${path}`);
929
+ }
930
+ }
931
+ return { path, encoded, digest: sha256(JSON.stringify(record)) };
225
932
  }
226
- function appendWalRecord(sessionStoreDir, session, baseEntryCount) {
227
- const record = {
228
- version: PERSISTED_SESSION_VERSION,
933
+ function writeHeadRecord(sessionStoreDir, sessionId, record) {
934
+ const prepared = prepareHeadWrite(sessionStoreDir, sessionId, record);
935
+ replaceDurableFileSync(prepared.path, prepared.encoded);
936
+ return prepared.digest;
937
+ }
938
+ function assertSnapshotSlotCanBeReused(sessionStoreDir, sessionId, generation) {
939
+ const path = snapshotPath(sessionStoreDir, sessionId, generation);
940
+ const wal = walPath(sessionStoreDir, sessionId, generation);
941
+ if (generation === 0) {
942
+ if (existsSync(path) || existsSync(wal)) {
943
+ throw new Error(`Persisted session initial durable slot already exists for ${sessionId}`);
944
+ }
945
+ return;
946
+ }
947
+ if (!existsSync(path)) {
948
+ if (existsSync(wal)) {
949
+ throw new Error(`Persisted session WAL slot exists without its snapshot: ${wal}`);
950
+ }
951
+ return;
952
+ }
953
+ const existing = parsePersistedSessionFile(readBoundedArtifactSync(path, "snapshot", sessionId).toString("utf-8"), path);
954
+ if (existing.version !== DURABLE_SESSION_VERSION || existing.generation !== generation - 2) {
955
+ throw new Error(`Persisted session snapshot slot contains an unexpected generation: ${path}`);
956
+ }
957
+ }
958
+ function prepareSnapshotWrite(sessionStoreDir, session, meta) {
959
+ const generation = meta?.format === "durable" ? meta.generation + 1 : meta?.format === "legacy" ? 1 : 0;
960
+ const headSequence = meta?.format === "durable" ? meta.headSequence + 1 : 0;
961
+ const previousHeadDigest = meta?.format === "durable" ? meta.headDigest : ZERO_DIGEST;
962
+ assertSnapshotSlotCanBeReused(sessionStoreDir, session.sessionId, generation);
963
+ const snapshot = encodeSnapshot(session, generation);
964
+ const headRecord = {
965
+ version: HEAD_VERSION,
229
966
  sessionId: session.sessionId,
230
- baseEntryCount,
231
- entries: session.persistenceChange?.kind === "wal" ? session.persistenceChange.entries : [],
232
- leafId: session.leafId,
967
+ sequence: headSequence,
968
+ generation,
969
+ previousHeadDigest,
970
+ snapshotSha256: snapshot.snapshotDigest,
971
+ snapshotStateDigest: snapshot.stateDigest,
972
+ snapshotRevision: snapshot.state.revision,
973
+ snapshotEntryCount: snapshot.state.entries.length,
974
+ walRecordCount: 0,
975
+ walByteLength: 0,
976
+ walTailDigest: snapshot.snapshotDigest,
977
+ stateDigest: snapshot.stateDigest,
978
+ revision: snapshot.state.revision,
979
+ entryCount: snapshot.state.entries.length,
980
+ };
981
+ const nextSnapshotPath = snapshotPath(sessionStoreDir, session.sessionId, generation);
982
+ const nextWalPath = walPath(sessionStoreDir, session.sessionId, generation);
983
+ assertArtifactWriteSize(nextSnapshotPath, "snapshot", session.sessionId, existsSync(nextSnapshotPath) ? statSync(nextSnapshotPath).size : 0, snapshot.encoded.byteLength);
984
+ assertArtifactWriteSize(nextWalPath, "wal", session.sessionId, 0, 0);
985
+ const preparedHead = prepareHeadWrite(sessionStoreDir, session.sessionId, headRecord);
986
+ return {
987
+ generation,
988
+ headSequence,
989
+ snapshot,
990
+ headRecord,
991
+ nextSnapshotPath,
992
+ nextWalPath,
993
+ headArtifactBytes: preparedHead.encoded.byteLength,
994
+ };
995
+ }
996
+ function writeSnapshot(sessionStoreDir, session, meta, faultInjector) {
997
+ ensureDurableDirectorySync(sessionStoreDir);
998
+ const plan = prepareSnapshotWrite(sessionStoreDir, session, meta);
999
+ replaceDurableFileSync(plan.nextSnapshotPath, plan.snapshot.encoded);
1000
+ replaceDurableFileSync(plan.nextWalPath, Buffer.alloc(0));
1001
+ faultInjector?.("snapshot_after_sync_before_head");
1002
+ const headDigest = writeHeadRecord(sessionStoreDir, session.sessionId, plan.headRecord);
1003
+ return {
1004
+ format: "durable",
1005
+ generation: plan.generation,
1006
+ headSequence: plan.headSequence,
1007
+ headDigest,
1008
+ snapshotSha256: plan.snapshot.snapshotDigest,
1009
+ snapshotStateDigest: plan.snapshot.stateDigest,
1010
+ snapshotRevision: plan.snapshot.state.revision,
1011
+ snapshotEntryCount: plan.snapshot.state.entries.length,
1012
+ snapshotByteLength: plan.snapshot.encoded.byteLength,
1013
+ entryCount: plan.snapshot.state.entries.length,
1014
+ walRecords: 0,
1015
+ walByteLength: 0,
1016
+ walTailDigest: plan.snapshot.snapshotDigest,
1017
+ stateDigest: plan.snapshot.stateDigest,
1018
+ revision: plan.snapshot.state.revision,
1019
+ };
1020
+ }
1021
+ function encodeWalRecord(record) {
1022
+ const payload = JSON.stringify(record);
1023
+ const digest = sha256(payload);
1024
+ const envelope = {
1025
+ version: DURABLE_WAL_VERSION,
1026
+ payload,
1027
+ sha256: digest,
1028
+ };
1029
+ return { encoded: Buffer.from(`${JSON.stringify(envelope)}\n`, "utf-8"), digest };
1030
+ }
1031
+ function prepareWalWrite(sessionStoreDir, session, meta) {
1032
+ if (session.persistenceChange?.kind !== "wal") {
1033
+ throw new Error(`Session ${session.sessionId} does not have a WAL persistence change`);
1034
+ }
1035
+ if (session.revision <= meta.revision) {
1036
+ throw new Error(`Session ${session.sessionId} revision did not advance before WAL persistence`);
1037
+ }
1038
+ const path = walPath(sessionStoreDir, session.sessionId, meta.generation);
1039
+ if (!existsSync(path) || statSync(path).size !== meta.walByteLength) {
1040
+ throw new Error(`Session ${session.sessionId} WAL does not match its in-memory durable head`);
1041
+ }
1042
+ const recordWithoutStateDigest = {
1043
+ version: DURABLE_WAL_VERSION,
1044
+ sessionId: session.sessionId,
1045
+ generation: meta.generation,
1046
+ sequence: meta.walRecords,
1047
+ baseRevision: meta.revision,
233
1048
  revision: session.revision,
1049
+ baseEntryCount: meta.entryCount,
1050
+ entries: session.entries.slice(meta.entryCount),
1051
+ leafId: session.leafId,
234
1052
  updatedAt: session.updatedAt,
235
- staticContext: session.staticContext,
1053
+ previousStateDigest: meta.stateDigest,
1054
+ previousWalDigest: meta.walTailDigest,
1055
+ staticContextChanged: session.persistenceChange.staticContextChanged,
1056
+ };
1057
+ if (session.persistenceChange.staticContextChanged) {
1058
+ recordWithoutStateDigest.staticContext = session.staticContext ?? null;
1059
+ }
1060
+ const record = {
1061
+ ...recordWithoutStateDigest,
1062
+ stateDigest: walTransitionStateDigest(recordWithoutStateDigest),
1063
+ };
1064
+ const encodedRecord = encodeWalRecord(record);
1065
+ const headRecord = {
1066
+ version: HEAD_VERSION,
1067
+ sessionId: session.sessionId,
1068
+ sequence: meta.headSequence + 1,
1069
+ generation: meta.generation,
1070
+ previousHeadDigest: meta.headDigest,
1071
+ snapshotSha256: meta.snapshotSha256,
1072
+ snapshotStateDigest: meta.snapshotStateDigest,
1073
+ snapshotRevision: meta.snapshotRevision,
1074
+ snapshotEntryCount: meta.snapshotEntryCount,
1075
+ walRecordCount: meta.walRecords + 1,
1076
+ walByteLength: meta.walByteLength + encodedRecord.encoded.byteLength,
1077
+ walTailDigest: encodedRecord.digest,
1078
+ stateDigest: record.stateDigest,
1079
+ revision: session.revision,
1080
+ entryCount: session.entries.length,
1081
+ };
1082
+ assertArtifactWriteSize(path, "wal", session.sessionId, meta.walByteLength, headRecord.walByteLength);
1083
+ const preparedHead = prepareHeadWrite(sessionStoreDir, session.sessionId, headRecord);
1084
+ return {
1085
+ path,
1086
+ encodedRecord,
1087
+ headRecord,
1088
+ headArtifactBytes: preparedHead.encoded.byteLength,
236
1089
  };
237
- appendFileSync(walPath(sessionStoreDir, session.sessionId), `${JSON.stringify(record)}\n`, "utf-8");
238
1090
  }
239
- export function savePersistedSession(sessionStoreDir, session) {
1091
+ function appendWalRecord(sessionStoreDir, session, meta, faultInjector) {
1092
+ const plan = prepareWalWrite(sessionStoreDir, session, meta);
1093
+ appendDurableFileSync(plan.path, plan.encodedRecord.encoded);
1094
+ faultInjector?.("wal_after_sync_before_head");
1095
+ const headDigest = writeHeadRecord(sessionStoreDir, session.sessionId, plan.headRecord);
1096
+ return {
1097
+ ...meta,
1098
+ headSequence: plan.headRecord.sequence,
1099
+ headDigest,
1100
+ entryCount: plan.headRecord.entryCount,
1101
+ walRecords: plan.headRecord.walRecordCount,
1102
+ walByteLength: plan.headRecord.walByteLength,
1103
+ walTailDigest: plan.headRecord.walTailDigest,
1104
+ stateDigest: plan.headRecord.stateDigest,
1105
+ revision: plan.headRecord.revision,
1106
+ };
1107
+ }
1108
+ function recoverFailedCommit(sessionStoreDir, session, key, meta) {
1109
+ if (!existsSync(sessionStoreDir)) {
1110
+ if (meta) {
1111
+ persistedSessions.set(key, meta);
1112
+ }
1113
+ else {
1114
+ persistedSessions.delete(key);
1115
+ }
1116
+ sessionsNeedingRecovery.delete(key);
1117
+ return;
1118
+ }
1119
+ syncDirectorySync(sessionStoreDir);
1120
+ const head = readLatestHead(sessionStoreDir, session.sessionId);
1121
+ if (!head) {
1122
+ if (meta?.format === "durable") {
1123
+ throw new Error(`Persisted session durable head disappeared during recovery for ${session.sessionId}`);
1124
+ }
1125
+ const uncommittedGeneration = meta?.format === "legacy" ? 1 : 0;
1126
+ rmSync(snapshotPath(sessionStoreDir, session.sessionId, uncommittedGeneration), { force: true });
1127
+ rmSync(walPath(sessionStoreDir, session.sessionId, uncommittedGeneration), { force: true });
1128
+ syncDirectorySync(sessionStoreDir);
1129
+ if (meta) {
1130
+ persistedSessions.set(key, meta);
1131
+ }
1132
+ else {
1133
+ persistedSessions.delete(key);
1134
+ }
1135
+ sessionsNeedingRecovery.delete(key);
1136
+ return;
1137
+ }
1138
+ const loaded = loadDurableSession(sessionStoreDir, session.sessionId, head);
1139
+ persistedSessions.set(key, loaded.meta);
1140
+ const current = exportSessionState(session);
1141
+ if (isDeepStrictEqual(current, loaded.session)) {
1142
+ markSessionPersisted(session);
1143
+ sessionsNeedingRecovery.delete(key);
1144
+ return;
1145
+ }
1146
+ if (session.persistenceChange?.kind === "wal") {
1147
+ if (loaded.session.entries.length > current.entries.length ||
1148
+ !isDeepStrictEqual(current.entries.slice(0, loaded.session.entries.length), loaded.session.entries)) {
1149
+ throw new Error(`Session ${session.sessionId} diverged from its recovered durable tree`);
1150
+ }
1151
+ session.persistenceChange = {
1152
+ kind: "wal",
1153
+ entries: current.entries.slice(loaded.session.entries.length),
1154
+ staticContextChanged: !isDeepStrictEqual(current.staticContext, loaded.session.staticContext),
1155
+ };
1156
+ }
1157
+ sessionsNeedingRecovery.delete(key);
1158
+ }
1159
+ function shouldWriteSnapshot(session, meta) {
1160
+ return (!meta ||
1161
+ meta.format === "legacy" ||
1162
+ session.persistenceChange?.kind === "snapshot" ||
1163
+ // Keep replay proportional to the checkpoint while amortizing full serialization over equivalent growth.
1164
+ (meta.format === "durable" &&
1165
+ (meta.walRecords >= Math.max(MIN_WAL_RECORDS_PER_SNAPSHOT, meta.snapshotEntryCount) ||
1166
+ meta.walByteLength >= Math.max(MIN_WAL_BYTES_PER_SNAPSHOT, meta.snapshotByteLength))));
1167
+ }
1168
+ export function preflightPersistedSession(sessionStoreDir, session) {
1169
+ const key = persistedSessionKey(sessionStoreDir, session.sessionId);
1170
+ if (sessionsNeedingRecovery.has(key)) {
1171
+ throw new Error(`Session ${session.sessionId} requires persistence recovery before it can be preflighted`);
1172
+ }
1173
+ const meta = persistedSessions.get(key);
1174
+ if (meta && !session.persistenceChange)
1175
+ return { strategy: "none" };
1176
+ if (shouldWriteSnapshot(session, meta)) {
1177
+ const plan = prepareSnapshotWrite(sessionStoreDir, session, meta);
1178
+ return {
1179
+ strategy: "snapshot",
1180
+ snapshotArtifactBytes: plan.snapshot.encoded.byteLength,
1181
+ walArtifactBytes: 0,
1182
+ headArtifactBytes: plan.headArtifactBytes,
1183
+ };
1184
+ }
1185
+ if (meta?.format !== "durable") {
1186
+ throw new Error(`Session ${session.sessionId} does not have durable persistence metadata`);
1187
+ }
1188
+ try {
1189
+ const plan = prepareWalWrite(sessionStoreDir, session, meta);
1190
+ return {
1191
+ strategy: "wal",
1192
+ walArtifactBytes: plan.headRecord.walByteLength,
1193
+ headArtifactBytes: plan.headArtifactBytes,
1194
+ };
1195
+ }
1196
+ catch (error) {
1197
+ if (!(error instanceof SessionPersistenceCapacityError) || error.resource !== "wal_artifact_bytes") {
1198
+ throw error;
1199
+ }
1200
+ const plan = prepareSnapshotWrite(sessionStoreDir, session, meta);
1201
+ return {
1202
+ strategy: "snapshot",
1203
+ snapshotArtifactBytes: plan.snapshot.encoded.byteLength,
1204
+ walArtifactBytes: 0,
1205
+ headArtifactBytes: plan.headArtifactBytes,
1206
+ };
1207
+ }
1208
+ }
1209
+ export function savePersistedSession(sessionStoreDir, session, options = {}) {
240
1210
  const key = persistedSessionKey(sessionStoreDir, session.sessionId);
1211
+ if (sessionsNeedingRecovery.has(key)) {
1212
+ recoverFailedCommit(sessionStoreDir, session, key, persistedSessions.get(key));
1213
+ }
241
1214
  const meta = persistedSessions.get(key);
242
1215
  if (meta && !session.persistenceChange)
243
1216
  return;
244
- if (!meta || session.persistenceChange?.kind === "snapshot" || meta.walRecords >= WAL_SNAPSHOT_INTERVAL) {
245
- writeSnapshot(sessionStoreDir, session);
246
- persistedSessions.set(key, { entryCount: session.entries.length, walRecords: 0 });
247
- markSessionPersisted(session);
1217
+ if (shouldWriteSnapshot(session, meta)) {
1218
+ try {
1219
+ const nextMeta = writeSnapshot(sessionStoreDir, session, meta, options.faultInjector);
1220
+ persistedSessions.set(key, nextMeta);
1221
+ markSessionPersisted(session);
1222
+ }
1223
+ catch (error) {
1224
+ if (!(error instanceof SessionPersistenceCapacityError))
1225
+ sessionsNeedingRecovery.add(key);
1226
+ throw error;
1227
+ }
248
1228
  return;
249
1229
  }
250
- appendWalRecord(sessionStoreDir, session, meta.entryCount);
251
- persistedSessions.set(key, { entryCount: session.entries.length, walRecords: meta.walRecords + 1 });
252
- markSessionPersisted(session);
1230
+ if (meta?.format !== "durable") {
1231
+ throw new Error(`Session ${session.sessionId} does not have durable persistence metadata`);
1232
+ }
1233
+ try {
1234
+ const nextMeta = appendWalRecord(sessionStoreDir, session, meta, options.faultInjector);
1235
+ persistedSessions.set(key, nextMeta);
1236
+ markSessionPersisted(session);
1237
+ }
1238
+ catch (error) {
1239
+ if (error instanceof SessionPersistenceCapacityError && error.resource === "wal_artifact_bytes") {
1240
+ try {
1241
+ const nextMeta = writeSnapshot(sessionStoreDir, session, meta, options.faultInjector);
1242
+ persistedSessions.set(key, nextMeta);
1243
+ markSessionPersisted(session);
1244
+ return;
1245
+ }
1246
+ catch (snapshotError) {
1247
+ if (!(snapshotError instanceof SessionPersistenceCapacityError))
1248
+ sessionsNeedingRecovery.add(key);
1249
+ throw snapshotError;
1250
+ }
1251
+ }
1252
+ sessionsNeedingRecovery.add(key);
1253
+ throw error;
1254
+ }
253
1255
  }
254
1256
  export function deletePersistedSession(sessionStoreDir, sessionId) {
255
- rmSync(sessionPath(sessionStoreDir, sessionId), { force: true });
256
- rmSync(walPath(sessionStoreDir, sessionId), { force: true });
257
- persistedSessions.delete(persistedSessionKey(sessionStoreDir, sessionId));
1257
+ const paths = [
1258
+ `${sessionPath(sessionStoreDir, sessionId)}.head.0`,
1259
+ `${sessionPath(sessionStoreDir, sessionId)}.head.1`,
1260
+ `${sessionPath(sessionStoreDir, sessionId)}.snapshot.0`,
1261
+ `${sessionPath(sessionStoreDir, sessionId)}.snapshot.1`,
1262
+ `${sessionPath(sessionStoreDir, sessionId)}.wal.0`,
1263
+ `${sessionPath(sessionStoreDir, sessionId)}.wal.1`,
1264
+ `${sessionPath(sessionStoreDir, sessionId)}.wal`,
1265
+ sessionPath(sessionStoreDir, sessionId),
1266
+ ];
1267
+ for (const path of paths)
1268
+ rmSync(path, { force: true });
1269
+ if (existsSync(sessionStoreDir))
1270
+ syncDirectorySync(sessionStoreDir);
1271
+ const key = persistedSessionKey(sessionStoreDir, sessionId);
1272
+ persistedSessions.delete(key);
1273
+ sessionsNeedingRecovery.delete(key);
258
1274
  }
259
1275
  //# sourceMappingURL=session-persistence.js.map