dsh-session-relay: Organizing multiple DSH root sessions into a handoff-capable collaboration team

前言

DeepSeek Harness 的理念是「一切皆插件」,会话本身也是可以被插件操作的对象。问题在于:当你用 DSH 跑一个看起来比较大的任务,比如「把上个版本的回归排查掉、顺手把 Lint 清理一遍」,通常的做法是在一个会话里塞满上下文,或者让模型在会话内派生子代理。前者很快到压缩边缘,后者随父会话的轮次结束消失,用户既不能手动拉起,也不能接管。

下面介绍的 dsh-session-relay 走的是第三条路:不包装子代理,而是把多个平级的根会话编成一组,其中一个作为主会话(协调者)负责版本线和看板结论,其余作为工作会话各自背自己的上下文。主攻方向是「一个主会话协调多个工作会话」,附带版本线公告、指令模板、汇报合并唤醒、任务看板核对、会话检查与主会话交接。插件由 SunsetRNE 维护,开源协议 MIT,当前版本 2.0.0,属于社区目录 SkillHub 上「工作流」分类下的条目。

它是做什么的,以及为什么不直接用任务板

README 里把边界讲得很清楚:单个会话的上下文有限,构建号到 300+ 之后,一个会话同时背版本线、看板、代码、测试和汇报会很快到压缩边缘,早期约束和证据会丢。平级会话每个都有自己的上下文窗口,可以分别承担调查/分析/解决/验证/汇报,主会话只保留版本线、看板结论和合并判断。

对比 DSH 原生子代理:子代理由模型在会话内派生、挂在父会话树里,用户不能从侧边栏手动拉一个「帮我调查」的子代理,插件也不直接打开子代理——subagent routing 是宿主独占的。平级会话是用户可见、可点开、可唤醒/接手/改派的根会话,出问题能直接接管,做完留下可审计的汇报和提交。插件不绑定模型厂商,只依赖 DSH 的会话与工具接口,因此 Claude、GPT、DeepSeek 等 DSH 能配置的模型都能当主会话或工作会话,也可以混用。

运行环境与安装

它要求宿主是 DeepSeek Harness 0.2.2-rc.1 及以上。package.json 里 engines.dsh 与 peerDependencies 的写法是:

"engines": {
  "dsh": ">=0.2.2-rc.1 <0.3.0-0 || >=0.2.3-rc.1 <0.3.0-0 || >=0.2.4-rc.1 <0.3.0-0"
}

README 解释了为什么要写成显式预发布分支:node-semver 只放行「范围里有同元组且带预发布标签的比较符」的预发布版本,否则 0.2.3-rc.1 这类会被静默排除、安装时撞 ERESOLVE。插件依赖的 @deepseek-ai/dsh-* 包由宿主提供,不单独安装。更老的宿主只保留基本功能,官方「更改请求与工作树」(forge)相关的工作树识别、声明外置不再保证可用。

安装不需要 npm 账号。在 DSHA 的插件管理器里任选一种来源:

  • GitHub Release(推荐,固定版本):仓库 SunsetRNE/dsh-session-relay 的 Release,每个版本附一个 dsh-session-relay-<版本>.tgz。
  • 仓库链接:https://github.com/SunsetRNE/dsh-session-relay 装 main 最新提交;指定版本用 https://github.com/SunsetRNE/dsh-session-relay/tree/v<版本>。
  • 本地压缩包:下载 Release 附件后导入。

需要注意:DSH 进程内加载的插件以当前进程权限运行,安装前应自行检查源码与 MIT 许可证。非 DSHA 环境下,依赖设备桥的功能(手机通知,读 /root/.dsh/.bridge_headers 与 /root/.dsh/.bridge_port)不可用。

核心能力

插件提供一个 /relay 命令,完整用法见 /relay help。下面只列 README 中明确写到的场景。

分工模型

主会话协调,工作会话用 peer_report 汇报,汇报被批量合并成一次唤醒,避免每条汇报都醒一次主会话。全名叫「平级」会话互通,指的就是这个结构:所有参与方都是同一层根会话,没有父子层级。

