在开发 AI Coding Agent(如 Claude Code、Codex、OpenCode 等)时,一个绕不开的底层灵魂拷问是:用户的对话历史与 Agent 的运行状态,究竟该存在哪里?
太多的工具在"纯文本文件(.jsonl)"的易读性与"关系型数据库(SQLite)"的结构化查询之间举棋不定。而 dsh(DeepSeek Harness) 给出了一套极其优雅且极具工程洁癖的解法。
本文将深入拆解 dsh 的会话管理架构,看看它如何通过 CQRS(命令查询职责分离) 思想,将"写入持久化"、"状态投影"与"全文检索"完美解耦。
一、 核心哲学:CQRS 与三层解耦架构
dsh 没有把所有的职责塞进同一个文件或同一个数据库,而是将会话生命周期拆分成了三个独立的底层模块:
-
权威持久化层(Single Source of Truth) :
- 对应包:
@deepseek-ai/dsh-session-persistence-jsonl - 技术选型:Concatenated Zstd Frames + Append-Only JSONL
- 职责:确保每一条历史事件以极速、防崩溃、防篡改的方式安全落盘。
- 对应包:
-
状态投影层(Read Caching & Projection) :
- 对应包:
@deepseek-ai/dsh-session-projection - 技术选型:增量状态机 + 磁盘 Checkpoint 检查点
- 职责:避免每次恢复会话时都从头重放(Replay)几千条原始事件,实现秒开。
- 对应包:
-
全文检索索引层(Disposable Derived Index) :
- 对应包:
@deepseek-ai/dsh-session-query-sqlite - 技术选型:SQLite(WAL 模式 + FTS5 全文索引)
- 职责:提供毫秒级的跨会话关键词搜索。
- 对应包:
二、 主存储真身:为什么 dsh 砍掉了 SQLite,拥抱 Zstd 曾分帧 JSONL?
在 dsh 的演进历程中(特别是在架构决策记录 ADR 中),官方曾同时维护过 SQLite 和 JSONL 两套持久化后端。但在 2026 年 8 月底,DeepSeek 团队做出了一个果断的决定:彻底砍掉 SQLite 持久化后端,将 Zstd 分帧 JSONL 确立为唯一的权威存储。
1. 为什么放弃 SQLite 作为主存储?
- 拒绝迁移地狱:如果保留双后端,每一次持久化格式升级或生命周期管理,都会让跨平台测试和数据迁移的成本翻倍。
- 严守生命周期隔离:搜索用的 SQLite 应该是"随时可丢弃的派生索引",如果把它当作权威数据源,一旦因为并发写锁损坏或迁移失败,用户的核心聊天记录就会永久丢失。
2. Zstd 分帧 + Checksum:鱼与熊掌兼得
传统 .jsonl 最大的痛点是包含大量 Tool Result 时文件体积膨胀快、且缺乏数据完整性校验。dsh 引入的 拼接 Zstandard 压缩帧(Concatenated Zstd Frames) 完美破解了这一矛盾:
- 像纯文本一样追加 :Zstandard 原生支持将多个独立压缩的 Frame 直接在文件末尾二进制拼接(Append),无需解压重写前文。这让
dsh保留了.jsonl毫秒级追加写入的极致性能。 - 压缩率惊人:将冗余的 JSON 结构和工具文本压缩掉了 80%~90%。
- Checksum 防篡改与断电保护 :每一个压缩帧尾部都带有 xxHash 校验和。当进程因
kill -9或断电在写入最后一帧时中断,系统能够精确识别出"残缺的最后一帧(Torn Record)"并干净利落地截断丢弃,前文毫发无损。
三、 内存加速器:增量 Projection 与状态折叠
如果把底层的 Zstd 压缩帧比作银行的"流水账单",那么每次打开会话总不能把几十万字从头算到尾。dsh 的 Projection(投影) 机制解决了"怎么读得快、内存怎么省"的问题:
- 增量更新(Rolling State) : 内存中只维护一个"当前最终状态对象"。当新事件进来时,直接在内存里原地修改字段,用完即扔原始 Event,不在内存里堆积历史中间态。
- 检查点快照(Checkpoint Cache) :
dsh会定期将算好的状态拍成快照存入磁盘缓存(SessionProjectionCache)。下次--resume时,直接加载最近的快照,并只在末尾增量计算最新的几条事件,实现真正意义上的秒开。
四、 外挂目录册:可丢弃的 SQLite FTS5 全文检索
当用户在多端或历史列表中搜索关键词时,dsh 重新启用了 SQLite------但这一次,它的身份变了:
- 只建目录,不存真身 :
dsh的 SQLite 数据库(dsh-session-query-sqlite)只负责监听.jsonl的更新,把用户、AI 和工具输出的纯文本抽出来,塞进 SQLite 的 FTS5 全文检索表 中。 - 坏了就扔,随时重建(Disposable) :因为真正的聊天记录 100% 安全地躺在底层的 Zstd JSONL 压缩帧里,这个 SQLite 文件只是一本"外挂目录册"。如果多个进程并发写入导致该索引库损坏,程序只需执行
rm -rf删掉它,系统会自动在后台重新扫描生成,永远不会丢失任何聊天数据。
五、 总结:dsh 架构带来的行业启示
回顾主流 AI Coding Agent 的存储演进:
-
Claude Code / Codex 坚持纯文本
.jsonl,换来了绝对的安全与 Unix 可观测性,但牺牲了压缩率与全局检索; -
OpenCode / Goose 走向全局 SQLite,换来了多端 UI 同步与查询便利,却背上了沉重的迁移包袱,且容易在 Headless 模式下遭遇
SQLITE_BUSY写锁争用; -
dsh则通过 CQRS 读写分离 给出了一份标杆级的答卷:- 写路径:用 Append-Only + Zstd 分帧,追求极致的安全与追加速度;
- 读路径:用增量 Checkpoint 投影,保障单会话秒开与低内存;
- 搜路径:用可丢弃的 SQLite FTS5 挂载索引,兼顾强大的全文检索。
这种"各司其职、坏了能扔"的模块化思路,堪称现代本地 AI 客户端架构设计的教科书。