DSH plugin 自定义持久化:SessionPersistence、JSONL 与崩溃恢复契约

插件开发发布于 2026-10-17作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginSessionPersistenceJSONL崩溃恢复持久化
DSH plugin 的会话持久化 seam:create/open/stat/list 与 SessionHandle 单写者、write-behind 窗口与 flush 检查点、崩溃后由 resume 追加闭合事件,以及 SessionHeader 与格式拒绝。

DSH plugin 的会话持久化有一个明确前提:仅追加的 SessionEvent 日志才是真源,持久化只是让它落地,不改写语义。 所以这个 seam 里没有「持久化专用事件」,只有 ctx.sessionPersistence 暴露的 create/open/stat/list,以及 create/open 返回的逐会话 SessionHandle——所有日志读写都经句柄,句柄同时代表单写者所有权。理解这条,就能解释三个看似奇怪的行为:为什么并发写打开会被拒、为什么崩溃后日志不会被「修好」、为什么元数据要单独放 SessionHeader。

DSH plugin 的 SessionPersistence 与 JSONL:日志是真源,句柄是唯一入口

一句话结论:seam 暴露四个方法,读写都走句柄;JSONL 只是默认提供方,换后端不改变契约。

  1. 四个方法:create、open、stat、list,全部作用在现有 SessionEvent 上。
  2. 句柄四件事:read、append、flush、close,读写不经过按 id 寻址的服务方法。
  3. 创建与可见性:会话在 create 完成时就对本进程可见,后端可以把物理实体化推迟到第一次 append 或 flush。
  4. 跨进程只认已实体化:其他进程只能看到已落地的会话——崩溃前从未实体化的会话等于从未存在。
  5. 写你自己的后端:实现同一契约即可,无需新增事件类型。

DSH plugin 的 SessionHandle 与单写者:为什么并发写会被拒

一句话结论:写句柄是跨进程写租约,同一会话只能有一个写者;第二个写打开会直接报错。

  1. 写所有权:活跃会话的写句柄由其生命周期所有者持有。
  2. 拒绝而非排队:并发的 open(id, 'write') 会以 SessionAlreadyOwnedError 拒绝,避免修复与活跃轮次竞速。
  3. 只读打开:open(id, 'read') 返回经过验证的连续前缀切片,绝不返回撕裂尾部;同一句柄的重复读取不会看到更旧的状态。
  4. 没有已准备会话缓存:持久化侧不缓存已准备的 Session;冷读缓存由 session-query 自己按 stat().revision 管理。
  5. 诊断字段别当查询用:SessionLocation 只为拒绝诊断存在,带出被拒绝解读的原始日志路径,不是消费者查询接口。

DSH plugin 的 flush 检查点:write-behind 窗口与两种排空

一句话结论:事件同步通知、后台批量落盘;session/flush 排空到停稳,session/disposed 做最终排空并关句柄。

  1. 同步通知:session/event 是同步通知,后端按会话 id 路由进活跃写句柄的有界 write-behind 窗口,不阻塞生产方。
  2. 窗口规则:第一个待处理事件开启固定批处理窗口,后续事件加入但不重置截止时间;到点后用该句柄发起一次持久化 append。
  3. 写入期间的事件:获得自己的截止时间,形成下一批。
  4. flush 的语义:取消等待、排空到完全停稳——循环在领取下一个普通轮次前用它做顺序与错误观察。
  5. 失败路径:后台写入被拒时按序保留事件、暂停自动路径并通过 logger 报告;下一次显式 flush 会重试,并向调用方明确报错。
  6. 关闭路径:session/disposed 做同样的最终排空并关闭句柄;close() 会经由仍打开的存储排空已路由缓冲,后端 teardown 不丢数据。

DSH plugin 的崩溃恢复:持久化不修日志,交给 resume

一句话结论:截断与修复都不做;持久化只保证物理连续,缺失的闭合事件由 resume 通过同一写句柄补上。

  1. 崩溃现场长什么样:日志以打开的 turn/start 结束、没有 turn/end。
  2. 持久化不修:不截断、不修复——长周期轮次的事件在崩溃前已经持久追加,截断会丢真实数据。
  3. 只丢撕裂尾部:只有属于未完成 append 的不完整碎片被丢弃;能解出的完整记录(JSONL 会部分解码撕裂的 Zstandard 帧)会在句柄第一次新 append 之前由写路径重写。
  4. 修复是读方的职责:resume 通过写句柄读取已存日志,计算 interruptedTurnClosers——缺失的工具错误、任何未闭合的 step/end,以及一条合成的 turn/end { reason: { kind: 'interrupted' } }——再作为普通批次通过同一句柄追加。
  5. interrupted 是特例:它是唯一不由循环发出的 TurnEndReason。
  6. 只读观察方不回写:session-query 只在内存里用同样的闭合事件配平冷日志。

