DeepSeek Harness 跨 workspace 会话恢复:让 /resume 回到任意项目目录的设计剖析
2026/9/20 23:54:42 网站建设 项目流程

DeepSeek Harness 跨 workspace 会话恢复:让 /resume 回到任意项目目录的设计剖析

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

导读

DeepSeek Harness(dsh)的/resume会话恢复能力在交付 TUI 组合后长期只能触达"启动目录内创建的会话"——想回到昨天在另一个项目里进行到一半的工作,必须记住项目路径、退出终端、再跑到那个目录重新启动。本篇文章以该仓库中已实现的特性记录 2026-07-28-cross-workspace-resume.zh.md(英文版见 2026-07-28-cross-workspace-resume.md)为骨架,结合仓库源码剖析这一限制的三个独立成因、三管齐下的修复方案、备选方案取舍与测试验证。读完你将理解:为什么存储根目录、选择器范围与恢复交接必须同时改动,以及"范围(scope)而非排除(exclusion)"这一设计理念在会话选择器中的落地方式。

问题背景:/resume的三重独立限制

在修复之前,跨 workspace 恢复存在三个彼此独立的原因,只修其中一个都不会有任何变化:

1. 存储根目录按启动目录隔离。已交付的 TUI 组合把持久化根默认成相对路径./.sessions,于是每个启动目录都独占一份互不相交的 JSONL 根目录,以及一份互不相交的派生session-query.db。来自另一个项目的会话并不是"在列表中被过滤掉了"——它们根本不存在于列表读取的存储中。需要强调的是,JSONL 后端本来就会在同一个根目录内部按 cwd 分区,所以分区被叠加了两层:一层按根目录,一层在根目录内部。

2. 选择器再次过滤。即使外部会话进入了存储,选择器在展示前也会丢弃cwd与当前会话不同的记录;而summarizeResumeCandidate又独立地把不同的cwd标记为disabledReason: 'different workspace'。于是确实进入了存储的外部会话既被隐藏,也会被拒绝。

3. 恢复流程从不切换目录。宿主通过process.execve重新执行dsh --resume=<id>,而新进程会继承当前 cwd。会话头部的 cwd 会从日志中还原,但dsh-fs-local、bash 执行器以及 glob/grep 解析路径时依据的是进程 cwd。因此,恢复一个外部会话会在回放其 transcript(文本记录)的同时,作用到错误的项目上。

dsh启动器对--resume的解析可参见 apps/cli/src/args.ts:launcher 只解析自己拥有的标志,dsh --profile tui --resume <session>会把--resume之后的参数原样交给被引导的应用树,因此恢复命令的参数由 TUI 应用内的插件自行解析。

决策总览:三管齐下的修复方案

共享 CLI 配置提供 Harness home 下的同一个会话根目录,选择器获得 workspace 范围,交接过程携带目标目录。三个改动分别对应上述三个成因:

成因修复关键落点
存储按启动目录隔离共享 base 默认persistenceRoot为 Harness home 下的sessionsapps/cli/config/base.cordis.yml中的session-persistence-jsonl配置项
选择器隐藏并拒绝外部会话把"workspace 之外"从禁用理由改为展示范围ResumePicker'workspace' \| 'all'scope
恢复不切换目录交接过程显式携带目标cwdTuiResumeHost.handoff+preflightResume

存储统一:Harness home 下的共享会话根目录

共享 base 在组合包配置(apps/cli/config/base.cordis.yml)中拥有session-persistence-jsonl配置项的默认值,它调用由 app-boot 提供的dshHomePath('sessions')。因此 TUI、Web 与 headless 三种 profile 使用同一个默认值,无需针对会话的启动器补丁或 slot。

dshHomePath使用规范的DSH_HOME解析器及其标准的~/.dsh回退值,其实现位于 packages/util/home-paths/src/index.ts:

/** Directory name for the default DeepSeek Harness home under the OS home. */ export const DSH_HOME_DIR_NAME = '.dsh' /** Stable user-facing display form for the default DeepSeek Harness home. */ export const DEFAULT_DSH_HOME_DISPLAY = `~/.dsh` /** Environment variable that overrides the default DeepSeek Harness home. */ export const DSH_HOME_ENV = 'DSH_HOME'

