首页 / DeepSeek Harness 入门教程 / 持久化与会话日志

DeepSeek Harness 入门教程

持久化与会话日志

本教程共 32 篇 · 第 18 篇 · 更新于 2026-08-15 · 约 7 分钟阅读

dsh持久化会话日志JSONLSQLitecompaction

本节目标:弄清会话日志存到哪、怎么保证不丢、崩溃后怎么恢复,以及日志太长时 dsh 怎么压缩,还有怎么从历史会话里检索信息。

日志是真源,持久化是副本

第 10 章讲过:会话(Session)是一份仅追加的类型化事件日志,模型历史从它派生。内存里的日志是运行时真源,但进程一退出就没了。让日志落盘的环节叫 session-persistence 缝:抽象服务 ctx.sessionPersistence + 两个可互换后端。

持久化插件订阅 session/event 事件流,把事件复制到逐会话的控制器,再异步写入。它不阻塞生产方——写入是批处理的。

两个后端实现同一套约定:

  • JSONL 后端dsh-session-persistence-jsonl):每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带校验和的连续 Zstandard frame(分片行会打包压缩),也可配置成原始行。支持崩溃安全的原子写入。
  • SQLite 后端dsh-session-persistence-sqlite):基于 Node 内置的 node:sqlite每个 SessionEvent 一行,字段 (session_id, seq, type, time, data, source_event_seqs, surface_op) 与事件 1:1 映射,没有需要保持同步的平行持久化 schema。初始化和修复用事务,原子性比 JSONL 的两步修复更强。

选哪个?官方没有「默认推荐」的绝对说法,组合包按部署需求配置。JSONL 可读、易排查;SQLite 查询快、事务原子。

Note

日志里每个事件的 data 都必须可序列化为 JSON,Session.append 在源头强制这一点。错误事件进不了日志,后端能持久化的内容与内存日志永远一致。

flush 检查点与批处理

写入不是「来一条写一条」。第一个待处理事件会开启一个固定批处理窗口,后续事件加入但不重置截止时间;窗口到期后启动一个持久化批次,期间新到的事件进入下一批。

session/flush 是显式的持久性检查点:取消等待并排空至完全停稳。agent loop 在领取下一个普通轮次之前用它做顺序与错误观察检查点。需要「写入已落盘」保证的消费方(比如读存储前)显式等待它。

后台写入失败时会保留对应事件并暂停自动重试,显式 flush 会立即重试并通过 agent/error 报告失败——失败永远不会被记录成「已关闭轮次之后的会话事件」。

崩溃恢复

如果进程在轮次中途崩溃,后端重新加载日志时会发现一个已打开的 turn/start 没有对应的 turn/end。它不会截断日志——长任务里单个轮次可能非常庞大,这些事件在崩溃前已经持久追加,删掉就丢了事实。

后端改为补一个合成的 turn/end { reason: { kind: 'interrupted' } } 来关闭这个遗留轮次,不改变其前后任何独立事件。interrupted 是唯一一个不由 agent loop 发出的结束原因,只在崩溃恢复时出现。

崩溃恢复会丢掉的,只有撕裂的尾部记录(半个 JSON 行)。JSONL 的扫描器在字节层跟踪已提交偏移:只接受换行终止且 seq 连续的已提交前缀。SQLite 后端则用事务完成同样的修复,并提供 JSONL 两步修复(先 fsync truncate,再追加恢复事件与闭合器)不具备的事务原子性。

格式版本由 header 把关:会话元数据(格式版本、cwd、血统、seed 边界)与事件日志分开存储,version 不匹配时后端拒绝加载并提示方向(「由更新的 harness 写入,请升级」或「本构建没有升级路径」)。当前格式版本 SESSION_FORMAT_VERSION = 0,是预发布格式,不承诺任何兼容性。

Warning

崩溃恢复有边界:如果工具的 call 已落盘、外部系统可能已经执行成功,但 result 没有持久记录,修复只能标记结果未知(TOOL_OUTCOME_UNKNOWN 一类事实),不能擅自重试并宣称 exactly-once

会话查询

日志落盘后怎么找?ctx.sessionQuery 提供统一的查询服务,live 数据优先,持久化数据兜底:

  • listSessions() / filterSessions():列出或按元数据过滤整个语料库;
  • filterEvents():按类型、时间、seq、surface、文本扫描某个会话的事件;
  • readSession() / readSurface():读取完整日志或当前模型可见 surface;
  • traceSession() / traceEvent():查会话谱系(祖先/后代)和事件关系(被谁替换、引用了谁);
  • searchSessions() / searchEvents():全文检索,带不透明游标分页。查询词按字面量处理,不执行 FTS 语法。

面向模型的入口是 5 个只读工具:session_search(跨会话搜)、session_tracesession_event_readsession_event_searchsession_event_trace。它们按调用 Agent 的会话做授权,只读不写。

Tip

全文搜索在部分组合里可显式关闭(如 Web bundle 默认 openAt: never,报 SESSION_QUERY_SEARCH_DISABLEDopenAt 默认值以 dsh --profile web --dump-config 实测为准)。精确读取和 trace 不受影响——「仓库里存在 Session Query」不等于「默认启用了全文搜索」。

compaction:日志太长怎么办

对话越长,模型请求越贵。compaction(压缩) 把一段旧的 surface 节点折叠成一条摘要,减少 token 占用。

它通过三个仅日志事件记录整个生命周期:compaction/start(拿锁)→ compaction/summary(摘要 + 被遮蔽范围/seq/token 数)→ compaction/end(放锁)。真正的 surface 变更只有一处:紧随 compaction/summary 的一条 user/message 事件,携带 surfaceOp: { op: 'replace', start, end } 把选中区间替换成摘要节点。摘要本身不进模型历史,模型看到的就是那条替换后的 user 消息。

触发方式有两种:

  • 自动compactIfNeeded(),压力触发(pressure,串行运行在 agent/pre-step)或上下文溢出触发(context-overflow,可强制做一次有效缩减);
  • 手动compactNow(),空闲会话主动压一次,返回 null 表示没有可压缩的安全范围。

压缩前后的 token 数由 token-meter 估算并写入事件(第 19 章详述)。配套的工具结果剪枝ctx.toolResultPruner)可以把超预算的工具结果替换成保留头尾的精简版,同样以 surface replace 落地,并记录 compaction/prune 影子计价事件。

Note

压缩有锁:compaction/startcompaction/end 之间不允许并发压缩。中途崩溃会留下可检测的遗留锁(有 start 无 end),而不会出现一个虚假声称压缩完成的 compaction/end

小结

  • 日志是唯一真源,持久化是副本:JSONL(可读、可压缩打包)或 SQLite(事务原子、查询快)。
  • flush 检查点保证「已落盘」;崩溃恢复补 interrupted 轮次边界,只丢撕裂尾部。
  • 会话查询统一 live/持久化两源,全文检索可关闭。
  • compaction 用摘要替换旧区间,锁 + 三事件 + 一条 replace user/message 保证可审计、可回放。