DSH plugin 的 SessionHeader 与格式拒绝:元数据不进日志

一句话结论:版本、cwd、谱系与预设放 header;读不了的日志用专门的错误拒绝,而不是当损坏处理。

  1. header 与日志分开存:SessionHeader 含 version、id、createdAt、cwd、parentSession、isSeeded、origin、delegationDepth、agentPreset。
  2. 绝不进事件词汇:header 不进入 SessionEventMap,也不会到达 deriveMessages();逻辑 header 通过 session.header 附加。
  3. 为什么存 delegationDepth:仅存在运行时的深度在重启后会归零,持久化后递归预算才能跨重启保留。
  4. 两种错误要分开:SessionFormatUnsupportedError 表示这个构建无法可靠解读(数据没坏),SessionPersistenceCorruptionError 才是损坏。
  5. 代际规则:stat 与 list 按最高规范代际分类;即使仍有旧的可读代际,更高的未来代际也会导致拒绝。
  6. 恢复的保留面:当前格式恢复会保留已安装扩展与带 ignorable: true 的未知事件;历史 v0/v1/v2 迁移则拒绝未知类型,即使带 ignorable 标记。

DSH plugin 排查注意事项

一句话结论:持久化问题先分「写不进」「读不到」「读不了」三类,分别对应所有权、可见性与格式代际。

  1. 写不进 → 检查是否有别的句柄持有写所有权(SessionAlreadyOwnedError)。
  2. 别的进程读不到 → 会话可能还没实体化;先触发一次 append 或 flush。
  3. 读不了 → 看是 SessionFormatUnsupportedError 还是损坏错误,前者不是数据坏。
  4. 崩溃后历史不完整 → 正常现象,由 resume 追加闭合事件,不要手动改日志。
  5. 以为 flush 之后一定落盘 → 它保证排空与错误暴露,但窗口只约束批处理等待,不约束后端完成延迟。
  6. 修复状态如实:本文的句柄语义、write-behind 窗口、崩溃恢复与格式拒绝规则均取自官方「会话持久化」页面;接口签名以该页 Cordis API 与你所用版本为准。

来源:会话持久化、会话、dsh-session-persistence-jsonl。

常见问题

DSH plugin 的持久化服务除了 JSONL,还能换成别的后端吗?

可以,DSH plugin 的 SessionPersistence 是一个能力 seam:抽象服务在现有 SessionEvent 上暴露 create/open/stat/list,仓库外的提供方可以实现同一契约。JSONL 只是随产品交付的默认后端,换后端不需要新的持久化事件类型。

DSH plugin 里,为什么两个进程同时以写模式打开同一个会话会被拒绝?

DSH plugin 的写句柄代表单写者所有权,跨进程写租约把守唯一入口,第二个 open(id, 'write') 会以 SessionAlreadyOwnedError 拒绝。这样崩溃修复与活跃轮次不会互相竞速。

DSH plugin 里,会话在轮次中途崩溃,日志里没有 turn/end,持久化会自动补吗?

不会,DSH plugin 的持久化刻意不截断也不修复日志,只返回物理上有效的连续日志。修复是读方的职责:resume 会计算缺失的工具错误、未闭合的 step/end 与一条合成的 interrupted 结束事件,再通过同一句柄追加。

DSH plugin 里,session/flush 到底保证了什么,为什么循环把它当检查点?

DSH plugin 的 session/flush 会取消等待并把 write-behind 缓冲排空到完全停稳,因此调用返回时数据已落盘、错误也已暴露。循环正是用它作为领取下一个轮次之前的顺序与错误观察检查点。

DSH plugin 里,会话元数据为什么不放在事件日志里,SessionHeader 存了什么?

DSH plugin 把元数据与事件日志分开存:SessionHeader 携带格式版本、id、创建时间、cwd、fork 谱系、isSeeded、delegationDepth 与 agentPreset 等字段。它不进入 SessionEventMap,也不参与 deriveMessages()。

相关术语

SessionPersistence(持久化服务)
SessionPersistence 是 DSH plugin 的会话持久化 seam,在现有 SessionEvent 上提供 create、open、stat 与 list,不存在平行的持久化事件类型。它只负责让日志落地,不改变日志语义。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
SessionHandle(会话句柄)
SessionHandle 是一条通向已存储会话的打开通道,提供 read、append、flush 与 close,并承载单写者所有权。所有日志读写都经句柄,而不是按 id 寻址的服务方法。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
session/flush
session/flush 是持久化的排空检查点:取消等待批处理窗口、把缓冲写到底并暴露错误。循环在领取下一个轮次前调用它,作为顺序与错误的观察点。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
SessionHeader(会话头)
SessionHeader 是与事件日志分开存储的不可变元数据,含格式版本、id、创建时间、cwd、fork 谱系与 agent 预设等字段。它不进入事件词汇,也不会到达消息派生逻辑。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence

来源