@deepseek-ai/dsh-session 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
@@ -24,8 +24,11 @@
24
24
  en: aa5f1951ecf3ac0c
25
25
  zh: 62a599ace8f2bf15
26
26
  /deepseek-ai-dsh-session/use-this-package/read-the-log:
27
- en: 2b29cfeac7a2baa8
28
- zh: b535b04dcbc29d3d
27
+ en: c4bf3103b9762b6c
28
+ zh: 369c734bb20de2ea
29
+ /deepseek-ai-dsh-session/use-this-package/write-experimental-plugin-records:
30
+ en: 6c758f98535b734c
31
+ zh: ac54342937477f37
29
32
  /deepseek-ai-dsh-session/use-this-package/fork-a-session:
30
33
  en: 1c40d17a786059d3
31
34
  zh: 81320ce118977c65
@@ -42,17 +45,17 @@
42
45
  en: bbfb53c50100ab13
43
46
  zh: 303a8ee47f4f656a
44
47
  /deepseek-ai-dsh-session/understand-the-implementation/source-map:
45
- en: 1268738282846f1d
46
- zh: 75a9fe0d5088ea09
48
+ en: c596a8f74b4f1b79
49
+ zh: a29f23c4c1d1ff75
47
50
  /deepseek-ai-dsh-session/understand-the-implementation/append-validation:
48
51
  en: 5c09583bc8e1ffc6
49
52
  zh: 3b25552de2d1f9b6
50
53
  /deepseek-ai-dsh-session/understand-the-implementation/shared-recovery:
51
- en: 76bb076736b2dd0f
52
- zh: de8306afc1e5faa1
54
+ en: 160b35afd9d866e4
55
+ zh: cd1021e0ef567f87
53
56
  /deepseek-ai-dsh-session/understand-the-implementation/derived-history:
54
- en: 0cc7a06f76bd7eca
55
- zh: 1db25185b15cad23
57
+ en: 4a9861492a8c3869
58
+ zh: e9566de647206230
56
59
  /deepseek-ai-dsh-session/understand-the-implementation/the-request-header:
57
60
  en: 1349f47bea4dab00
58
61
  zh: 20cc03f2c87ec492
@@ -78,8 +81,8 @@
78
81
  en: 9c2efc3e9064e3c4
79
82
  zh: 9a7783991b322750
80
83
  /deepseek-ai-dsh-session/model-experience/tool-result-recovery-and-fork-results/what-the-model-sees:
81
- en: b2ae41b23519cdc6
82
- zh: 0e80c34e3267d098
84
+ en: d15c350efc66e832
85
+ zh: b769ba23dbb374e7
83
86
  /deepseek-ai-dsh-session/model-experience/tool-result-recovery-and-fork-results/token-effect:
84
87
  en: 1339facbd3d7cee0
85
88
  zh: 9d392a053ef90032
@@ -99,8 +102,8 @@
99
102
  en: 5eb30cfb410d52c0
100
103
  zh: da56365b8ab896da
101
104
  /deepseek-ai-dsh-session/known-limitations-and-deferred-work:
102
- en: daaf56dda2bcaff1
103
- zh: 7d24de2deb1c5dc5
105
+ en: 45afe96a4e252db9
106
+ zh: 920bcc011823817b
104
107
  /deepseek-ai-dsh-session/known-limitations-and-deferred-work/dev-note:
105
108
  en: e67f633dc6fe7773
106
109
  zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -63,6 +63,18 @@ Append, seed/restore, and event adoption/snapshot reject any `header.system` and
63
63
 
64
64
  Session log positions use two numeric types. `SessionSeq` identifies an existing event or inclusive event watermark; `SessionLogOffset` identifies a gap, prefix length, or read boundary and may equal the event count. `SessionSeqCursor` adds the `-1` “no event yet” value, while `OptionalSessionSeq` uses `null` when absence is data. The constructors validate non-negative safe integers, and the brands disappear at runtime, so durable JSON and wire values remain ordinary numbers.
65
65
 
66
+ <a id="write-experimental-plugin-records"></a>
67
+
68
+ ### Write experimental plugin records
69
+
70
+ `appendPluginRecord(session, type, data)` appends one plugin record for a package under `packages/experimental/`; the `verify-plugin-record-callers` check rejects any other production caller in this repository. Plugins outside this repository must not call it either, although no repository check can inspect them.
71
+
72
+ Declare each record in `PluginRecordMap` through `@deepseek-ai/dsh-session/types`, with a description and an explicit payload type annotation in the owning experimental package's `src/`. Names use `plugin:<owner>/<record>`, where `owner` is the suffix of the package name `@deepseek-ai/dsh-experimental-<owner>`; for example, `plugin:pi-extensions/entry`. TypeScript checks each name and payload against its declaration; the writer's runtime JSON snapshot rejects values that cannot be preserved losslessly. Put extension-defined or other dynamic names inside the payload of a declared record.
73
+
74
+ The [experimental persistence catalog](../../../docs/experimental-persistence-catalog.md) lists current plugin-record declarations with their owners, descriptions, payload type annotations, and source files. These declarations remain separate from `SessionEventMap`, expanded persistence schemas, released type history, and `KNOWN_SESSION_EVENT_TYPES`. Records carry `ignorable: true`: a build that does not recognize one retains and skips it on read. Records never enter the model-visible surface. Resume and fork carry them with the rest of the log; Session format migration keeps them on a best-effort basis.
75
+
76
+ `pluginRecordOf(event)` returns an event as a record, or `undefined` for any other event; a `ctx.sessionProjections` unit that passes each event to it rebuilds plugin state on resume. Its `data` remains `unknown` even when the current map declares the name. The owner validates it before use because earlier builds can write different payloads, and the V3-to-V4 format edge can rename an unknown ignorable V3 event into the same `plugin:` namespace. Removing or renaming a declaration does not discard stored records. The [ignorable events decision](../../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md) owns the compatibility policy.
77
+
66
78
  ### Fork a session
67
79
 
68
80
  `ctx.sessions.fork(source, boundary?, childSessionId?)` copies an exact inclusive event prefix (default: the last event) from a live source. `buildForkSeed` in `dsh-session/fork` places an inherited marker after the copied events, adds missing error tool results only for the open step, and closes that step and turn with a `forked` reason. Closed steps and turns remain unchanged, including historical missing results. The marker and closers belong to the child; `inheritedEventCount` counts only the copied prefix.
@@ -101,7 +113,6 @@ The package is built on event sourcing: a `Session` is an append-only log of typ
101
113
  | [`src/request-header.ts`](src/request-header.ts) | `request/header` folding and reconstruction |
102
114
  | [`dsh-util-values`](../../util/values/README.md) | Shared lossless JSON validation and detached snapshots |
103
115
  | [`src/repair.ts`](src/repair.ts) | Shared tool-result recovery for failed steps, interrupted logs, and fork seeds |
104
- | [`src/invariant.ts`](src/invariant.ts) | Invariant companion: seq, turn/step enclosure, tool call/result pairing |
105
116
 
106
117
  ### Append validation
107
118
 
@@ -109,11 +120,11 @@ Every append uses the shared iterative `snapshotJsonValue()` pass, which reads,
109
120
 
110
121
  ### Shared recovery
111
122
 
112
- `ToolCallRecovery` tracks unanswered requests from committed events without retaining event history. AgentLoop observes live steps; crash recovery and fork-seed construction replay their prefixes through `openTurnClosers`. Live failures and crash recovery use interrupted-result wording by default; fork construction passes the fork cause to select its distinct retry guidance. The caller appends recovery results before closing the step ([decision](../../../.agents/notes/implemented/bug-fix/2026-09-19-failed-step-tool-results.md)).
123
+ `ToolCallRecovery` tracks unanswered requests from committed events without retaining event history. AgentLoop observes live steps; crash recovery and fork-seed construction replay their prefixes through `openTurnClosers`. Live failures and crash recovery use interrupted-result wording by default; fork construction passes the fork cause to select its distinct retry guidance. The caller appends recovery results before closing the step ([reference](../agent-loop/README.md)).
113
124
 
114
125
  ### Derived history
115
126
 
116
- `deriveMessages()` caches deep-frozen projections and returns a fresh array per call. The surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) supply their recorded message identities and content; empty-content system and developer nodes project to no message. Plugin-owned projections change derived content without mutating recorded messages. Replacements and projection decisions invalidate the cache. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only.
127
+ `deriveMessages()` caches deep-frozen projections and returns a fresh array per call. The surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) supply their recorded message identities and content; empty-content system and developer nodes project to no message. Plugin-owned projections change derived content without mutating recorded messages. Replacements and projection decisions invalidate the cache. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only. Ordinary prompts and injected context project without automatic type- or source-specific wrappers. Producers own any framing in their recorded content.
117
128
 
118
129
  ### The request header
119
130
 
@@ -157,7 +168,7 @@ Appended surface entries preserve reusable prefixes. A `replace` operation inval
157
168
 
158
169
  #### What the model sees
159
170
 
160
- If recovery finds an assistant tool request with no durable `tool/call`, its synthetic `TOOL_NOT_STARTED` result says `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.` If a durable `tool/call` has no result, its `TOOL_OUTCOME_UNKNOWN` result says `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.` Fork-generated results describe only the inherited records: the parent may have started or completed a call after the selected event. `TOOL_NOT_STARTED` means the prefix contains no start record; `TOOL_OUTCOME_UNKNOWN` means it contains a start but no result. Both tell the model to retry only read-only or idempotent operations without further checks; operations with side effects require external verification or user input. See the [fork decision](../../../.agents/notes/implemented/feature/2026-08-18-arbitrary-seq-session-fork.md).
171
+ If recovery finds an assistant tool request with no durable `tool/call`, its synthetic `TOOL_NOT_STARTED` result says `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.` If a durable `tool/call` has no result, its `TOOL_OUTCOME_UNKNOWN` result says `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.` Fork-generated results describe only the inherited records: the parent may have started or completed a call after the selected event. `TOOL_NOT_STARTED` means the prefix contains no start record; `TOOL_OUTCOME_UNKNOWN` means it contains a start but no result. Both tell the model to retry only read-only or idempotent operations without further checks; operations with side effects require external verification or user input. See the [fork reference](src/fork.ts).
161
172
 
