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

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/session/session-persistence-jsonl/README.md
5
- README.md: 4d815cb3dac33463adfd67a8355bdfac5dfbfa27
6
- README.zh.md: 1ef84df9b6b2c4d36788c4bd30498d615d57513d
5
+ README.md: d3dfaf8affb142979ede69d86bbf0b1587e8c39c
6
+ README.zh.md: 15fc815523355674a1f325b604dd2655f56f7f58
package/README.md CHANGED
@@ -54,7 +54,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
54
54
 
55
55
  ### On-disk layout
56
56
 
57
- Each session gets a session-owned directory under a readable project directory; the first logical line of the log is the immutable `SessionHeader`, followed by one storage record per logical event (or one packed chunk row per eligible run). Storage records use the lossless provenance representation described below:
57
+ Each session gets a session-owned directory under a readable project directory; the first logical line is the private v0 physical header, followed by one storage record per logical event (or one packed chunk row per eligible run). Its optional numeric `seedLength` remains byte-compatible: absence decodes to `SessionHeader.isSeeded: false`, while zero or a positive value decodes to `isSeeded: true` plus the exact `inheritedEventCount`. Storage records use the lossless provenance representation described below:
58
58
 
59
59
  ```text
60
60
  <root>/
@@ -72,7 +72,7 @@ A session is materialized lazily: `create(meta)` writes nothing, and the first `
72
72
 
73
73
  ### Reading the logs
74
74
 
75
- `inspect(id)` returns an immutable balanced view without committing recovery. `readFrom(id, fromSeq)` returns stored events at or past a sequence number for watermark consumers; sequential media like JSONL parse the whole artifact and skip forward. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend.
75
+ `inspect(id)` returns an immutable balanced view with its exact inherited cut without committing recovery. `readFrom(id, fromOffset)` accepts a `SessionLogOffset`, returns stored events at or past that offset, and retains the same cut beside the suffix; sequential media like JSONL parse the whole artifact and skip forward. Header-only listing exposes `isSeeded` without reading event bodies. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend.
76
76
 
77
77
  -----
78
78
 
@@ -100,7 +100,7 @@ The default artifact is a standard concatenation of independent [Zstandard frame
100
100
  | [`src/format.ts`](src/format.ts) | Log path derivation, header encoding, record scanning, packed-row layout |
101
101
  | [`src/zstd.ts`](src/zstd.ts) | Zstandard frame compression, decoding, and frame scanning |
102
102
  | [`src/win32.ts`](src/win32.ts) | Windows write-through publish and directory creation |
103
- | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; identity is enforced at the storage layer) |
103
+ | | No runtime invariant companion is published; persistence correctness requires backend round-trip and crash-tail tests; this package exposes no continuously observable in-process relation. |
104
104
 
105
105
  </details>
106
106
 
package/README.zh.md CHANGED
@@ -54,7 +54,7 @@ kind: "package-reference"
54
54
 
55
55
  ### 磁盘布局
56
56
 
57
- 每个会话在可读项目目录下获得一个会话自有目录;日志第一个逻辑行是不可变 `SessionHeader`,之后每个逻辑事件一条存储记录(或每个符合条件的连续段一条打包分片行)。存储记录使用下文所述的无损来源序列表示:
57
+ 每个会话在可读项目目录下获得一个会话自有目录;第一个逻辑行是私有 v0 物理 header,之后每个逻辑事件一条存储记录(或每个符合条件的连续段一条打包分片行)。其可选数字 `seedLength` 保持字节兼容:缺席解码为 `SessionHeader.isSeeded: false`,零或正值解码为 `isSeeded: true` 加精确 `inheritedEventCount`。存储记录使用下文所述的无损来源序列表示:
58
58
 