模型工具与命令

模型侧暴露 14 个 peer_* 工具:peer_list、peer_report、peer_send、peer_spawn、peer_alias、peer_inspect、peer_tasks、peer_task、peer_mail,主会话另有 peer_line、peer_assign、peer_handoff、peer_ask_user、peer_watch。其中 peer_send 的 mode=interrupt 是主会话专用的 nudge(stop:true 用「停止并汇报」信封),取代了 1.4.0 的独立工具 peer_interrupt。

用户侧命令覆盖会话与协同:

/relay tasks
/relay task update --stage
/relay approve|reject
/relay inspect
/relay search
/relay repo
/relay sync
/relay task pr
/relay mail
/relay tpl save|list|show|rm
/relay wt <会话> [--from <基分支|提交>] [--name <名字>]

任务看板

任务与认领的事实源在插件数据目录 tasks/<工作区>/,仓库里的 .agents/TASKS.md 与 claims/ 是只读导出快照。写操作是 /relay task new|claim|takeover|handoff|release|done|abandon|update,模型侧用 peer_task。1.9 起支持任务派发:peer_task action=split 拆父子任务(可串行依赖),action=dispatch 按任务/子树唤醒持有者或拉起新会话,依赖未完成不派,父任务在子任务全结束后自动完成。1.10 的任务库维护提供 /relay task import --force(把手改过的导出快照重新导入,覆盖前备份)、/relay task export,并带两个容量阈值:活动库超过 taskStoreMaxActive(默认 500)归档最老的终态任务,TASKS-archive.md 超过 taskArchiveMaxBytes(默认 1 MiB)把最老的整段移入 tasks-archive.jsonl 冷存。

通知与背压

2.0 引入协作邮箱:核验成功这类公共通知只写进工作区邮箱 mailbox.json,同一会话 5 分钟窗口内的多条通知合并成一条短通知,需要细节时用 peer_mail read,主会话可用 send/force 推送。用户可用 /relay mail 看未读、读全文、归档,并查看最近 7 天「短通知 / 全文」统计。

通知取走有时限和背压机制,这部分规则比较细:

  1. peer_send 默认 3 分钟超时,timeout 可自定义 90s/5m/2h,范围 10 秒–24 小时;对方实际取走消息(由 DSH user/message 事件按 id 确认)时只销账、不再给发送方发回执,避免多唤醒一轮。
  2. 消息一直排队、超过时限仍未取走时,对方正在运行就由插件把原消息插进当前轮;对方不在运行或 mode=queue 才提醒发送方,并给出强制通知(priority:"urgent")或撤回(action=withdraw)的下一步。
  3. 每个会话待收消息到 8 条时拒绝普通发送(steer 可插队);待收 ≥4 条或最早一条 ≥5 分钟标落后,≥30 分钟标卡住。peer_list、看板和侧栏卡片都会显示「待收 N」。
  4. 明确不用回复的待命通知用 expect_reply:false 或 /relay --no-reply;普通 followup 排队超过 60 秒自动升级为 steer。/relay all 会跳过待收落后/卡住的会话并在结果里列出。

工作区定位与主会话交接

右侧边栏「协作」标签会显示每个会话的目录、分支、是否在 git 工作树里、从哪个分支哪个提交分叉,只读、按目录缓存 60 秒。/relay wt 走官方工作树服务给会话建分支与新目录并切过去,宿主没启用该功能时明确报错。交接流程分交给次确认与历史两类,主会话交接也可以通过 peer_handoff 与 /relay 命令触发。

指令模板与全文搜索

2.0 T3 加了两层:/relay tpl save|list|show|rm 管理指令模板(设置页同一份存储,可编辑、重命名、删除、复制用法,模板名小写校验),/relay search [--in|--re|--case|--kind|--tool|--turn|--limit] <内容> 搜本工作区最近 25 个会话的原始日志,按会话分组显示命中。这两项都只由用户操作,模型没有调用入口。

GitHub 集成(默认关)