162
173
  #### Token effect
163
174
 
@@ -189,7 +200,7 @@ Logging causes no invalidation, and exact reconstruction preserves request-prefi
189
200
  These limits define when the session store needs special care. They are current package constraints, not a task backlog.
190
201
 
191
202
  - **Live-source Store API** — persisted-but-unloaded sources use the Host observation path. Fork closes only the open tail and does not repair missing results in previously closed steps.
192
- - **`SESSION_FORMAT_VERSION` names the [current logical representation](../../../docs/session-format-status.md)** — the current reader rejects retired `header.system` and validates `system/message` payloads and protected-head rewrites. Historical headers and events belong to adjacent format packages; the [adjacent migration chain](../../session/session-format-catalog/README.md) converts supported history before constructing `Session`, and the write-open path publishes only the current-format successor. Equal-version unknown events require the envelope's explicit `ignorable` marker, which does not promise safe structural migration ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)).
203
+ - **`SESSION_FORMAT_VERSION` names the [current logical representation](../../../docs/session-format-status.md)** — the current reader rejects retired `header.system` and validates `system/message` payloads and protected-head rewrites. Historical headers and events belong to adjacent format packages; the [adjacent migration chain](../../session/session-format-catalog/README.md) converts supported history before constructing `Session`, and the write-open path publishes only the current-format successor. Equal-version unknown events require the envelope's explicit `ignorable` marker, which does not promise safe structural migration ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)). [Reader compatibility](../../../.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md) determines whether a change needs a version increase.
193
204
  - **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them.
194
205
  - **No session tree beyond fork** — a pi-style entry tree over branched sessions is deferred unless a consumer needs more than boundary-based forking.
195
206
 
package/README.zh.md CHANGED
@@ -63,6 +63,18 @@ session.deriveMessages() // the derived model history
63
63
 
64
64
  会话日志位置使用两种数字类型。`SessionSeq` 标识已有事件或包含端点的事件水位;`SessionLogOffset` 标识间隙、前缀长度或读取边界,并且可以等于事件数量。`SessionSeqCursor` 添加 `-1` 这个“尚无事件”值,`OptionalSessionSeq` 则在缺失本身属于数据时使用 `null`。构造函数会校验非负安全整数,brand 在运行时会被擦除,因此持久 JSON 与 wire 值仍是普通数字。
65
65
 
66
+ <a id="write-experimental-plugin-records"></a>
67
+
68
+ ### 写入实验性插件记录
69
+
70
+ `appendPluginRecord(session, type, data)` 为 `packages/experimental/` 下的包追加一条插件记录;`verify-plugin-record-callers` 检查拒绝本仓库中任何其他生产调用方。本仓库之外的插件同样不得调用它,尽管仓库检查无法检查它们。
71
+
72
+ 在所属实验性包的 `src/` 中,通过 `@deepseek-ai/dsh-session/types` 的 `PluginRecordMap` 为每条记录声明描述与显式 payload 类型注解。名称使用 `plugin:<owner>/<record>`,其中 `owner` 是包名 `@deepseek-ai/dsh-experimental-<owner>` 的后缀;例如 `plugin:pi-extensions/entry`。TypeScript 按声明检查每个名称与 payload;写入方运行时的 JSON 快照拒绝无法无损保留的值。扩展自定义名称或其他动态名称放在已声明记录的 payload 中。
73
+
74
+ [实验性持久化目录](../../../docs/experimental-persistence-catalog.zh.md)列出当前插件记录声明及其所属包、描述、payload 类型注解与源文件。这些声明与 `SessionEventMap`、展开的持久化 schema、已发布类型历史及 `KNOWN_SESSION_EVENT_TYPES` 分开。记录带有 `ignorable: true`:不认识某条记录的构建在读取时保留并跳过它。记录从不进入模型可见的 surface。resume 与 fork 随日志其余部分一起携带它们;Session 格式迁移以尽力而为的方式保留它们。
75
+
76
+ `pluginRecordOf(event)` 把事件作为记录返回,对其他任何事件返回 `undefined`;把每个事件都交给它的 `ctx.sessionProjections` 单元在 resume 时重建插件状态。即使当前映射声明了该名称,其 `data` 仍为 `unknown`。所属包在使用前校验数据,因为早先构建可能写入不同的 payload,V3-to-V4 格式迁移边也可能把未知的 ignorable V3 事件改名放入同一个 `plugin:` 命名空间。删除或重命名声明不会丢弃已存记录。[ignorable 事件决策](../../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md)定义兼容性策略。
77
+
66
78
  ### 派生会话的 fork
