@deepseek-ai/dsh-session-persistence-jsonl 0.2.0-rc.2 → 0.2.1-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/README.i18n.yaml CHANGED
@@ -30,8 +30,8 @@
30
30
  en: 1eaa87736cac029b
31
31
  zh: 26224e8889786d69
32
32
  /deepseek-ai-dsh-session-persistence-jsonl/use-this-package/reading-the-logs:
33
- en: c06983c4a73bebe1
34
- zh: 7319f8f44223685f
33
+ en: 3e53fc90a486d373
34
+ zh: 00e6c6e65c56629e
35
35
  /deepseek-ai-dsh-session-persistence-jsonl/understand-the-implementation:
36
36
  en: a35407bb25671e7e
37
37
  zh: 6a00f9510c85adcb
@@ -39,14 +39,14 @@
39
39
  en: b69d4565a11b6f02
40
40
  zh: c8309690fc931f8d
41
41
  /deepseek-ai-dsh-session-persistence-jsonl/understand-the-implementation/physical-encoding:
42
- en: 011d56563b81acd9
43
- zh: 69122dbade603fe9
42
+ en: 5aaf80115840318f
43
+ zh: 0b0fb2b2e085a111
44
44
  /deepseek-ai-dsh-session-persistence-jsonl/understand-the-implementation/source-map:
45
- en: 3bdb0f1dd4d69fba
46
- zh: 8198d655d73d42e3
45
+ en: 2a4fd4bde1da0a1b
46
+ zh: d7a8cbdad7beee09
47
47
  /deepseek-ai-dsh-session-persistence-jsonl/further-exploration:
48
- en: 827c04be7cd01514
49
- zh: 5e87603b3463db38
48
+ en: 71fbff06a098440a
49
+ zh: d3c07db42668ea18
50
50
  /deepseek-ai-dsh-session-persistence-jsonl/model-experience:
51
51
  en: 215e7ba838619b7b
52
52
  zh: a311da3843709f90
package/README.md CHANGED
@@ -79,8 +79,12 @@ The current-generation scanner applies the current codec owner’s structural ad
79
79
 
80
80
  ### Reading the logs
81
81
 
82
+ Materialized `stat` and `list` snapshots include the format catalog's header classification as `formatStatus`, alongside the translated logical `header`. A read-only preparation leaves the selected historical version unchanged; the status becomes `current` after publication of the successor. Unmaterialized pending Sessions omit `formatStatus`.
83
+
82
84
  `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; every nested object and array is frozen, and 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 the selected file’s `sizeBytes` and a best-effort revision. Current revisions identify that file; historical revisions also fingerprint the selected files across the persistence root, so child changes invalidate cached logical events. Fingerprinting reads filesystem metadata only; unrelated changes conservatively invalidate historical revisions. One `list()` call shares a corpus fingerprint across its historical entries. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend.
83
85
 
86
+ Verification accepts a successful Worker response only after the Worker exits naturally with code 0. Its concurrency slot remains held through that exit; cancellation and Worker errors terminate and join the Worker before rejecting.
87
+
84
88
  Historical body preparation completes the parent catalog through [V3→V4](../session-format-v3-to-v4/README.md). It finds candidate direct children from headers, reads each child's own descriptor through historical codecs, and retains compact evidence plus source revisions. It neither prepares child catalogs nor publishes child successors. Unreadable or unsupported headers, including corrupt Zstandard header frames, are omitted from discovery and `list()`. Direct access to a corrupt compressed header still rejects; header I/O and cancellation errors propagate. Child decoding and descriptor-field failures produce warnings naming the child path and retain header identity through `subagent/catalog` when the parent has no complete entry; healthy children and existing parent catalog entries remain available. Opening a damaged child still reports that child's error. Missing, unsupported, or multiple descriptors likewise produce unknown-mode membership without inventing a label. Published unknown entries remain browsable; child history reads retry the actual log and resolve its mode from a valid descriptor. Preparation rechecks membership and inspected source revisions before returning, reuse, and publication, including failed child reads so a repaired child invalidates stale preparation. Source drift retries a read open once and refuses a write open. Cancellation still aborts the operation. Current V4 opens bypass discovery and validate catalog fields, uniqueness, and current delivery ownership before exposing events.
85
89
 
86
90
  Historical `stat` and `list` revisions require metadata work proportional to the root’s Session count. Fresh body preparation scans all selected headers and decodes direct-child bodies; memo reuse still scans membership and checks revisions. Read-only access never publishes an upgrade, so cold processes and evicted preparations repeat that work. Current V4 body reads and revisions avoid the historical corpus scan. See the [measured costs and diagnostic command](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md#catalog-scan-measurements).
@@ -101,7 +105,7 @@ The backend owns its complete storage runtime (`src/storage.ts`): `JsonlSessionH
101
105
 
102
106
  ### Physical encoding
103
107
 
104
- The default artifact is a standard concatenation of independent [Zstandard frames](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md): one checksummed frame containing only the header line, then one checksummed frame per durable append batch, using Node's built-in Zstandard API at its default compression level (no level knob). The current format writes one event per row; `sourceEventSeqs` uses a lossless storage representation in which consecutive runs of at least three sequence numbers become `[start, end]` pairs, any other list stays verbatim, and reading expands the exact in-memory array. Historical migration reuses one Zstandard decoder, passes parsed rows through stateful format stages, and streams current records through one compression context in about 1 MiB main-thread slices while retaining only final current events, bounded decoder state, and the required sequence-remap table. Listing reads and validates only the header frame. `compression: 'none'` keeps the same storage-form logical lines without frame compression. A root belongs to one encoding: startup discovery and targeted lookup reject generations with the other suffix; format migration preserves the configured encoding, while compression conversion, mixed-root fallback, and dual write remain unsupported. Frozen v0 and v1 codecs retain their packed-row decoders solely for historical generations.
108
+ The default artifact is a standard concatenation of independent Zstandard frames: one checksummed frame containing only the header line, then one checksummed frame per durable append batch, using Node's built-in Zstandard API at its default compression level (no level knob). The current format writes one event per row; `sourceEventSeqs` uses a lossless storage representation in which consecutive runs of at least three sequence numbers become `[start, end]` pairs, any other list stays verbatim, and reading expands the exact in-memory array. Historical migration reuses one Zstandard decoder, passes parsed rows through stateful format stages, and streams current records through one compression context in about 1 MiB main-thread slices while retaining only final current events, bounded decoder state, and the required sequence-remap table. Listing reads and validates only the header frame. `compression: 'none'` keeps the same storage-form logical lines without frame compression. A root belongs to one encoding: startup discovery and targeted lookup reject generations with the other suffix; format migration preserves the configured encoding, while compression conversion, mixed-root fallback, and dual write remain unsupported. Frozen v0 and v1 codecs retain their packed-row decoders solely for historical generations.
105
109
 
106
110
  ### Source map
107
111
 