59
59
  ```text
60
60
  <root>/
@@ -72,7 +72,7 @@ kind: "package-reference"
72
72
 
73
73
  ### 读取日志
74
74
 
75
- `inspect(id)` 返回不可变的平衡视图,不提交恢复。`readFrom(id, fromSeq)` 为水位消费方返回该序列号及之后的已存储事件;JSONL 这类顺序介质解析整个产物并向前跳过。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。
75
+ `inspect(id)` 返回带精确继承切点的不可变平衡视图,不提交恢复。`readFrom(id, fromOffset)` 接受 `SessionLogOffset`,返回该偏移及之后的已存储事件,并在后缀旁保留同一切点;JSONL 这类顺序介质解析整个产物并向前跳过。仅 header 的列表读取不读事件正文即可公开 `isSeeded`。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。
76
76
 
77
77
  -----
78
78
 
@@ -100,7 +100,7 @@ kind: "package-reference"
100
100
  | [`src/format.ts`](src/format.ts) | 日志路径派生、header 编码、记录扫描、打包行布局 |
101
101
  | [`src/zstd.ts`](src/zstd.ts) | Zstandard 帧压缩、解码与帧扫描 |
102
102
  | [`src/win32.ts`](src/win32.ts) | Windows write-through 发布与目录创建 |
103
- | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;身份在存储层强制) |
103
+ | | 不发布运行时不变式伴生入口;身份在存储层强制。 |
104
104
 
105
105
  </details>
106
106
 
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { performance } from "node:perf_hooks";
6
6
  import { scheduler } from "node:timers/promises";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, PersistenceCoordinator, SessionFormatUnsupportedError, SessionPersistence, SessionPersistenceRevision, sessionFormatVersionRefusal } from "@deepseek-ai/dsh-session-persistence";
9
- import { SESSION_FORMAT_VERSION, decodeSeqRanges, decodeStorageRecord, encodeSeqRanges, packChunkRuns } from "@deepseek-ai/dsh-session";
9
+ import { SESSION_FORMAT_VERSION, SessionLogOffset, decodeSeqRanges, decodeStorageRecord, encodeSeqRanges, packChunkRuns } from "@deepseek-ai/dsh-session";
10
10
  import { constants, createZstdDecompress, zstdCompress, zstdDecompress, zstdDecompressSync } from "node:zlib";
11
11
  import { promisify } from "node:util";
12
12
  import { constants as constants$1 } from "node:buffer";
@@ -31,9 +31,14 @@ function logSuffix(compression) {
31
31
  /**
32
32
  * Build the header line object from a {@link SessionHeader}.
33
33
  * @param header - the immutable session metadata to serialize.
34
+ * @param inheritedEventCount - exact inherited prefix length; required for a
35
+ * seeded header and omitted only for an unseeded header.
34
36
  * @returns the `type: 'session'`-tagged line object, absent optional fields omitted (never null).
35
37
  */
36
- function toHeaderLine(header) {
38
+ function toHeaderLine(header, inheritedEventCount) {
39
+ if (header.isSeeded && inheritedEventCount === void 0) throw new Error("seeded session header requires an inherited event count");
40
+ const cut = SessionLogOffset(inheritedEventCount ?? 0);
41
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded session header inherited event count must be 0");
37
42
  return {
38
43
  type: "session",
39
44
  version: header.version,
@@ -41,34 +46,37 @@ function toHeaderLine(header) {
41
46
  createdAt: header.createdAt,
42
47
  ...header.cwd !== void 0 ? { cwd: header.cwd } : {},
43
48
  ...header.parentSession !== void 0 ? { parentSession: header.parentSession } : {},
44
- ...header.seedLength !== void 0 ? { seedLength: header.seedLength } : {},
49
+ ...header.isSeeded ? { seedLength: cut } : {},
45
50
  ...header.origin !== void 0 ? { origin: header.origin } : {},
46
51
  delegationDepth: header.delegationDepth ?? 0,
47
52
  ...header.agentPreset !== void 0 ? { agentPreset: header.agentPreset } : {}
48
53
  };
49
54
  }
50
55
  /**
51
- * Parse a header line back into a {@link SessionHeader}.
56
+ * Translate one version-0 physical header into logical metadata and its cut.
52
57
  * @param line - the shape-checked first line of a log (see the `isHeaderLine` guard).
53
- * @returns the header, absent optional fields omitted.
58
+ * @returns logical Session metadata paired with the exact inherited prefix length.
54
59
  */
55
60
  function fromHeaderLine(line) {
56
61
  if (Object.hasOwn(line, "sandboxMode") || Object.hasOwn(line, "approvalPolicy")) throw new Error("session header uses retired policy baseline fields");
57
62
  return {
58
- version: line.version,
59
- id: line.id,
60
- createdAt: line.createdAt,
61
- ...line.cwd !== void 0 ? { cwd: line.cwd } : {},
62
- ...line.parentSession !== void 0 ? { parentSession: line.parentSession } : {},
63
- ...line.seedLength !== void 0 ? { seedLength: line.seedLength } : {},
64
- ...line.origin !== void 0 ? { origin: line.origin } : {},
65
- delegationDepth: line.delegationDepth,
66
- ...line.agentPreset !== void 0 ? { agentPreset: line.agentPreset } : {}
63
+ meta: {
64
+ version: line.version,
65
+ id: line.id,
66
+ createdAt: line.createdAt,
67
+ ...line.cwd !== void 0 ? { cwd: line.cwd } : {},
68
+ ...line.parentSession !== void 0 ? { parentSession: line.parentSession } : {},
69
+ isSeeded: line.seedLength !== void 0,
70
+ ...line.origin !== void 0 ? { origin: line.origin } : {},
71
+ delegationDepth: line.delegationDepth,
72
+ ...line.agentPreset !== void 0 ? { agentPreset: line.agentPreset } : {}
73
+ },
74
+ inheritedEventCount: SessionLogOffset(line.seedLength ?? 0)
67
75
  };
68
76
  }
69
77
  /** Type guard: a parsed first line is a well-formed session header. */
70
78
  function isHeaderLine(value) {
71
- return typeof value === "object" && value !== null && 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.origin === void 0 || value.origin === "subagent") && (value.agentPreset === void 0 || typeof value.agentPreset === "string");
79
+ return typeof value === "object" && value !== null && 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.seedLength === void 0 || typeof value.seedLength === "number" && Number.isSafeInteger(value.seedLength) && value.seedLength >= 0 && !Object.is(value.seedLength, -0)) && (value.origin === void 0 || value.origin === "subagent") && (value.agentPreset === void 0 || typeof value.agentPreset === "string");
72
80
  }
73
81
  /**
74
82
  * Encode an arbitrary string as a single safe path segment, injectively over ALL JS (UTF-16)
@@ -176,7 +184,7 @@ function eventLines(events, packChunks) {
176
184
  * stays verbatim.
177
185
  * @param record - one stored record (event or packed row).
178
186
  * @returns the record with its provenance in storage form (widened from the
179
- * in-memory `number[]`; {@link expandProvenanceFromStorage} restores it).
187
+ * in-memory `SessionSeq[]`; {@link expandProvenanceFromStorage} restores it).
180
188
  */
181
189
  function encodeProvenanceForStorage(record) {
182
190
  if (!("sourceEventSeqs" in record)) return record;
@@ -186,7 +194,7 @@ function encodeProvenanceForStorage(record) {
186
194
  };
187
195
  }
188
196
  /**
189
- * Expand a parsed line's storage-form provenance back to `number[]`.
197
+ * Expand a parsed line's storage-form provenance back to `SessionSeq[]`.
190
198
  * @param parsed - the JSON-parsed value of one stored line.
191
199
  * @returns the value with provenance expanded.
192
200
  * @throws when the record or its storage-form provenance is malformed.
@@ -235,6 +243,7 @@ function parseHeaderRecord(record) {
235
243
  */
236
244
  var SessionLogScanner = class {
237
245
  meta;
246
+ inheritedEventCount;
238
247
  events = [];
239
248
  fragments = [];
240
249
  fragmentBytes = 0;
@@ -248,7 +257,9 @@ var SessionLogScanner = class {
248
257
  * @param headerRecord - the complete first JSONL record, including its newline.
249
258
  */
250
259
  constructor(headerRecord) {
251
- this.meta = parseHeaderRecord(headerRecord);
260
+ const parsed = parseHeaderRecord(headerRecord);
261
+ this.meta = parsed.meta;
262
+ this.inheritedEventCount = parsed.inheritedEventCount;
252
263
  this.inputBytes = headerRecord.length;
253
264
  this.committedBytes = headerRecord.length;
254
265
  }
@@ -287,7 +298,7 @@ var SessionLogScanner = class {
287
298
  return {
288
299
  inputBytes: this.inputBytes,
289
300
  committedBytes: this.committedBytes,
290
- eventCount: this.events.length
301
+ eventCount: SessionLogOffset(this.events.length)
291
302
  };
292
303
  }
293
304
  /**
@@ -298,6 +309,7 @@ var SessionLogScanner = class {
298
309
  this.finished = true;
299
310
  return {
300
311
  meta: this.meta,
312
+ inheritedEventCount: this.inheritedEventCount,
301
313
  events: this.events,
302
314
  committedBytes: this.committedBytes
303
315
  };
@@ -346,23 +358,30 @@ function scanLog(buffer) {
346
358
  return scanner.finish();
347
359
  }
348
360
  /**
349
- * Parse just the header line of a log into a {@link SessionHeader}, or
350
- * `undefined` if it is missing/not a header. Used by `list()` to read session
351
- * metadata WITHOUT parsing the whole log: a session picker scales with the
352
- * number of sessions, not the total size of every conversation.
361
+ * Parse just the header line of a log into logical metadata plus its exact
362
+ * inherited cut, or `undefined` if it is missing/not a header.
353
363
  * @param firstLine - the first line of a log file (without its trailing newline).
354
- * @returns the parsed header, or `undefined` when the line is not a well-formed session header.
364
+ * @returns parsed storage metadata, or `undefined` for a malformed header.
355
365
  */
356
- function parseHeaderMeta(firstLine) {
366
+ function parseHeader(firstLine) {
357
367
  let parsed;
358
368
  try {
359
369
  parsed = JSON.parse(firstLine);
360
370
  } catch {
361
371
  return;
362
372
  }
373
+ refuseForeignFormatVersion(parsed);
363
374
  if (!isHeaderLine(parsed)) return void 0;
364
375
  return fromHeaderLine(parsed);
365
376
  }
377
+ /**
378
+ * Parse only the logical header fields needed by lightweight listing.
379
+ * @param firstLine - first JSONL line without its trailing newline.
380
+ * @returns the logical Session header, or `undefined` for a malformed line.
381
+ */
382
+ function parseHeaderMeta(firstLine) {
383
+ return parseHeader(firstLine)?.meta;
384
+ }
366
385
  //#endregion
367
386
  //#region lib/types/zstd-private-decoder.js
368
387
  /**
@@ -837,8 +856,8 @@ var JsonlSessionPersistence = class extends SessionPersistence {
837
856
  path: logPath(this.root, meta.cwd, meta.id, this.compression)
838
857
  };
839
858
  }
840
- create(meta) {
841
- return this.coordinator.create(meta);
859
+ create(meta, inheritedEventCount) {
860
+ return this.coordinator.create(meta, inheritedEventCount);
842
861
  }
843
862
  ensureMaterialized(session) {
844
863
  return this.coordinator.ensureMaterialized(session);
@@ -922,10 +941,10 @@ var JsonlSessionPersistence = class extends SessionPersistence {
922
941
  }
923
942
  content = Buffer.concat(plaintexts).toString("utf8");
924
943
  } else content = buffer.toString("utf8");
925
- const meta = parseHeaderMeta(content.split("\n", 1)[0]);
926
- if (meta === void 0 || meta.id !== id) throw new Error(`corrupt session log: invalid header line in "${path}"`);
944
+ const storage = parseHeader(content.split("\n", 1)[0]);
945
+ if (storage === void 0 || storage.meta.id !== id) throw new Error(`corrupt session log: invalid header line in "${path}"`);
927
946
  return {
928
- meta,
947
+ ...storage,
929
948
  filename: "session.jsonl",
930
949
  content
931
950
  };
@@ -962,10 +981,11 @@ var JsonlSessionPersistence = class extends SessionPersistence {
962
981
  if (this.compression === "zstd") prefix = await this.readZstdPrefix(buffer, signal);
963
982
  else {
964
983
  signal?.throwIfAborted();
965
- const { meta, events, committedBytes } = scanLog(buffer);
984
+ const { meta, inheritedEventCount, events, committedBytes } = scanLog(buffer);
966
985
  signal?.throwIfAborted();
967
986
  prefix = {
968
987
  meta,
988
+ inheritedEventCount,
969
989
  events,
970
990
  ...committedBytes < buffer.byteLength ? { tornMarker: {
971
991
  truncateTo: committedBytes,
@@ -1023,6 +1043,7 @@ var JsonlSessionPersistence = class extends SessionPersistence {
1023
1043
  const prefix = scanner.finish();
1024
1044
  return {
1025
1045
  meta: prefix.meta,
1046
+ inheritedEventCount: prefix.inheritedEventCount,
1026
1047
  events: prefix.events
1027
1048
  };
1028
1049
  }
@@ -1040,6 +1061,7 @@ var JsonlSessionPersistence = class extends SessionPersistence {
1040
1061
  signal?.throwIfAborted();
1041
1062
  return {
1042
1063
  meta: recoveredPrefix.meta,
1064
+ inheritedEventCount: recoveredPrefix.inheritedEventCount,
1043
1065
  events: recoveredPrefix.events,
1044
1066
  tornMarker: {
1045
1067
  truncateTo: tornStart,
@@ -1055,21 +1077,22 @@ var JsonlSessionPersistence = class extends SessionPersistence {
1055
1077
  }
1056
1078
  }
1057
1079
  /** Durably append a batch, lazily materializing the file when not yet present. */
1058
- async appendBatch(meta, events, isMaterialized) {
1080
+ async appendBatch(storage, events, isMaterialized) {
1059
1081
  await this.ensureRootEncoding();
1060
- if (isMaterialized) await this.appendLines(meta, events);
1061
- else await this.materialize(meta, events);
1082
+ if (isMaterialized) await this.appendLines(storage.meta, events);
1083
+ else await this.materialize(storage, events);
1062
1084
  }
1063
1085
  /** Materialize a header-only JSONL artifact for an explicitly durable empty session. */
1064
- async materializeHeader(meta) {
1065
- await this.materialize(meta, []);
1086
+ async materializeHeader(storage) {
1087
+ await this.materialize(storage, []);
1066
1088
  }
1067
1089
  /**
1068
1090
  * Make a crash repair durable: truncate a torn tail, restore complete events
1069
1091
  * decoded from it, then append synthetic closers. Two fsync'd steps — the seam
1070
1092
  * does not require this to be atomic.
1071
1093
  */
1072
- async commitRepair(meta, tornMarker, closers) {
1094
+ async commitRepair(storage, tornMarker, closers) {
1095
+ const { meta } = storage;
1073
1096
  if (tornMarker !== void 0) await this.repair(meta, tornMarker.truncateTo);
1074
1097
  const repairedEvents = [...tornMarker?.recoveredEvents ?? [], ...closers];
1075
1098
  if (repairedEvents.length > 0) await this.appendLines(meta, repairedEvents);
@@ -1136,12 +1159,13 @@ var JsonlSessionPersistence = class extends SessionPersistence {
1136
1159
  return artifacts;
1137
1160
  }
1138
1161
  /** Atomically write the header line + first batch (temp-write, fsync, publish). */
1139
- async materialize(meta, events) {
1162
+ async materialize(storage, events) {
1163
+ const { meta } = storage;
1140
1164
  const project = projectDir(this.root, meta.cwd);
1141
1165
  const dir = sessionDir(this.root, meta.cwd, meta.id);
1142
1166
  const finalPath = logPath(this.root, meta.cwd, meta.id, this.compression);
1143
1167
  await this.rejectOppositeArtifact(meta.cwd, meta.id);
1144
- const content = await this.encodeMaterialization(meta, events);
1168
+ const content = await this.encodeMaterialization(storage, events);
1145
1169
  /* v8 ignore next -- native Windows coverage exercises this platform dispatch; Linux covers the POSIX peer */
1146
1170
  if (process.platform === "win32") await this.materializeWin32(project, dir, finalPath, meta.id, content);
1147
1171
  else await this.materializePosix(project, dir, finalPath, meta.id, content);
@@ -1210,8 +1234,8 @@ var JsonlSessionPersistence = class extends SessionPersistence {
1210
1234
  return tmp;
1211
1235
  }
1212
1236
  /** Encode the header and first batch without combining their frame boundaries. */
1213
- async encodeMaterialization(meta, events) {
1214
- const header = JSON.stringify(toHeaderLine(meta)) + "\n";
1237
+ async encodeMaterialization(storage, events) {
1238
+ const header = JSON.stringify(toHeaderLine(storage.meta, storage.inheritedEventCount)) + "\n";
1215
1239
  if (events.length === 0) return this.compression === "none" ? header : compressZstdFrame(header);
1216
1240
  const body = eventLines(events, this.packChunks) + "\n";
1217
1241
  if (this.compression === "none") return header + body;
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * @module dsh-session-persistence-jsonl/format
9
9
  */
10
- import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
10
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session';
11
+ import { type SessionStorageMetadata } from '@deepseek-ai/dsh-session-persistence';
11
12
  /** Physical encoding selected for JSONL session artifacts. */
12
13
  export type JsonlCompression = 'zstd' | 'none';
13
14
  /**
@@ -17,11 +18,11 @@ export type JsonlCompression = 'zstd' | 'none';
17
18
  */
18
19
  export declare function logSuffix(compression: JsonlCompression): '.jsonl.zstd' | '.jsonl';
19
20
  /**
20
- * The first JSONL record of a session artifact: the immutable
21
- * {@link SessionHeader} tagged as a `session` record so a reader can tell it
22
- * apart from an event line.
21
+ * The private version-0 physical header stored as the first JSONL record.
22
+ * Its optional numeric `seedLength` translates to logical lineage metadata
23
+ * plus a separately carried exact inherited cut.
23
24
  */
24
- export interface HeaderLine {
25
+ interface HeaderLine {
25
26
  type: 'session';
26
27
  version: number;
27
28
  id: SessionId;
@@ -36,15 +37,11 @@ export interface HeaderLine {
36
37
  /**
37
38
  * Build the header line object from a {@link SessionHeader}.
38
39
  * @param header - the immutable session metadata to serialize.
40
+ * @param inheritedEventCount - exact inherited prefix length; required for a
41
+ * seeded header and omitted only for an unseeded header.
39
42
  * @returns the `type: 'session'`-tagged line object, absent optional fields omitted (never null).
40
43
  */
41
- export declare function toHeaderLine(header: SessionHeader): HeaderLine;
42
- /**
43
- * Parse a header line back into a {@link SessionHeader}.
44
- * @param line - the shape-checked first line of a log (see the `isHeaderLine` guard).
45
- * @returns the header, absent optional fields omitted.
46
- */
47
- export declare function fromHeaderLine(line: HeaderLine): SessionHeader;
44
+ export declare function toHeaderLine(header: SessionHeader, inheritedEventCount?: SessionLogOffsetType): HeaderLine;
48
45
  /**
49
46
  * Encode an arbitrary string as a single safe path segment, injectively over ALL JS (UTF-16)
50
47
  * strings — including lone surrogates. A {@link SessionId} is an unvalidated branded string,
@@ -107,6 +104,7 @@ export declare function logPath(root: string, cwd: string | undefined, id: Sessi
107
104
  export declare function eventLines(events: readonly SessionEvent[], packChunks: boolean): string;
108
105
  interface SessionLogScan {
109
106
  meta: SessionHeader;
107
+ inheritedEventCount: SessionLogOffsetType;
110
108
  events: SessionEvent[];
111
109
  committedBytes: number;
112
110
  }
@@ -118,6 +116,7 @@ interface SessionLogScan {
118
116
  */
119
117
  export declare class SessionLogScanner {
120
118
  private readonly meta;
119
+ private readonly inheritedEventCount;
121
120
  private readonly events;
122
121
  private fragments;
123
122
  private fragmentBytes;
@@ -143,7 +142,7 @@ export declare class SessionLogScanner {
143
142
  checkpoint(): {
144
143
  inputBytes: number;
145
144
  committedBytes: number;
146
- eventCount: number;
145
+ eventCount: SessionLogOffsetType;
147
146
  };
148
147
  /**
149
148
  * Finish scanning, ignoring a final record without a newline as a torn tail.
@@ -163,12 +162,16 @@ export declare class SessionLogScanner {
163
162
  */
164
163
  export declare function scanLog(buffer: Buffer): SessionLogScan;
165
164
  /**
166
- * Parse just the header line of a log into a {@link SessionHeader}, or
167
- * `undefined` if it is missing/not a header. Used by `list()` to read session
168
- * metadata WITHOUT parsing the whole log: a session picker scales with the
169
- * number of sessions, not the total size of every conversation.
165
+ * Parse just the header line of a log into logical metadata plus its exact
166
+ * inherited cut, or `undefined` if it is missing/not a header.
170
167
  * @param firstLine - the first line of a log file (without its trailing newline).
171
- * @returns the parsed header, or `undefined` when the line is not a well-formed session header.
168
+ * @returns parsed storage metadata, or `undefined` for a malformed header.
169
+ */
170
+ export declare function parseHeader(firstLine: string): SessionStorageMetadata | undefined;
171
+ /**
172
+ * Parse only the logical header fields needed by lightweight listing.
173
+ * @param firstLine - first JSONL line without its trailing newline.
174
+ * @returns the logical Session header, or `undefined` for a malformed line.
172
175
  */
173
176
  export declare function parseHeaderMeta(firstLine: string): SessionHeader | undefined;
174
177
  export {};
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import { Context } from '@deepseek-ai/cordis';
9
9
  import z from '@deepseek-ai/schemastery';
10
- import { SessionPersistence, type BorrowedSessionSource, type PersistenceBackend, type SessionLocation, type SessionPersistenceSnapshot, type SessionInspection, type SessionPersistenceRevision as PersistenceRevision, type SessionRawArtifact, type StoredPrefix } from '@deepseek-ai/dsh-session-persistence';
11
- import type { Session, SessionEvent, SessionId, SessionHeader, SessionPreparation } from '@deepseek-ai/dsh-session';
10
+ import { SessionPersistence, type BorrowedSessionSource, type PersistenceBackend, type SessionLocation, type SessionPersistenceSnapshot, type SessionEventSuffix, type SessionInspection, type SessionPersistenceRevision as PersistenceRevision, type SessionRawArtifact, type SessionStorageMetadata, type StoredPrefix } from '@deepseek-ai/dsh-session-persistence';
11
+ import type { Session, SessionEvent, SessionId, SessionHeader, SessionLogOffset, SessionPreparation } from '@deepseek-ai/dsh-session';
12
12
  import { type JsonlCompression } from './format.ts';
13
13
  export type { JsonlCompression } from './format.ts';
14
14
  /** Loader schema for the JSONL artifact's physical encoding. */
@@ -68,17 +68,14 @@ export declare class JsonlSessionPersistence extends SessionPersistence implemen
68
68
  constructor(ctx: Context, config: Config);
69
69
  /** Resolve the absolute target path without touching the filesystem. */
70
70
  locate(meta: SessionHeader): SessionLocation;
71
- create(meta: SessionHeader): Promise<void>;
71
+ create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>;
72
72
  ensureMaterialized(session: Session): Promise<void>;
73
73
  append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
74
74
  prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation>;
75
75
  load(id: SessionId): Promise<SessionInspection>;
76
76
  inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>;
77
77
  borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSessionSource>;
78
- readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{
79
- meta: SessionHeader;
80
- events: SessionEvent[];
81
- }>;
78
+ readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>;
82
79
  /** Read a stored prefix by id across all project directories when cwd is unknown. */
83
80
  loadStored(id: SessionId, signal?: AbortSignal): Promise<StoredPrefix<JsonlTornMarker> | undefined>;
84
81
  /**
@@ -117,15 +114,15 @@ export declare class JsonlSessionPersistence extends SessionPersistence implemen
117
114
  /** Decode complete frames and retain complete JSONL records from a torn final frame. */
118
115
  private readZstdPrefix;
119
116
  /** Durably append a batch, lazily materializing the file when not yet present. */
120
- appendBatch(meta: SessionHeader, events: readonly SessionEvent[], isMaterialized: boolean): Promise<void>;
117
+ appendBatch(storage: SessionStorageMetadata, events: readonly SessionEvent[], isMaterialized: boolean): Promise<void>;
121
118
  /** Materialize a header-only JSONL artifact for an explicitly durable empty session. */
122
- materializeHeader(meta: SessionHeader): Promise<void>;
119
+ materializeHeader(storage: SessionStorageMetadata): Promise<void>;
123
120
  /**
124
121
  * Make a crash repair durable: truncate a torn tail, restore complete events
125
122
  * decoded from it, then append synthetic closers. Two fsync'd steps — the seam
126
123
  * does not require this to be atomic.
127
124
  */
128
- commitRepair(meta: SessionHeader, tornMarker: JsonlTornMarker | undefined, closers: readonly SessionEvent[]): Promise<void>;
125
+ commitRepair(storage: SessionStorageMetadata, tornMarker: JsonlTornMarker | undefined, closers: readonly SessionEvent[]): Promise<void>;
129
126
  /** List valid unique stored sessions' metadata (header line only — no full-log parse). */
130
127
  list(signal?: AbortSignal): Promise<SessionHeader[]>;
131
128
  /** List metadata plus a stat-derived identity for each append-only log. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-persistence-jsonl",
3
3
  "description": "JSONL durable session persistence backend for the DeepSeek Harness",
4
- "version": "0.1.2-alpha.3",
4
+ "version": "0.1.2-alpha.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,33 +18,26 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
24
- },
25
21
  "./src/*": "./src/*",
26
22
  "./package.json": "./package.json"
27
23
  },
28
24
  "files": [
29
25
  "lib/index.js",
30
- "lib/invariant.js",
31
26
  "lib/types/**/*.d.ts"
32
27
  ],
33
28
  "license": "MIT",
34
29
  "peerDependencies": {
35
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
36
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3",
37
- "@deepseek-ai/cordis": "^4.0.2",
38
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.3"
30
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
31
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
32
+ "@deepseek-ai/cordis": "^4.0.2"
39
33
  },
40
34
  "dependencies": {
41
35
  "koffi": "^3.1.0",
42
36
  "@deepseek-ai/schemastery": "^3.18.2"
43
37
  },
44
38
  "devDependencies": {
45
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
46
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
47
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3",
39
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
40
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
48
41
  "@deepseek-ai/cordis": "^4.0.2"
49
42
  }
50
43
  }
package/lib/invariant.js DELETED
@@ -1,23 +0,0 @@
1
- //#region lib/types/invariant.js
2
- /**
3
- * Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence-jsonl`.
4
- * @module @deepseek-ai/dsh-session-persistence-jsonl/invariant
5
- */
6
- const PACKAGE_NAME = "@deepseek-ai/dsh-session-persistence-jsonl";
7
- /** Cordis companion plugin name. */
8
- const name = "session-persistence-jsonl-invariant";
9
- /** Service required before the companion can reserve package ownership. */
10
- const inject = ["invariants"];
11
- /**
12
- * No runtime invariant: persistence correctness requires backend round-trip and crash-tail tests;
13
- * this package exposes no continuously observable in-process relation.
14
- */
15
- const install = () => {};
16
- /**
17
- * Register this package's invariant companion.
18
- * @param ctx - Cordis context carrying the invariant service.
19
- * @returns the installed registration's disposer after setup succeeds.
20
- */
21
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
- //#endregion
23
- export { apply, inject, name };
@@ -1,16 +0,0 @@
1
- /**
2
- * Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence-jsonl`.
3
- * @module @deepseek-ai/dsh-session-persistence-jsonl/invariant
4
- */
5
- import type { Context } from '@deepseek-ai/cordis';
6
- /** Cordis companion plugin name. */
7
- export declare const name = "session-persistence-jsonl-invariant";
8
- /** Service required before the companion can reserve package ownership. */
9
- export declare const inject: string[];
10
- /**
11
- * Register this package's invariant companion.
12
- * @param ctx - Cordis context carrying the invariant service.
13
- * @returns the installed registration's disposer after setup succeeds.
14
- */
15
- export declare const apply: (ctx: Context) => Promise<() => void>;
16
- //# sourceMappingURL=invariant.d.ts.map