@deepseek-ai/dsh-session-persistence-jsonl 0.1.2-alpha.5 → 0.1.3-alpha.2

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/lib/worker.cjs ADDED
@@ -0,0 +1,1235 @@
1
+ let node_worker_threads = require("node:worker_threads");
2
+ let node_crypto = require("node:crypto");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+ let node_perf_hooks = require("node:perf_hooks");
6
+ let node_stream = require("node:stream");
7
+ let node_timers_promises = require("node:timers/promises");
8
+ let node_util = require("node:util");
9
+ let node_zlib = require("node:zlib");
10
+ let _deepseek_ai_dsh_session = require("@deepseek-ai/dsh-session");
11
+ let _deepseek_ai_dsh_llm = require("@deepseek-ai/dsh-llm");
12
+ let _deepseek_ai_dsh_session_persistence = require("@deepseek-ai/dsh-session-persistence");
13
+ let _deepseek_ai_dsh_session_format = require("@deepseek-ai/dsh-session-format");
14
+ let _deepseek_ai_dsh_session_format_catalog = require("@deepseek-ai/dsh-session-format-catalog");
15
+ let node_buffer = require("node:buffer");
16
+ //#region lib/types/format.js
17
+ /**
18
+ * On-disk format helpers for the JSONL session-persistence backend: path
19
+ * sanitization (a {@link SessionId} is an unvalidated branded string, so it
20
+ * MUST be encoded before use in a path — no traversal, no collision), the
21
+ * per-project/session directory layout, header-line (de)serialization, and the
22
+ * truncation-repair offset computation.
23
+ *
24
+ * @module dsh-session-persistence-jsonl/format
25
+ */
26
+ /**
27
+ * Return the artifact suffix for one physical encoding.
28
+ * @param compression - configured JSONL artifact encoding.
29
+ * @returns `.jsonl.zstd` for Zstandard or `.jsonl` for plaintext.
30
+ */
31
+ function logSuffix(compression) {
32
+ return `.jsonl${compressionSuffix(compression)}`;
33
+ }
34
+ function compressionSuffix(compression) {
35
+ return compression === "zstd" ? ".zstd" : "";
36
+ }
37
+ /**
38
+ * Return the canonical filename for one immutable Session format generation.
39
+ * Version zero retains the original suffix-only name; every later generation
40
+ * carries a lowercase numeric `vN` component.
41
+ * @param version - non-negative safe Session format version.
42
+ * @param compression - configured JSONL artifact encoding.
43
+ * @returns the generation filename inside one Session directory.
44
+ */
45
+ function generationLogFilename(version, compression) {
46
+ return `${(0, _deepseek_ai_dsh_session_format.sessionFormatLogFilename)(version)}${compressionSuffix(compression)}`;
47
+ }
48
+ const HEADER_REQUIRED_KEYS = [
49
+ "type",
50
+ "version",
51
+ "id",
52
+ "createdAt",
53
+ "isSeeded",
54
+ "delegationDepth"
55
+ ];
56
+ const HEADER_OPTIONAL_KEYS = [
57
+ "cwd",
58
+ "parentSession",
59
+ "origin",
60
+ "agentPreset"
61
+ ];
62
+ const HEADER_KEYS = new Set([...HEADER_REQUIRED_KEYS, ...HEADER_OPTIONAL_KEYS]);
63
+ /**
64
+ * Refuse policy fields that never belong to a released Session header.
65
+ * @param value - parsed physical header candidate.
66
+ * @returns nothing after successful validation.
67
+ */
68
+ function assertNoRetiredHeaderFields(value) {
69
+ if (typeof value !== "object" || value === null) return;
70
+ if (Object.hasOwn(value, "sandboxMode") || Object.hasOwn(value, "approvalPolicy")) throw new Error("session header uses retired policy baseline fields");
71
+ }
72
+ /**
73
+ * Translate one current physical header into logical metadata and its cut.
74
+ * @param line - the shape-checked first line of a log (see the `isHeaderLine` guard).
75
+ * @returns logical Session metadata paired with the exact inherited prefix length.
76
+ */
77
+ function fromHeaderLine(line) {
78
+ return {
79
+ meta: {
80
+ version: _deepseek_ai_dsh_session.SESSION_FORMAT_VERSION,
81
+ id: line.id,
82
+ createdAt: line.createdAt,
83
+ ...line.cwd !== void 0 ? { cwd: line.cwd } : {},
84
+ ...line.parentSession !== void 0 ? { parentSession: line.parentSession } : {},
85
+ isSeeded: line.isSeeded,
86
+ ...line.origin !== void 0 ? { origin: line.origin } : {},
87
+ delegationDepth: line.delegationDepth,
88
+ ...line.agentPreset !== void 0 ? { agentPreset: line.agentPreset } : {}
89
+ },
90
+ inheritedEventCount: (0, _deepseek_ai_dsh_session.SessionLogOffset)(0)
91
+ };
92
+ }
93
+ /** Type guard: a parsed first line is a well-formed session header. */
94
+ function isHeaderLine(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value) && HEADER_REQUIRED_KEYS.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => HEADER_KEYS.has(key)) && value.type === "session" && typeof value.version === "number" && typeof value.id === "string" && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && !Object.is(value.createdAt, -0) && typeof value.delegationDepth === "number" && Number.isSafeInteger(value.delegationDepth) && value.delegationDepth >= 0 && !Object.is(value.delegationDepth, -0) && (value.cwd === void 0 || typeof value.cwd === "string" && (0, node_path.isAbsolute)(value.cwd)) && (value.parentSession === void 0 || typeof value.parentSession === "string") && typeof value.isSeeded === "boolean" && (value.origin === void 0 || value.origin === "subagent") && (value.agentPreset === void 0 || typeof value.agentPreset === "string");
96
+ }
97
+ /**
98
+ * Refuse a header carrying a format version this build does not read BEFORE
99
+ * validating the current header shape or decoding any event row: a future
100
+ * format need not satisfy this build's structural checks at all, and its user
101
+ * must see "upgrade the harness", never "corrupt session log".
102
+ * @param parsed - the JSON-parsed first line of a session artifact.
103
+ */
104
+ function refuseForeignFormatVersion(parsed) {
105
+ const { version, id } = parsed;
106
+ if (typeof version !== "number" || version === _deepseek_ai_dsh_session.SESSION_FORMAT_VERSION) return;
107
+ throw new _deepseek_ai_dsh_session_persistence.SessionFormatUnsupportedError((0, _deepseek_ai_dsh_session_persistence.sessionFormatVersionRefusal)(typeof id === "string" ? id : String(id), version));
108
+ }
109
+ /** Parse one complete header record supplied independently from event rows. */
110
+ function parseHeaderRecord(record) {
111
+ if (record.length === 0 || record.at(-1) !== 10 || record.indexOf(10) !== record.length - 1) throw new Error("empty or header-less session log");
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(record.subarray(0, -1).toString("utf8"));
115
+ } catch {
116
+ throw new Error("corrupt session log: header line is not valid JSON");
117
+ }
118
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("corrupt session log: first line is not a JSON object");
119
+ refuseForeignFormatVersion(parsed);
120
+ assertNoRetiredHeaderFields(parsed);
121
+ if (!isHeaderLine(parsed)) throw new Error("corrupt session log: first line is not a session header");
122
+ let restore;
123
+ try {
124
+ restore = _deepseek_ai_dsh_session_format_catalog.sessionFormatCatalog.createRestore(parsed, {
125
+ recovery: "strict",
126
+ validation: "transformed"
127
+ });
128
+ } catch {
129
+ /* v8 ignore next -- isHeaderLine matches the current codec; this preserves classification if it tightens. */
130
+ throw new Error("corrupt session log: first line is not a session header");
131
+ }
132
+ return {
133
+ meta: fromHeaderLine(parsed).meta,
134
+ restore
135
+ };
136
+ }
137
+ /**
138
+ * Incrementally scan complete JSONL event records after an independently
139
+ * supplied header record. Newline search and byte offsets stay on raw buffers;
140
+ * only complete records are decoded to UTF-8. A fragment crossing writes is
141
+ * copied because a decoder may reuse its output buffer after `write()` returns.
142
+ */
143
+ var SessionLogScanner = class {
144
+ recovery;
145
+ meta;
146
+ restore;
147
+ eventCount = 0;
148
+ fragments = [];
149
+ fragmentBytes = 0;
150
+ inputBytes;
151
+ committedBytes;
152
+ eventLine = 0;
153
+ issue;
154
+ finished = false;
155
+ /**
156
+ * Create an event scanner from exactly one newline-terminated header record.
157
+ * @param headerRecord - the complete first JSONL record, including its newline.
158
+ */
159
+ constructor(headerRecord, recovery = "recoverable") {
160
+ this.recovery = recovery;
161
+ const parsed = parseHeaderRecord(headerRecord);
162
+ this.meta = parsed.meta;
163
+ this.restore = parsed.restore;
164
+ this.inputBytes = headerRecord.length;
165
+ this.committedBytes = headerRecord.length;
166
+ }
167
+ /**
168
+ * Consume the next raw plaintext chunk, retaining only an incomplete final record.
169
+ * @param chunk - bytes immediately following all previously supplied bytes.
170
+ */
171
+ write(chunk) {
172
+ if (this.finished) throw new Error("cannot write to a finished session log scanner");
173
+ const chunkStart = this.inputBytes;
174
+ this.inputBytes += chunk.length;
175
+ let lineStart = 0;
176
+ for (let newline = chunk.indexOf(10); newline !== -1; newline = chunk.indexOf(10, lineStart)) {
177
+ const fragment = chunk.subarray(lineStart, newline);
178
+ let line = fragment;
179
+ if (this.fragments.length > 0) {
180
+ if (fragment.length > 0) this.fragments.push(fragment);
181
+ line = Buffer.concat(this.fragments, this.fragmentBytes + fragment.length);
182
+ this.fragments = [];
183
+ this.fragmentBytes = 0;
184
+ }
185
+ this.consumeEventLine(line, chunkStart + newline + 1);
186
+ lineStart = newline + 1;
187
+ }
188
+ if (lineStart < chunk.length) {
189
+ const fragment = Buffer.from(chunk.subarray(lineStart));
190
+ this.fragments.push(fragment);
191
+ this.fragmentBytes += fragment.length;
192
+ }
193
+ }
194
+ /**
195
+ * Snapshot progress before appending a recoverable torn-frame prefix.
196
+ * @returns byte, committed-prefix, and expanded-event cursors.
197
+ */
198
+ checkpoint() {
199
+ return {
200
+ inputBytes: this.inputBytes,
201
+ committedBytes: this.committedBytes,
202
+ eventCount: (0, _deepseek_ai_dsh_session.SessionLogOffset)(this.eventCount)
203
+ };
204
+ }
205
+ /**
206
+ * Finish scanning, ignoring a final record without a newline as a torn tail.
207
+ * @returns the header, contiguous event prefix, and safe truncation offset.
208
+ */
209
+ finish() {
210
+ this.finished = true;
211
+ const artifact = this.restore.finish();
212
+ return {
213
+ meta: this.meta,
214
+ inheritedEventCount: (0, _deepseek_ai_dsh_session.SessionLogOffset)(artifact.inheritedEventCount),
215
+ events: artifact.events,
216
+ committedBytes: this.committedBytes
217
+ };
218
+ }
219
+ /** Decode one complete event row and update the contiguous prefix. */
220
+ consumeEventLine(line, endByte) {
221
+ this.eventLine += 1;
222
+ let decoded;
223
+ try {
224
+ decoded = JSON.parse(line.toString("utf8"));
225
+ } catch {
226
+ const issue = /* @__PURE__ */ new Error(`corrupt session log: unparsable committed event at line ${this.eventLine}`);
227
+ if (this.recovery === "strict") throw issue;
228
+ this.issue ??= issue;
229
+ return;
230
+ }
231
+ if (this.issue !== void 0) {
232
+ if (typeof decoded === "object" && decoded !== null && decoded.type === "turn/end") throw this.issue;
233
+ return;
234
+ }
235
+ try {
236
+ this.restore.decodeRow(decoded);
237
+ } catch (error) {
238
+ /* v8 ignore next -- every production Session format decoder rejects with Error. */
239
+ const detail = error instanceof Error ? error.message : String(error);
240
+ const issue = new Error(`corrupt session log: invalid committed event at line ${this.eventLine}: ${detail}`, { cause: error });
241
+ if (this.recovery === "strict") throw issue;
242
+ this.issue = issue;
243
+ if (typeof decoded === "object" && decoded !== null && decoded.type === "turn/end") throw issue;
244
+ return;
245
+ }
246
+ this.eventCount += 1;
247
+ this.committedBytes = endByte;
248
+ }
249
+ };
250
+ //#endregion
251
+ //#region lib/types/win32.js
252
+ /**
253
+ * Windows durable namespace helpers for the JSONL backend.
254
+ *
255
+ * POSIX publishes a newly-created log by creating a directory entry and then
256
+ * fsyncing the parent directory. Windows does not expose that parent-directory
257
+ * fsync contract through Node, so the Windows path uses the native durable
258
+ * namespace primitive instead: create a staging object in the target directory
259
+ * and publish it with `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)` without
260
+ * replacement or cross-volume copy fallback.
261
+ *
262
+ * @module dsh-session-persistence-jsonl/win32
263
+ */
264
+ const MOVEFILE_WRITE_THROUGH = 8;
265
+ const ERROR_FILE_NOT_FOUND = 2;
266
+ const ERROR_PATH_NOT_FOUND = 3;
267
+ const ERROR_ACCESS_DENIED = 5;
268
+ const ERROR_NOT_SAME_DEVICE = 17;
269
+ const ERROR_SHARING_VIOLATION = 32;
270
+ const ERROR_FILE_EXISTS = 80;
271
+ const ERROR_INVALID_NAME = 123;
272
+ const ERROR_ALREADY_EXISTS = 183;
273
+ let bindings;
274
+ /** Load the small Win32 API lazily so non-Windows processes never load Koffi. */
275
+ async function win32() {
276
+ if (bindings !== void 0) return bindings;
277
+ const kernel32 = (await import("koffi")).default.load("kernel32.dll");
278
+ bindings = {
279
+ moveFileExW: kernel32.func("__stdcall", "MoveFileExW", "int", [
280
+ "str16",
281
+ "str16",
282
+ "uint"
283
+ ]),
284
+ createSemaphoreW: kernel32.func("__stdcall", "CreateSemaphoreW", "intptr", [
285
+ "void*",
286
+ "int",
287
+ "int",
288
+ "str16"
289
+ ]),
290
+ waitForSingleObject: kernel32.func("__stdcall", "WaitForSingleObject", "uint", ["intptr", "uint"]),
291
+ releaseSemaphore: kernel32.func("__stdcall", "ReleaseSemaphore", "int", [
292
+ "intptr",
293
+ "int",
294
+ "void*"
295
+ ]),
296
+ closeHandle: kernel32.func("__stdcall", "CloseHandle", "int", ["intptr"]),
297
+ getLastError: kernel32.func("__stdcall", "GetLastError", "uint", [])
298
+ };
299
+ return bindings;
300
+ }
301
+ function errnoCode(win32Code) {
302
+ switch (win32Code) {
303
+ case ERROR_FILE_NOT_FOUND:
304
+ case ERROR_PATH_NOT_FOUND: return "ENOENT";
305
+ case ERROR_ACCESS_DENIED: return "EACCES";
306
+ case ERROR_NOT_SAME_DEVICE: return "EXDEV";
307
+ case ERROR_SHARING_VIOLATION: return "EBUSY";
308
+ case ERROR_FILE_EXISTS:
309
+ case ERROR_ALREADY_EXISTS: return "EEXIST";
310
+ case ERROR_INVALID_NAME: return "EINVAL";
311
+ default: return "EIO";
312
+ }
313
+ }
314
+ function win32Error(syscall, win32Code, path, dest) {
315
+ const code = errnoCode(win32Code);
316
+ const error = /* @__PURE__ */ new Error(`${syscall} ${code} (Win32 ${win32Code}): ${path} -> ${dest}`);
317
+ error.code = code;
318
+ error.errno = win32Code;
319
+ error.syscall = syscall;
320
+ error.path = path;
321
+ error.dest = dest;
322
+ error.win32Code = win32Code;
323
+ return error;
324
+ }
325
+ /**
326
+ * Publish `existing` at `replacement` with Windows write-through rename
327
+ * semantics. The destination must not already exist; the move must stay within
328
+ * the volume (no copy fallback flag is set).
329
+ * @param existing - the synced staging path to move.
330
+ * @param replacement - the final path, which must not already exist.
331
+ */
332
+ async function publishNewFileWin32(existing, replacement) {
333
+ const api = await win32();
334
+ if (api.moveFileExW((0, node_path.toNamespacedPath)(existing), (0, node_path.toNamespacedPath)(replacement), MOVEFILE_WRITE_THROUGH) === 0) throw win32Error("MoveFileExW", api.getLastError(), existing, replacement);
335
+ }
336
+ //#endregion
337
+ //#region lib/types/zstd-private-decoder.js
338
+ /**
339
+ * Node-private synchronous Zstandard frame decoder optimization.
340
+ * @module dsh-session-persistence-jsonl/zstd-private-decoder
341
+ */
342
+ const DECODE_CHUNK_SIZE = 1024 * 1024;
343
+ /** Return the stream with its observed private Node contract, or reject that optimization. */
344
+ function privateZstdStream(stream) {
345
+ const candidate = stream;
346
+ const handle = candidate._handle;
347
+ const errorKey = Reflect.ownKeys(stream).find((key) => typeof key === "symbol" && key.description === "kError");
348
+ /* v8 ignore next -- one test runtime exposes one Node-private shape; the Node 22/24/26 matrix checks compatibility. */
349
+ if (typeof handle !== "object" || handle === null || typeof handle.writeSync !== "function" || !(candidate._writeState instanceof Uint32Array) || candidate._writeState.length < 2 || typeof candidate._defaultFlushFlag !== "number" || errorKey === void 0 || candidate[errorKey] !== null) return void 0;
350
+ return {
351
+ stream,
352
+ errorKey
353
+ };
354
+ }
355
+ /**
356
+ * Synchronous multi-frame decoder backed by one Node Zstd stream handle. Node
357
+ * exposes synchronous decoding only as a one-shot API, so this adapter uses
358
+ * the stream's private handle contract to reuse its native context and output
359
+ * chunks across frames.
360
+ */
361
+ var NodePrivateZstdFrameDecoder = class NodePrivateZstdFrameDecoder {
362
+ stream;
363
+ errorKey;
364
+ output = Buffer.allocUnsafe(DECODE_CHUNK_SIZE);
365
+ decoderError;
366
+ started = false;
367
+ closed = false;
368
+ constructor(stream, errorKey) {
369
+ this.stream = stream;
370
+ this.errorKey = errorKey;
371
+ this.stream.on("error", (error) => {
372
+ this.decoderError ??= error;
373
+ });
374
+ }
375
+ /**
376
+ * Create the optimized decoder when this Node release exposes the expected
377
+ * private stream shape.
378
+ * @returns a shared decoder, or `undefined` when callers must use the public fallback.
379
+ */
380
+ static create() {
381
+ const stream = (0, node_zlib.createZstdDecompress)({ chunkSize: DECODE_CHUNK_SIZE });
382
+ const privateAccess = privateZstdStream(stream);
383
+ /* v8 ignore next -- reached only when a supported Node release changes its private stream shape. */
384
+ if (privateAccess !== void 0) return new NodePrivateZstdFrameDecoder(privateAccess.stream, privateAccess.errorKey);
385
+ /* v8 ignore next -- the active Node runtime passed the private-shape probe above. */
386
+ stream.close();
387
+ }
388
+ /** @inheritdoc */
389
+ *decode(source, frames) {
390
+ if (this.started) throw new Error("Zstandard frame decoder was already started");
391
+ if (this.closed) throw new Error("cannot start a closed Zstandard frame decoder");
392
+ this.started = true;
393
+ try {
394
+ for (const frame of frames) try {
395
+ yield this.decodeFrame(source.subarray(frame.start, frame.end));
396
+ } catch (error) {
397
+ throw new Error(`corrupt Zstandard session log: frame at byte ${frame.start} failed validation`, { cause: error });
398
+ }
399
+ } finally {
400
+ this.close();
401
+ }
402
+ }
403
+ /** Decode one frame; its returned scratch view remains valid until the next call. */
404
+ decodeFrame(input) {
405
+ const handle = this.stream._handle;
406
+ /* v8 ignore next -- decode() rejects closed instances before entering this private frame operation. */
407
+ if (this.closed || handle === null) throw new Error("cannot decode with a closed Zstandard frame decoder");
408
+ let inputOffset = 0;
409
+ let inputRemaining = input.length;
410
+ let outputBytes = 0;
411
+ const fullChunks = [];
412
+ for (;;) {
413
+ handle.writeSync(this.stream._defaultFlushFlag, input, inputOffset, inputRemaining, this.output, 0, this.output.length);
414
+ if (this.decoderError !== void 0) throw this.decoderError;
415
+ const internalError = this.stream[this.errorKey];
416
+ if (internalError !== null) {
417
+ if (internalError instanceof Error) throw internalError;
418
+ throw new Error("Zstandard decoder exposed a non-Error internal failure");
419
+ }
420
+ const outputAfter = this.stream._writeState[0];
421
+ const inputAfter = this.stream._writeState[1];
422
+ const consumed = inputRemaining - inputAfter;
423
+ const produced = this.output.length - outputAfter;
424
+ if (produced > 0) {
425
+ outputBytes += produced;
426
+ /* v8 ignore next -- Buffer cannot materialize a frame beyond its own process-wide maximum length. */
427
+ if (outputBytes > node_buffer.constants.MAX_LENGTH) throw new Error(`Zstandard frame output exceeds ${node_buffer.constants.MAX_LENGTH} bytes`);
428
+ }
429
+ if (outputAfter !== 0) {
430
+ /* v8 ignore next -- structurally scanned ranges contain exactly one complete frame and no trailing bytes. */
431
+ if (inputAfter !== 0) throw new Error("Zstandard frame decoder left trailing input");
432
+ const finalChunk = this.output.subarray(0, produced);
433
+ if (fullChunks.length === 0) return finalChunk;
434
+ if (produced > 0) fullChunks.push(Buffer.from(finalChunk));
435
+ const onlyChunk = fullChunks[0];
436
+ return fullChunks.length === 1 ? onlyChunk : Buffer.concat(fullChunks, outputBytes);
437
+ }
438
+ fullChunks.push(Buffer.from(this.output));
439
+ inputOffset += consumed;
440
+ inputRemaining = inputAfter;
441
+ }
442
+ }
443
+ /** @inheritdoc */
444
+ close() {
445
+ if (this.closed) return;
446
+ this.closed = true;
447
+ this.stream.close();
448
+ }
449
+ };
450
+ //#endregion
451
+ //#region lib/types/zstd-public-decoder.js
452
+ /**
453
+ * Public-API synchronous Zstandard frame decoder fallback.
454
+ * @module dsh-session-persistence-jsonl/zstd-public-decoder
455
+ */
456
+ /** Multi-frame adapter built exclusively from Node's supported one-shot API. */
457
+ var PublicZstdFrameDecoder = class {
458
+ started = false;
459
+ closed = false;
460
+ /** @inheritdoc */
461
+ *decode(source, frames) {
462
+ if (this.started) throw new Error("Zstandard frame decoder was already started");
463
+ if (this.closed) throw new Error("cannot start a closed Zstandard frame decoder");
464
+ this.started = true;
465
+ try {
466
+ for (const { start, end } of frames) {
467
+ let decoded;
468
+ try {
469
+ decoded = (0, node_zlib.zstdDecompressSync)(source.subarray(start, end));
470
+ } catch (error) {
471
+ throw new Error(`corrupt Zstandard session log: frame at byte ${start} failed validation`, { cause: error });
472
+ }
473
+ yield decoded;
474
+ }
475
+ } finally {
476
+ this.close();
477
+ }
478
+ }
479
+ /** @inheritdoc */
480
+ close() {
481
+ this.closed = true;
482
+ }
483
+ };
484
+ //#endregion
485
+ //#region lib/types/zstd.js
486
+ /**
487
+ * Zstandard frame primitives for the JSONL persistence backend. The backend
488
+ * owns a concatenated-frame container so it can append and recover batches
489
+ * without exposing compression mechanics through the persistence seam.
490
+ * @module dsh-session-persistence-jsonl/zstd
491
+ */
492
+ const ZSTD_MAGIC = 4247762216;
493
+ const zstdCompressAsync = (0, node_util.promisify)(node_zlib.zstdCompress);
494
+ const zstdDecompressAsync = (0, node_util.promisify)(node_zlib.zstdDecompress);
495
+ const CHECKSUM_OPTIONS = { params: { [node_zlib.constants.ZSTD_c_checksumFlag]: 1 } };
496
+ const INCOMPLETE_FRAME_OPTIONS = { finishFlush: node_zlib.constants.ZSTD_e_flush };
497
+ /**
498
+ * Locate complete frames without decompressing their blocks. Invalid complete
499
+ * structure rejects; EOF inside the final frame returns its start for repair.
500
+ * @param buffer - complete bytes currently present in the session artifact.
501
+ * @param maxFrames - optional complete-frame limit for metadata-only readers.
502
+ * @returns complete frame ranges and an optional incomplete-final-frame start.
503
+ */
504
+ function scanZstdFrames(buffer, maxFrames = Number.POSITIVE_INFINITY) {
505
+ const frames = [];
506
+ let offset = 0;
507
+ while (offset < buffer.length) {
508
+ const start = offset;
509
+ if (buffer.length - offset < 4) return {
510
+ frames,
511
+ tornStart: start
512
+ };
513
+ if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) throw new Error(`corrupt Zstandard session log: invalid frame magic at byte ${offset}`);
514
+ offset += 4;
515
+ if (offset === buffer.length) return {
516
+ frames,
517
+ tornStart: start
518
+ };
519
+ const descriptor = buffer.readUInt8(offset);
520
+ offset += 1;
521
+ if ((descriptor & 24) !== 0) throw new Error(`corrupt Zstandard session log: reserved frame-header bit at byte ${offset - 1}`);
522
+ const contentSizeFlag = descriptor >>> 6;
523
+ const singleSegment = (descriptor & 32) !== 0;
524
+ const checksum = (descriptor & 4) !== 0;
525
+ const dictionaryFlag = descriptor & 3;
526
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
527
+ const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
528
+ const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
529
+ if (buffer.length - offset < remainingHeaderBytes) return {
530
+ frames,
531
+ tornStart: start
532
+ };
533
+ offset += remainingHeaderBytes;
534
+ for (;;) {
535
+ if (buffer.length - offset < 3) return {
536
+ frames,
537
+ tornStart: start
538
+ };
539
+ const blockHeader = buffer.readUIntLE(offset, 3);
540
+ offset += 3;
541
+ const lastBlock = (blockHeader & 1) !== 0;
542
+ const blockType = blockHeader >>> 1 & 3;
543
+ const blockSize = blockHeader >>> 3;
544
+ if (blockType === 3) throw new Error(`corrupt Zstandard session log: reserved block type at byte ${offset - 3}`);
545
+ const payloadBytes = blockType === 1 ? 1 : blockSize;
546
+ if (buffer.length - offset < payloadBytes) return {
547
+ frames,
548
+ tornStart: start
549
+ };
550
+ offset += payloadBytes;
551
+ if (lastBlock) break;
552
+ }
553
+ if (checksum) {
554
+ if (buffer.length - offset < 4) return {
555
+ frames,
556
+ tornStart: start
557
+ };
558
+ offset += 4;
559
+ }
560
+ frames.push({
561
+ start,
562
+ end: offset
563
+ });
564
+ if (frames.length === maxFrames) return { frames };
565
+ }
566
+ return { frames };
567
+ }
568
+ /**
569
+ * Compress one independently decodable, checksummed Zstandard frame.
570
+ * @param input - JSONL bytes for a header or durable event batch.
571
+ * @returns the complete encoded frame.
572
+ */
573
+ async function compressZstdFrame(input) {
574
+ return zstdCompressAsync(input, CHECKSUM_OPTIONS);
575
+ }
576
+ /**
577
+ * Select the shared private decoder when the running Node 22/24/26 shape is
578
+ * compatible, otherwise preserve correctness with the public one-shot API.
579
+ * @returns a synchronous decoder with an implementation-independent lifecycle.
580
+ */
581
+ function createZstdFrameDecoder() {
582
+ return NodePrivateZstdFrameDecoder.create() ?? new PublicZstdFrameDecoder();
583
+ }
584
+ /**
585
+ * Recover available plaintext from a structurally incomplete final frame.
586
+ * `ZSTD_e_flush` deliberately suppresses final-frame and checksum completion;
587
+ * callers must establish the torn frame boundary before using this helper.
588
+ * @param input - available bytes from a known incomplete Zstandard frame.
589
+ * @returns plaintext produced from the available input.
590
+ */
591
+ async function decompressZstdPrefix(input) {
592
+ return zstdDecompressAsync(input, INCOMPLETE_FRAME_OPTIONS);
593
+ }
594
+ //#endregion
595
+ //#region lib/types/generation.js
596
+ /**
597
+ * Durable whole-generation publication for JSONL Session artifacts.
598
+ *
599
+ * Format packages transform parsed JSON values. This module owns the physical
600
+ * encoding, exact source identity, immutable generation files, and exclusive
601
+ * current-generation publication for both configured JSONL suffixes.
602
+ * @module @deepseek-ai/dsh-session-persistence-jsonl/generation
603
+ */
604
+ /** Internal scheduling bounds: preserve old decode cadence and cap each synchronous encode slice. */
605
+ const MIGRATION_DECODE_YIELD_INTERVAL_MS = 500;
606
+ const MIGRATION_WORK_CHUNK_BYTES = 1024 * 1024;
607
+ const MIGRATION_WRITE_CHUNK_BYTES = 4 * 1024 * 1024;
608
+ const ZSTD_CHECKSUM_OPTIONS = {
609
+ chunkSize: MIGRATION_WORK_CHUNK_BYTES,
610
+ params: { [node_zlib.constants.ZSTD_c_checksumFlag]: 1 }
611
+ };
612
+ /** A historical source changed after its single decode and migration pass. */
613
+ var JsonlGenerationSourceChangedError = class extends Error {
614
+ path;
615
+ name = "JsonlGenerationSourceChangedError";
616
+ /** @param path - historical generation whose revision changed. */
617
+ constructor(path) {
618
+ super(`historical session generation changed during migration: "${path}"`);
619
+ this.path = path;
620
+ }
621
+ };
622
+ /** A historical artifact is intact, but the format edge refuses its contents. */
623
+ var JsonlGenerationUnsupportedMigrationError = class extends Error {
624
+ fromVersion;
625
+ reason;
626
+ name = "JsonlGenerationUnsupportedMigrationError";
627
+ /**
628
+ * @param fromVersion - unchanged source generation version.
629
+ * @param reason - format-edge refusal.
630
+ */
631
+ constructor(fromVersion, reason) {
632
+ super(reason.message, { cause: reason });
633
+ this.fromVersion = fromVersion;
634
+ this.reason = reason;
635
+ }
636
+ };
637
+ /** A current-generation filename already names different or invalid bytes. */
638
+ var JsonlGenerationTargetConflictError = class extends Error {
639
+ path;
640
+ reason;
641
+ name = "JsonlGenerationTargetConflictError";
642
+ /**
643
+ * @param path - immutable target that prevented exclusive publication.
644
+ * @param reason - why the existing target cannot be accepted.
645
+ */
646
+ constructor(path, reason) {
647
+ super(`current session generation already exists at "${path}": ${reason.message}`, { cause: reason });
648
+ this.path = path;
649
+ this.reason = reason;
650
+ }
651
+ };
652
+ const defaultFileSystem = {
653
+ open: (path, flags, mode) => (0, node_fs_promises.open)(path, flags, mode),
654
+ readFile: (path, signal) => (0, node_fs_promises.readFile)(path, signal === void 0 ? void 0 : { signal }),
655
+ readdir: (path) => (0, node_fs_promises.readdir)(path),
656
+ stat: (path) => (0, node_fs_promises.stat)(path, { bigint: true }),
657
+ lstat: (path) => (0, node_fs_promises.lstat)(path),
658
+ link: node_fs_promises.link,
659
+ rm: (path) => (0, node_fs_promises.rm)(path, { force: true })
660
+ };
661
+ const defaultInternals = {
662
+ fs: defaultFileSystem,
663
+ randomToken: () => (0, node_crypto.randomBytes)(8).toString("hex"),
664
+ platform: process.platform,
665
+ publishNewWin32: publishNewFileWin32,
666
+ barrier: () => {}
667
+ };
668
+ function isEEXIST(error) {
669
+ return error?.code === "EEXIST";
670
+ }
671
+ /** Whether a filesystem-owned failure should retain its original errno and path. */
672
+ function isErrnoException(error) {
673
+ return typeof error?.code === "string";
674
+ }
675
+ function identity(value) {
676
+ return [
677
+ value.dev,
678
+ value.ino,
679
+ value.size,
680
+ value.mtimeNs,
681
+ value.ctimeNs
682
+ ].join(":");
683
+ }
684
+ async function readStableSnapshot(path, signal, fs) {
685
+ signal?.throwIfAborted();
686
+ let before = await fs.stat(path);
687
+ for (let attempt = 0;; attempt += 1) {
688
+ const bytes = await fs.readFile(path, signal);
689
+ signal?.throwIfAborted();
690
+ const after = await fs.stat(path);
691
+ if (identity(before) === identity(after)) {
692
+ signal?.throwIfAborted();
693
+ return {
694
+ bytes,
695
+ identity: after
696
+ };
697
+ }
698
+ if (attempt === 1) return {
699
+ bytes: bytes.subarray(0, Number(before.size)),
700
+ identity: before
701
+ };
702
+ before = after;
703
+ }
704
+ }
705
+ /** Parse the version discriminator without validating any version-specific field. */
706
+ function storedVersion(header) {
707
+ if (typeof header !== "object" || header === null || Array.isArray(header)) throw new Error("corrupt session log: first line is not a JSON object");
708
+ const version = header.version;
709
+ if (!Number.isSafeInteger(version) || version < 0 || Object.is(version, -0)) throw new Error("corrupt session log: header version is not a non-negative safe integer");
710
+ return version;
711
+ }
712
+ function parseJson(text, subject) {
713
+ try {
714
+ return JSON.parse(text);
715
+ } catch (error) {
716
+ throw new Error(`corrupt session log: ${subject} is not valid JSON`, { cause: error });
717
+ }
718
+ }
719
+ /** Incremental JSONL parser that retains only one cross-frame record fragment. */
720
+ var MigratingJsonlRows = class {
721
+ restore;
722
+ fragments = [];
723
+ fragmentBytes = 0;
724
+ rowIndex = 0;
725
+ issue;
726
+ constructor(restore) {
727
+ this.restore = restore;
728
+ }
729
+ /** Consume plaintext bytes following the independently decoded header. */
730
+ write(chunk) {
731
+ let lineStart = 0;
732
+ for (let newline = chunk.indexOf(10); newline !== -1; newline = chunk.indexOf(10, lineStart)) {
733
+ const fragment = chunk.subarray(lineStart, newline);
734
+ let line = fragment;
735
+ if (this.fragments.length > 0) {
736
+ if (fragment.length > 0) this.fragments.push(fragment);
737
+ line = Buffer.concat(this.fragments, this.fragmentBytes + fragment.length);
738
+ this.fragments = [];
739
+ this.fragmentBytes = 0;
740
+ }
741
+ this.consume(line);
742
+ lineStart = newline + 1;
743
+ }
744
+ if (lineStart < chunk.length) {
745
+ const fragment = Buffer.from(chunk.subarray(lineStart));
746
+ this.fragments.push(fragment);
747
+ this.fragmentBytes += fragment.length;
748
+ }
749
+ }
750
+ /** Refuse a record fragment left by structurally complete Zstandard frames. */
751
+ assertCompleteFramesEndOnRecord() {
752
+ if (this.fragments.length > 0) throw new Error("corrupt Zstandard session log: complete frame contains a torn JSONL record");
753
+ }
754
+ finish() {
755
+ return this.restore.finish();
756
+ }
757
+ consume(line) {
758
+ const index = this.rowIndex;
759
+ this.rowIndex += 1;
760
+ let row;
761
+ try {
762
+ row = parseJson(line.toString("utf8"), `row ${index + 1}`);
763
+ } catch (error) {
764
+ this.issue ??= asError(error);
765
+ return;
766
+ }
767
+ if (this.issue !== void 0) {
768
+ if (typeof row === "object" && row !== null && row.type === "turn/end") throw this.issue;
769
+ return;
770
+ }
771
+ this.restore.decodeRow(row);
772
+ }
773
+ };
774
+ async function startMigrationStream(headerRecord, sourceVersion, format, validateHistoricalHeader) {
775
+ const value = parseJson(headerRecord.subarray(0, -1).toString("utf8"), "header line");
776
+ const version = storedVersion(value);
777
+ if (version !== sourceVersion) throw new Error(`resolved JSONL source filename identifies v${sourceVersion}, but its header identifies v${version}`);
778
+ const header = value;
779
+ const validation = validateHistoricalHeader?.(header);
780
+ if (validation !== void 0) await validation;
781
+ return { parser: new MigratingJsonlRows(format.createRestore(header)) };
782
+ }
783
+ async function consumeMigrationBytes(rows, chunks, signal) {
784
+ signal?.throwIfAborted();
785
+ let yieldDeadline = node_perf_hooks.performance.now() + MIGRATION_DECODE_YIELD_INTERVAL_MS;
786
+ for (const bytes of chunks) for (let offset = 0; offset < bytes.length; offset += MIGRATION_WORK_CHUNK_BYTES) {
787
+ rows.write(bytes.subarray(offset, offset + MIGRATION_WORK_CHUNK_BYTES));
788
+ if (node_perf_hooks.performance.now() < yieldDeadline) continue;
789
+ await node_timers_promises.scheduler.yield();
790
+ signal?.throwIfAborted();
791
+ yieldDeadline = node_perf_hooks.performance.now() + MIGRATION_DECODE_YIELD_INTERVAL_MS;
792
+ }
793
+ }
794
+ async function decodeStreamingMigration(bytes, compression, sourceVersion, format, validateHistoricalHeader, signal) {
795
+ signal?.throwIfAborted();
796
+ if (compression === "none") {
797
+ const headerEnd = bytes.indexOf(10);
798
+ if (headerEnd === -1) throw new Error("empty or header-less session log");
799
+ const stream = await startMigrationStream(bytes.subarray(0, headerEnd + 1), sourceVersion, format, validateHistoricalHeader);
800
+ signal?.throwIfAborted();
801
+ const bodyEnd = bytes.lastIndexOf(10);
802
+ if (bodyEnd > headerEnd) await consumeMigrationBytes(stream.parser, [bytes.subarray(headerEnd + 1, bodyEnd + 1)], signal);
803
+ return stream.parser.finish();
804
+ }
805
+ const { frames, tornStart } = scanZstdFrames(bytes);
806
+ if (frames.length === 0) throw new Error("empty or header-less Zstandard session log");
807
+ const decoder = createZstdFrameDecoder();
808
+ try {
809
+ const decoded = decoder.decode(bytes, frames);
810
+ const first = decoded.next();
811
+ /* v8 ignore next -- a non-empty structural frame list yields once or throws. */
812
+ if (first.done) throw new Error("empty or header-less Zstandard session log");
813
+ assertIndependentHeaderFrame(first.value);
814
+ const stream = await startMigrationStream(first.value, sourceVersion, format, validateHistoricalHeader);
815
+ signal?.throwIfAborted();
816
+ await consumeMigrationBytes(stream.parser, decoded, signal);
817
+ stream.parser.assertCompleteFramesEndOnRecord();
818
+ if (tornStart !== void 0) {
819
+ let recovered = Buffer.alloc(0);
820
+ try {
821
+ recovered = await decompressZstdPrefix(bytes.subarray(tornStart));
822
+ } catch {
823
+ /* v8 ignore next -- decoder failure plus concurrent abort is timing-dependent. */
824
+ if (signal?.aborted) signal.throwIfAborted();
825
+ }
826
+ signal?.throwIfAborted();
827
+ const newline = recovered.lastIndexOf(10);
828
+ if (newline !== -1) await consumeMigrationBytes(stream.parser, [recovered.subarray(0, newline + 1)], signal);
829
+ }
830
+ return stream.parser.finish();
831
+ } finally {
832
+ decoder.close();
833
+ }
834
+ }
835
+ /**
836
+ * Read and validate one complete current generation for an isolated verifier.
837
+ * @param path - staged or competing current-generation path.
838
+ * @param compression - configured physical encoding.
839
+ * @param expectedId - Session identity expected in the header.
840
+ * @param expectedEventCount - exact logical event count expected after decoding.
841
+ * @param expectedPrefix - verified migration prefix; an append tail may be present and is not validated.
842
+ * @returns stable physical identity and digest for publication comparison.
843
+ */
844
+ async function verifyJsonlCurrentGeneration(path, compression, expectedId, expectedEventCount, expectedPrefix) {
845
+ return defaultGenerationRuntime.verify(path, compression, expectedId, expectedEventCount, expectedPrefix);
846
+ }
847
+ async function verifyCurrentGeneration(path, compression, expectedId, expectedEventCount, fs, expectedPrefix) {
848
+ const before = await fs.stat(path);
849
+ const bytes = await fs.readFile(path);
850
+ const after = await fs.stat(path);
851
+ if (expectedPrefix !== void 0) {
852
+ if (bytes.length < expectedPrefix.bytes) throw new Error("target bytes are shorter than the migrated generation");
853
+ const digest = (0, node_crypto.createHash)("sha256").update(bytes.subarray(0, expectedPrefix.bytes)).digest("hex");
854
+ if (digest !== expectedPrefix.digest) throw new Error("target bytes do not begin with the migrated generation");
855
+ return {
856
+ identity: after,
857
+ bytes: expectedPrefix.bytes,
858
+ digest
859
+ };
860
+ }
861
+ if (identity(before) !== identity(after)) throw new Error("current session generation changed during verification");
862
+ const snapshot = {
863
+ bytes,
864
+ identity: after
865
+ };
866
+ const generation = decodeCurrentGeneration(snapshot.bytes, compression);
867
+ (0, _deepseek_ai_dsh_session_persistence.validateStoredEvents)(generation.meta, generation.events, {
868
+ kind: "jsonl",
869
+ path
870
+ });
871
+ if (generation.meta.id !== expectedId) throw new Error(`current session generation contains id "${generation.meta.id}", expected "${expectedId}"`);
872
+ if (generation.events.length !== expectedEventCount) throw new Error(`current session generation contains ${generation.events.length} events, expected ${expectedEventCount}`);
873
+ _deepseek_ai_dsh_session.Session.fromRestore(generation.meta.id, generation.events, generation.meta, generation.inheritedEventCount, "detached");
874
+ assertCurrentAssistantStreams(generation.events);
875
+ return {
876
+ identity: snapshot.identity,
877
+ bytes: snapshot.bytes.length,
878
+ digest: (0, node_crypto.createHash)("sha256").update(snapshot.bytes).digest("hex")
879
+ };
880
+ }
881
+ /** Fully replay embedded streams only inside isolated current-generation verification. */
882
+ function assertCurrentAssistantStreams(events) {
883
+ for (const [index, event] of events.entries()) {
884
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
885
+ const assembler = new _deepseek_ai_dsh_llm.BlockAssembler();
886
+ let timed;
887
+ try {
888
+ timed = (0, _deepseek_ai_dsh_llm.expandAssistantStream)(event.data.stream);
889
+ for (const member of timed) assembler.push(member.chunk);
890
+ } catch (error) {
891
+ throw new Error(`seed ${event.type} at index ${index} has an invalid embedded stream`, { cause: error });
892
+ }
893
+ if (event.type === "assistant/attempt" || timed.length === 0) continue;
894
+ const content = event.data.interrupted === true ? assembler.interruptedBlocks() : assembler.blocks();
895
+ if (!(0, node_util.isDeepStrictEqual)(event.data.message.content, content)) throw new Error(`seed assistant/message at index ${index} content disagrees with its embedded stream`);
896
+ if (!(0, node_util.isDeepStrictEqual)(event.data.usage, assembler.usage)) throw new Error(`seed assistant/message at index ${index} usage disagrees with its embedded stream`);
897
+ if (!(0, node_util.isDeepStrictEqual)(event.data.message.source.replayState, assembler.replayState)) throw new Error(`seed assistant/message at index ${index} replay state disagrees with its embedded stream`);
898
+ }
899
+ }
900
+ function decodeCurrentGeneration(bytes, compression) {
901
+ if (compression === "none") {
902
+ const headerEnd = bytes.indexOf(10);
903
+ if (headerEnd === -1) throw new Error("empty or header-less session log");
904
+ const scanner = new SessionLogScanner(bytes.subarray(0, headerEnd + 1), "strict");
905
+ scanner.write(bytes.subarray(headerEnd + 1));
906
+ return finishCurrentGenerationScan(scanner);
907
+ }
908
+ const { frames, tornStart } = scanZstdFrames(bytes);
909
+ if (frames.length === 0) throw new Error("empty or header-less Zstandard session log");
910
+ if (tornStart !== void 0) throw new Error("current session generation has a torn physical tail");
911
+ const decoder = createZstdFrameDecoder();
912
+ try {
913
+ const plaintext = decoder.decode(bytes, frames);
914
+ const header = plaintext.next();
915
+ /* v8 ignore next -- a non-empty structural frame list yields once or throws. */
916
+ if (header.done) throw new Error("empty or header-less Zstandard session log");
917
+ assertIndependentHeaderFrame(header.value);
918
+ const scanner = new SessionLogScanner(header.value, "strict");
919
+ for (const chunk of plaintext) scanner.write(chunk);
920
+ return finishCurrentGenerationScan(scanner);
921
+ } finally {
922
+ decoder.close();
923
+ }
924
+ }
925
+ function finishCurrentGenerationScan(scanner) {
926
+ const inputBytes = scanner.checkpoint().inputBytes;
927
+ const decoded = scanner.finish();
928
+ if (decoded.committedBytes !== inputBytes) throw new Error("current session generation has a torn physical tail");
929
+ return decoded;
930
+ }
931
+ function stringifyJson(value, subject) {
932
+ let text;
933
+ try {
934
+ text = JSON.stringify(value);
935
+ } catch (error) {
936
+ throw new Error(`${subject} is not lossless JSON`, { cause: error });
937
+ }
938
+ if (typeof text !== "string") throw new Error(`${subject} is not lossless JSON`);
939
+ return text;
940
+ }
941
+ function assertIndependentHeaderFrame(plaintext) {
942
+ if (plaintext.length === 0 || plaintext.indexOf(10) !== plaintext.length - 1) throw new Error("corrupt Zstandard session log: first frame is not exactly one header line");
943
+ }
944
+ function assertGenerationPaths(sourcePath, sourceVersion, currentPath, currentVersion, compression) {
945
+ const expectedSource = generationLogFilename(sourceVersion, compression);
946
+ const expectedCurrent = generationLogFilename(currentVersion, compression);
947
+ if ((0, node_path.basename)(sourcePath) !== expectedSource) throw new Error(`resolved JSONL source path must end with "${expectedSource}": ${sourcePath}`);
948
+ if ((0, node_path.basename)(currentPath) !== expectedCurrent) throw new Error(`current JSONL generation path must end with "${expectedCurrent}": ${currentPath}`);
949
+ if ((0, node_path.dirname)(sourcePath) !== (0, node_path.dirname)(currentPath)) throw new Error("source and current JSONL generations must share one Session directory");
950
+ return logSuffix(compression);
951
+ }
952
+ async function syncDirectory(path, internals) {
953
+ /* v8 ignore next -- Windows namespace operations request write-through directly. */
954
+ if (internals.platform === "win32") return;
955
+ const handle = await internals.fs.open(path, "r");
956
+ try {
957
+ await handle.sync();
958
+ } finally {
959
+ await handle.close();
960
+ }
961
+ }
962
+ /** Produce bounded JSONL chunks while yielding between main-thread encoding slices. */
963
+ async function* encodeMigrationRows(artifact, format, signal) {
964
+ signal?.throwIfAborted();
965
+ let lines = [];
966
+ let bytes = 0;
967
+ for (const value of artifact.events) {
968
+ const line = `${stringifyJson(format.encodeEvent(value), `migrated Session event ${value.seq}`)}\n`;
969
+ const lineBytes = Buffer.byteLength(line);
970
+ if (bytes > 0 && bytes + lineBytes > MIGRATION_WORK_CHUNK_BYTES) {
971
+ yield Buffer.from(lines.join(""));
972
+ await node_timers_promises.scheduler.yield();
973
+ signal?.throwIfAborted();
974
+ lines = [];
975
+ bytes = 0;
976
+ }
977
+ lines.push(line);
978
+ bytes += lineBytes;
979
+ }
980
+ yield Buffer.from(lines.join(""));
981
+ }
982
+ async function writeMigrationChunks(chunks, write) {
983
+ let pending = [];
984
+ let bytes = 0;
985
+ for await (const chunk of chunks) {
986
+ pending.push(chunk);
987
+ bytes += chunk.length;
988
+ if (bytes < MIGRATION_WRITE_CHUNK_BYTES) continue;
989
+ await write(pending.length === 1 ? pending[0] : Buffer.concat(pending, bytes));
990
+ pending = [];
991
+ bytes = 0;
992
+ }
993
+ if (bytes > 0) await write(pending.length === 1 ? pending[0] : Buffer.concat(pending, bytes));
994
+ }
995
+ /** Encode directly into one synced stage without a whole-artifact row or byte buffer. */
996
+ async function writeSyncedTemp(currentPath, suffix, compression, artifact, format, signal, internals) {
997
+ signal?.throwIfAborted();
998
+ let path;
999
+ let handle;
1000
+ for (;;) {
1001
+ path = (0, node_path.join)((0, node_path.dirname)(currentPath), `session.migration.${internals.randomToken()}${suffix}.tmp`);
1002
+ try {
1003
+ handle = await internals.fs.open(path, "wx", 384);
1004
+ break;
1005
+ } catch (error) {
1006
+ if (isEEXIST(error)) continue;
1007
+ throw error;
1008
+ }
1009
+ }
1010
+ const hash = (0, node_crypto.createHash)("sha256");
1011
+ let bytes = 0;
1012
+ const write = async (chunk) => {
1013
+ await handle.writeFile(chunk);
1014
+ hash.update(chunk);
1015
+ bytes += chunk.length;
1016
+ };
1017
+ let failure;
1018
+ try {
1019
+ const headerValue = format.encodeHeader(artifact.header, artifact.inheritedEventCount);
1020
+ const header = Buffer.from(`${stringifyJson(headerValue, "migrated session header")}\n`);
1021
+ await write(compression === "zstd" ? await compressZstdFrame(header) : header);
1022
+ if (artifact.events.length > 0) {
1023
+ const rows = encodeMigrationRows(artifact, format, signal);
1024
+ if (compression === "none") await writeMigrationChunks(rows, write);
1025
+ else await new Promise((resolve, reject) => {
1026
+ (0, node_stream.pipeline)(node_stream.Readable.from(rows, {
1027
+ objectMode: false,
1028
+ highWaterMark: MIGRATION_WORK_CHUNK_BYTES
1029
+ }), (0, node_zlib.createZstdCompress)(ZSTD_CHECKSUM_OPTIONS), async (source) => {
1030
+ await writeMigrationChunks(source, write);
1031
+ }, (error) => {
1032
+ if (error instanceof Error) reject(error);
1033
+ else resolve();
1034
+ });
1035
+ });
1036
+ }
1037
+ signal?.throwIfAborted();
1038
+ await handle.sync();
1039
+ } catch (error) {
1040
+ failure = error;
1041
+ }
1042
+ try {
1043
+ await handle.close();
1044
+ } catch (error) {
1045
+ failure = failure === void 0 ? error : new AggregateError([failure, error], `failed to write and close migration stage "${path}"`);
1046
+ }
1047
+ if (failure !== void 0) {
1048
+ const writeError = failure instanceof Error ? failure : new Error("migration stage write failed with a non-Error rejection", { cause: failure });
1049
+ await removeTemporary(path, writeError, internals);
1050
+ throw writeError;
1051
+ }
1052
+ return {
1053
+ path,
1054
+ bytes,
1055
+ digest: hash.digest("hex")
1056
+ };
1057
+ }
1058
+ /** Remove one temporary file without hiding the operation failure that made it disposable. */
1059
+ async function removeTemporary(path, primaryFailure, internals) {
1060
+ try {
1061
+ await internals.fs.rm(path);
1062
+ } catch (cleanupFailure) {
1063
+ throw new AggregateError([primaryFailure, cleanupFailure], `failed to clean migration temporary "${path}" after an earlier failure`);
1064
+ }
1065
+ }
1066
+ /** Remove a redundant stage after the target has been validated as committed. */
1067
+ async function removeCommittedTemporary(path, internals) {
1068
+ try {
1069
+ await internals.fs.rm(path);
1070
+ } catch {}
1071
+ }
1072
+ async function publishCurrentExclusive(staged, currentPath, internals) {
1073
+ if (internals.platform === "win32") try {
1074
+ await internals.publishNewWin32(staged, currentPath);
1075
+ return true;
1076
+ } catch (error) {
1077
+ /* v8 ignore else -- native helper tests own non-collision Win32 failures. */
1078
+ if (isEEXIST(error)) return false;
1079
+ /* v8 ignore next -- the filesystem error is already complete. */
1080
+ throw error;
1081
+ }
1082
+ try {
1083
+ await internals.fs.link(staged, currentPath);
1084
+ } catch (error) {
1085
+ /* v8 ignore else -- a non-collision filesystem error propagates unchanged. */
1086
+ if (isEEXIST(error)) return false;
1087
+ /* v8 ignore next -- the filesystem error is already complete. */
1088
+ throw error;
1089
+ }
1090
+ await syncDirectory((0, node_path.dirname)(currentPath), internals);
1091
+ return true;
1092
+ }
1093
+ function asError(error) {
1094
+ return error instanceof Error ? error : new Error("current-generation validation failed with a non-Error rejection", { cause: error });
1095
+ }
1096
+ async function inspectExpectedCurrent(currentPath, internals, inspect) {
1097
+ try {
1098
+ const expectedName = (0, node_path.basename)(currentPath);
1099
+ const names = await internals.fs.readdir((0, node_path.dirname)(currentPath));
1100
+ if (!names.includes(expectedName)) {
1101
+ const noncanonical = names.find((name) => name.toLowerCase() === expectedName.toLowerCase());
1102
+ if (noncanonical !== void 0) throw new Error(`target resolves to noncanonical directory entry "${noncanonical}"`);
1103
+ }
1104
+ const info = await internals.fs.lstat(currentPath);
1105
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error(`target is a ${info.isSymbolicLink() ? "symbolic link" : "non-regular file"}`);
1106
+ return await inspect();
1107
+ } catch (error) {
1108
+ if (isErrnoException(error)) throw error;
1109
+ throw new JsonlGenerationTargetConflictError(currentPath, asError(error));
1110
+ }
1111
+ }
1112
+ function withOverrides(overrides) {
1113
+ return {
1114
+ ...defaultInternals,
1115
+ ...overrides,
1116
+ fs: {
1117
+ ...defaultFileSystem,
1118
+ ...overrides.fs
1119
+ }
1120
+ };
1121
+ }
1122
+ async function publishPreparedMigration(options, suffix, artifact, sourceIdentity, internals) {
1123
+ await node_timers_promises.scheduler.yield();
1124
+ const { sourcePath, currentPath, compression, verifyCurrentFile } = options;
1125
+ const eventCount = artifact.events.length;
1126
+ let staged = await writeSyncedTemp(currentPath, suffix, compression, artifact, options.format, void 0, internals);
1127
+ try {
1128
+ const verifiedStage = await verifyCurrentFile(staged.path, compression, artifact.header.id, eventCount);
1129
+ if (verifiedStage.bytes !== staged.bytes || verifiedStage.digest !== staged.digest) throw new Error("staged session generation changed during verification");
1130
+ await internals.barrier("before-source-check", 1);
1131
+ if (identity(await internals.fs.stat(sourcePath)) !== identity(sourceIdentity)) throw new JsonlGenerationSourceChangedError(sourcePath);
1132
+ const published = await publishCurrentExclusive(staged.path, currentPath, internals);
1133
+ if (published && internals.platform === "win32") staged = {
1134
+ ...staged,
1135
+ path: ""
1136
+ };
1137
+ await internals.barrier("after-publication", 1);
1138
+ let currentIdentity;
1139
+ if (published) {
1140
+ if (staged.path !== "") {
1141
+ await removeCommittedTemporary(staged.path, internals);
1142
+ staged = {
1143
+ ...staged,
1144
+ path: ""
1145
+ };
1146
+ }
1147
+ currentIdentity = await internals.fs.stat(currentPath);
1148
+ } else {
1149
+ currentIdentity = (await inspectExpectedCurrent(currentPath, internals, async () => {
1150
+ const candidate = await verifyCurrentFile(currentPath, compression, artifact.header.id, eventCount, staged);
1151
+ if (candidate.bytes !== staged.bytes || candidate.digest !== staged.digest) throw new Error("target bytes differ from the migrated generation");
1152
+ return candidate;
1153
+ })).identity;
1154
+ await removeCommittedTemporary(staged.path, internals);
1155
+ staged = {
1156
+ ...staged,
1157
+ path: ""
1158
+ };
1159
+ }
1160
+ return currentIdentity;
1161
+ } catch (error) {
1162
+ if (staged.path !== "") await removeTemporary(staged.path, error, internals);
1163
+ throw error;
1164
+ }
1165
+ }
1166
+ async function prepareMigration(options, internals) {
1167
+ const { sourcePath, sourceVersion, currentPath, compression, format, signal } = options;
1168
+ const suffix = assertGenerationPaths(sourcePath, sourceVersion, currentPath, format.currentVersion, compression);
1169
+ if (sourceVersion >= format.currentVersion) throw new Error(`migration preparation requires a historical source, got v${sourceVersion}`);
1170
+ const source = await readStableSnapshot(sourcePath, signal, internals.fs);
1171
+ let artifact;
1172
+ try {
1173
+ artifact = await decodeStreamingMigration(source.bytes, compression, sourceVersion, format, options.validateHistoricalHeader, signal);
1174
+ } catch (error) {
1175
+ if (format.isUnsupportedMigrationError?.(error) === true) throw new JsonlGenerationUnsupportedMigrationError(sourceVersion, error);
1176
+ throw error;
1177
+ }
1178
+ if (artifact.header.version !== format.currentVersion) throw new Error(`format migration returned v${artifact.header.version}, expected v${format.currentVersion}`);
1179
+ const sourceIdentity = source.identity;
1180
+ let publication;
1181
+ return {
1182
+ sourceIdentity,
1183
+ artifact,
1184
+ publish() {
1185
+ if (publication === void 0) publication = publishPreparedMigration(options, suffix, artifact, sourceIdentity, internals);
1186
+ return publication;
1187
+ }
1188
+ };
1189
+ }
1190
+ /**
1191
+ * Create one generation runtime with fixed filesystem and publication dependencies.
1192
+ * @param overrides - deterministic filesystem, platform, and race dependencies.
1193
+ * @returns bound generation operations.
1194
+ */
1195
+ function createJsonlGenerationRuntime(overrides = {}) {
1196
+ const internals = withOverrides(overrides);
1197
+ return {
1198
+ readStable: (path, signal) => readStableSnapshot(path, signal, internals.fs),
1199
+ prepare: (options) => prepareMigration(options, internals),
1200
+ verify: (path, compression, expectedId, expectedEventCount, expectedPrefix) => verifyCurrentGeneration(path, compression, expectedId, expectedEventCount, internals.fs, expectedPrefix)
1201
+ };
1202
+ }
1203
+ const defaultGenerationRuntime = createJsonlGenerationRuntime();
1204
+ //#endregion
1205
+ //#region lib/types/worker.js
1206
+ /** Worker entry for current-generation physical and logical verification. */
1207
+ function parseRequest(value) {
1208
+ if (typeof value !== "object" || value === null) throw new Error("migration verifier request must be an object");
1209
+ const request = value;
1210
+ if (typeof request.path !== "string" || request.compression !== "none" && request.compression !== "zstd" || typeof request.expectedId !== "string" || !Number.isSafeInteger(request.expectedEventCount) || request.expectedEventCount < 0 || request.expectedPrefix !== void 0 && (!Number.isSafeInteger(request.expectedPrefix.bytes) || request.expectedPrefix.bytes < 0 || !/^[0-9a-f]{64}$/.test(request.expectedPrefix.digest))) throw new Error("migration verifier request is malformed");
1211
+ return request;
1212
+ }
1213
+ if (node_worker_threads.parentPort === null) throw new Error("migration verifier requires a parent port");
1214
+ const port = node_worker_threads.parentPort;
1215
+ const request = parseRequest(node_worker_threads.workerData);
1216
+ async function verify() {
1217
+ try {
1218
+ const result = await verifyJsonlCurrentGeneration(request.path, request.compression, request.expectedId, request.expectedEventCount, request.expectedPrefix);
1219
+ port.postMessage({
1220
+ ok: true,
1221
+ result
1222
+ });
1223
+ } catch (error) {
1224
+ const failure = error instanceof Error ? error : new Error(String(error));
1225
+ port.postMessage({
1226
+ ok: false,
1227
+ message: failure.message,
1228
+ stack: failure.stack
1229
+ });
1230
+ } finally {
1231
+ port.close();
1232
+ }
1233
+ }
1234
+ verify();
1235
+ //#endregion