Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	packages/context/workspace-context/README.md
#	packages/llm/llm-retry/README.md
#	packages/session-persistence/session-checkpoint-policy/README.md
#	scripts/type-equiv.manifest.json
This commit is contained in:
_Kerman
2026-07-26 16:45:27 +08:00
699 changed files with 15263 additions and 1394 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: ac4e0a8310152b9d2ba5daae61fbbf1eb0ed54ec
README.zh.md: cae5cbd83bac5ceed59067217635b98b194949ce

View File

@@ -1,5 +1,7 @@
# session-persistence/ — persistence capability family
English | [中文](README.zh.md)
The durable session-persistence seam and its storage backends. The interface package owns the abstract `SessionPersistence` service and the shared write coordinator; the backends are concrete implementations that register on `ctx.sessionPersistence`. All **product** packages.
| Package | Role | ctx key |

View File

@@ -0,0 +1,14 @@
# session-persistence/:持久化功能家族
[English](README.md) | 中文
持久会话持久化 seam 及其存储后端。接口包负责抽象 `SessionPersistence` 服务和共享写入协调器;后端是注册到 `ctx.sessionPersistence` 的具体实现。全部都是**产品** 包。
| 包 | 职责 | ctx 键 |
|---|---|---|
| `session-persistence/` | 持久化 seam + 共享写入协调器 | `ctx.sessionPersistence` |
| `session-checkpoint-policy/` | agent 请求和工具执行的语义持久性屏障 | (包装 `ctx.llm` / `ctx.tools`,监听 agent 事件) |
| `session-persistence-jsonl/` | JSONL sidecar 持久化后端 | (注册 `ctx.sessionPersistence` |
| `session-persistence-sqlite/` | SQLite 持久化后端 | (注册 `ctx.sessionPersistence` |
接口位于 `session-persistence/session-persistence/`;后端是平级同级包。新存储后端在此加入,并注册到 `ctx.sessionPersistence`。详见[会话持久化](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: aba0f00a2960eca3db06dda267cc954f602c1b09
README.zh.md: ea17fb00ec22cb3a94ae22dc4a37aa7e2f73a225

View File

@@ -1,5 +1,7 @@
# dsh-session-checkpoint-policy
English | [中文](README.zh.md)
Semantic durability policy for persisted agents. It checkpoints the event-sourced session before a model adapter receives a request, before a top-level tool body may produce an external side effect, and at each `agent/step` boundary so the preceding response and ordered tool results are durable before the next request.
## Plugin (namespace: `session-checkpoint-policy`)

View File

@@ -0,0 +1,45 @@
# dsh-session-checkpoint-policy
[English](README.md) | 中文
持久化 agent 的语义持久性策略。它会在模型适配器收到请求前、顶层工具正文可产生外部副作用前,以及每个 `agent/step` 边界为事件溯源会话创建检查点,使前一响应与有序工具结果在下一个请求前持久。
## 插件(命名空间:`session-checkpoint-policy`
该零配置函数插件消费 `ctx.sessions``ctx.llm``ctx.tools` 以及 `ctx.sessionPersistence` 的存在性。将其与一个持久化后端一起加载:
```yaml
- id: session-persistence
name: '@deepseek-ai/dsh-session-persistence-jsonl'
- id: session-checkpoints
name: '@deepseek-ai/dsh-session-checkpoint-policy'
```
持久化与检查点调度刻意拆分为独立 Cordis 插件。持久化后端会立即写入 `session/event` 追加,并把每个已请求 `session/flush` 变成观测屏障;该策略选择请求、工具分派和下一步骤屏障。不带此策略加载后端是有效的,但崩溃可能丢失最新的立即缓冲事件。第一方持久化应用和运行时显式挂载两个插件;专用部署可以刻意省略或替换策略。
策略延迟包装 `llm/stream`,因此下游流只会在实时会话缓冲请求事件持久后构造。它在预执行策略和保护后包装 `tools/execute`;只有在已记录调用持久后,顶层工具正文才会运行。如果取消在 flush 等待期间到达,包装层会返回规范 `ABORTED_BEFORE_DISPATCH` 结果,不进入工具正文。嵌套工具分派重用外层模型可见调用的检查点。`agent/step` 在派生请求前持久前一响应/结果批次。
在模型和工具边界,检查点拒绝会快速失败:适配器和顶层工具正文都不运行。步骤边界拒绝会在另一个请求开始前使轮次失败。并发工具检查点共享会话存储的串行持久化 drain无法复制序列号。
## 模型体验
### 中断调用
#### 模型所见
插件不添加提示词或工具 schema。工具检查点后、结果前的硬崩溃会留下持久的未匹配调用会话恢复会提供模型可见的 `TOOL_OUTCOME_UNKNOWN` 结果,该结果由 `dsh-session` 负责。该消息允许重试只读或幂等工作,并要求对可能有副作用的调用验证状态或请求用户确认。
#### Token 影响
成功检查点不添加 token也不改变请求。恢复会添加一条短工具结果消息以平衡中断 transcript。
#### KV 缓存影响
修复结果追加在可重用前缀之后,因此不会使较早的缓存条目失效。
## 已知限制与待完成工作
- 该策略持久记录执行意图,而非通用的精确一次副作用。当提供方支持时,有副作用的工具应将 `exec.callId` 作为幂等键转发。
- 流式 `assistant/chunk` 事件没有每分片检查点。它们在下一个语义检查点到达存储,因此硬崩溃可能丢失当前部分响应。
- 持久调用没有结果时,无法证明其外部副作用是否完成。因此,恢复会记录未知结果,而不是自动重试。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: a0d718cf8bd0090df0409e7c60e6f7fd559b6f7d
README.zh.md: 307bef8efb506c2df7ef229e85b3224a8e7c29e1

View File

@@ -1,5 +1,7 @@
# @deepseek-ai/dsh-session-persistence-jsonl
English | [中文](README.zh.md)
The JSONL durable session-persistence backend — a concrete `SessionPersistence` (the `dsh-session-persistence` seam). Each session has one append-only logical JSONL log, stored as `.jsonl.zstd` by default or raw `.jsonl` when compression is disabled.
## On-disk layout

View File

@@ -0,0 +1,75 @@
# @deepseek-ai/dsh-session-persistence-jsonl
[English](README.md) | 中文
JSONL 持久会话持久化后端:一个具体 `SessionPersistence``dsh-session-persistence` seam。每个会话有一个仅追加逻辑 JSONL 日志,默认存储为 `.jsonl.zstd`;禁用压缩时使用原始 `.jsonl`
## 磁盘布局
```
<root>/
--<normalized-cwd>--/ # readable project directory (or _no-cwd/)
<encoded-id>/ # session-owned directory
session.jsonl.zstd # default: checksummed header frame + append frames
session.jsonl # only with compression: 'none'
```
- 第一个逻辑行是不可变的 `SessionHeader`,标记为 `{ type: 'session', version, id, cwd?, createdAt, parentSession?, seedLength?, delegationDepth }``delegationDepth` 在磁盘上必需,顶层会话为 `0`;缺失或无效值会拒绝日志。后续每个逻辑行是一条存储记录;`assistant/chunk` 事件绝不丢弃,且 `seq` 在解码日志中保持连续(`events[i].seq === i`)。
- 存储记录是原样 `SessionEvent` JSON或仅在 `packChunks` 下写入的**打包分片行**`text-chunks` / `reasoning-chunks` / `tool-call-chunks`;像 header 的 `session` 一样不带斜杠,因此行 tag 不会与事件类型混淆):一行保存至少 3 个连续同 block `assistant/chunk` delta 事件,`seq0`/`time0` 和每成员 `dt` 间隔精确重建每个成员的 `seq`/`time`。无损 codec 位于 `@deepseek-ai/dsh-session``packChunkRuns`/`decodeStorageRecord`),并使用精确形态 allowlist任何未识别内容原样存储。读取与布局无关`load` 始终解码行,因此打包、非打包和混合文件加载结果一致。
- 项目目录保留规范化 cwd 可读,并限制在文件系统组件上限内。分隔符替换和截断刻意有损,因此规范化相同的 cwd 字符串共享项目目录;会话 id 仍选择不同会话目录。在不区分大小写的文件系统上,只有文件系统规范化将两种写法解析到同一 transcript 时,身份验证才接受备选路径写法。配置根仍由部署控制:可以是项目本地、共享、临时或集中式。[项目会话目录决策](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.md) 记录这项取舍。
- 会话 id 是未验证的品牌化字符串,因此在使用前单射转义为一个安全路径段(无遍历、无冲突)。结果目录保留给其他会话自有产物;发现只读取固定 transcript 文件名。
## 配置
| 键 | 类型 | 说明 |
|---|---|---|
| `root` | `string` (required) | 所有会话文件的根目录。**无默认值**`process.cwd()` 默认值会随进程 cwd 变更bash 调用、子进程)而分散文件。现有根必须是可读目录;缺失根在第一次实体化时创建。 |
| `packChunks` | `boolean` (default `false`) | 将 delta 分片运行写为打包行(在真实编码会话上测得逻辑日志约小 60%)。关闭时,写入逻辑布局与打包前格式字节相同;无论开关如何,都能读取打包行。快照预期输出仍是每事件一行时默认关闭:开启打包记录会重写每个 fixture `session.jsonl`。 |
| `compression` | `'zstd' \| 'none'` | 默认 `'zstd'``'none'` 保留换行分隔 UTF-8 文本。 |
`locate(meta)` 返回已解析项目/会话目录内固定 transcript 的 `{ kind: 'jsonl', path }`。它不执行文件系统 I/O可以在目录或文件存在前返回目标现有文件也只包含最后 flush 前缀。
## 物理编码
默认产物是独立 [Zstandard frame](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md) 的标准连接:一个仅包含 header 行的带 checksum frame后跟每个持久 append 批次一个带 checksum frame。后端使用 Node 内置 Zstandard API 和默认压缩级别,不提供级别开关。列表只读取并验证 header frame。`compression: 'none'` 在原始表示中保留相同逻辑行。
一个根只属于一种编码。启动发现和定向查找会拒绝相反 suffix错误会命名不兼容产物并指示调用方选择匹配 mode 或独立根。平铺 `<project>/<id>.jsonl*` 产物也会被拒绝,而不是忽略。不提供迁移、混合根回退或双写。
## 持久性与崩溃语义
- **绑定存储身份。** 查找要求可读项目目录中只有一个匹配会话目录,然后验证 header id 等于请求 id且 header id/cwd 派生所选 transcript 路径。列表应用同一路径检查,并拒绝重复 id。身份失败发生在修复或 append 前。
- **延迟实体化。**`create(meta)` 不写入;第一次 `append` 将编码 header 和第一批写入临时文件并执行 `fsync`。POSIX 通过硬链接无覆盖发布,并对父目录 `fsync`。Windows 通过 `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)` 无覆盖发布,并通过同一 write-through pattern 创建缺失目录。已创建但从未 append 的会话不留下磁盘内容,不在 `list` 中。
- **仅追加。** 已 flush 事件绝不重写。后续原始批次 append 行;压缩批次 append 一个 frame。两条路径都执行 `fsync`,并在捕获写入或同步失败时回滚到之前字节长度。
- **崩溃恢复:保留有效尾部工作。**`load` 验证每个完整压缩 frame并扫描解压 JSONL。最后 frame 结构不完整时,读取器保留其完整解码记录,从 frame 开头截断,并使用共享[持久化契约](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md) 需要的合成工具、步骤和轮次 closer 重新编码这些记录。原始 mode 从第一个不完整行截断。完整 frame 中的 checksum/解压失败,或最后已提交 `turn/end` 之前或当时的缺陷属于损坏,会被拒绝。
- **非变更检查。**`inspect()` 返回脱离的有效前缀,不截断不完整尾部或关闭中断轮次,并保持轻量修订不变。
- **连续 seq。**`append` 拒绝第一个 `seq` 不继续已存储日志的批次,并拒绝非 JSON 可序列化 `event.data`,同时命名违规事件类型。
- **轻量修订。**`listSnapshots(signal?)` 使用 device、inode、size 和纳秒时间戳标识日志,避免解析完整日志,同时在 append、修复、替换或存储变更后改变。它通过产物发现转发精确信号并在每个 `stat` 前后检查取消;由于文件系统 `stat` 不可中断,取消会等待活动调用结算,然后在不启动另一次调用的情况下拒绝。
## 写入路径
插件将冻结会话事件复制到每个实时会话的一个 controller并启动急切 drain。并发事件共享当前写入期间接纳的事件形成后续批次`session/flush` 则等待当前和 pending 批次持久。每会话游标防止恢复会话重新 append 已存储事件插件加载时会为实时会话播种。所属后端实例串行化单会话操作dispose 在拆卸前 drain 每个保留 controller。
## 模型体验
### 恢复的对话历史
#### 模型所见
JSONL 存储不贡献实时提示词或 schema。加载恢复已存储接口历史并保留之前的请求 header 用于重建;新 loop 组合当前 envelope。恢复将无持久调用的 assistant 请求平衡为 `TOOL_NOT_STARTED`;有持久调用但无结果时变为 `TOOL_OUTCOME_UNKNOWN`,它要求模型只重试只读或幂等工作,并验证可能副作用或请求用户。原始 `assistant/chunk` 记录不重复消息。
#### Token 影响
实时请求为零 token。恢复 agent 支付已保留历史和当前 envelope以及每个中断调用的引用修复结果。
#### KV 缓存影响
JSONL 存储不修改实时请求前缀。只有重建历史、当前 envelope 和模型路由匹配时,恢复 loop 才能重用提供方缓存;崩溃修复结果仅追加。
## 已知限制与待完成工作
- **只加载已配置编码和当前 `SESSION_FORMAT_VERSION` (v0)**:更改压缩需要独立/全新根,或选择遗留原始 mode预发布格式没有迁移。
- **平铺文件存储布局不加载**:加载前使用独立根,或将预发布产物移入项目/会话目录布局。
- **压缩文件不能直接按行读取**:使用后端加载;或在写入新根前选择 `compression: 'none'`,以便文本 fixture 或外部行 reader 使用。
- **不删除会话文件**:日志在 `root` 下累积直到外部移除seam 无删除接口)。
- **每会话一个实时 writer**append 和修复只在所属后端实例内协调。在 owner 完全停稳 dispose 前,其他后端实例或进程不得写入同一会话;初始同 id 发布仍通过 POSIX 无覆盖硬链接或 Windows 无替换 write-through rename 保持冲突安全。
- **POSIX 实体化需要硬链接支持**:第一次 append 使用 `link()`,使同 id 竞态失败而不覆盖已提交日志Windows 使用无替换 write-through rename。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 394b10a70fc757d75f19178050c0d63699a59e54
README.zh.md: f186d71912e61c5eb664973195ec1d05070a3cef