GitHub 部分全部默认关闭,且刻意把写入权留在用户手里:

  1. /relay repo 只读探测 origin 仓库能力(issues/projects/合并方式/分支保护/sub-issues),仓库开关按 owner/repo 存,只由用户用 /relay repo on|off <开关> 或协作面板点开,模型没有写入口,开关改动写本地审计。
  2. issues 开关打开后,任务变化单向投影成 GitHub issue(marker 幂等、unknown 退避重试、重启恢复),配套 closeOnTerminal、labelSync(维护 relay:* 标签)、mapIssues(sub-issues 不可用时降级为父票正文 tasklist)。用户命令 /relay sync status|flush|backfill|retry|detach|on|off。
  3. projectStatus 开关打开时,任务详情按需读该任务 issue 的 GitHub Projects Status(只读、2 分钟缓存),并可在任务详情里点「写回 Projects」由用户写 Status;匹配不到选项不写。
  4. 任务 PR 只读:/relay task pr <任务号> <PR 号|URL> 把任务关联到 PR,只读展示 state、可合并性、审批决定、checks,不代开 PR、不代合并,PR 状态不会自动改任务阶段或审批。

典型用法

假设你在 /root/relay-demo 这个工作区,下面这组步骤来自 README 的命令面,用于走通「建任务 → 派发 → 汇报 → 审批」这条链路。

先建任务并指派:

/relay task new --title "排查上版本回归"
/relay task claim <任务号>

主会话想拆父子任务并派发给工作会话,用模型侧工具:

peer_task action=split <父任务号> --sub "确认失败用例" --sub "修复与验证"
peer_task action=dispatch <子任务号>

工作会话完成需要口头汇报时:

peer_report <任务号> --status done --summary "失败用例已收敛到 3 个,均为环境时序"

主会话收到汇报只销账、不发消息,汇报在批量窗口内合并成一次唤醒。需要用户拍板的走到审批:

/relay task update <任务号> --stage 待审查
/relay approve <任务号>

需要临时唤醒一个正在跑的工作会话,主会话用 peer_send mode=interrupt;明确不用回复的待命通知用 /relay --no-reply 或 expect_reply:false。

改完代码想把这个会话挪到新分支上继续,用 /relay wt:

/relay wt <会话> --from develop --name fix-regression

宿主没有启用官方工作树服务时,这一步会明确报错,不会退化成手动 git worktree add。

适用场景与注意点

适合这几类情况:单个会话上下文已经吃紧、任务需要跨天跨版本追踪、希望每个子目标有独立的可见会话和可审计汇报、需要主动唤醒或接管某个正在跑的会话。插件只依赖 DSH 的会话与工具接口,模型厂商怎么配都行。

需要注意的边界:

  • 主会话与工作会话是平级的根会话,插件不直接打开子代理,subagent routing 仍是宿主独占。
  • 需要 DSH 0.2.2-rc.1 及以上;更老的宿主只保留基本功能,forge 相关工作树识别、声明外置不再保证可用。
  • GitHub 写入(issue 投影、Projects Status 写回)默认关闭,只有用户显式打开开关并手动触发才写。
  • 部分功能读取 DSHA 容器内的 /root/.dsh/.bridge_headers 与 /root/.dsh/.bridge_port,非 DSHA 环境下手机通知等依赖设备桥的功能不可用。
  • 插件以当前 dsh 进程权限运行,安装前应检查源码与 MIT 许可证。
  • 星标数较低(SkillHub 目录页显示 0 星),采用前建议先跑一遍 /relay help 与 /relay reload 确认行为符合预期。

小结

dsh-session-relay 的价值不在于又造了一个任务板,而是把协作做在根会话之间:让上下文各自独立、汇报合并唤醒、待收队列可见、交接可确认、GitHub 写入留在用户手里。如果你在 DSH 里已经开始用多个会话并行干活,它会把这些会话从「各自为战」变成一支可以交接的队伍。

目录页:https://www.skillhub.cn/plugins/SunsetRNE/dsh-session-relay
GitHub:https://github.com/SunsetRNE/dsh-session-relay

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye