@deepseek-ai/dsh-session-persistence-jsonl 0.1.2-alpha.2 → 0.1.2-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +4 -5
- package/README.zh.md +4 -5
- package/lib/index.js +65 -41
- package/lib/types/format.d.ts +21 -18
- package/lib/types/index.d.ts +7 -10
- package/package.json +6 -13
- package/lib/invariant.js +0 -23
- package/lib/types/invariant.d.ts +0 -16
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: d3dfaf8affb142979ede69d86bbf0b1587e8c39c
|
|
6
|
+
README.zh.md: 15fc815523355674a1f325b604dd2655f56f7f58
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Mount this backend when a composition needs durable sessions backed by per-sessi
|
|
|
29
29
|
|
|
30
30
|
### When to choose it
|
|
31
31
|
|
|
32
|
-
Choose this backend when consumers benefit from one artifact per session — navigation, external tooling, or a raw line-readable log.
|
|
32
|
+
Choose this backend when consumers benefit from one artifact per session — navigation, external tooling, or a raw line-readable log. It is the sole first-party Session-persistence provider. The backend keeps sessions under a deployment-controlled root: project-local, shared, temporary, or centralized.
|
|
33
33
|
|
|
34
34
|
### Minimal configuration
|
|
35
35
|
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
|
@@ -113,7 +113,6 @@ Read these pages when the package-level contract is not enough. They move from t
|
|
|
113
113
|
|
|
114
114
|
- [Session persistence subsystem](../../../docs/subsystems/persistence.md) — backend-neutral service semantics and provider relationships.
|
|
115
115
|
- [Session persistence seam](../session-persistence/README.md) — the service contract this backend implements.
|
|
116
|
-
- [SQLite persistence backend](../session-persistence-sqlite/README.md) — the opt-in single-database alternative.
|
|
117
116
|
- [Project-session directory decision](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.md) — the layout tradeoff behind project and session directories.
|
|
118
117
|
- [Zstandard JSONL session logs](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md) — the checksummed-frame encoding rationale.
|
|
119
118
|
|
package/README.zh.md
CHANGED
|
@@ -29,7 +29,7 @@ kind: "package-reference"
|
|
|
29
29
|
|
|
30
30
|
### 何时选择
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
当消费方受益于每会话一份产物——导航、外部工具或可逐行读取的原始日志——时选择此后端。它是随产品交付的唯一 Session 持久化 provider。后端把会话保存在部署控制的根下:项目本地、共享、临时或集中式。
|
|
33
33
|
|
|
34
34
|
### 最小配置
|
|
35
35
|
|
|
@@ -54,7 +54,7 @@ kind: "package-reference"
|
|
|
54
54
|
|
|
55
55
|
### 磁盘布局
|
|
56
56
|
|
|
57
|
-
|
|
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)`
|
|
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
|
-
|
|
|
103
|
+
| — | 不发布运行时不变式伴生入口;身份在存储层强制。 |
|
|
104
104
|
|
|
105
105
|
</details>
|
|
106
106
|
|
|
@@ -113,7 +113,6 @@ kind: "package-reference"
|
|
|
113
113
|
|
|
114
114
|
- [会话持久化子系统](../../../docs/subsystems/persistence.zh.md)——后端无关的服务语义与提供方关系。
|
|
115
115
|
- [会话持久化 seam](../session-persistence/README.zh.md)——本后端实现的服务约定。
|
|
116
|
-
- [SQLite 持久化后端](../session-persistence-sqlite/README.zh.md)——可选启用的单数据库替代方案。
|
|
117
116
|
- [项目会话目录决策](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.zh.md)——项目与会话目录布局背后的取舍。
|
|
118
117
|
- [Zstandard JSONL 会话日志](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md)——带校验和帧编码的理由。
|
|
119
118
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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
|
|
350
|
-
* `undefined` if it is missing/not a header.
|
|
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
|
|
364
|
+
* @returns parsed storage metadata, or `undefined` for a malformed header.
|
|
355
365
|
*/
|
|
356
|
-
function
|
|
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
|
|
926
|
-
if (
|
|
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
|
-
|
|
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(
|
|
1080
|
+
async appendBatch(storage, events, isMaterialized) {
|
|
1059
1081
|
await this.ensureRootEncoding();
|
|
1060
|
-
if (isMaterialized) await this.appendLines(meta, events);
|
|
1061
|
-
else await this.materialize(
|
|
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(
|
|
1065
|
-
await this.materialize(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
package/lib/types/format.d.ts
CHANGED
|
@@ -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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
|
167
|
-
* `undefined` if it is missing/not a header.
|
|
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
|
|
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 {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
4
|
+
"version": "0.1.2-alpha.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,23 +18,17 @@
|
|
|
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-
|
|
36
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.
|
|
37
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
|
|
30
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.4",
|
|
31
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.4",
|
|
38
32
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
39
33
|
},
|
|
40
34
|
"dependencies": {
|
|
@@ -42,9 +36,8 @@
|
|
|
42
36
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
43
37
|
},
|
|
44
38
|
"devDependencies": {
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.
|
|
47
|
-
"@deepseek-ai/cordis": "^4.0.2"
|
|
48
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2"
|
|
39
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.4",
|
|
40
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.4",
|
|
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 };
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -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
|