View File

@@ -1,5 +1,7 @@
# @deepseek-ai/dsh-session-persistence-sqlite
English | [中文](README.zh.md)
A SQLite durable session-persistence backend — a second `SessionPersistence` implementation ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the abstract seam and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes.
`locate(meta)` returns `undefined`: all sessions share one database, so there is no honest independent per-session transcript path.

View File

@@ -0,0 +1,61 @@
# @deepseek-ai/dsh-session-persistence-sqlite
[English](README.md) | 中文
SQLite 持久会话持久化后端:第二个 `SessionPersistence` 实现(见[会话持久化](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)),用于验证抽象 seam 和共享 `runPersistenceContract` 套件真正与后端无关。它满足与 `dsh-session-persistence-jsonl` 相同的契约(仅追加、连续 seq、延迟实体化、在 load 时关闭中断轮次),但用 `node:sqlite` 行而非文件字节表达。
`locate(meta)` 返回 `undefined`:所有会话共享一个数据库,因此不存在真实的独立每会话 transcript 路径。
> **TODO** 该后端直接调用 `node:sqlite`。如果采用 Cordis 数据库服务(`cordis/db` / `@cordisjs` SQL driver 插件),应改为通过该服务路由,而不在此保持原始 `DatabaseSync`;契约接口(`SessionPersistence`)不会变,只更换存储 driver。
## 存储模型
每个 `SessionEvent` 1:1 映射到 `events` 表中的一行 `(session_id, seq, type, time, data, source_event_seqs, surface_op)``data` 是作为 JSON 文本的事件 payload因此行形态就是原样事件包括 `assistant/chunk`,保持 `seq` 连续)。两个 `TEXT``source_event_seqs``surface_op` 可为空,存储事件可选接口元数据字段(见[会话接口](../../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md))。日志外元数据(`SessionHeader`)、每实体化 incarnation id 和每日志单调修订位于 `sessions` 行;`createdAt` 是存储在 strict `INTEGER` 列中的非负安全整数。单例状态行携带不可变存储 id。`sessions` 行只由第一次 `append` 写入,其存在性是延迟实体化信号(`list` 精确报告有行的会话)。
仓库支持的 Node 范围可不加 flag 使用 `node:sqlite`。数据库启用外键,并使用已配置 journal mode默认 `wal`WAL 共享内存文件不适用时使用 rollback mode`PRAGMA application_id` 标识规范持久化数据库,`PRAGMA user_version` 存储布局版本。新数据库必须没有 application identity 或用户定义 schema 对象;初始化在一个事务中创建全部表并盖上两个 pragma。非 pristine 无版本数据库、外部 application identity 和所有非当前版本在 journal-mode 变更前拒绝,因为该未发布格式无迁移。
在具有 POSIX mode 的文件系统上,后端为缺失目录请求 mode `0700`,并在 SQLite 打开前以 mode `0600` 排他创建缺失数据库;进程 umask 可进一步限制两者。新 WAL、共享内存和持久 rollback-journal sidecar 获得数据库最终的仅所有者 mode。现有目录、数据库文件和 sidecar 保留原 mode除已存在数据库外的文件系统设置错误会使初始化失败。这些默认值防止宽松进程 umask 造成的意外暴露,但当其他 principal 能替换父目录中的数据库条目时,不保护数据库机密性或完整性。
## 行上的契约语义
- **Append = 事务。**`append` 围绕批次运行 `BEGIN`/`COMMIT`:它实体化 `sessions` 行(如果仍延迟),并 INSERT 每个事件,首先断言连续 seq 契约(第一个事件 `seq` 必须等于已存储 next-seq。批次中失败重复 seq 上的 UNIQUE 违规)会完全回滚,使已存储日志和内存游标保持一致。(`load()` 已平衡已存储日志,因此 `append` 不必修复崩溃尾部。)
- **延迟实体化。**`create()` 只在内存记录意图,第一次 `append` 前不写行。从未 append 的会话没有 `sessions` 行,因此不在 `list()` 中(它精确报告有行的会话)。
- **在 load 时关闭中断轮次。**`load()` 实现共享[崩溃恢复契约](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md):保留有效中断轮次,在一个事务中追加合成关闭事件,并只移除撕裂尾部行。已提交解析错误或序列缺口使会话无法加载。恢复会变更已存储行,因此下一次 append 从平衡日志和准确游标开始。
- **非变更检查。**`inspect()` 返回脱离的有效行前缀,不删除撕裂尾部行或追加恢复 closer并保持轻量修订不变。
- **轻量修订。**`listSnapshots(signal?)` 组合不可变存储与数据库文件身份、每实体化 incarnation id以及在每个变更事务中递增的每会话计数器。它在不解析事件行的情况下保持未变观察稳定并区分独立存储和重建的同 id 日志。它在共享就绪和同步元数据查询前后检查取消;查询本身不可抢占。
## 配置schemastery
```ts
interface Config {
path: string // SQLite database file path, or ':memory:' for an in-process DB
journalMode?: 'wal' | 'delete' | 'truncate' | 'persist' // journal_mode pragma; default 'wal'
}
```
## 写入路径
与 JSONL 后端一样,插件将每个冻结 `session/event` 复制到每个实时会话的一个 controller并启动急切 drain。并发事件共享当前事务期间接纳的事件形成后续批次`session/flush` 则等待当前和 pending 批次持久。Controller 对 fork 种子持久一次,保留写入游标,使 resume 绝不重新 append 已存储事件,并在 apply 时为实时会话播种,因为 HMR 不回放 `session/created`。Dispose 在关闭数据库前 drain 每个保留 controller。
## 模型体验
### 恢复的对话历史
#### 模型所见
SQLite 存储不贡献实时提示词或 schema。加载恢复与 JSONL 相同的接口历史,并保留之前的 header 用于重建;新 loop 组合当前 envelope。恢复将无持久调用的 assistant 请求平衡为 `TOOL_NOT_STARTED`;有持久调用但无结果时变为 `TOOL_OUTCOME_UNKNOWN`,它要求模型只重试只读或幂等工作,并验证可能副作用或请求用户。行元数据和原始分片不是消息。
#### Token 影响
实时请求为零 token。Resume 恢复已保留历史并支付当前 envelope以及每个中断调用的引用修复结果。
#### KV 缓存影响
SQLite 存储不修改实时请求前缀。只有重建历史、当前 envelope 和模型路由匹配时,恢复 loop 才能重用提供方缓存;崩溃修复结果仅追加。
## 已知限制与待完成工作
- **`DatabaseSync` 是同步的**:每个 append 事务在整个期间阻塞事件 loop对本地存储可接受对繁忙多会话服务器是吞吐上限。
- **写入争用无等待或重试策略**:后端不设置 busy timeout也不重试 locked-database 错误,因此其他连接持有写事务时操作立即拒绝。
- **只打开 pristine 新数据库或当前自有 `SCHEMA_VERSION`**:无版本 schema 对象、外部 application identity 和所有其他 schema 版本被拒绝,而不是迁移(未发布软件,无持久用户数据需要保留)。
- **不删除已存储会话**行会累积直到外部移除seam 无删除接口;`ON DELETE CASCADE` 已为这种带外清理接线)。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: c99905e8aca0bdaf810de34841ea277b105a9d0f
README.zh.md: 106c28c5f9cd4330cf69b1648b669a04392e4e8a

