DeepSeek Harness 会话持久化身份绑定:JSONL 存储的 id/cwd 校验与修复前拒绝机制 📅 发布时间:2026/9/20 3:24:14 👁 浏览次数: DeepSeek Harness 会话持久化身份绑定JSONL 存储的 id/cwd 校验与修复前拒绝机制【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读本文围绕 DeepSeek Harness 中一条已落地的 Agent Note.agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.md讲解 JSONL 会话持久化后端如何把按 id 选中的物理日志与从日志首行解析出的SessionHeader元数据绑定起来堵住日志错配、错位与重复三大身份缺陷。你将掌握loadStored单一查找入口的设计、assertStoredIdentity的路径重算与realpath归一化校验、协调器PersistenceCoordinator在修复与追加前的独立身份断言以及这些机制对应的源码与测试证据可直接用于理解或扩展该仓库的会话存储层。一、问题背景JSONL 查找与元数据脱节的隐患DeepSeek Harness 的会话持久化采用插件式架构服务端deepseek-ai/dsh-session-persistence提供协调器具体后端各自实现底层存储原语。JSONL 后端deepseek-ai/dsh-session-persistence-jsonl把每个会话存成一个追加写的 JSONL 文件并在存储时按如下目录布局落盘见 format.ts 的projectDir/sessionDir/logPath配置的 root/ └── --Users-qyj-work-deepseek-harness--/ ← projectKey(cwd)可读的项目目录名 └── aB3c9dEf.../ ← encodeSegment(id)单个安全路径段 └── session.jsonl.zstd ← 或 session.jsonl取决于 compression 配置这条链路隐含一个事实按请求的 session id 去磁盘上找物理日志与从该日志首行解析出的SessionHeader元数据是两个独立来源的事实。在未做身份绑定之前两者可能互相矛盾按 id A 找到的日志首行却声明自己是 id B或被篡改、被错误拷贝日志首行声明的cwd与它实际存放的项目目录不一致错位此时按cwd重建路径会导致后续 repair / append 写到 B 的路径同一个编码后的 id 出现在多个项目目录中时查找结果不确定而create的冲突探测、resume/load恢复都依赖id 全局唯一这一前提。作为对照SQLite 后端不存在这种歧义其主键查询天然把元数据与事件绑定到被请求的 id 上见 store.ts。因此这次修复的实质就是为文件型后端补上与 SQLite 等价的身份不变量。二、核心决策loadStored成为唯一的存储前缀查找入口Agent Note 明确的设计结论是loadStored(id)是协调器coordinator对已存储前缀的唯一查找入口并承担全部身份校验。这一决策在 coordinator.ts 的PersistenceBackendTornMarker接口注释中被固化为契约Read a stored prefix by id, scanning every backend storage scope… Returned metadata must identifyidbefore repair or state publication.即返回的元数据必须在修复repair与状态发布state publication之前就证明自己属于id。JSONL 后端在 index.ts 中这样落实该契约扫描每个项目目录loadStored(id)调用findLog(id)遍历 root 下所有项目目录逐个检查sessionDir(project, id)下是否存在 transcript 文件要求至多一个匹配若同一 id 在多个项目目录中都存在日志findLog直接抛错见 index.ts错误信息为duplicate JSONL session id ... appears in multiple project directories解析文件并校验身份readPrefix在解析完首行后调用assertStoredIdentity(path, prefix.meta, expectedId)强制header.id id且所选路径与由元数据重算的logPath(root, header.cwd, header.id)一致详见下一节。list()走的是同一条校验链路listArtifacts对每个日志只读首行保证列表面向会话数扩展而非日志大小对每条元数据执行assertStoredIdentity并用SetSessionId拒绝跨项目目录的重复 id见 index.ts。2.1 协调器层的独立断言后端校验之外协调器不依赖后端内部做了什么而是对loadStored的返回值再做一次独立断言。prepareCore冷加载/恢复的公共路径在拿到stored后依次执行见 coordinator.tsconst stored await this.backend.loadStored(id) if (stored undefined) throw new SessionPersistenceNotFoundError(id) const { meta, events, revision, tornMarker } stored this.assertStoredId(id, meta) // ① header.id id否则抛 stored session identity mismatch this.assertVersion(meta) // ② 格式版本必须被本 build 支持 const storedEvents adoptStoredEvents(events, id) this.assertEventsSupported(meta, storedEvents) // ③ 拒绝未知事件类型assertStoredId的实现非常直接见 coordinator.tsif (meta.id ! id) { throw new Error(stored session identity mismatch: requested ${id}, header contains ${meta.id}) }值得强调的是这些校验发生在修复commitRepair与任何协调器状态变更之前。prepareCore中补全被中断 turn 的 closers、commitPrepared中截断 torn tail 追加合成 closers都在校验通过之后才会触达后端见 coordinator.ts 中source.tornMarker ! undefined || source.closers.length 0分支。这保证了身份错配的日志会先失败而不是先被修复成另一个会话的数据。2.2 元数据的脱钩副本与接口零改动协调器还独立维护一份已验证元数据的副本JSONL 的 append 与 repair 所推导的路径全部来自这份副本而不是来自易被篡改的入参。典型体现是load返回的meta是冻结Object.freeze的不可变对象见 coordinator.ts对应测试load returns immutable meta without exposing backend pathing见 jsonl.spec.ts调用方修改返回的meta.cwd会直接抛错后续 append 依然落到原始/proj日志。由此带来的接口收益是PersistenceBackendTornMarker既不需要一个 scope 相关的 live lookup也不需要引入新的 storage-locator 类型参数。loadStored(id)是全后端共享的最小原语SQLite、JSONL 与测试后端都无需为文件后端独有的概念买单详见第四节备选方案。三、路径校验的实现细节路径重算 realpath 归一化assertStoredIdentity见 index.ts是 JSONL 后端身份校验的核心分三步// ① 请求 id 与首行 header.id 必须一致 if (expectedId ! undefined meta.id ! expectedId) { throw new Error(corrupt session log ${path}: requested id ${expectedId} does not match header id ${meta.id}) } // ② 用元数据重算应该所在的路径 expectedPath logPath(this.root, meta.cwd, meta.id, this.compression) // ③ 实际路径与期望路径不一致时允许通过 realpath 归一化后指向同一文件 if (path ! expectedPath !await this.sameFile(path, expectedPath, signal)) { throw new Error(corrupt session log ${path}: header id ${meta.id} and cwd identify ${expectedPath}) }第 ② 步的logPath由projectKey(cwd)encodeSegment(id)推导见 format.tsencodeSegment对SessionId做单段路径转义../、绝对路径、NUL、分隔符全部被中性化例如../../etc/pwn→~002E~002E~002Fetc~002Fpwn~本身也被转义为~007E从而既防目录穿越又保持对全部 UTF-16 字符串含孤立代理对的单射性projectKey把 cwd 归一化为有界可读的项目目录名分隔符折叠为-长度截断到 251 字符并包裹--/Users/qyj/work/deepseek-harness会变成--Users-qyj-work-deepseek-harness--。注意该映射是有损归一化/a/b-c与/a-b/c归一化到同一目录测试groups sessions whose cwd paths normalize to the same project directory印证了这一点见 jsonl.spec.ts。第 ③ 步的sameFile见 index.ts用realpath同时解析实际路径与期望路径并比较在不区分大小写的文件系统上放行大小写别名在区分大小写的存储上则保持校验强度。测试accepts an alternate project path only when it identifies the same physical log用一个指向真实项目目录的符号链接来验证改写 header 的cwd为别名后load/list依然接受该日志因为realpath解析到同一物理文件见 jsonl.spec.ts而把cwd改成不指向该文件的/elsewhere时list拒绝见同文件 L1343-L1350。此外listArtifacts还会拒绝header id 无法命名存储路径的日志例如空 idencodeSegment()抛错对应测试见 jsonl.spec.ts。四、运行边界root 校验与单活写者约束Agent Note 同时明确了两个部署层面的前提1已存在的配置 root 必须是可读目录。插件加载时构造函数调用assertUsableRoot见 index.tsreaddirSync(root)成功即通过ENOENT视为尚未创建放行其余 I/O 错误直接抛出。不存在的 root 保持合法在首次物化时按0o700逐级创建mkdir(..., { recursive: true, mode: 0o700 })。测试plugin load rejects an existing root that is not a directory验证了 root 是文件时插件加载抛ENOTDIR见 jsonl.spec.ts。注意config.root是必填的源码注释解释得很清楚默认到process.cwd()会让会话文件随进程 cwd 漂移bash 调用、子进程都会改变它。2每个会话只支持一个活写者。协调器内部以MapSessionId, Promise维护每条会话的串行链serialize见 coordinator.ts并要求另一个后端实例或进程不得在所有者完成 disposal、所有写操作停止之前修改该会话。这是显式支持的拓扑也是文档声明的限制见 Agent Note Consequences 一节。在单活写者前提下同一 id 的并发首次物化竞争由文件系统的 no-overwrite 发布机制仲裁POSIX 路径用link(tmp, finalPath)unlink(tmp)而非rename()发布link在目标已存在时返回EEXIST两个进程同时物化同一 id 不会互相覆盖见 index.tsmaterialize前还有rejectExistingLog作为 TOCTOU 后手拒绝覆盖已存在的 committed 日志。五、备选方案与取舍原决策记录Agent Note 记录了三条被否决的路线理解它们有助于把握当前设计的边界备选方案思路否决理由按 session id 扁平化存储单一扁平命名空间重复发布天然冲突在一条路径上路径校验 重复拒绝已能关闭身份缺陷无需让校验依赖一个全局扁平命名空间让协调器携带不透明的 storage locator把 JSONL 变更直接绑定到已选路径JSONL 完全可以用已验证过的元数据重算出该路径引入新泛型与参数会让 SQLite、测试后端、append、repair 全部背上只有文件后端才需要的概念协调多个活写者专用协调服务、进程级注册表或跨进程锁这定义了一种新部署拓扑而不是修复身份校验受支持的拓扑就是单活写者第三条的补充说明是并发创建同 id 的竞争交给no-overwrite 硬链接发布仲裁即上一节的link方案而非新增锁机制。六、行为后果与测试锁定修复后的可观测行为Agent Note Consequences 源码/测试印证错配、错位、重复的 JSONL 日志会在修复或协调器状态变更之前失败。决定性测试是rejects a mismatched header before repairing either session log见 jsonl.spec.ts把 A 日志的 header.id 改写为 B 后load(a.id)抛requested id identity-a does not match header id identity-b且A、B 两个日志的字节均保持原样expect(await readFile(aPath)).toEqual(beforeA)。查找代价与项目目录数量成正比findLog需要遍历每个项目目录做存在性探测这是文件型存储按 id 反查 cwd 的固有成本list则通过只读首行readFirstLine/readFirstZstdLine把开销控制在会话数而非日志总大小。跨项目目录的重复 id 同时被load与list拒绝测试load and list reject one id materialized in multiple project directories在两个 cwd/a、/b下写入同一 id 的日志两个入口都抛appears in multiple project directories见 jsonl.spec.tscreateCore也会在创建时跨所有项目扫描已存在的磁盘日志直接拒绝重复创建见 coordinator.ts 与测试 jsonl.spec.ts。协调器与 JSONL 测试共同锁定修复前拒绝、两个受影响日志字节不变、列举时路径校验、重复 id 拒绝、归一化项目碰撞与大小写/符号链接别名、加载期 root 校验。除上文逐条引用的用例全部位于 jsonl.spec.ts外共享协调器契约测试位于 session-persistence/tests/coordinator-contract.ts跨后端持久化契约位于 session-persistence/tests/contract.ts它们以mount/corruptTail/cleanup夹具驱动 JSONL 与 SQLite 等后端跑同一套行为规范。七、给使用与扩展者的实操要点配置层root必填且尽量使用绝对路径compression默认zstd校验和 Zstandard 帧物理文件后缀.jsonl.zstdpackChunks默认true把连续assistant/chunk增量打包为text-chunks/reasoning-chunks/tool-call-chunks行实测可让日志缩小约 60%读取侧布局无关preparedSessionCacheSize默认 5writeBatchMaxDelayMs默认 200ms完整参数见 index.ts 的Config定义。同一 root 下不要混用两种 compression后端会在发现.jsonl与.jsonl.zstd并存时拒绝encodingMismatch根因是身份校验依赖确定性的物理后缀。迁移提示旧版扁平布局project/encoded-id.jsonl会被显式拒绝legacyLayout需迁移到project/session两级目录后再加载而不是被静默忽略。排查身份错误当看到stored session identity mismatch/requested id ... does not match header id/header id ... and cwd identify三类报错时直接核对日志首行 JSON 与文件所在目录readRaw(id)可拿到与磁盘逐字节一致的原文见 index.ts是审计日志身份的便捷入口。综上这次修复没有引入新的接口或部署拓扑而是把选中哪个文件与文件自称是谁两个事实在loadStored边界内强制绑定并用协调器侧的独立断言形成纵深防御——这正是 DeepSeek Harness 一切皆插件架构下文件型后端与关系型后端在持久化语义上对齐身份不变量的一次完整闭环。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考