@deepseek-ai/dsh-session-persistence-jsonl 0.1.3-alpha.2 → 0.1.5-alpha.1

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: cce62bf98f1e906d8800b71e215e53b96bee8044
6
- README.zh.md: 6bc5f574ecd7266375039858cc9974e47618cc74
5
+ README.md: 76d4c635d52d705adc401120a8acbee0fb86adf4
6
+ README.zh.md: e2ed4465f1132c43ee7d13e1d36e8e34d6d53a5b
package/README.md CHANGED
@@ -73,6 +73,8 @@ Session ids are injectively escaped to one safe path segment before use (no trav
73
73
 
74
74
  A session is materialized lazily: `create(header)` writes nothing and returns the owned write handle, and the handle's first `append` writes and `fsync`s the encoded header and first batch through a no-overwrite publish — so a created-but-never-appended session leaves nothing on disk unless its owner calls `handle.flush()`, which publishes one header frame without an event. Each subsequent batch appends lines or one compressed frame and `fsync`s before the append resolves; a caught write or sync failure rolls the file back to its prior length. Committed events are never rewritten. After a crash, the stored log keeps its interrupted final turn — every record in the committed prefix survives, and the resuming reader appends synthetic closers through its write handle. An incomplete final raw line is discarded. A torn final Zstandard frame contributes only its complete decoded JSONL records; a write handle truncates the torn bytes and durably rewrites those recovered records before its first new batch. Checksum, decompression, or structural failure in a complete committed frame rejects as corruption.
75
75
 
76
+ The current-generation scanner applies the current codec owner’s structural admission checks before recoverable-tail handling. Retired required PTC tags and `request/header.header.system` refuse the file even after an earlier malformed row; recovery never truncates them as ordinary damaged tail data.
77
+
76
78
  ### Reading the logs
77
79
 
78
80
  `open(id, 'read'|'write')` selects the highest canonical generation. Current input follows the ordinary fast path. For historical input, a read open decodes and migrates the source once, validates the current logical result, and returns it without publishing a successor. A write open reuses that revision-keyed preparation when available, or performs the same preparation, then encodes a same-directory temporary file in bounded chunks, verifies it in a Worker Thread, rechecks the source revision, and publishes the current successor without overwrite before returning. The source remains byte-identical. Source drift after preparation rejects that write open without replacing the logical history already returned to readers; a later write open prepares the new revision. The backend marks decoded event graphs `shared-frozen` when it freezes them before memoization; handle reads and slices preserve that state, including empty slices. Only an unmaterialized pending log reports `detached`. `stat(id)` and `list()` select and translate only the highest generation header without reading event rows or starting migration; snapshots carry `sizeBytes` and a best-effort stat-derived revision for the selected file. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend.
@@ -155,6 +157,7 @@ These limits define when this backend is a poor fit or needs special operational
155
157
  - **Nothing deletes session files** — logs accumulate under `root` until removed externally; the seam has no deletion API.
156
158
  - **One live writer per session** — the write-handle claim excludes a second writer inside the owning backend instance, and a kernel lock (non-blocking `flock(2)` on `session.lock`; on Windows a named kernel semaphore derived from that path, with no filesystem footprint) excludes every other instance and process; the lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write, so an unmaterialized session leaves no filesystem footprint. A crashed holder's lock dies with its process, so its session is writable again immediately, while a live-but-wedged holder blocks writers until its process exits (on POSIX, removing the lock file forfeits that exclusion; release itself never removes it). Advisory `flock` is unreliable on some network filesystems (NFSv3), and the Windows semaphore name is per login session.
157
159
  - **POSIX materialization requires hard-link support** — first append uses `link()` so same-id races fail instead of overwriting a committed log; Windows uses write-through rename without replacement.
160
+ - **POSIX writes require the matching prebuilt system addon** — [`node-addon-system`](../../../native/system/README.md) supplies asynchronous flock without consumer-side compilation. A missing addon rejects write ownership; Windows retains its semaphore implementation.
158
161
 
159
162
  <a id="dev-note"></a>
160
163
  ### Dev Note
package/README.zh.md CHANGED
@@ -73,6 +73,8 @@ kind: "package-reference"
73
73
 
74
74
  会话延迟实体化:`create(header)` 不写入任何内容并返回持有的写句柄,句柄的第一次 `append` 通过无覆盖发布写入并 `fsync` 编码后的 header 与第一批——因此已创建但从未 append 的会话不留下任何磁盘内容,除非其所有者调用 `handle.flush()`,以无事件的单个 header 帧发布它。后续每个批次追加行或一个压缩帧,并在 append 完成前 `fsync`;捕获到写入或同步失败时把文件回滚到之前的字节长度。已提交事件绝不重写。崩溃后,已存储日志保留被中断的最终轮次——已提交前缀中的每条记录都保留下来,由执行恢复的读方通过其写句柄追加合成 closer。不完整的最终原始行会被丢弃。撕裂的最终 Zstandard 帧只贡献其中完整解码出的 JSONL 记录;写句柄会截掉撕裂字节,并在第一次新批次之前持久重写这些恢复出的记录。完整已提交帧中的校验和、解压或结构失败以损坏拒绝。
75
75
 
76
+ 当前代际扫描器在处理可恢复尾部之前,执行当前编解码器所有者的结构准入检查。已退役的必需 PTC 标签与 `request/header.header.system` 即使出现在较早的畸形行之后也会导致文件被拒绝;恢复绝不将它们作为普通损坏尾部数据截断。
77
+
76
78
  ### 读取日志
77
79
 
78
80
  `open(id, 'read'|'write')` 选择最高规范 generation。当前格式输入走普通快速路径。对于历史输入,只读 open 会单遍解码并迁移源、校验当前逻辑结果,然后在不发布后继的情况下返回。写 open 会在可用时复用按 revision 为键的 preparation,否则执行同一套 preparation,再按有界分片编码同目录临时文件、在 Worker Thread 中校验、复查源修订,并在返回前以不覆盖方式发布当前后继。源保持逐字节不变。如果源在 preparation 后发生变化,该次写 open 会失败,已经返回给读方的逻辑历史不会被替换;后续写 open 会针对新的 revision 重新执行 preparation。Backend 在 memo 化前冻结已解码的 event graph,并在此时将其标记为 `shared-frozen`;句柄读取和 slice 即使为空也保留该状态。只有尚未实体化的 pending 空日志报告 `detached`。`stat(id)` 与 `list()` 只选择并转换最高 generation 的 header,不读取事件行,也不启动迁移;快照携带所选文件的 `sizeBytes` 与尽力而为的 stat 派生修订号。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。
@@ -155,6 +157,7 @@ JSONL 存储不修改实时请求前缀。只有重建历史、当前 envelope
155
157
  - **不删除会话文件**——日志在 `root` 下累积,直到外部移除;seam 无删除接口。
156
158
  - **每会话一个活动写入方**——写句柄认领在所属后端实例内排除第二个写入方,内核锁(`session.lock` 上的非阻塞 `flock(2)`;Windows 上为由该路径派生的命名内核信号量,零文件系统足迹)排除其他所有实例与进程;锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取,因此未物化的会话不留任何文件系统足迹。崩溃持有者的锁随其进程消亡,会话立即可再写入,而活着但卡死的持有者会阻塞写入方直到其进程退出(POSIX 上删除锁文件即放弃该排他;释放本身从不删除它)。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠,Windows 信号量名按登录会话隔离。
157
159
  - **POSIX 实体化需要硬链接支持**——第一次 append 使用 `link()`,使同 id 竞态失败而不覆盖已提交日志;Windows 使用无替换 write-through rename。
160
+ - **POSIX 写入需要匹配的预编译系统 addon**——[`node-addon-system`](../../../native/system/README.zh.md) 提供异步 flock,无须在用户侧编译。addon 缺失时拒绝写入所有权;Windows 保留其信号量实现。
158
161
 
159
162
  <a id="dev-note"></a>
160
163
  ### 开发备注
package/lib/index.js CHANGED
@@ -8,9 +8,10 @@ import { scheduler } from "node:timers/promises";
8
8
  import { createHash, randomBytes } from "node:crypto";
9
9
  import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceCorruptionError, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertStoredId, materializeAppendBatch, materializeCreateHeader, sessionFormatVersionRefusal, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
10
10
  import { BlockAssembler, errorChain, expandAssistantStream } from "@deepseek-ai/dsh-llm";
11
- import { flock } from "fs-ext";
11
+ import { tryLockExclusive } from "@deepseek-ai/node-addon-system/flock";
12
12
  import { SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
13
- import { parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
13
+ import { SessionFormatUnsupportedMigrationError as SessionFormatUnsupportedMigrationError$1, parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
14
+ import { assertV3RowAdmission } from "@deepseek-ai/dsh-session-format-v2-to-v3";
14
15
  import { constants, createZstdCompress, createZstdDecompress, zstdCompress, zstdDecompress, zstdDecompressSync } from "node:zlib";
15
16
  import { isDeepStrictEqual, promisify } from "node:util";
16
17
  import { constants as constants$1 } from "node:buffer";
@@ -611,7 +612,7 @@ async function createLeafDirectoryWin32(parent, target) {
611
612
  /**
612
613
  * Cross-process write-ownership lock for one session's artifact directory,
613
614
  * held for the whole life of a write handle. The arbiter is the kernel:
614
- * POSIX takes a non-blocking `flock(2)` (through fs-ext) on `session.lock`
615
+ * POSIX takes a non-blocking `flock(2)` via native system support on `session.lock`
615
616
  * beside the log, and Windows holds a named kernel semaphore derived from
616
617
  * that path — never a file lock or handle, so readers, searches, and
617
618
  * directory removal proceed freely while the lock is held. Contention maps
@@ -632,22 +633,13 @@ async function createLeafDirectoryWin32(parent, target) {
632
633
  * unmaterialized session has no filesystem footprint. Release never removes
633
634
  * the POSIX lock file: every acquired lock belongs to a materialized or
634
635
  * materializing session, and the surviving file keeps the stable inode later
635
- * lockers verify against. The browser worker deployment stubs fs-ext to
636
+ * lockers verify against. The browser worker stubs the native flock entry to
636
637
  * immediate success: it is single-process, so the in-process write claim
637
638
  * already excludes every writer.
638
639
  * @module @deepseek-ai/dsh-session-persistence-jsonl/lease
639
640
  */
640
641
  /** Base name of the kernel lock file inside a session's directory. */
641
642
  const LEASE_FILENAME = "session.lock";
642
- /** Promise face over fs-ext's callback flock, pinned to its string-flag overload. */
643
- function flockAsync(fd, flags) {
644
- return new Promise((resolve, reject) => {
645
- flock(fd, flags, (error) => {
646
- if (error) reject(error);
647
- else resolve();
648
- });
649
- });
650
- }
651
643
  /** Whether a flock failure means another descriptor holds the lock. */
652
644
  function isLockContention(error) {
653
645
  const code = error?.code;
@@ -695,7 +687,7 @@ var SessionWriteLease = class SessionWriteLease {
695
687
  const handle = await open(path, "w");
696
688
  try {
697
689
  try {
698
- await flockAsync(handle.fd, "exnb");
690
+ await tryLockExclusive(handle.fd);
699
691
  } catch (error) {
700
692
  if (isLockContention(error)) throw new SessionAlreadyOwnedError(id);
701
693
  throw error;
@@ -1095,6 +1087,12 @@ var SessionLogScanner = class {
1095
1087
  this.issue ??= issue;
1096
1088
  return;
1097
1089
  }
1090
+ try {
1091
+ assertV3RowAdmission(decoded);
1092
+ } catch (error) {
1093
+ if (error instanceof SessionFormatUnsupportedMigrationError$1) throw new SessionFormatUnsupportedError(error.message);
1094
+ throw error;
1095
+ }
1098
1096
  if (this.issue !== void 0) {
1099
1097
  if (typeof decoded === "object" && decoded !== null && decoded.type === "turn/end") throw this.issue;
1100
1098
  return;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Cross-process write-ownership lock for one session's artifact directory,
3
3
  * held for the whole life of a write handle. The arbiter is the kernel:
4
- * POSIX takes a non-blocking `flock(2)` (through fs-ext) on `session.lock`
4
+ * POSIX takes a non-blocking `flock(2)` via native system support on `session.lock`
5
5
  * beside the log, and Windows holds a named kernel semaphore derived from
6
6
  * that path — never a file lock or handle, so readers, searches, and
7
7
  * directory removal proceed freely while the lock is held. Contention maps
@@ -22,7 +22,7 @@
22
22
  * unmaterialized session has no filesystem footprint. Release never removes
23
23
  * the POSIX lock file: every acquired lock belongs to a materialized or
24
24
  * materializing session, and the surviving file keeps the stable inode later
25
- * lockers verify against. The browser worker deployment stubs fs-ext to
25
+ * lockers verify against. The browser worker stubs the native flock entry to
26
26
  * immediate success: it is single-process, so the in-process write claim
27
27
  * already excludes every writer.
28
28
  * @module @deepseek-ai/dsh-session-persistence-jsonl/lease
package/lib/worker.cjs CHANGED
@@ -12,6 +12,7 @@ let _deepseek_ai_dsh_llm = require("@deepseek-ai/dsh-llm");
12
12
  let _deepseek_ai_dsh_session_persistence = require("@deepseek-ai/dsh-session-persistence");
13
13
  let _deepseek_ai_dsh_session_format = require("@deepseek-ai/dsh-session-format");
14
14
  let _deepseek_ai_dsh_session_format_catalog = require("@deepseek-ai/dsh-session-format-catalog");
15
+ let _deepseek_ai_dsh_session_format_v2_to_v3 = require("@deepseek-ai/dsh-session-format-v2-to-v3");
15
16
  let node_buffer = require("node:buffer");
16
17
  //#region lib/types/format.js
17
18
  /**
@@ -228,6 +229,12 @@ var SessionLogScanner = class {
228
229
  this.issue ??= issue;
229
230
  return;
230
231
  }
232
+ try {
233
+ (0, _deepseek_ai_dsh_session_format_v2_to_v3.assertV3RowAdmission)(decoded);
234
+ } catch (error) {
235
+ if (error instanceof _deepseek_ai_dsh_session_format.SessionFormatUnsupportedMigrationError) throw new _deepseek_ai_dsh_session_persistence.SessionFormatUnsupportedError(error.message);
236
+ throw error;
237
+ }
231
238
  if (this.issue !== void 0) {
232
239
  if (typeof decoded === "object" && decoded !== null && decoded.type === "turn/end") throw this.issue;
233
240
  return;
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.3-alpha.2",
4
+ "version": "0.1.5-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,23 +28,23 @@
28
28
  ],
29
29
  "license": "MIT",
30
30
  "peerDependencies": {
31
- "@deepseek-ai/cordis": "^4.0.2",
32
- "@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
33
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
31
+ "@deepseek-ai/dsh-session-persistence": "^0.1.5-alpha.1",
32
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
33
+ "@deepseek-ai/cordis": "^4.0.2"
34
34
  },
35
35
  "dependencies": {
36
- "fs-ext": "2.1.1",
37
36
  "koffi": "^3.1.0",
38
- "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
39
- "@deepseek-ai/dsh-session-format": "^0.1.3-alpha.2",
40
- "@deepseek-ai/dsh-session-format-catalog": "^0.1.3-alpha.2",
41
- "@deepseek-ai/schemastery": "^3.18.2"
37
+ "@deepseek-ai/dsh-session-format": "^0.1.5-alpha.1",
38
+ "@deepseek-ai/dsh-session-format-catalog": "^0.1.5-alpha.1",
39
+ "@deepseek-ai/node-addon-system": "^0.1.2",
40
+ "@deepseek-ai/schemastery": "^3.18.2",
41
+ "@deepseek-ai/dsh-session-format-v2-to-v3": "^0.1.5-alpha.1",
42
+ "@deepseek-ai/dsh-llm": "^0.1.5-alpha.1"
42
43
  },
43
44
  "devDependencies": {
44
- "@types/fs-ext": "2.0.3",
45
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
46
- "@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
47
- "@deepseek-ai/dsh-session-format-v0-to-v1": "^0.1.3-alpha.2",
45
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
46
+ "@deepseek-ai/dsh-session-format-v0-to-v1": "^0.1.5-alpha.1",
47
+ "@deepseek-ai/dsh-session-persistence": "^0.1.5-alpha.1",
48
48
  "@deepseek-ai/cordis": "^4.0.2"
49
49
  }
50
50
  }