从源码结构看,解析顺序为:显式设置的环境变量DSH_HOME优先,未设置时回退到 OS 主目录下的~/.dsh

一个关键的配置语义:若 overlay 或个人 patch 显式声明根目录,它会整体替换该配置项的config,并继续作为部署的权威选择。也就是说,默认值只是"回退值",显式声明仍然优先——这是理解本方案不破坏现有部署的前提。

该配置项在多个 bundle 的cordis.patch.yml中也有引用(如packages/bundle/base/cordis.patch.ymlpackages/bundle/sdk-minimal/cordis.patch.yml),说明共享根目录这一默认值面向所有交付形态生效。

选择器:是范围(scope),不是排除(exclusion)

当前 workspace 之外的 workspace 是一种展示范围,而不是禁用理由。核心改动如下:

  • showResume()汇总每一条记录,不再按 cwd 丢弃。
  • ResumePicker持有一个'workspace' | 'all'scope,默认值为当前 workspace,因此常见场景毫无变化。
  • Tab 键切换范围;范围行会说明当前生效的范围,以及另一个范围下的数量。
  • 在全 workspace 范围中,每一行都报告自己的 workspace;而该 workspace 标签只在展示它的范围里才加入可搜索文本(避免默认范围下的搜索被无关 workspace 名污染)。
  • 切换范围会清空查询和选中项,使高亮行始终属于可见列表——这避免了"查询/选中项指向一个已不在可见列表中的行"的陈旧状态。
  • 逐行的 workspace 行会让该范围下的每一行在终端里多占一行,可见条数预算已经把这一点计入(终端行数有限,选择器按行数而非条目数预算)。

与此对应,summarizeResumeCandidate去掉了'different workspace'拒绝理由,并新增'session has no recorded workspace'。这是一条真正新增的拒绝理由,而不是改名:没有cwd的头部没有指明任何目录供宿主进入,所以即便它的日志完好也无法完成交接——范围可以放宽,但交接所需的目标目录信息不可缺失。

交接:把目标目录带进execve

TuiResumeHost.handoffSessionId之外还接收目标cwdpreflightResume把两者一起解析并一起返回。这一设计的关键意义:

  • 消除陈旧目录竞态:调用方无法从它展示过的那一行里重新推导出一个陈旧目录。在列表展示与预检之间cwd发生了变化的记录,会在重新读取到的目录中恢复——这正是原先「拒绝发生变化的 cwd」的行为,如今变成携带新路径完成交接的原因。
  • 在 dispose 之前切换目录:已交付的宿主在应用资源释放(dispose)之前切换目录。不可达的目录必须在调用方还能恢复终端时就拒绝,因为拆卸之后已经没有任何所有者可供汇报错误。
  • 统一使用默认接口:恢复始终使用默认的dsh --resume接口,因为meta会拒绝父级选项;交接过程本身已经进入持久化保存的目标目录,因此无需再向新进程传额外的目录参数。

从 apps/cli/src/args.ts 的注释可以看到dsh --profile tui --resume abc的实际行为:--resume abc作为内层参数到达 TUI 应用,宿主进程据此重新执行自身并带上会话 id,而工作目录已经在 execve 之前被切换为交接目标。

备选方案权衡

原设计文档明确记录了五个被否决的备选方案,理解它们有助于把握最终方案的边界:

1. 从dsh启动器给persistenceRoot打补丁,而不是改动组合包默认值。否决原因:loader 补丁会整体赋值config。个人的~/.dsh/config.yaml覆盖层已经用一份局部配置给tui-agent那一项打了补丁,这恰恰就是persistenceRoot一开始会退回到组合包默认值的原因;启动器补丁要么会被该覆盖层擦除,要么必须压过它,从而让覆盖层再也无法设置这个字段。把默认值放在组合包里能经受任何局部补丁,并让这项事实只有一个归属(single source of truth)。

