DeepSeek Harness 里四个都叫 version 的数字:0、15、8、1

2026-08-16

deepseek-harness 的会话与存储这一块,最容易出的一类事故不是看不懂代码,而是看懂了、但把版本号认错了对象。这个仓库里至少有四个不同的常量都在表达”版本”,它们的数字分别是 0、15、8、1,出处分散在四个包里,含义互不相干。你要是在排查一个”日志读不进来”的问题时脑子里只有一个版本号,基本一定会走错方向。

先说清限定:以下全部来自 2026-08-16 我们采集的仓库快照 47f9438,版本 0.1.0-rc.5。这个仓库建立于 2026-08-13,README 自述处于开发者预览阶段,并明确写了未来会有破坏兼容性的变更——下面每一个常量名和数值都带着这层限定,不是稳定契约。

四个数按住

常量所在文件版本化的对象
SESSION_FORMAT_VERSION0packages/core/session/src/types.ts:56会话日志的事件词汇表 / 逻辑格式
SCHEMA_VERSION15packages/session/session-persistence-sqlite/src/schema.ts:20会话持久化 SQLite 后端的盘上表结构
SESSION_QUERY_SQLITE_SCHEMA_VERSION8packages/session-query/session-query-sqlite/src/schema.ts:8会话查询全文检索后端的派生索引表结构
STORAGE_SQLITE_SCHEMA_VERSION1packages/storage/storage-sqlite/src/schema.ts:20通用 storage 子系统 SQLite 的物理布局

四条线之间不是嵌套关系,是并列关系。packages/session/session-persistence-sqlite/src/schema.ts:16-19 的注释自己就把话挑明了:盘上 schema 版本与会话自身的 version正交的,后者版本化的是事件词汇表。会话自身的那个 version 是会话元数据 SessionHeader 的一个字段,而 SessionHeader 本身不进事件日志docs/subsystems/persistence.md:47-89)。

为什么会长成四条独立的线

从子系统的切分能看出来。docs/subsystems/storage.md:5 原文说,storage 子系统持久化的是”除会话事件日志以外的一切”,会话日志有它自己的 seam。也就是说,从设计上会话日志和通用 storage 就是两套东西,各自的盘上格式当然各有版本。

再往里看,会话日志这条线本身还要再分两层:

  • 逻辑层Session 的事件日志。docs/subsystems/session.md:5 把它描述为 append-only 的 SessionEvent 日志,是一次 agent 交互的唯一真相源,给模型看的消息历史是从日志派生的、不单独存一份。这一层要版本化的是”日志里可能出现哪些事件类型”。
  • 物理层是后端怎么落盘。同一份逻辑日志,JSONL 后端写成 <root>/--<normalized-cwd>--/<encoded-id>/session.jsonl.zstdpackages/session/session-persistence-jsonl/README.md:9-15),SQLite 后端则一条事件落一行,列是 (session_id, seq, type, time, data, source_event_seqs, surface_op),另有 ignorabledocs/subsystems/persistence.md:236schema.ts:48-60)。表结构变了,逻辑格式未必要动。

SCHEMA_VERSION = 15 只管后面那一层。而查询侧的 8 又是另一码事:那是一个派生索引,不是真相源。

SESSION_FORMAT_VERSION = 0 是四个里最特别的一个

它特别在三点。

第一,它是一个单一单调整数,没有主版本次版本之分(packages/core/session/src/types.ts:40-55)。第二,是否要 bump 的判断标准是”写入方发出什么”,而不是”新的读取方能接受什么”——方向和大多数人的直觉相反。第三,packages/core/session/README.md:143 原文写着:SESSION_FORMAT_VERSION钉在 0,属于 pre-release,不承诺广泛兼容;比它新的日志会报”written by a newer harness — upgrade”,比它旧的日志则说明当前 build 不带升级路径。

这个常量在运行路径上是硬校验。packages/core/session/src/index.ts:101-102 里,头部的 version 与常量对不上就直接抛错,错误文案是 session header version must be ${SESSION_FORMAT_VERSION}, got ...。注意这里的 version 来自 SessionHeader——那是会话元数据,不进事件日志,字段有 versionidcreatedAtcwd?parentSession?seedLength?origin?: 'subagent'delegationDepth?agentPreset?docs/subsystems/persistence.md:47-89)。

顺带一提,这个 0 校验的对象——事件词汇表——本身也有可以数的数:packages/core/session/src/types.tsSessionEventMap 的核心成员是 13 个,而全仓已声明的事件类型被生成到 packages/core/session/src/known-event-types.ts:19-64,我们数出 44 个。读日志时遇到不在这张表里的事件类型,行为是拒绝重建,除非该事件信封上带 ignorable: truedocs/subsystems/persistence.md:94known-event-types.ts:8-17)。所以”版本号对得上”和”这条事件认得出”是两道各自独立的关卡。

三个 SQLite 版本号各管各的库

它们连”这个库是不是我的”都是各判各的。会话持久化用 SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850schema.ts:23),会话查询用 SESSION_QUERY_SQLITE_APPLICATION_ID = 0x44534851src/schema.ts:11)——只差最后一位。

会话持久化侧的拒绝规则写得很硬:非纯净的无版本库、外来 application id、任何非当前版本,都在改 journal 模式之前就直接拒绝,理由是这个未发布格式没有迁移(packages/session/session-persistence-sqlite/README.md:13)。同一句”没有迁移”在这个仓库里出现了不止一处:docs/subsystems/storage.md:47packages/session/session-persistence-jsonl/README.md:38 各以不同措辞写了同一条。

通用 storage 这边给的是错误码而不是文案:版本不匹配报 version-mismatch,解析不了报 malformed-mediumstorage.md:47 明写 “no migration, pre-release stance”。

查询侧的 8 则有一个容易被忽略的前提:出厂 bundle 基线给的是 path: ':memory:'openAt: neverpackages/bundle/base/cordis.patch.yml:117-121),上方 :109-116 的注释解释了这个选择——全文检索是 opt-in。openAt 的三态是 'startup' | 'first-search' | 'never',库里默认 'startup',而 'never' 意味着彻底关掉全文检索:精确读、过滤、追踪仍在,searchSessions / searchEventsSESSION_QUERY_SEARCH_DISABLED 失败,SQLite 永远不会被 import 或打开(packages/session-query/session-query-sqlite/src/index.ts:85,95-101)。换句话说,按出厂基线跑,这个版本号对应的库压根不会被创建。

还有第五、第六个”version”,别一起混进来

排查时最好把下面这两类也隔开:

  • npm 包版本。上述这些包在采集时全是 0.1.0-rc.5,根 package.json@deepseek-ai/dsh-root0.1.0-rc.5。它和任何一个盘上格式版本都没有换算关系。
  • SessionHeader.version 字段。它是每份会话日志里存的那个值,被拿去和 SESSION_FORMAT_VERSION 比对;它不是”schema 版本”。

撞上问题时怎么区分

给一套可执行的判定顺序,不用猜:

  1. 先看报错的形状。文案里带 session header version must be ... 的,是逻辑格式那条线,去核 packages/core/session/src/types.ts:56。返回码是 version-mismatch / malformed-medium 的,是通用 storage 那条线,去核 packages/storage/storage-sqlite/src/schema.ts:20
  2. 再看是哪个库。若报错涉及 SQLite 文件,用 application id 区分:0x44534850 是会话持久化,0x44534851 是会话查询。这一步能直接排掉一半可能。
  3. 对照你的 composition 实际装了什么。持久化后端是 JSONL 还是 SQLite,决定了 SCHEMA_VERSION = 15 是否与你有关;openAt 是不是 never,决定了查询侧那个 8 是否与你有关。
  4. 确认自己在读哪一份文件packages/core/session/ 与顶层 packages/session/ 是两个不同的目录(前者是核心会话模型 @deepseek-ai/dsh-session,后者是一堆持久化、标题、遥测、投影的外围包),写路径时很容易串行。
  5. 验证的办法:把你手上的常量名逐个 grep 回仓库,看它是不是我上面表格里那四个之一、文件路径对不对得上。四个常量名互不相同,这一步不会有歧义。
  6. 什么情况说明不是版本号的问题:如果日志能被加载、只是某一条事件被拒绝,那更可能是事件类型不在 known-event-types.ts 的那 44 个里且没带 ignorable: true,这与版本校验是两道关。同样,如果你遇到的是查询搜不到东西而不是报错,先看 openAt 是不是 never,那不是版本不匹配。

一句话收尾

这四个数字(0 / 15 / 8 / 1)在仓库里长得像同一类东西,实际管的是四件事:事件词汇表、会话持久化表结构、派生索引表结构、通用存储物理布局。它们各自独立地 bump,对不上时的表现也各写各的:会话格式版本对不上直接抛 session header version must be ...,会话持久化 SQLite 在改 journal 模式之前就拒绝这个库,通用 storage 则返回 version-mismatch。把这四个数当成一个版本号来推理,是这一块最省时间的翻车方式。

延伸阅读


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。