@@ -114,7 +118,6 @@ The default artifact is a standard concatenation of independent [Zstandard frame
114
118
  | [`src/migration-verifier.ts`](src/migration-verifier.ts) | Worker lifecycle for staged and competing-generation verification |
115
119
  | [`src/zstd.ts`](src/zstd.ts) | Zstandard frame compression, decoding, and frame scanning |
116
120
  | [`src/win32.ts`](src/win32.ts) | Windows write-through publish and directory creation |
117
- | — | 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. |
118
121
 
119
122
  </details>
120
123
 
@@ -127,8 +130,6 @@ Read these pages when the package-level contract is not enough. They move from t
127
130
 
128
131
  - [Session persistence subsystem](../../../docs/subsystems/persistence.md) — backend-neutral service semantics and provider relationships.
129
132
  - [Session persistence seam](../session-persistence/README.md) — the service contract this backend implements.
130
- - [Project-session directory decision](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.md) — the layout tradeoff behind project and session directories.
131
- - [Zstandard JSONL session logs](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md) — the checksummed-frame encoding rationale.
132
133
  - [Released Session format migrations](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md) — immutable generations, adjacent migration edges, and publication rules.
133
134
 
134
135
  -----
package/README.zh.md CHANGED
@@ -79,8 +79,12 @@ kind: "package-reference"
79
79
 
80
80
  ### 读取日志
81
81
 
82
+ 已实体化的 `stat` 与 `list` 快照在转换后的逻辑 `header` 之外,通过 `formatStatus` 保留格式 catalog 的头部分类。只读准备不会改变所选历史版本;发布后继之后,状态才变为 `current`。尚未实体化的待定 Session 省略 `formatStatus`。
83
+
82
84
  `open(id, 'read'|'write')` 选择最高规范 generation。当前格式输入走普通快速路径。对于历史输入,只读 open 会单遍解码并迁移源、校验当前逻辑结果,然后在不发布后继的情况下返回。写 open 会在可用时复用按 revision 为键的 preparation,否则执行同一套 preparation,再按有界分片编码同目录临时文件、在 Worker Thread 中校验、复查源修订,并在返回前以不覆盖方式发布当前后继。源保持逐字节不变。如果源在 preparation 后发生变化,该次写 open 会失败,已经返回给读方的逻辑历史不会被替换;后续写 open 会针对新的 revision 重新执行 preparation。后端在 memo 化前冻结已解码的 event graph,并在此时将其标记为 `shared-frozen`;每个嵌套对象和数组都会冻结,句柄读取和 slice 即使为空也保留该状态。只有尚未实体化的 pending 空日志报告 `detached`。`stat(id)` 与 `list()` 只选择并转换最高 generation 的 header,不读取事件行,也不启动迁移;快照携带所选文件的 `sizeBytes` 与尽力而为的修订号。当前格式修订号标识该文件;历史格式修订号还包含持久化根目录中所选文件的指纹,因此子日志变化会使缓存的逻辑事件失效。指纹只读取文件系统元数据;无关变化也会保守地使历史修订号失效。一次 `list()` 为其历史条目共用一个全库指纹。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。
83
85
 
86
+ 校验只有在 Worker 自然以退出码 0 结束后,才接受其成功响应。并发配额一直保留到该次退出;取消或 Worker 错误会先终止并等待 Worker 退出,再使校验失败。
87
+
84
88
  历史正文准备通过 [V3→V4](../session-format-v3-to-v4/README.zh.md) 补齐父目录:从 header 找到候选直属子 Session,通过历史编解码器逐个读取其自身 descriptor,保留紧凑证据与来源修订。此过程不准备子目录,也不发布子后继。不可读或不支持的 header(包括损坏的 Zstandard header 帧)不参与发现,也不出现在 `list()` 中。直接访问损坏的压缩 header 仍会失败;header 的 I/O 错误与取消错误继续传播。子日志解码或 descriptor 字段失败会产生带子路径的警告;父目录没有完整条目时,通过 `subagent/catalog` 保留 header 身份信息。健康子项和已有父目录项仍可使用。打开损坏子 Session 时仍报告该子会话的错误。缺失、不支持或多个 descriptor 同样生成模式未知的目录项,不编造标签。已发布的未知条目仍可浏览;读取子历史时会重试实际日志,并从有效 descriptor 确定模式。准备返回、复用与发布前会重新检查成员集合及已检查来源的修订,也包括读取失败的子日志,使修复后的子日志能够使旧准备缓存失效。来源变化时只读打开重试一次,写打开拒绝发布。取消仍会中止操作。当前 V4 打开跳过发现,并在暴露事件前校验目录字段、唯一性及当前投递归属。
85
89
 
86
90
  历史格式的 `stat` 与 `list` 修订号需要与根目录 Session 数量成正比的元数据工作。首次正文准备扫描所有所选 header 并解码直属子正文;复用准备缓存仍扫描成员集合并检查修订。只读访问从不发布升级,因此冷进程与被淘汰的准备缓存会重复这些工作。当前 V4 正文读取和修订号避开历史全库扫描。见[实测成本与诊断命令](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md#catalog-scan-measurements)。
@@ -101,7 +105,7 @@ kind: "package-reference"
101
105
 
102
106
  ### 物理编码
103
107
 
104
- 默认产物是独立 [Zstandard 帧](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md) 的标准拼接:一个仅包含 header 行的带校验和帧,后跟每个持久 append 批次一个带校验和帧,使用 Node 内置 Zstandard API 的默认压缩级别(无级别开关)。当前格式为每个事件写一行;`sourceEventSeqs` 使用无损存储形式:至少包含三个序列号的连续段会变成 `[start, end]` 区间对,其他列表原样保留;读取时会展开回精确的内存数组。历史迁移会复用一个 Zstandard decoder,让已解析行流经有状态格式 Stage,并通过一个压缩 context 以约 1 MiB 主线程分片流式写入当前记录,同时只保留最终当前事件、有界 decoder 状态与必需的序号重映射表。列表只读取并验证 header 帧。`compression: 'none'` 保留相同的存储形式逻辑行,但不使用帧压缩。一个根只属于一种编码:启动发现与定向查找会拒绝使用另一后缀的 generation;格式迁移保留已配置编码,而压缩转换、混合根回退与双写仍不受支持。冻结的 v0 与 v1 codec 仅为历史 generation 保留 packed-row decoder。
108
+ 默认产物是独立 Zstandard 帧 的标准拼接:一个仅包含 header 行的带校验和帧,后跟每个持久 append 批次一个带校验和帧,使用 Node 内置 Zstandard API 的默认压缩级别(无级别开关)。当前格式为每个事件写一行;`sourceEventSeqs` 使用无损存储形式:至少包含三个序列号的连续段会变成 `[start, end]` 区间对,其他列表原样保留;读取时会展开回精确的内存数组。历史迁移会复用一个 Zstandard decoder,让已解析行流经有状态格式 Stage,并通过一个压缩 context 以约 1 MiB 主线程分片流式写入当前记录,同时只保留最终当前事件、有界 decoder 状态与必需的序号重映射表。列表只读取并验证 header 帧。`compression: 'none'` 保留相同的存储形式逻辑行,但不使用帧压缩。一个根只属于一种编码:启动发现与定向查找会拒绝使用另一后缀的 generation;格式迁移保留已配置编码,而压缩转换、混合根回退与双写仍不受支持。冻结的 v0 与 v1 codec 仅为历史 generation 保留 packed-row decoder。
105
109
 
106
110
  ### 源码地图
107
111
 
@@ -114,7 +118,6 @@ kind: "package-reference"
114
118
  | [`src/migration-verifier.ts`](src/migration-verifier.ts) | stage 与竞争 generation 校验的 Worker 生命周期 |
115
119
  | [`src/zstd.ts`](src/zstd.ts) | Zstandard 帧压缩、解码与帧扫描 |
116
120
  | [`src/win32.ts`](src/win32.ts) | Windows write-through 发布与目录创建 |
117
- | — | 不发布运行时不变式伴生入口;身份在存储层强制;持久化正确性依赖后端往返与崩溃尾部测试,本包不公开可持续观察的进程内关系。 |
118
121
 
119
122
  </details>
120
123
 
@@ -127,8 +130,6 @@ kind: "package-reference"
127
130
 
128
131
  - [会话持久化子系统](../../../docs/subsystems/persistence.zh.md)——后端无关的服务语义与提供方关系。
129
132
  - [会话持久化 seam](../session-persistence/README.zh.md)——本后端实现的服务约定。
130
- - [项目会话目录决策](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.zh.md)——项目与会话目录布局背后的取舍。
131
- - [Zstandard JSONL 会话日志](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md)——带校验和帧编码的理由。
132
133
  - [已发布 Session 格式迁移](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)——不可变 generation、相邻迁移边与发布规则。
133
134
 
134
135
  -----
package/lib/index.js CHANGED
@@ -1476,7 +1476,7 @@ function workerSpawn(request) {
1476
1476
  * @param expectedEventCount - exact logical event count expected after decoding.
1477
1477
  * @param expectedPrefix - verified physical prefix; an append tail may be present and is not validated.
1478
1478
  * @param signal - optional cancellation for scheduler wait and Worker execution.
1479
- * @returns stable physical identity and digest observed by the worker.
1479
+ * @returns the worker's physical identity and digest after its natural zero exit.
1480
1480
  */
1481
1481
  function verifyCurrentGenerationInWorker(path, compression, expectedId, expectedEventCount, expectedPrefix, signal) {
1482
1482
  return verificationScheduler.run(() => runVerificationWorker(path, compression, expectedId, expectedEventCount, expectedPrefix, signal), signal);
@@ -1493,6 +1493,7 @@ function runVerificationWorker(path, compression, expectedId, expectedEventCount
1493
1493
  const worker = new Worker(entry, options);
1494
1494
  return new Promise((resolve, reject) => {
1495
1495
  let settled = false;
1496
+ let success;
1496
1497
  const cleanup = () => {
1497
1498
  signal?.removeEventListener("abort", abort);
1498
1499
  };
@@ -1521,17 +1522,16 @@ function runVerificationWorker(path, compression, expectedId, expectedEventCount
1521
1522
  fail(error);
1522
1523
  return;
1523
1524
  }
1524
- settled = true;
1525
- cleanup();
1526
- worker.terminate().then(() => {
1527
- resolve(response.result);
1528
- }, (error) => {
1529
- reject(error instanceof Error ? error : new Error(String(error)));
1530
- });
1525
+ success = response;
1531
1526
  });
1532
1527
  worker.once("error", fail);
1533
1528
  worker.once("exit", (code) => {
1534
- if (!settled) fail(/* @__PURE__ */ new Error(`migration verifier exited before reporting a result (code ${code})`));
1529
+ if (settled) return;
1530
+ settled = true;
1531
+ cleanup();
1532
+ if (success === void 0) reject(/* @__PURE__ */ new Error(`migration verifier exited before reporting a result (code ${code})`));
1533
+ else if (code !== 0) reject(/* @__PURE__ */ new Error(`migration verifier exited after reporting a result (code ${code})`));
1534
+ else resolve(success.result);
1535
1535
  });
1536
1536
  const abort = () => {
1537
1537
  fail(verifierAbortError(signal));
@@ -2544,13 +2544,13 @@ var JsonlSessionPersistence = class extends SessionPersistence {
2544
2544
  };
2545
2545
  const selected = await this.findLog(id, options?.signal);
2546
2546
  if (selected === void 0) return void 0;
2547
- const header = await this.readGenerationHeader(selected, id, options?.signal);
2548
- if (header === void 0) return void 0;
2547
+ const metadata = await this.readGenerationHeader(selected, id, options?.signal);
2548
+ if (metadata === void 0) return void 0;
2549
2549
  try {
2550
2550
  const identity = await stat(selected.sourcePath, { bigint: true });
2551
2551
  options?.signal?.throwIfAborted();
2552
2552
  return {
2553
- header,
2553
+ ...metadata,
2554
2554
  revision: selected.sourceVersion < SESSION_FORMAT_VERSION ? SessionPersistenceRevision(`${fileRevision(identity)}:${await this.historicalCorpusRevision(options?.signal)}`) : fileRevision(identity),
2555
2555
  sizeBytes: Number(identity.size)
2556
2556
  };
@@ -2581,6 +2581,7 @@ var JsonlSessionPersistence = class extends SessionPersistence {
2581
2581
  listed.add(artifact.header.id);
2582
2582
  snapshots.push({
2583
2583
  header: artifact.header,
2584
+ formatStatus: artifact.formatStatus,
2584
2585
  revision: artifact.sourceVersion < SESSION_FORMAT_VERSION ? SessionPersistenceRevision(`${fileRevision(identity)}:${corpusRevision}`) : fileRevision(identity),
2585
2586
  sizeBytes: Number(identity.size)
2586
2587
  });
@@ -3036,18 +3037,18 @@ var JsonlSessionPersistence = class extends SessionPersistence {
3036
3037
  const ids = /* @__PURE__ */ new Set();
3037
3038
  for (const selected of await this.listGenerations(signal)) {
3038
3039
  signal?.throwIfAborted();
3039
- let header;
3040
+ let metadata;
3040
3041
  try {
3041
- header = await this.readGenerationHeader(selected, void 0, signal);
3042
+ metadata = await this.readGenerationHeader(selected, void 0, signal);
3042
3043
  } catch (error) {
3043
3044
  if (error instanceof SessionFormatUnsupportedError || error instanceof SessionPersistenceCorruptionError) continue;
3044
3045
  throw error;
3045
3046
  }
3046
- if (header === void 0) continue;
3047
- if (ids.has(header.id)) throw new Error(`duplicate JSONL session id "${header.id}" appears in multiple project directories`);
3048
- ids.add(header.id);
3047
+ if (metadata === void 0) continue;
3048
+ if (ids.has(metadata.header.id)) throw new Error(`duplicate JSONL session id "${metadata.header.id}" appears in multiple project directories`);
3049
+ ids.add(metadata.header.id);
3049
3050
  artifacts.push({
3050
- header,
3051
+ ...metadata,
3051
3052
  path: selected.sourcePath,
3052
3053
  sourceVersion: selected.sourceVersion
3053
3054
  });
@@ -3089,7 +3090,10 @@ var JsonlSessionPersistence = class extends SessionPersistence {
3089
3090
  if (result.status === "malformed") return void 0;
3090
3091
  const header = this.currentHeader(result.header);
3091
3092
  await this.assertStoredIdentity(selected.sourcePath, selected.sourceVersion, header, expectedId, signal);
3092
- return header;
3093
+ return {
3094
+ header,
3095
+ formatStatus: result.status
3096
+ };
3093
3097
  }
3094
3098
  /** Convert format-catalog string identities to current branded Session metadata. */
3095
3099
  currentHeader(header) {
@@ -9,7 +9,7 @@ import type { JsonlExpectedPrefix, JsonlVerifiedGeneration } from './generation.
9
9
  * @param expectedEventCount - exact logical event count expected after decoding.
10
10
  * @param expectedPrefix - verified physical prefix; an append tail may be present and is not validated.
11
11
  * @param signal - optional cancellation for scheduler wait and Worker execution.
12
- * @returns stable physical identity and digest observed by the worker.
12
+ * @returns the worker's physical identity and digest after its natural zero exit.
13
13
  */
14
14
  export declare function verifyCurrentGenerationInWorker(path: string, compression: JsonlCompression, expectedId: string, expectedEventCount: number, expectedPrefix?: JsonlExpectedPrefix, signal?: AbortSignal): Promise<JsonlVerifiedGeneration>;
15
15
  //# sourceMappingURL=migration-verifier.d.ts.map
package/lib/worker.cjs CHANGED
@@ -9,6 +9,630 @@ let node_util = require("node:util");
9
9
  let node_zlib = require("node:zlib");
10
10
  let node_module = require("node:module");
11
11
  let node_buffer = require("node:buffer");
12
+ //#region ../../util/values/src/partial-json.ts
13
+ /**
14
+ * Lazily scanned view of one JSON object's top-level fields, built from text
15
+ * that may still be streaming or from an already parsed object. Nothing is
16
+ * scanned until a reader asks; the view remembers every question it answered
17
+ * and reports changed answers when the owner refreshes for publication.
18
+ * Used for model tool-call arguments: a row reads the fields it
19
+ * cares about at whatever granularity it displays, at every stage of the call.
20
+ * @module @deepseek-ai/dsh-util-values/src/partial-json
21
+ */
22
+ const SIMPLE_ESCAPES = {
23
+ "\"": "\"",
24
+ "\\": "\\",
25
+ "/": "/",
26
+ b: "\b",
27
+ f: "\f",
28
+ n: "\n",
29
+ r: "\r",
30
+ t: " "
31
+ };
32
+ const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
33
+ function isWhitespace(c) {
34
+ return c === " " || c === "\n" || c === "\r" || c === " ";
35
+ }
36
+ function isHex(c) {
37
+ return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
38
+ }
39
+ (class PartialArguments {
40
+ /** The view of a call with no arguments available. */
41
+ static EMPTY = PartialArguments.fromObject({});
42
+ /**
43
+ * View finished argument text without scanning it until a reader asks.
44
+ * @param text - the complete argument JSON text.
45
+ * @returns a sealed view.
46
+ */
47
+ static fromText(text) {
48
+ const view = new PartialArguments();
49
+ view.append(text);
50
+ view.sealed = true;
51
+ return view;
52
+ }
53
+ /**
54
+ * View an already parsed argument payload, such as a PTC dispatch object.
55
+ * @param value - the parsed argument value.
56
+ * @returns a sealed view; a non-object payload has no fields.
57
+ */
58
+ static fromObject(value) {
59
+ const view = new PartialArguments();
60
+ view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
61
+ view.sealed = true;
62
+ return view;
63
+ }
64
+ /**
65
+ * The source: text so far or a parsed object, plus whether it can still grow.
66
+ * These are the only enumerable fields, so two views over the same source
67
+ * compare equal structurally however far each has been read.
68
+ */
69
+ chunks = [];
70
+ object;
71
+ sealed = false;
72
+ #ends = [];
73
+ #size = 0;
74
+ #consumed = 0;
75
+ #mode = "root";
76
+ #escape = false;
77
+ #keyStart = 0;
78
+ #keyEscaped = false;
79
+ #key = "";
80
+ #current = null;
81
+ #nestedEnds = [];
82
+ #nestedInString = false;
83
+ #invalidAt;
84
+ #invalidValue = false;
85
+ #entries = /* @__PURE__ */ new Map();
86
+ #order = [];
87
+ #reads = /* @__PURE__ */ new Map();
88
+ /** Whether this view rejects further appends; does not scan text or register reads. */
89
+ get isSealed() {
90
+ return this.sealed;
91
+ }
92
+ /** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
93
+ get invalid() {
94
+ this.scan();
95
+ return this.#mode === "invalid" || this.#invalidValue;
96
+ }
97
+ /**
98
+ * Retain streamed argument text without scanning or comparing observed answers.
99
+ * @param fragment - the text following every fragment appended before.
100
+ */
101
+ append(fragment) {
102
+ if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
103
+ if (fragment.length === 0) return;
104
+ this.chunks.push(fragment);
105
+ this.#size += fragment.length;
106
+ this.#ends.push(this.#size);
107
+ }
108
+ /**
109
+ * Reconcile a streamed prefix with authoritative complete text without joining the fragments.
110
+ * @param text - the final argument text, which replaces missing or conflicting deltas.
111
+ * @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
112
+ */
113
+ settle(text) {
114
+ if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
115
+ let offset = 0;
116
+ for (const chunk of this.chunks) {
117
+ if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
118
+ offset += chunk.length;
119
+ }
120
+ this.chunks = text.length === 0 ? [] : [text];
121
+ this.#ends = text.length === 0 ? [] : [text.length];
122
+ this.sealed = true;
123
+ return this;
124
+ }
125
+ /**
126
+ * Compare observed answers and advance their publication baseline. Unread views remain unscanned.
127
+ * @returns whether any observed answer changed since its first read or the preceding refresh.
128
+ */
129
+ refresh() {
130
+ if (this.#reads.size === 0) return false;
131
+ this.scan();
132
+ let changed = false;
133
+ let completions = false;
134
+ for (const read of this.#reads.values()) {
135
+ if (read.completion) {
136
+ completions = true;
137
+ continue;
138
+ }
139
+ changed = this.refreshRead(read) || changed;
140
+ }
141
+ if (completions) {
142
+ for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
143
+ }
144
+ if (this.sealed) this.#reads.clear();
145
+ return changed;
146
+ }
147
+ refreshRead(read) {
148
+ const now = read.answer();
149
+ if (Object.is(now, read.last)) return false;
150
+ read.last = now;
151
+ return true;
152
+ }
153
+ /**
154
+ * Check whether no further fields can arrive.
155
+ * @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
156
+ */
157
+ closed() {
158
+ return this.remember("closed", "", () => this.closedNow());
159
+ }
160
+ /**
161
+ * List discovered fields in first-appearance order.
162
+ * @returns top-level keys seen so far, in first-appearance order.
163
+ */
164
+ keys() {
165
+ return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
166
+ }
167
+ /**
168
+ * Check whether a top-level field has appeared.
169
+ * @param key - argument name.
170
+ * @returns whether the field has appeared (a string opened or another value began).
171
+ */
172
+ has(key) {
173
+ return this.remember("has", key, () => this.hasNow(key));
174
+ }
175
+ /**
176
+ * Check whether a field's closing delimiter has arrived, without validating its contents.
177
+ * @param key - argument name.
178
+ * @returns whether its delimiter arrived and no content reader has reported an error for this value.
179
+ */
180
+ complete(key) {
181
+ return this.remember("complete", key, () => this.completeNow(key));
182
+ }
183
+ /**
184
+ * Read string length without materializing its text.
185
+ * @param key - argument name.
186
+ * @param options - change granularity for a streaming string.
187
+ * @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
188
+ */
189
+ stringLength(key, options) {
190
+ const step = Math.max(1, Math.floor(options?.step ?? 1));
191
+ const offset = options?.offset ?? 0;
192
+ return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
193
+ }
194
+ /**
195
+ * Check a string against a decoded UTF-16 length limit without materializing it.
196
+ * @param key - argument name.
197
+ * @param maxLength - decoded UTF-16 limit, floored to at least zero.
198
+ * @returns whether the string is longer than the limit; false when absent or not a string.
199
+ */
200
+ stringExceeds(key, maxLength) {
201
+ const limit = Math.max(0, Math.floor(maxLength));
202
+ return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
203
+ }
204
+ /**
205
+ * Read a decoded string, including a streaming prefix.
206
+ * @param key - argument name.
207
+ * @returns the string field's decoded text so far; undefined when absent or not a string.
208
+ */
209
+ text(key) {
210
+ return this.remember("text", key, () => this.textNow(key));
211
+ }
212
+ /**
213
+ * Read at most the first decoded UTF-16 units of a string.
214
+ * @param key - argument name.
215
+ * @param maxLength - maximum decoded UTF-16 length, floored to at least one.
216
+ * @returns the bounded string prefix; undefined when absent or not a string.
217
+ */
218
+ textPrefix(key, maxLength) {
219
+ const limit = Math.max(1, Math.floor(maxLength));
220
+ return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
221
+ }
222
+ /**
223
+ * Read a completed non-string argument.
224
+ * @param key - argument name.
225
+ * @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
226
+ */
227
+ value(key) {
228
+ return this.remember("value", key, () => this.valueNow(key));
229
+ }
230
+ /** Answer a question and, on a streaming view, remember it for change detection. */
231
+ remember(kind, key, read, comparison) {
232
+ this.scan();
233
+ const result = read();
234
+ if (!this.sealed) {
235
+ const id = `${kind}/${key}`;
236
+ if (!this.#reads.has(id)) this.#reads.set(id, {
237
+ completion: kind === "complete",
238
+ answer: comparison === void 0 ? read : () => comparison(read()),
239
+ last: comparison === void 0 ? result : comparison(result)
240
+ });
241
+ }
242
+ return result;
243
+ }
244
+ closedNow() {
245
+ return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
246
+ }
247
+ keysNow() {
248
+ return this.object === void 0 ? this.#order : Object.keys(this.object);
249
+ }
250
+ hasNow(key) {
251
+ return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
252
+ }
253
+ completeNow(key) {
254
+ if (this.object !== void 0) return Object.hasOwn(this.object, key);
255
+ const entry = this.#entries.get(key);
256
+ return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
257
+ }
258
+ lengthNow(key, limit = Number.POSITIVE_INFINITY) {
259
+ if (this.object !== void 0) {
260
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
261
+ return typeof field === "string" ? field.length : void 0;
262
+ }
263
+ const entry = this.#entries.get(key);
264
+ if (entry?.kind !== "string") return void 0;
265
+ if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
266
+ const read = entry.length ??= {
267
+ at: entry.start,
268
+ length: 0,
269
+ text: ""
270
+ };
271
+ this.readString(entry, read, limit, false);
272
+ return read.length;
273
+ }
274
+ textNow(key) {
275
+ if (this.object !== void 0) {
276
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
277
+ return typeof field === "string" ? field : void 0;
278
+ }
279
+ const entry = this.#entries.get(key);
280
+ if (entry?.kind !== "string") return void 0;
281
+ if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
282
+ let text;
283
+ try {
284
+ text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
285
+ } catch (_error) {}
286
+ if (text !== void 0) entry.text = {
287
+ at: entry.end,
288
+ length: text.length,
289
+ text
290
+ };
291
+ }
292
+ const read = entry.text ??= {
293
+ at: entry.start,
294
+ length: 0,
295
+ text: ""
296
+ };
297
+ this.readString(entry, read, Number.POSITIVE_INFINITY, true);
298
+ return read.text;
299
+ }
300
+ textPrefixNow(key, maxLength) {
301
+ if (this.object !== void 0) {
302
+ const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
303
+ return typeof field === "string" ? field.slice(0, maxLength) : void 0;
304
+ }
305
+ const entry = this.#entries.get(key);
306
+ if (entry?.kind !== "string") return void 0;
307
+ const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
308
+ let read = prefixes.get(maxLength);
309
+ if (read === void 0) {
310
+ read = {
311
+ at: entry.start,
312
+ length: 0,
313
+ text: ""
314
+ };
315
+ prefixes.set(maxLength, read);
316
+ }
317
+ this.readString(entry, read, maxLength, true);
318
+ return read.text;
319
+ }
320
+ valueNow(key) {
321
+ if (this.object !== void 0) {
322
+ if (!Object.hasOwn(this.object, key)) return void 0;
323
+ const field = this.object[key];
324
+ return typeof field === "string" ? void 0 : field;
325
+ }
326
+ const entry = this.#entries.get(key);
327
+ if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
328
+ if (entry.parsed === void 0) try {
329
+ entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
330
+ } catch (_error) {
331
+ entry.invalid = true;
332
+ this.#invalidValue = true;
333
+ }
334
+ return entry.parsed;
335
+ }
336
+ chunkAt(at) {
337
+ let low = 0;
338
+ let high = this.#ends.length;
339
+ while (low < high) {
340
+ const mid = low + high >>> 1;
341
+ if (this.#ends[mid] <= at) low = mid + 1;
342
+ else high = mid;
343
+ }
344
+ return low;
345
+ }
346
+ /** Materialize only a requested range, never the cumulative source. */
347
+ slice(start, end) {
348
+ if (start >= end) return "";
349
+ const first = this.chunkAt(start);
350
+ const last = this.chunkAt(end - 1);
351
+ const base = first === 0 ? 0 : this.#ends[first - 1];
352
+ if (first === last) return this.chunks[first].slice(start - base, end - base);
353
+ const parts = [this.chunks[first].slice(start - base)];
354
+ for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
355
+ parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
356
+ return parts.join("");
357
+ }
358
+ readString(entry, read, limit, materialize) {
359
+ const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
360
+ if (!entry.needsDecoding) {
361
+ const length = Math.min(end - read.at, limit - read.length);
362
+ if (length <= 0) return;
363
+ if (materialize) read.text += this.slice(read.at, read.at + length);
364
+ read.at += length;
365
+ read.length += length;
366
+ return;
367
+ }
368
+ let chunkIndex = this.chunkAt(read.at);
369
+ while (read.at < end && read.length < limit) {
370
+ const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
371
+ const chunk = this.chunks[chunkIndex];
372
+ const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
373
+ const boundary = remaining.search(CONTENT_ESCAPE);
374
+ const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
375
+ if (length > 0) {
376
+ if (materialize) read.text += remaining.slice(0, length);
377
+ read.at += length;
378
+ read.length += length;
379
+ if (read.at === base + chunk.length) chunkIndex++;
380
+ continue;
381
+ }
382
+ const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
383
+ let decoded;
384
+ let width = 2;
385
+ if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
386
+ if (remaining[0] === "\\" && type === "u") {
387
+ const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
388
+ let valid = true;
389
+ for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
390
+ if (valid) {
391
+ if (hex.length < 4 && entry.end < 0) return;
392
+ if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
393
+ }
394
+ width = 6;
395
+ } else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
396
+ if (decoded === void 0) {
397
+ entry.invalidAt = read.at;
398
+ this.#invalidValue = true;
399
+ return;
400
+ }
401
+ if (materialize) read.text += decoded;
402
+ read.length++;
403
+ read.at += width;
404
+ while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
405
+ }
406
+ }
407
+ /** Locate new field ranges without decoding or parsing their contents. */
408
+ scan() {
409
+ if (this.object !== void 0 || this.#consumed === this.#size) return;
410
+ for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
411
+ const pending = this.chunks[i];
412
+ const base = i === 0 ? 0 : this.#ends[i - 1];
413
+ for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
414
+ if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
415
+ const end = this.stringBoundary(pending, index);
416
+ this.#consumed += end - index;
417
+ index = end;
418
+ if (index === pending.length) break;
419
+ }
420
+ this.step(pending[index], this.#consumed);
421
+ this.#consumed++;
422
+ }
423
+ }
424
+ }
425
+ /** Only raw quotes and their preceding backslash runs can terminate a string. */
426
+ stringBoundary(fragment, start) {
427
+ let at = start;
428
+ while (true) {
429
+ const quote = fragment.indexOf("\"", at);
430
+ const end = quote < 0 ? fragment.length : quote;
431
+ if (this.#mode === "string") {
432
+ const entry = this.#current;
433
+ if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
434
+ }
435
+ let slashStart = end;
436
+ while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
437
+ const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
438
+ this.#escape = quote < 0 && escaped;
439
+ if (quote < 0 || !escaped) return end;
440
+ at = quote + 1;
441
+ }
442
+ }
443
+ step(c, at) {
444
+ switch (this.#mode) {
445
+ case "root":
446
+ if (isWhitespace(c)) return;
447
+ if (c === "{") {
448
+ this.#mode = "key-or-end";
449
+ return;
450
+ }
451
+ this.fail();
452
+ return;
453
+ case "key-or-end":
454
+ if (isWhitespace(c)) return;
455
+ if (c === "}") {
456
+ this.#mode = "closed";
457
+ return;
458
+ }
459
+ if (c === "\"") {
460
+ this.beginKey(at);
461
+ return;
462
+ }
463
+ this.fail();
464
+ return;
465
+ case "key-only":
466
+ if (isWhitespace(c)) return;
467
+ if (c === "\"") {
468
+ this.beginKey(at);
469
+ return;
470
+ }
471
+ this.fail();
472
+ return;
473
+ case "key":
474
+ this.stepKey(c, at);
475
+ return;
476
+ case "colon":
477
+ if (isWhitespace(c)) return;
478
+ if (c === ":") {
479
+ this.#mode = "value";
480
+ return;
481
+ }
482
+ this.fail();
483
+ return;
484
+ case "value":
485
+ this.beginValue(c, at);
486
+ return;
487
+ case "string": {
488
+ const entry = this.#current;
489
+ entry.end = at;
490
+ this.#current = null;
491
+ this.#mode = "comma-or-end";
492
+ return;
493
+ }
494
+ case "scalar":
495
+ this.stepScalar(c, at);
496
+ return;
497
+ case "nested":
498
+ this.stepNested(c, at);
499
+ return;
500
+ case "comma-or-end":
501
+ if (isWhitespace(c)) return;
502
+ if (c === ",") {
503
+ this.#mode = "key-only";
504
+ return;
505
+ }
506
+ if (c === "}") {
507
+ this.#mode = "closed";
508
+ return;
509
+ }
510
+ this.fail();
511
+ return;
512
+ case "closed":
513
+ if (isWhitespace(c)) return;
514
+ this.fail();
515
+ return;
516
+ /* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
517
+ case "invalid": return;
518
+ /* v8 ignore next 2 -- Every scanner mode has a handler above. */
519
+ default: assertNever(this.#mode);
520
+ }
521
+ }
522
+ fail() {
523
+ this.#invalidAt = this.#consumed;
524
+ this.#mode = "invalid";
525
+ this.#current = null;
526
+ }
527
+ beginKey(at) {
528
+ this.#mode = "key";
529
+ this.#keyStart = at + 1;
530
+ this.#keyEscaped = false;
531
+ this.#escape = false;
532
+ }
533
+ stepKey(c, at) {
534
+ if (c < " ") {
535
+ this.fail();
536
+ return;
537
+ }
538
+ if (this.#escape) {
539
+ this.#escape = false;
540
+ return;
541
+ }
542
+ if (c === "\\") {
543
+ this.#escape = true;
544
+ this.#keyEscaped = true;
545
+ return;
546
+ }
547
+ if (c !== "\"") return;
548
+ const raw = this.slice(this.#keyStart, at);
549
+ if (this.#keyEscaped) try {
550
+ this.#key = JSON.parse(`"${raw}"`);
551
+ } catch (_error) {
552
+ this.fail();
553
+ return;
554
+ }
555
+ else this.#key = raw;
556
+ this.#mode = "colon";
557
+ }
558
+ open(entry) {
559
+ if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
560
+ this.#entries.set(this.#key, entry);
561
+ this.#current = entry;
562
+ }
563
+ beginValue(c, at) {
564
+ if (isWhitespace(c)) return;
565
+ if (c === "\"") {
566
+ this.open({
567
+ kind: "string",
568
+ start: at + 1,
569
+ end: -1,
570
+ needsDecoding: false,
571
+ invalidAt: void 0,
572
+ length: void 0,
573
+ text: void 0,
574
+ prefixes: void 0
575
+ });
576
+ this.#escape = false;
577
+ this.#mode = "string";
578
+ return;
579
+ }
580
+ if (c === "}" || c === "," || c === ":" || c === "]") {
581
+ this.fail();
582
+ return;
583
+ }
584
+ this.open({
585
+ kind: "value",
586
+ start: at,
587
+ end: -1,
588
+ parsed: void 0,
589
+ invalid: false
590
+ });
591
+ if (c === "{" || c === "[") {
592
+ this.#mode = "nested";
593
+ this.#nestedEnds = [c === "{" ? "}" : "]"];
594
+ this.#nestedInString = false;
595
+ this.#escape = false;
596
+ return;
597
+ }
598
+ this.#mode = "scalar";
599
+ }
600
+ stepScalar(c, at) {
601
+ if (c !== "," && c !== "}" && !isWhitespace(c)) return;
602
+ this.closeValue(at);
603
+ this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
604
+ }
605
+ stepNested(c, at) {
606
+ if (this.#nestedInString) {
607
+ this.#nestedInString = false;
608
+ return;
609
+ }
610
+ if (c === "\"") {
611
+ this.#nestedInString = true;
612
+ return;
613
+ }
614
+ if (c === "{" || c === "[") {
615
+ this.#nestedEnds.push(c === "{" ? "}" : "]");
616
+ return;
617
+ }
618
+ if (c === "}" || c === "]") {
619
+ if (this.#nestedEnds.pop() !== c) {
620
+ this.fail();
621
+ return;
622
+ }
623
+ if (this.#nestedEnds.length === 0) {
624
+ this.closeValue(at + 1);
625
+ this.#mode = "comma-or-end";
626
+ }
627
+ }
628
+ }
629
+ closeValue(end) {
630
+ const entry = this.#current;
631
+ entry.end = end;
632
+ this.#current = null;
633
+ }
634
+ });
635
+ //#endregion
12
636
  //#region ../../util/values/src/index.ts
13
637
  /**
14
638
  * Mark an unreachable closed-union branch.
@@ -2560,6 +3184,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
2560
3184
  "turn/start",
2561
3185
  "user/message",
2562
3186
  "web/deepseek-search-llm-request",
3187
+ "working-directory/change",
2563
3188
  "workspace/changes"
2564
3189
  ]);
2565
3190
  /** Event types whose model-visible effects require an explicit pure interpreter. */
@@ -3801,7 +4426,7 @@ const MAX_TIMER_DELAY_MS = 2147483647;
3801
4426
  */
3802
4427
  /**
3803
4428
  * Base class for all harness errors. Carries a `code` (stable, programmatic —
3804
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
4429
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
3805
4430
  * human-readable `message`, and supports `cause` chaining via the standard
3806
4431
  * `ErrorOptions`. `name` defaults to the subclass constructor name.
3807
4432
  */
@@ -4224,7 +4849,7 @@ function projectToolUpdates(messages, tools, toolUpdate, history) {
4224
4849
  /**
4225
4850
  * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
4226
4851
  * adapters from drifting. See
4227
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
4852
+ * `docs/subsystems/llm-streaming.md#appidentity--app-attribution`.
4228
4853
  *
4229
4854
  * App-attribution vocabulary for provider requests.
4230
4855
  * @module @deepseek-ai/dsh-llm/attribution
@@ -4815,20 +5440,14 @@ var LlmError = class extends HarnessError {
4815
5440
  }
4816
5441
  /** Notify topology observers without letting one broken listener veto the commit. */
4817
5442
  emitAdaptersUpdated() {
4818
- let invariantFailure;
4819
5443
  for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
4820
5444
  const returned = listener();
4821
5445
  if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
4822
5446
  this.warnAdaptersListenerFailure(error);
4823
5447
  });
4824
5448
  } catch (error) {
4825
- if (error?.code === "INVARIANT") {
4826
- invariantFailure ??= error;
4827
- continue;
4828
- }
4829
5449
  this.warnAdaptersListenerFailure(error);
4830
5450
  }
4831
- if (invariantFailure !== void 0) throw invariantFailure;
4832
5451
  }
4833
5452
  /** Contained-listener diagnostic shared by the sync and async failure paths. */
4834
5453
  warnAdaptersListenerFailure(error) {
@@ -5209,20 +5828,33 @@ var LlmError = class extends HarnessError {
5209
5828
  * Resolve one call under its current adapter registration. The returned
5210
5829
  * one-shot handle keeps that registration across header logging and dispatch,
5211
5830
  * so HMR cannot combine one adapter's capability result with another adapter.
5212
- * @param config - provider/model route and optional request controls.
5831
+ * An optional synchronous callback selects concrete controls using captured
5832
+ * model metadata. Defaults and validation apply to its result. Callback
5833
+ * failures and cancellation reject preparation before dispatch.
5834
+ * @param config - provider/model route and optional concrete request controls.
5213
5835
  * @param signal - optional cancellation for adapter-owned capability lookup.
5836
+ * @param configure - pure control selection, called once before defaults and validation.
5214
5837
  * @returns a prepared config and its registration-bound stream entry point.
5215
5838
  */
5216
- async prepareCall(config, signal) {
5217
- const registration = this.registration(config.provider);
5218
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
5219
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
5220
- const resolved = this.resolveCallWithInfo(config, modelInfo);
5839
+ async prepareCall(config, signal, configure) {
5840
+ signal?.throwIfAborted();
5841
+ const { provider, model, ...controls } = config;
5842
+ const registration = this.registration(provider);
5843
+ const adapterCall = await registration.adapter.prepareCall(provider, model, signal);
5844
+ signal?.throwIfAborted();
5845
+ const modelInfo = this.normalizeModelInfo(registration, model, adapterCall.model);
5846
+ const proposed = configure === void 0 ? config : {
5847
+ ...configure(deepFreeze(structuredClone(controls)), deepFreeze(modelInfo)),
5848
+ provider,
5849
+ model
5850
+ };
5851
+ signal?.throwIfAborted();
5852
+ const resolved = this.resolveCallWithInfo(proposed, modelInfo);
5221
5853
  const resolvedConfig = deepFreeze(structuredClone(resolved.config));
5222
5854
  const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
5223
5855
  const adapterDefaults = deepFreeze({
5224
- ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
5225
- ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
5856
+ ...proposed.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
5857
+ ...proposed.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
5226
5858
  });
5227
5859
  let dispatched = false;
5228
5860
  return Object.freeze({
@@ -5703,6 +6335,32 @@ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
5703
6335
  /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
5704
6336
  const attachments = /* @__PURE__ */ new WeakMap();
5705
6337
  /**
6338
+ * Grammar of a {@link PluginRecordType}: `plugin:` followed by
6339
+ * slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
6340
+ * each segment starting with a letter or digit.
6341
+ */
6342
+ const PLUGIN_RECORD_TYPE = /^plugin:[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/u;
6343
+ /**
6344
+ * Whether one event type name follows the plugin record grammar.
6345
+ * @param type - an event type name.
6346
+ * @returns whether the name is a {@link PluginRecordType}.
6347
+ */
6348
+ function isPluginRecordType(type) {
6349
+ return PLUGIN_RECORD_TYPE.test(type);
6350
+ }
6351
+ /**
6352
+ * Type one constructed plugin record as a log event after checking its type
6353
+ * against the plugin record grammar. A restored Session already holds events
6354
+ * whose types are outside `SessionEventMap` when they are marked ignorable;
6355
+ * a plugin record is such an event.
6356
+ * @param value - the frozen record envelope built by {@link Session}.
6357
+ * @throws when the record type is outside the plugin record grammar.
6358
+ */
6359
+ function assertPluginRecordEvent(value) {
6360
+ const { type } = value;
6361
+ if (!isPluginRecordType(type)) throw new Error(`plugin record type "${type}" must be "plugin:" followed by lowercase slash-separated segments`);
6362
+ }
6363
+ /**
5706
6364
  * An event-sourced session: an append-only log of {@link SessionEvent}s.
5707
6365
  *
5708
6366
  * Plain class (not a Service) — create live instances via
@@ -5913,8 +6571,7 @@ var Session = class Session {
5913
6571
  if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
5914
6572
  const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
5915
6573
  if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
5916
- const entry = attachments.get(this);
5917
- if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
6574
+ const entry = this.publicationEntry();
5918
6575
  const event = deepFreeze({
5919
6576
  type,
5920
6577
  seq: SessionSeq(this.log.length),
@@ -5923,6 +6580,48 @@ var Session = class Session {
5923
6580
  ...surfaceMetadataSnapshot
5924
6581
  });
5925
6582
  validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
6583
+ this.commit(event, entry);
6584
+ return event;
6585
+ }
6586
+ static {}
6587
+ /**
6588
+ * Commit one plugin record; {@link appendPluginRecord} is the only caller.
6589
+ * @param type - record type, checked against the plugin record grammar.
6590
+ * @param data - JSON payload, snapshotted before it enters the log.
6591
+ * @returns the record's sequence number.
6592
+ */
6593
+ #appendRecord(type, data) {
6594
+ const dataSnapshot = snapshotJsonValue(data);
6595
+ if (dataSnapshot === void 0) throw new Error(`plugin record "${type}" carries non-JSON-serializable data`);
6596
+ const entry = this.publicationEntry();
6597
+ const record = deepFreeze({
6598
+ type,
6599
+ seq: SessionSeq(this.log.length),
6600
+ time: Date.now(),
6601
+ data: dataSnapshot,
6602
+ ignorable: true
6603
+ });
6604
+ assertPluginRecordEvent(record);
6605
+ this.commit(record, entry);
6606
+ return record.seq;
6607
+ }
6608
+ /**
6609
+ * Read the store attachment for one append, refusing an append that
6610
+ * reenters while another append is being published.
6611
+ * @returns the attachment, or undefined for a detached Session.
6612
+ */
6613
+ publicationEntry() {
6614
+ const entry = attachments.get(this);
6615
+ if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
6616
+ return entry;
6617
+ }
6618
+ /**
6619
+ * Accept one frozen candidate at the next sequence number, enter it into the
6620
+ * log, and notify `session/event` observers with per-listener containment.
6621
+ * @param event - the candidate built at the current log length.
6622
+ * @param entry - the attachment read before the candidate was built.
6623
+ */
6624
+ commit(event, entry) {
5926
6625
  this.surfaceManager.validateNext(event);
5927
6626
  if (entry !== void 0) entry.appending = true;
5928
6627
  try {
@@ -5936,7 +6635,6 @@ var Session = class Session {
5936
6635
  this.log.push(event);
5937
6636
  this.eventsSnapshot = void 0;
5938
6637
  if (callbacks !== void 0 && entry !== void 0) invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
5939
- return event;
5940
6638
  } finally {
5941
6639
  if (entry !== void 0) {
5942
6640
  entry.appending = false;
@@ -11583,7 +12281,7 @@ function assertV4LifecycleRelationships(artifact, knownEventTypes) {
11583
12281
  * @returns validated catalog payload.
11584
12282
  */
11585
12283
  function catalogFact(value, subject = "subagent/catalog") {
11586
- if (!isSessionFormatJsonObject(value) || value["version"] !== 0 && value["version"] !== 1 || typeof value["childId"] !== "string" || value["mode"] !== "continuable" && value["mode"] !== "one-shot" && value["mode"] !== "unknown" || value["version"] === 0 && value["mode"] === "unknown" || value["mode"] === "continuable" && typeof value["label"] !== "string" || value["label"] !== void 0 && typeof value["label"] !== "string") throw new SessionFormatError(`${subject} requires a supported versioned catalog fact`);
12284
+ if (!isSessionFormatJsonObject(value) || value["version"] !== 0 && value["version"] !== 1 && value["version"] !== 2 || typeof value["childId"] !== "string" || value["mode"] !== "continuable" && value["mode"] !== "one-shot" && value["mode"] !== "external" && value["mode"] !== "unknown" || value["version"] === 2 !== (value["mode"] === "external") || value["version"] === 0 && value["mode"] === "unknown" || value["mode"] === "continuable" && typeof value["label"] !== "string" || value["label"] !== void 0 && typeof value["label"] !== "string") throw new SessionFormatError(`${subject} requires a supported versioned catalog fact`);
11587
12285
  sessionFormatCount(value["childCreatedAt"], "catalog child creation time");
11588
12286
  return value;
11589
12287
  }
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.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
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/dsh-session": "0.2.0-rc.2",
32
- "@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2",
33
- "@deepseek-ai/cordis": "~4.0.4"
31
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
32
+ "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.2",
33
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
34
34
  },
35
35
  "dependencies": {
36
- "koffi": "3.1.1",
37
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
38
- "@deepseek-ai/dsh-session-format": "0.2.0-rc.2",
39
- "@deepseek-ai/dsh-session-format-catalog": "0.2.0-rc.2",
40
- "@deepseek-ai/dsh-session-format-v3-to-v4": "0.2.0-rc.2",
36
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
37
+ "@deepseek-ai/dsh-session-format": "0.2.1-alpha.2",
38
+ "@deepseek-ai/dsh-session-format-catalog": "0.2.1-alpha.2",
39
+ "@deepseek-ai/dsh-session-format-v3-to-v4": "0.2.1-alpha.2",
41
40
  "@deepseek-ai/node-addon-system": "~0.1.2",
42
- "@deepseek-ai/schemastery": "~3.18.4"
41
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
42
+ "koffi": "3.1.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
46
- "@deepseek-ai/dsh-session-format-v0-to-v1": "0.2.0-rc.2",
47
- "@deepseek-ai/cordis": "~4.0.4",
48
- "@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2"
45
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
46
+ "@deepseek-ai/dsh-session-format-v0-to-v1": "0.2.1-alpha.2",
47
+ "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.2",
48
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
49
49
  }
50
50
  }