View File

@@ -1,5 +1,7 @@
# @deepseek-ai/dsh-session-persistence
English | [中文](README.zh.md)
The abstract durable session-persistence seam (`ctx.sessionPersistence`). Defines WHAT a persistence backend does — durably store, reload, and list sessions — without saying HOW. Mirrors the `dsh-bash` capability-seam template ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract service here, a concrete implementation in a sibling package, consumers that inject the interface.
The persisted unit IS the existing `SessionEvent` (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage, seed boundary, delegation depth) travels separately as `SessionHeader`, owned by `dsh-session` and re-exported here.

View File

@@ -0,0 +1,83 @@
# @deepseek-ai/dsh-session-persistence
[English](README.md) | 中文
抽象的持久会话持久化 seam`ctx.sessionPersistence`)。它定义持久化后端做什么:持久存储、重新加载和列出会话,而不规定如何实现。它与 `dsh-bash` 功能 seam 模板一致(见[功能 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):本包提供抽象服务,同级包提供具体实现,消费方注入接口。
持久化单元就是现有 `SessionEvent`事件溯源模型日志是唯一真源因此不存在并行的「持久消息」类型。不可回放的对话状态元数据格式版本、cwd、血缘、种子边界、委托深度作为 `SessionHeader` 单独传输,该类型归 `dsh-session` 所有,并在此重新导出。
## 服务 API`ctx.sessionPersistence`
| 方法 | 契约 |
|---|---|
| `locate(meta): SessionLocation \| undefined` | 在不执行 I/O 或实体化的情况下解析绝对的每会话产物目标。没有独立本地产物的后端返回 `undefined`。 |
| `create(meta): Promise<void>` | 注册新会话元数据。可以将物理写入延迟到第一次 `append`(延迟实体化)。 |
| `append(id, events): Promise<void>` | 持久保存一个批次。仅追加;任何修复后,第一个事件 `seq` == 已存储 next-seq非 JSON 可序列化数据会被拒绝,并命名违规类型。 |
| `load(id): Promise<{ meta; events }>` | 返回已存储 header 和平衡、连续日志。实时 load 先 flush 其快照,并在轮次开放时拒绝;冷 load 保留中断的最终轮次,并用合成 `tool/result`/`step/end?`/`turn/end {interrupted}` 事件关闭它。只丢弃撕裂尾部碎片;已提交损坏和未知 `version` 会被拒绝。 |
| `inspect(id, signal?): Promise<{ meta; events }>` | 返回脱离的有效已存储前缀,不截断撕裂尾部、合成恢复 closer 或发布协调器状态。它与同 id 写入串行化;可选信号会迅速拒绝已排队调用方,阻止该后端读取启动,并取消活动后端读取工作。用于绝不应恢复日志的读模型和其他观察者。 |
| `list(signal?): Promise<SessionHeader[]>` | 从元数据轻量列出,不解析完整日志。可选信号取消后端列表工作。零事件延迟实体化会话不在 `list` 中。 |
| `listSnapshots(signal?): Promise<SessionPersistenceSnapshot[]>` | 返回轻量元数据和不透明品牌化每日志修订不加载事件日志。日志及其后端存储不变时修订保持相等append 或变更性 load 修复后会改变;不会仅因两个存储使用相同本地计数器而冲突。可选信号请求取消后端发现工作;第一方后端在拒绝前结算已启动列表工作,使已等待调用完全停稳。 |
## 每个后端必须遵守的不变量
- **仅追加;崩溃轮次会被关闭,而非截断。** 已 flush 事件绝不重写。崩溃可留下未关闭最终轮次,其事件真实且可能很大;`load` 保留它们,并持久追加合成 closer为每个未回答 assistant 调用添加按风险分类错误 `tool/result`,再添加 `step/end?`+`turn/end {interrupted}`),以平衡日志,并确保重新载入的历史仍是有效的提供方 transcript。只丢弃从未完整写入的撕裂尾部碎片。
- **连续 seq。**`load` 拒绝日志中间的 `seq` 缺口/解析错误;`append` 的第一个 `seq` 必须等于已存储 next-seq。
- **JSON 可序列化数据。**`append` 通过共享单遍无损 JSON 边界实体化每个直接/回放批次。实时 `Session` 事件已深度冻结,但写入协调器仍将每个事件复制到持久化自有缓冲区。
- **持久性。**`append` 只在批次持久后返回。
## 写入协调器
`PersistenceCoordinator` 负责每 id 状态和串行化、每个实时会话的一个急切写入 controller、延迟实体化、崩溃尾部修复、会话接管和完全停稳 dispose。第一方后端组合一个协调器实现小型 `PersistenceBackend` 存储钩子接口,并委托其有状态方法。因此 JSONL 和 SQLite 共享生命周期正确性,同时保留不同存储原语;见[协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) 和 [flush controller 简化](../../../.agents/notes/implemented/simplification/2026-07-23-collapse-persistence-flush-state.md)。
每个 `session/event` 将事件复制到会话 controller并在不阻塞生产者的情况下启动急切 drain。并发通知共享当前 drain写入期间接纳的事件保持 pending并触发下一批。`session/flush` 是观察屏障,会等待 controller 无当前或 pending 批次。急切失败会记录日志并保留批次;下一次显式 flush 或后端拆卸重试,并向调用方公开失败。
崩溃修复只适用于冷状态。对于实时 id`load(id)` 为权威内存日志制作快照,等待该快照持久,并只在平衡时将其与协调器已存储 header 一起返回;开放实时轮次会被拒绝,而不会收到合成中断 closer。冷 load 在后端读取和修复写入期间保留 id因此同 id 实时 `Session` 的并发发布会拒绝并回滚。HMR 接管通过 `loadStored` 读取,应用协调器 cwd 检查,并绝不关闭活动轮次。
实时会话发出 `session/disposed` 时,协调器等待其 controller串行化最终 drain然后释放该精确 `Session` 对象拥有的状态。失败退役会将 controller 保留在实时会话 map 中使后端拆卸可重试。后端拆卸先停止事件接纳flush 每个剩余 controller等待每 id 操作,最后才关闭存储句柄。
无副作用 `locate` 和轻量 `listSnapshots` 查询仍由后端负责,因为它们描述存储拓扑和修订身份,而非写入编排。`listSnapshots(signal?)` 将调用方的精确信号传入后端发现,使观察者可在不脱离该工作的情况下取消。
`PersistenceBackend<TornMarker>` 钩子(协调器与存储之间的唯一 seam
| 钩子 | 职责 |
|---|---|
| `name` | dispose 失败 `AggregateError` 的后端标签。 |
| `loadStored(id, signal?)` | 在全部存储范围中按 id 读取已存储前缀。用于 resume/load、非变更 inspect、实时接管和 create 冲突探测。可选信号属于仅观察读取。返回元数据标识 `id`;当且仅当必须截断撕裂尾部时才存在不透明 `tornMarker`。 |
| `appendBatch(meta, events, isMaterialized)` | 持久追加连续批次;尚未实体化时以原子方式延迟实体化。 |
| `commitRepair(meta, tornMarker, closers)` | 使崩溃修复持久:截断撕裂尾部(当且仅当 `tornMarker !== undefined`;标记可为 falsy例如 seq/offset `0`),并追加 `closers`。不要求原子性。由 load截断 + closer和实时接管仅截断使用。 |
| `list(signal?)` | 列出全部已存储元数据,观察可选取消。 |
| `close?()` | 可选生命周期拆卸(例如关闭 db 句柄),在 dispose drain 后等待。 |
协调器断言已存储 id并在修复或实时接管前比较已存储/实时 cwd。其 `inspect()` 路径验证并克隆前缀,不调用 `commitRepair` 或发布写入状态。`tornMarker` 完全不透明:协调器只测试 `!== undefined`,并将其原样往返给 `commitRepair`绝不检查值JSONL 后端使用待截断字节偏移SQLite 后端使用待删除 seq。第三方后端可以不用协调器直接实现抽象服务但必须提供相同非变更检查和可信轻量快照修订。详见[写入协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)。
## 测试后端
导入 `runPersistenceContract`(公开 API包括稳定/变更敏感的轻量修订),其来源为 `tests/contract.ts`;再导入 `runCoordinatorContract`共享写入路径编排接管、HMR、冲突、dispose drain、崩溃尾部修复其来源为 `tests/coordinator-contract.ts`,并使用后端 fixture 调用两者。每个后端都遵守相同仅追加/连续 seq/延迟实体化/可序列化语义和相同编排,因此后端自身 spec 只需在其上测试存储机制路径净化、fsync 回滚schema 版本、事务回滚)。
三个后端运行这些套件:内存参考(位于 `tests/`)、`dsh-session-persistence-jsonl`(仅追加文件日志)和 `dsh-session-persistence-sqlite``node:sqlite`,每个 `SessionEvent` 是一行 `(session_id, seq, type, time, data, source_event_seqs, surface_op)`)。它们全部通过同一契约 + 协调器套件,证明 seam 真正与后端无关延迟实体化、load 时崩溃尾部和连续 seq 在文件字节与事务存储上表现相同。
## 元数据与位置类型
`dsh-session` 重新导出:`SessionHeader`(不可变会话元数据:`version``id``createdAt``cwd?``parentSession?``seedLength?``delegationDepth?`)。`SessionLocation``{ readonly kind: string; readonly path: string }`;其 path 是绝对后端目标,不证明产物已存在或包含未 flush 轮次。
## 模型体验
### 恢复的对话历史
#### 模型所见
该 seam 不添加提示词或 schema。Resume 将已存储接口事件恢复为消息历史;已存储请求 header 重建较早调用,新 loop 则为下一次请求组合当前系统提示词、工具和会话前缀。崩溃修复将没有持久调用的 assistant 请求标记为 `TOOL_NOT_STARTED`;有持久调用但无结果时变为 `TOOL_OUTCOME_UNKNOWN`,其文本允许模型重试只读或幂等工作,但要求验证副作用或请求用户,而不是盲目重试。
#### Token 影响
普通持久化期间为零 token。Resume 恢复已保留历史成本,并正常支付当前请求 envelope每个已修复调用添加引用的已保留错误文本。
#### KV 缓存影响
持久化不修改实时请求前缀。只有当重建历史、当前 envelope 和模型路由匹配时,恢复 loop 才能重用提供方缓存;崩溃修复结果仅追加,不重写较早历史。
## 已知限制与待完成工作
- **无删除或保留接口**:剪枝已存储会话是带外后端维护。
- **`list()` 无分页且无过滤**:它返回每个已存储会话的 header适合本地存储大规模时无索引。
- **修复时合成 closer 是唯一崩溃方案**:后端必须在 load 时合成 `tool/result`/`step/end`/`turn/end` closer没有继续中断轮次而不先关闭它的部分轮次 resume。