2. 保留./.sessions,并额外扫描 Harness home 根目录。否决:两个根目录意味着两份 SQLite 索引,以及一份合并列表——其中各行的活跃状态与版本权威来源并不相同,而这一切只是为了保住"不做迁移"的决策本就已经放弃的那部分日志可见性。

3. 把现有的项目本地日志迁移到共享根目录。被需求方否决。项目./.sessions下的会话仍留在磁盘上,从该目录显式执行dsh --resume <id>仍可恢复,只是不再出现在/resume中。

4. 把所有 workspace 铺成一个扁平列表。否决:这会丢掉绝大多数场景想要的"本项目"默认值;在一个繁忙的 home 目录里,当前项目的会话会和无关会话争夺注意力。这正是引入'workspace' | 'all'两级 scope 的动机。

5. 让宿主从还原后的会话头部推断目录。否决:会话头部是面向模型与提示词的状态,在启动之后才还原,而目录必须在execve之前进入。显式传递它能让这个顺序在 seam(接缝)处保持可见——即交接点上的时序依赖不能被隐式推断掩盖。

影响与取舍

实现这一特性带来三条明确后果,均属设计接受的代价:

  • 已经存放在项目本地./.sessions下的会话会从/resume中消失。这是不做迁移所接受的代价(旧会话仍可通过在该目录下显式执行dsh --resume <id>恢复,相关 CLI 行为可对照 apps/cli/src/args.ts 与 apps/cli/tests/args.spec.ts)。
  • 恢复一个会话可以改变进程的工作目录,因此恢复外部会话不是单纯的 transcript 还原——每个解析路径的工具(dsh-fs-local、bash 执行器、glob/grep)都会随之移动到新 workspace。
  • Harness home 现在保存着这台机器上每个项目的会话日志。它的增长不再受单个 checkout 约束,而该特性记录本身没有引入任何保留策略——部署者需要自行考虑日志留存。

测试与验证

该特性的测试覆盖相当完整,主要验证点包括:

  • 默认范围行为:隐藏其他 workspace 但报告其数量。
  • Tab 切换:显示其他 workspace 并带上逐行 workspace 标签;再按 Tab 返回时清空查询与选中项。
  • 按 workspace 标签搜索:workspace 标签在全 workspace 范围下可搜索。
  • 无 cwd 记录:仍可见但不可选(对应新增的'session has no recorded workspace'拒绝理由)。
  • 交接语义:交接同时收到 id 和在预检时重新读取到的 workspace;原先「拒绝发生变化的 cwd」的用例现在断言交接携带新目录。
  • 构建后 CLI PTY 测试:检验共享配置默认值与每进程派生的查询索引。
  • 无密钥 TUI 快照:固定选择器的两个范围,包括范围行、逐行 workspace 行,以及页脚中的 Tab 提示。
  • 手动端到端验证:一次手动执行的跨 workspace 恢复在进程层面验证了替换后进程(execve 之后)的工作目录变为目标 workspace。

仓库中的 e2e 测试也为"持久化会话 + workspace 上下文"的组合提供了回归覆盖,例如 workspace-context-resume.expected.e2e.ts 会构造带cwdAGENTS.mdworkspace 指令的会话基线,再验证恢复后 workspace 上下文的正确性。

小结

跨 workspace 会话恢复的落地表明:一个"恢复会话"的功能,其正确性同时取决于存储布局、选择器语义与进程交接三个层面。存储统一解决"会话是否存在",scope 机制解决"会话是否可见可选",交接携带目标目录解决"会话恢复到哪"——三者缺一不可。对于想深入或扩展该能力的开发者,建议按以下顺序阅读:

  1. 共享配置默认值:apps/cli/config/base.cordis.ymlsession-persistence-jsonl配置项)
  2. 路径解析基础:packages/util/home-paths/src/index.tsdshHomePathDSH_HOME环境变量)
  3. CLI 参数边界:apps/cli/src/args.ts
  4. 测试回归:apps/cli/tests/profiles/headless/workspace-context-resume.cordis.snapshot.ymlapps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询