dsh-session-dm
跨会话投递:往同工作区的另一个会话发消息,并可新建顶层会话。
安装
dsh plugin --profile web add dsh-session-dm
沙箱测试
这个插件还没测过。提交测试 →
README
dsh-session-dm
给 DSH(DeepSeek Harness) 的智能体加上跨会话能力:往另一个会话发消息、直接开一个新的顶层对话,以及等它跑完再继续,全程在同一个工作区里。
DSH 自带的会话能力是只读的——session_search / session_event_read 能看别的会话,send_message 只能到自己的子智能体树。缺的正是顶层会话之间的写方向,这个插件补的就是它。
工具 · 作用
session_dm_send · 往同一工作区的另一个会话投递一条文本。mode: "queue"(默认)排队等下一回合;mode: "steer" 插队——目标正在跑回合时直接插进当前回合,模型下一步就读到。
session_create · 在调用方工作区开一个新的顶层会话(真·新对话,不是子智能体),并带上首轮提示词。可指定标题。
session_dm_wait · 阻塞到另一个会话跑完当前回合,返回结束原因或失败信息。发完活直接等它,不用 sleep 猜。
排队 vs 插队(对齐宿主输入框自己的 beginSubmission 逻辑——只有目标正在跑回合时两者才有区别):
目标状态 · mode: "queue" · mode: "steer"
正在跑回合 · 排进队列,当前回合结束后模型才读到 · 插入当前回合,模型下一步就读到
空闲 · 立即开新回合 · 回落为排队(立即开新回合),回执说明发生了回落
冷会话 · 唤醒后开新回合 · 同左
前两个返回的是「已受理」回执,不是对方的回答。典型用法是三步走:session_dm_send 发活 → session_dm_wait 等它跑完 → session_event_read 读结果。