67
79
 
68
80
  `ctx.sessions.fork(source, boundary?, childSessionId?)` 从实时源会话复制包含切点的精确事件前缀(默认:最后一个事件)。`dsh-session/fork` 的 `buildForkSeed` 在复制事件之后放置继承标记,仅为开放步骤补缺失的错误工具结果,并以 `forked` 原因关闭步骤和轮次。已关闭的步骤与轮次保持原样,包括历史缺失结果。标记和结束事件属于子会话;`inheritedEventCount` 只统计复制的前缀。
@@ -101,7 +113,6 @@ session.deriveMessages() // the derived model history
101
113
  | [`src/request-header.ts`](src/request-header.ts) | `request/header` 折叠与重建 |
102
114
  | [`dsh-util-values`](../../util/values/README.zh.md) | 共享无损 JSON 校验与分离式快照 |
103
115
  | [`src/repair.ts`](src/repair.ts) | 失败步骤、中断日志与 fork 种子共享的工具结果恢复 |
104
- | [`src/invariant.ts`](src/invariant.ts) | 不变式配套:序号、轮次/步骤闭合、工具调用/结果配对 |
105
116
 
106
117
  ### 追加校验
107
118
 
@@ -109,11 +120,11 @@ session.deriveMessages() // the derived model history
109
120
 
110
121
  ### 共享恢复逻辑
111
122
 
112
- `ToolCallRecovery` 从已提交事件中跟踪尚无结果的请求,不保留事件历史。AgentLoop 观察实时步骤;崩溃恢复与 fork 种子构造通过 `openTurnClosers` 回放各自的前缀。实时失败与崩溃恢复默认使用中断结果文案;fork 构造传入 fork 原因,以选择其专用的重试指引。调用方在关闭步骤之前追加恢复结果([决策](../../../.agents/notes/implemented/bug-fix/2026-09-19-failed-step-tool-results.zh.md))。
123
+ `ToolCallRecovery` 从已提交事件中跟踪尚无结果的请求,不保留事件历史。AgentLoop 观察实时步骤;崩溃恢复与 fork 种子构造通过 `openTurnClosers` 回放各自的前缀。实时失败与崩溃恢复默认使用中断结果文案;fork 构造传入 fork 原因,以选择其专用的重试指引。调用方在关闭步骤之前追加恢复结果([参考](../agent-loop/README.zh.md))。
113
124
 
114
125
  ### 派生历史
115
126
 
116
- `deriveMessages()` 缓存深度冻结的派生消息,每次调用返回新数组。surface 事件类型(`system/message`、`developer/message`、`user/message`、`assistant/message`、`tool/result`)提供记录的消息身份和内容,空内容的 system 和 developer 节点不派生消息。插件拥有的投影修改派生内容,不修改记录的消息。替换和投影决策使缓存失效。嵌入式 Assistant stream 与 `assistant/attempt` 事件只保留回放和诊断数据。
127
+ `deriveMessages()` 缓存深度冻结的派生消息,每次调用返回新数组。surface 事件类型(`system/message`、`developer/message`、`user/message`、`assistant/message`、`tool/result`)提供记录的消息身份和内容,空内容的 system 和 developer 节点不派生消息。插件拥有的投影修改派生内容,不修改记录的消息。替换和投影决策使缓存失效。嵌入式 Assistant stream 与 `assistant/attempt` 事件只保留回放和诊断数据。普通提示与注入上下文在派生时不会按类型或来源自动添加包装。生产方负责记录内容中的所有框定格式。
117
128
 
118
129
  ### 请求头
119
130
 
@@ -157,7 +168,7 @@ session.deriveMessages() // the derived model history
157
168
 
158
169
  #### 模型看到什么
159
170
 
160
- 如果恢复发现 assistant 工具请求没有持久 `tool/call`,其合成 `TOOL_NOT_STARTED` 结果内容为 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。如果持久 `tool/call` 没有结果,其 `TOOL_OUTCOME_UNKNOWN` 结果内容为 `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.`。 Fork 生成的结果只描述继承记录:父会话可能已经在所选事件之后启动或完成调用。`TOOL_NOT_STARTED` 表示前缀中没有启动记录;`TOOL_OUTCOME_UNKNOWN` 表示有启动记录但没有结果。两者都要求模型仅对只读或幂等操作直接重试;有副作用的操作需要先验证外部状态或询问用户。参见 [fork 决策](../../../.agents/notes/implemented/feature/2026-08-18-arbitrary-seq-session-fork.zh.md)。
171
+ 如果恢复发现 assistant 工具请求没有持久 `tool/call`,其合成 `TOOL_NOT_STARTED` 结果内容为 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。如果持久 `tool/call` 没有结果,其 `TOOL_OUTCOME_UNKNOWN` 结果内容为 `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.`。 Fork 生成的结果只描述继承记录:父会话可能已经在所选事件之后启动或完成调用。`TOOL_NOT_STARTED` 表示前缀中没有启动记录;`TOOL_OUTCOME_UNKNOWN` 表示有启动记录但没有结果。两者都要求模型仅对只读或幂等操作直接重试;有副作用的操作需要先验证外部状态或询问用户。参见 [fork 参考](src/fork.ts)。
161
172
 
