DeepSeek Harness 跨 workspace 会话恢复:让 /resume 回到任意项目目录的设计剖析 📅 发布时间:2026/9/20 23:54:45 👁 浏览次数: DeepSeek Harness 跨 workspace 会话恢复让 /resume 回到任意项目目录的设计剖析【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读DeepSeek Harnessdsh的/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 --resumeid而新进程会继承当前 cwd。会话头部的 cwd 会从日志中还原但dsh-fs-local、bash 执行器以及 glob/grep 解析路径时依据的是进程 cwd。因此恢复一个外部会话会在回放其 transcript文本记录的同时作用到错误的项目上。dsh启动器对--resume的解析可参见 apps/cli/src/args.tslauncher 只解析自己拥有的标志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 \| allscope恢复不切换目录交接过程显式携带目标cwdTuiResumeHost.handoffpreflightResume存储统一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.yml、packages/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的头部没有指明任何目录供宿主进入所以即便它的日志完好也无法完成交接——范围可以放宽但交接所需的目标目录信息不可缺失。交接把目标目录带进execveTuiResumeHost.handoff在SessionId之外还接收目标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 会构造带cwd与AGENTS.mdworkspace 指令的会话基线再验证恢复后 workspace 上下文的正确性。小结跨 workspace 会话恢复的落地表明一个恢复会话的功能其正确性同时取决于存储布局、选择器语义与进程交接三个层面。存储统一解决会话是否存在scope 机制解决会话是否可见可选交接携带目标目录解决会话恢复到哪——三者缺一不可。对于想深入或扩展该能力的开发者建议按以下顺序阅读共享配置默认值apps/cli/config/base.cordis.ymlsession-persistence-jsonl配置项路径解析基础packages/util/home-paths/src/index.tsdshHomePath、DSH_HOME环境变量CLI 参数边界apps/cli/src/args.ts测试回归apps/cli/tests/profiles/headless/workspace-context-resume.cordis.snapshot.yml与apps/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),仅供参考