会话 A 会话 B
│ session_dm_send({ session_id, message })
▼
sessionController.resolveAgent(B) ← 冷会话在这里被唤醒
├─ agent.followup(消息) ← 进持久化 next-turn 收件箱
└─ sessions.flush(session) ← 等持久化确认才回执
▼
会话 B 把这条当普通用户输入处理
▲
│ session_dm_wait({ session_id }) ← 阻塞到 turn/end,不是 sleep 猜
│
会话 A 拿到「跑完了」→ session_event_read 读产出
安装
方式一:从 npm 装(最简单)
dsh plugin --profile <你的 profile> add dsh-session-dm
方式二:从本仓库装
dsh plugin --profile <你的 profile> add github:Venshell/dsh-session-dm
方式三:从 Release 附件装
去 Releases 下 dsh-session-dm-0.4.1.tgz:
dsh plugin --profile <你的 profile> add <下载路径>\dsh-session-dm-0.4.1.tgz
方式四:从源码目录装
git clone https://github.com/Venshell/dsh-session-dm dsh plugin --profile <你的 profile> add <克隆路径>\dsh-session-dm
⚠️ 装完必须重启 Harness。宿主在启动时加载插件模块,新装或改过的代码只对重启后的会话生效。
⚠️ 某些dsh plugin add版本会把 profile 的 bundle 列表重写成只剩新装的那个。装完扫一眼dsh.profile.bundles是否还完整。
bundle 会自动启用哪些组件
DSH 的 profile 是分层叠加的:出厂默认层 → 各 bundle 的 patch 层(按 dsh.profile.bundles 顺序)→ 你自己 profile 的 cordis.patch.yml → --patch 覆盖层。本插件的 patch 利用这个机制,装完即把跨会话需要的组件全部打开,包括那些出厂被关掉或压根没有的:
组件 · 出厂状态 · 本 bundle 做什么
dsh-session-dm · 不存在 · insert 到 host plane:所有 preset 的会话都看得到三个工具
@deepseek-ai/dsh-tool-session-query · 不存在——可选包,任何默认层都没有 · insert(常规 id tool-session-query):挂上五个读回工具 session_search / session_event_search / session_trace / session_event_trace / session_event_read,否则模型只能发、读不回来
@deepseek-ai/dsh-session-query-sqlite · 每层都有,但搜索是关的(openAt: never:服务在,只支持精确读;内容搜索会答 SESSION_QUERY_SEARCH_DISABLED) · id 定向覆盖(幂等——这行到处都在):openAt: first-search + 持久化路径 <home>/cache/session-search.db,首次搜索才打开 SQLite
两点要说清:
- 打开搜索是 profile 级的副作用。 覆盖
openAt之后,SQLite FTS 内容搜索整个 profile 都开了,不只是本插件——侧边栏搜索也会跟着变。这是官方文档给的部署级开关;不想要就从 bundle 的cordis.patch.yml里删掉session-query-sqlite那一条。没有它两个工具照样能投递(回执读标题,openAt: never下可用),只是模型搜不了历史。 - loader 不会给 insert 去重。 如果你的 profile 层里已经自己插了
tool-session-query行(在本 bundle 出现之前手改过 profile 的人常见),删掉其中一个——挂两次会重复注册五个工具。profile 层对自己 id 的 override 不会和 bundle 冲突:后层胜出,所以你改的openAt/path优先。
session_dm_send / session_create 另外消费的宿主服务(sessionController、sessions、systemPrompt、tools,以及回执标题用的 sessionQuery)任何部署都自带,无需启用。
工作原理
插件不起后台进程、不存状态、不加 schema,全部走宿主自己的会话机制:
消息投递用的是内建 schedule 服务给「原会话提醒」投递时的同一套:
ctx.sessionController.resolveAgent(target) // 活的 Agent,或冷唤醒 -> agent.followup(userMessage) // 持久化 next-turn 收件箱 -> ctx.sessions.flush(agent.session) // 等持久化确认
会话创建驱动的是 Web UI「新对话」按钮背后的同一个宿主 API(sessionController.create)。请求优先用 workspaceId 形式,好让新会话落进调用方所在的 UI 工作区分组而不是「未分组」;匹配不到工作区时退回裸 { cwd }。两者严格互斥——宿主会拒掉同时给 workspaceId 和 cwd 的请求。
授权边界:只有当目标会话的 cwd 与调用方完全一致时才能发消息(沿用 session-query 的保守规则)。没有工作区的会话既不能发消息也不能建会话。session_create 永远在调用方自己的工作区里创建——不接受目标参数。
可追溯:每条投递的用户消息都带 source: { kind: "session-dm", fromSessionId },从目标会话的事件日志里能反查是谁发的。
模块零依赖:除 node:crypto 外不 import 任何东西,所有宿主能力都通过注入的 Cordis 上下文拿到(tools、systemPrompt、sessionQuery、sessionController、sessions)。
踩过的坑(照抄前必读)
这几条都是真金白银的调试时间换来的。
1. 裸注册工具必须写完整 JSON Schema
本插件为了零依赖,刻意绕开了宿主的 defineTool 辅助函数。那个函数平时负责把简写参数({ field: { type, required: true } })转成 JSON Schema。裸注册时没人做这层转换,简写会被原样发布:模型侧看到每个参数类型都是 object,宿主按这份错 schema 校验时把字符串实参全剥掉,execute 永远收到 {}。
所以只要绕开 defineTool,parameters 就必须是完整 JSON Schema:type: 'object' + properties + required + additionalProperties: false。
2. sessionController 有两张脸
注入的 SessionController 服务暴露的是 create({ cwd }) -> { sessionId } 和 rename({ sessionId, title })。它内部的 this.agents(ApiSessionAgentController)暴露的是 createSession({ cwd }) -> { agent }。名字像、形状不一样。调用前先把注入服务自己的方法表从宿主 bundle 里挖出来看。
3. 工作区归属要 workspaceId,且与 cwd 互斥
只用 { cwd } 创建能成功,但会跳过 workspace.attachSession,于是会话在 UI 里显示成未分组。同时传 workspaceId 和 cwd 是硬错误("accepts workspaceId or cwd, not both")。
4. ESM 缓存不会因为改文件而失效
profile 里装的这个插件是指向仓库目录的符号链接,所以源码一改磁盘上立刻生效——但运行中的宿主还握着旧模块。每次改动都要重启 Harness。
5. loader 的 patch 语义(写 patch 前必须知道)
从宿主 bundle 里扒出来的 applyEntryPatches:
insert无条件追加,不去重;只是把新行加进索引,让同一个 patch 列表里靠后的 patch 能定位到它。- id 定向 patch(有
id无insert)替换指定字段,config是整体替换不做深合并;目标行不存在就警告 + 跳过。 - profile 层叠在 bundle 层之后,所以后层胜出。
6. 等会话结束必须盯 turn/end,不能轮询 status
宿主的开发者文档把话说死了:
Wait on durable events such asturn/end… Do not pollagent/status.whenIdle()does not mean one follow-up finished: several inputs can share one running interval.
session_dm_wait 因此以 `
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:3,最后更新 2026-11-06