162
173
  #### Token 影响
163
174
 
@@ -189,7 +200,7 @@ session.deriveMessages() // the derived model history
189
200
  这些限制说明会话存储何时需要特别留意。它们是当前包约束,不是任务积压。
190
201
 
191
202
  - **Store API 仅接受活跃源会话** — 已持久化但未加载的源会话通过 Host 观察路径处理。Fork 仅闭合开放尾部,不修复先前已闭合步骤中缺失的结果。
192
- - **`SESSION_FORMAT_VERSION` 命名[当前逻辑表示](../../../docs/session-format-status.zh.md)**——当前读取器拒绝已退役的 `header.system`,并校验 `system/message` 载荷与受保护头节点的重写。历史 header 与事件归相邻格式包所有;[相邻迁移链](../../session/session-format-catalog/README.zh.md)在构造 `Session` 前转换受支持的历史,写打开只发布当前格式的后继代际。同版本未知事件要求信封显式带有 `ignorable` 标记,但这不保证结构迁移的安全性([机制](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。
203
+ - **`SESSION_FORMAT_VERSION` 命名[当前逻辑表示](../../../docs/session-format-status.zh.md)**——当前读取器拒绝已退役的 `header.system`,并校验 `system/message` 载荷与受保护头节点的重写。历史 header 与事件归相邻格式包所有;[相邻迁移链](../../session/session-format-catalog/README.zh.md)在构造 `Session` 前转换受支持的历史,写打开只发布当前格式的后继代际。同版本未知事件要求信封显式带有 `ignorable` 标记,但这不保证结构迁移的安全性([机制](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。[读取器兼容性](../../../.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.zh.md)决定变更是否需要升版本。
193
204
  - **`TurnEndReasonMap` 不含 ACP(Agent Client Protocol)命名的 `refusal`/`max_turn_requests` 变体**:受生产方约束;只有当适配器或循环首次产生这些变体时才加入。
194
205
  - **fork 之外没有会话树**:基于分支会话的 pi 风格条目树被推迟,除非消费方需要超越基于边界的 forking 的能力。
195
206
 
package/lib/index.js CHANGED
@@ -38,20 +38,20 @@ function SessionLogOffset(value) {
38
38
  * event-body read composes the build-static adjacent chain and publishes only
39
39
  * this final generation before constructing a Session.
40
40
  *
41
- * The version is a single monotonic integer with no major/minor split. Whether
42
- * a bump is needed is decided by what the WRITER emits, never by what a newer
43
- * reader can accept: bump exactly when an older runtime could no longer handle
44
- * a new log with full semantic correctness ("parses without error" is not
45
- * correctness — silently skipping content that shapes reconstruction is a
46
- * wrong read). Only structural changes reach that bar: the header shape, the
47
- * {@link SessionEvent} envelope, core event semantics, or the surface
48
- * mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).
49
- * Adding an ordinary event type does not bump — the per-event
50
- * {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
51
- * in doubt, bump: a near-identity upgrade step is almost free, a missed bump
52
- * makes older runtimes read new logs wrong silently. The released migration,
53
- * immutable prior-generation, and current fast-path rules are recorded in
54
- * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
41
+ * The version is a single monotonic integer with no major/minor split. Retain
42
+ * it when older readers resume correctly, preserve safely ignorable records,
43
+ * or refuse required features before execution, mutation, or tail repair.
44
+ * New readers must preserve historical meanings, including logs mixing old
45
+ * and new record representations. Bump when an existing discriminator cannot
46
+ * prevent unsafe interpretation; a structural difference alone is insufficient.
47
+ * Unknown fields and values require evidence of the released reader's actual
48
+ * admission behavior. The per-event {@link SessionEvent.ignorable} marker
49
+ * governs unknown event types, not arbitrary nested payload changes.
50
+ * Same-version readers retain earlier representations: the adjacent migration
51
+ * chain does not run when stored and current versions match. A subsequent
52
+ * migration must account for all supported representations of its source.
53
+ * Compatibility review and the retained migration obligations are recorded in
54
+ * `.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md`.
55
55
  */
56
56
  const SESSION_FORMAT_VERSION = 4;
57
57
  //#endregion
@@ -135,6 +135,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
135
135
  "turn/start",
136
136
  "user/message",
137
137
  "web/deepseek-search-llm-request",
138
+ "working-directory/change",
138
139
  "workspace/changes"
139
140
  ]);
140
141
  /** Event types whose model-visible effects require an explicit pure interpreter. */
@@ -1238,6 +1239,79 @@ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
1238
1239
  /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
1239
1240
  const attachments = /* @__PURE__ */ new WeakMap();
1240
1241
  /**
1242
+ * Grammar of a {@link PluginRecordType}: `plugin:` followed by
1243
+ * slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
1244
+ * each segment starting with a letter or digit.
1245
+ */
1246
+ const PLUGIN_RECORD_TYPE = /^plugin:[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/u;
1247
+ /**
1248
+ * Whether one event type name follows the plugin record grammar.
1249
+ * @param type - an event type name.
1250
+ * @returns whether the name is a {@link PluginRecordType}.
1251
+ */
1252
+ function isPluginRecordType(type) {
1253
+ return PLUGIN_RECORD_TYPE.test(type);
1254
+ }
1255
+ /**
1256
+ * Type one constructed plugin record as a log event after checking its type
1257
+ * against the plugin record grammar. A restored Session already holds events
1258
+ * whose types are outside `SessionEventMap` when they are marked ignorable;
1259
+ * a plugin record is such an event.
1260
+ * @param value - the frozen record envelope built by {@link Session}.
1261
+ * @throws when the record type is outside the plugin record grammar.
1262
+ */
1263
+ function assertPluginRecordEvent(value) {
1264
+ const { type } = value;
1265
+ if (!isPluginRecordType(type)) throw new Error(`plugin record type "${type}" must be "plugin:" followed by lowercase slash-separated segments`);
1266
+ }
1267
+ /** Module-private access to {@link Session}'s record commit, assigned once by its static block. */
1268
+ let commitPluginRecord;
1269
+ /**
1270
+ * Append one experimental plugin record: an event marked `ignorable` whose
1271
+ * type is declared in {@link PluginRecordMap}, outside {@link SessionEventMap}.
1272
+ * The plugin record catalog lists it separately from released event schemas.
1273
+ * A reader that does not recognize the type retains and skips the record, which never
1274
+ * enters the model-visible surface, and fork and resume carry it with the rest
1275
+ * of the log. A Session format migration retains records on a best-effort
1276
+ * basis, so a record holds plugin-owned state that its owner can lose without
1277
+ * changing how the rest of the log is interpreted.
1278
+ *
1279
+ * Only production source under `packages/experimental/` may call this
1280
+ * function; the `verify-plugin-record-callers` gate rejects any other
1281
+ * production caller in this repository. Read records back with
1282
+ * {@link pluginRecordOf}.
1283
+ * @param session - the Session whose log receives the record.
1284
+ * @param type - declared record name: `plugin:` followed by slash-separated
1285
+ * segments of lowercase letters, digits, `.`, `_`, and `-`, each starting
1286
+ * with a letter or digit.
1287
+ * @param data - JSON payload, snapshotted before it enters the log.
1288
+ * @returns the sequence number of the committed record.
1289
+ * @throws when `type` is outside that grammar, when `data` is not losslessly
1290
+ * JSON-serializable, or when the call reenters another append's publication;
1291
+ * a rejected record does not change the log.
1292
+ */
1293
+ function appendPluginRecord(session, type, data) {
1294
+ return commitPluginRecord(session, type, data);
1295
+ }
1296
+ /**
1297
+ * Read one committed event as a plugin record. The V3-to-V4 format edge also
1298
+ * renames each unknown ignorable V3 event into the `plugin:` namespace, so an
1299
+ * owner names its records under its own package name and validates `data`.
1300
+ * @param event - any committed Session event, live or restored.
1301
+ * @returns the record, or undefined when the event is not an ignorable event
1302
+ * whose type follows the plugin record grammar.
1303
+ */
1304
+ function pluginRecordOf(event) {
1305
+ const type = event.type;
1306
+ if (event.ignorable !== true || !isPluginRecordType(type)) return void 0;
1307
+ return {
1308
+ type,
1309
+ seq: event.seq,
1310
+ time: event.time,
1311
+ data: event.data
1312
+ };
1313
+ }
1314
+ /**
1241
1315
  * An event-sourced session: an append-only log of {@link SessionEvent}s.
1242
1316
  *
1243
1317
  * Plain class (not a Service) — create live instances via
@@ -1448,8 +1522,7 @@ var Session = class Session {
1448
1522
  if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
1449
1523
  const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
1450
1524
  if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
1451
- const entry = attachments.get(this);
1452
- if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
1525
+ const entry = this.publicationEntry();
1453
1526
  const event = deepFreeze({
1454
1527
  type,
1455
1528
  seq: SessionSeq(this.log.length),
@@ -1458,6 +1531,50 @@ var Session = class Session {
1458
1531
  ...surfaceMetadataSnapshot
1459
1532
  });
1460
1533
  validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
1534
+ this.commit(event, entry);
1535
+ return event;
1536
+ }
1537
+ static {
1538
+ commitPluginRecord = (session, type, data) => session.#appendRecord(type, data);
1539
+ }
1540
+ /**
1541
+ * Commit one plugin record; {@link appendPluginRecord} is the only caller.
1542
+ * @param type - record type, checked against the plugin record grammar.
1543
+ * @param data - JSON payload, snapshotted before it enters the log.
1544
+ * @returns the record's sequence number.
1545
+ */
1546
+ #appendRecord(type, data) {
1547
+ const dataSnapshot = snapshotJsonValue(data);
1548
+ if (dataSnapshot === void 0) throw new Error(`plugin record "${type}" carries non-JSON-serializable data`);
1549
+ const entry = this.publicationEntry();
1550
+ const record = deepFreeze({
1551
+ type,
1552
+ seq: SessionSeq(this.log.length),
1553
+ time: Date.now(),
1554
+ data: dataSnapshot,
1555
+ ignorable: true
1556
+ });
1557
+ assertPluginRecordEvent(record);
1558
+ this.commit(record, entry);
1559
+ return record.seq;
1560
+ }
1561
+ /**
1562
+ * Read the store attachment for one append, refusing an append that
1563
+ * reenters while another append is being published.
1564
+ * @returns the attachment, or undefined for a detached Session.
1565
+ */
1566
+ publicationEntry() {
1567
+ const entry = attachments.get(this);
1568
+ if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
1569
+ return entry;
1570
+ }
1571
+ /**
1572
+ * Accept one frozen candidate at the next sequence number, enter it into the
1573
+ * log, and notify `session/event` observers with per-listener containment.
1574
+ * @param event - the candidate built at the current log length.
1575
+ * @param entry - the attachment read before the candidate was built.
1576
+ */
1577
+ commit(event, entry) {
1461
1578
  this.surfaceManager.validateNext(event);
1462
1579
  if (entry !== void 0) entry.appending = true;
1463
1580
  try {
@@ -1471,7 +1588,6 @@ var Session = class Session {
1471
1588
  this.log.push(event);
1472
1589
  this.eventsSnapshot = void 0;
1473
1590
  if (callbacks !== void 0 && entry !== void 0) invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
1474
- return event;
1475
1591
  } finally {
1476
1592
  if (entry !== void 0) {
1477
1593
  entry.appending = false;
@@ -1822,8 +1938,8 @@ var SessionStore = class extends Service {
1822
1938
  * store owns the carrier, so callers (the checkpoint policy's per-request
1823
1939
  * barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
1824
1940
  * that flush themselves before reading storage) must come through here
1825
- * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
1826
- * one spelling, and the scoped-dispatch invariant can pin it.
1941
+ * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
1942
+ * and one spelling.
1827
1943
  * @param session - the session whose buffered events must reach durable storage.
1828
1944
  * @returns whether at least one durability listener participated, after every
1829
1945
  * listener has settled successfully.
@@ -1930,4 +2046,4 @@ var SessionStore = class extends Service {
1930
2046
  }
1931
2047
  };
1932
2048
  //#endregion
1933
- export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, ToolCallRecovery, adoptSessionEvent, buildForkSeed, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, snapshotSessionEvent };
2049
+ export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, ToolCallRecovery, adoptSessionEvent, appendPluginRecord, buildForkSeed, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, pluginRecordOf, snapshotSessionEvent };
@@ -10,7 +10,7 @@ import type { Scoped } from '@deepseek-ai/dsh-scope';
10
10
  import type { Message } from '@deepseek-ai/dsh-llm';
11
11
  import { SessionLogOffset, SessionSeq } from './types.ts';
12
12
  import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
13
- import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
13
+ import type { CreateSessionOptions, EpochHeader, PluginRecord, PluginRecordMap, PluginRecordType, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
14
14
  import type { SessionSurface, SessionMessageProjection } from './surface.ts';
15
15
  import type { ToolHistory } from '@deepseek-ai/dsh-llm';
16
16
  export { buildForkSeed } from './fork.ts';
@@ -36,7 +36,6 @@ declare module '@deepseek-ai/cordis' {
36
36
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
37
37
  * receive only sessions entered through that agent's context.
38
38
  * @param session - the session just entered and announced.
39
- * @dshScopeScan unsupported
40
39
  * @mode emit
41
40
  */
42
41
  'session/created'(this: Scoped<Session>, session: Session): void;
@@ -46,7 +45,6 @@ declare module '@deepseek-ai/cordis' {
46
45
  * did not begin. Listener failures are logged and contained.
47
46
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
48
47
  * @param session - the session that is no longer live in the store.
49
- * @dshScopeScan unsupported
50
48
  * @mode emit
51
49
  */
52
50
  'session/disposed'(this: Scoped<Session>, session: Session): void;
@@ -58,7 +56,6 @@ declare module '@deepseek-ai/cordis' {
58
56
  * receive only events from sessions entered through that agent's context.
59
57
  * @param session - the session whose log grew.
60
58
  * @param event - the appended event, exactly as recorded.
61
- * @dshScopeScan unsupported
62
59
  * @mode emit
63
60
  */
64
61
  'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void;
@@ -67,7 +64,6 @@ declare module '@deepseek-ai/cordis' {
67
64
  * caller awaits all of them, with no waterfall veto. Scope-filtered dispatch
68
65
  * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
69
66
  * @param session - the session whose buffered events must reach durable storage.
70
- * @dshScopeScan unsupported
71
67
  * @mode parallel
72
68
  */
73
69
  'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void;
@@ -94,6 +90,40 @@ export declare function adoptSessionEvent<T extends SessionEvent>(event: T): T;
94
90
  * @returns a detached event snapshot with a validated, deeply frozen message.
95
91
  */
96
92
  export declare function snapshotSessionEvent<T extends SessionEvent>(event: T): T;
93
+ /**
94
+ * Append one experimental plugin record: an event marked `ignorable` whose
95
+ * type is declared in {@link PluginRecordMap}, outside {@link SessionEventMap}.
96
+ * The plugin record catalog lists it separately from released event schemas.
97
+ * A reader that does not recognize the type retains and skips the record, which never
98
+ * enters the model-visible surface, and fork and resume carry it with the rest
99
+ * of the log. A Session format migration retains records on a best-effort
100
+ * basis, so a record holds plugin-owned state that its owner can lose without
101
+ * changing how the rest of the log is interpreted.
102
+ *
103
+ * Only production source under `packages/experimental/` may call this
104
+ * function; the `verify-plugin-record-callers` gate rejects any other
105
+ * production caller in this repository. Read records back with
106
+ * {@link pluginRecordOf}.
107
+ * @param session - the Session whose log receives the record.
108
+ * @param type - declared record name: `plugin:` followed by slash-separated
109
+ * segments of lowercase letters, digits, `.`, `_`, and `-`, each starting
110
+ * with a letter or digit.
111
+ * @param data - JSON payload, snapshotted before it enters the log.
112
+ * @returns the sequence number of the committed record.
113
+ * @throws when `type` is outside that grammar, when `data` is not losslessly
114
+ * JSON-serializable, or when the call reenters another append's publication;
115
+ * a rejected record does not change the log.
116
+ */
117
+ export declare function appendPluginRecord<K extends Extract<keyof PluginRecordMap, PluginRecordType>>(session: Session, type: K, data: NoInfer<PluginRecordMap[K]>): SessionSeq;
118
+ /**
119
+ * Read one committed event as a plugin record. The V3-to-V4 format edge also
120
+ * renames each unknown ignorable V3 event into the `plugin:` namespace, so an
121
+ * owner names its records under its own package name and validates `data`.
122
+ * @param event - any committed Session event, live or restored.
123
+ * @returns the record, or undefined when the event is not an ignorable event
124
+ * whose type follows the plugin record grammar.
125
+ */
126
+ export declare function pluginRecordOf(event: SessionEvent): PluginRecord | undefined;
97
127
  /**
98
128
  * An event-sourced session: an append-only log of {@link SessionEvent}s.
99
129
  *
@@ -103,6 +133,7 @@ export declare function snapshotSessionEvent<T extends SessionEvent>(event: T):
103
133
  * @typert object
104
134
  */
105
135
  export declare class Session {
136
+ #private;
106
137
  private log;
107
138
  /** Single incremental owner of surface acceptance and projection state. */
108
139
  private readonly surfaceManager;
@@ -244,6 +275,19 @@ export declare class Session {
244
275
  * rejects before the log changes.
245
276
  */
246
277
  append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;
278
+ /**
279
+ * Read the store attachment for one append, refusing an append that
280
+ * reenters while another append is being published.
281
+ * @returns the attachment, or undefined for a detached Session.
282
+ */
283
+ private publicationEntry;
284
+ /**
285
+ * Accept one frozen candidate at the next sequence number, enter it into the
286
+ * log, and notify `session/event` observers with per-listener containment.
287
+ * @param event - the candidate built at the current log length.
288
+ * @param entry - the attachment read before the candidate was built.
289
+ */
290
+ private commit;
247
291
  /** Cached fold of the request-header events — see {@link requestHeader}. */
248
292
  private headerFold;
249
293
  /** Log position (events consumed) the header fold has reached. */
@@ -429,8 +473,8 @@ export declare class SessionStore extends Service {
429
473
  * store owns the carrier, so callers (the checkpoint policy's per-request
430
474
  * barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
431
475
  * that flush themselves before reading storage) must come through here
432
- * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
433
- * one spelling, and the scoped-dispatch invariant can pin it.
476
+ * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
477
+ * and one spelling.
434
478
  * @param session - the session whose buffered events must reach durable storage.
435
479
  * @returns whether at least one durability listener participated, after every
436
480
  * listener has settled successfully.