Plugin directory / Knowledge / dsh-interconnect
dsh-interconnect
Verified · install-tested on dsh Chinesezjc
What it does
Cross-instance message and event handoff plugins for DSH (interconnect service + tools).
Works — verified, growing community
Cross-instance message and event handoff plugins for DSH (interconnect service + tools). It installs cleanly and boots without issues in our testing. It has a growing community — a solid choice.
“Verified” means our automated CI actually ran dsh plugin add in a clean profile and it booted — nothing more. Feature descriptions and version compatibility are the author’s claims. This is not a security audit and not an endorsement of third-party code.
README
dsh-interconnect
跨实例消息互通与事件通知插件,用于 DeepSeek Harness (DSH)。
让一个 DSH 实例能向同一个实例、另一台机器、或另一台机器上的别的 DSH 实例发送消息、探测活性,并在实例之间双向推送事件。
项目状态(2026-09-16):本仓库是该插件的树外(out-of-tree)主源。并入 DSH monorepo 的计划(PR #3243)已按「保持树外」的评审意见关闭;插件继续经 npm 分发(
dsh-interconnect),并由团队内部的插件集合仓以 git submodule 引用本仓库。
包含三个插件
interconnect —— host 服务(ctx.interconnect):
- 全走持久 WebSocket 链接:跨实例、跨机器投递消息、枚举 live session、探测活性(
send/reply/ping/list经/interconnect/link的msg/query帧) /interconnect/linkWebSocket 端点:双向实时事件推流,含心跳与指数退避重连;也承载send/reply消息(msg/msg-result帧,WS 优先 + HTTP 回退)- 事件 fan-out(HTTP + WebSocket),入站事件以
interconnect/event发出 - 共享密钥鉴权(
DSH_INTERCONNECT_TOKEN,bearer,fail-closed,timing-safe 比较)
tool-interconnect —— 模型可见工具:
interconnect_send:向对端实例的指定 session 投递消息;可选delivery选投递模式、resume唤醒离线 sessioninterconnect_list:列出对端实例的 live session(id + 标题 + 状态),用于在不预先知道 session id 时寻址interconnect_ping:探测对端实例活性与身份interconnect_reply:向记录过的发送方回传消息,只需文本;回信 session 是本 agent 自己的,无需再次寻址
skill-interconnect —— 配套 skill:
- 向模型注册
dsh-interconnectskill,说明list/ping/send/reply的完整用法、
投递模式、resume唤醒语义与失败处理。 - 明确告知模型:
interconnect_send会自动注入发送方的instanceId和sessionId,
接收方凭记录的 sender 即可用interconnect_reply回信,不需要手工传地址。 - 依赖
interconnect服务,只有传输层存在时才注册进ctx.skills。
用法
寻址(0.9 起用 instanceId,全走持久链接)
从 0.9 起,传输只走 WebSocket 持久链接,不再有 HTTP 端点,寻址参数从 baseUrl 改为 instanceId:
interconnect_send(instanceId="peer", sessionId=..., text=...)interconnect_ping(instanceId="peer")interconnect_list(instanceId="peer")interconnect_reply(text=...)(只需文本;回信 session 是本 agent 自己的,目标从记录的 sender 解析)
instanceId 是 interconnect 行 peers 映射里的键;真正用来拨号的 origin 由该映射的值给出(例如隧道端点 http://127.0.0.1:13080),instanceId 本身从不出现在线上,也不参与路由——origin 才是唯一的拨号依据。到未配置 / 未联通的对端 send/ping/list 返回 unreachable(无 HTTP 回退)。
interconnect_list 返回对端当前 live 的 session,每一行的 sessionId 在调用时刻都是合法的投递目标:
session-264d37b0-… 重构 interconnect 插件 [idle]
session-b07326da-… [running]
title 与 status 是尽力而为的:标题来自可选的 title projection 服务,对端没装该服务、或
该 session 还没有标题时,整个键不出现(而不是空字符串),所以「无标题」与「该对端不提供
标题」可以区分。projection 抛错只会让那一行降级成只有 id,不会让整个列表失败。
只列 live session 是有意的:send 能到达的正好是这些。对端存在但没有运行 agent 的 session
不会出现在列表里,也收不到消息。
答案有双重上限:行数(MAX_LISTED_SESSIONS,100 行)与字节预算(MAX_LIST_ROWS_BYTES,等于链路
帧上限减去 4 KiB 的 query-result 信封)。整帧必须待在链路的帧上限内,ws 对超限帧会直接关闭
链路,所以 live session 极多、或标题很长时,对端只回答能装下的前若干行,而不是把传输打断。
对端不上报截断标志,所以 interconnect_list 只在收到的行数正好等于该上限时追加一行提示
(100 of possibly more live sessions)——满页可能被截断,缺失的目标仍可能 live。
ping 与 list 的答复还会按调用方问的 kind 校验形状,形状不符按「无答复」处理(对应 unreachable)。
回复(reply)
send 的线负载带一个 sender 身份(无地址:instanceId + sessionId),收到消息的
instance 会按「本地 session id → 该 sender」记下这份身份。之后那个 session 回信只需要文本:
回信 session 就是执行 interconnect_reply 的 agent 自己的 session,工具据此查出记录的 sender,
不用再次给出本地 session id、对端 instanceId 或远程 session id——回信走的是本机到那个
instance 的持久链接。
# 源实例 A 指定目标 B 的 session,并带上自己的身份(无 baseUrl)
interconnect_send(instanceId="b", sessionId=B-sess, text="…", sender={instanceId:A, sessionId:A-sess})
# B 回传:只给文本,回信 session 即执行该工具的 agent 自己的 session
interconnect_reply(text="reply")
sender是自报的,只用于 reply 归因与寻址,不是路由或鉴权依据——连接本身仍由
共享密钥在每个 origin 上独立鉴权。- 回复的消息也带
sender(本机恒带,不再需要配置 origin),所以对话可双向多轮延续。 - 只有当入站 send 带了
sender时 reply 才有目标;对端版本没带、或本 session 从未收过
互联消息时,reply返回delivered: false, reason: "no-sender-known"。 sender不进模型上下文(和source一样只落到持久化日志与 UI 归因)——这条消息的
内容字面就是 wire 上传来的text,模型看到的仍是普通 user 文本,只是不带任何结构化的
发送方标记。
消息信道:全走 WS(msg / query 帧)
从 0.9 起不再有任何 HTTP 端点——send/reply/ping/list 全部在持久 WebSocket 链路
(/interconnect/link)上完成。peers 映射在激活时自动 link() 每个对端(心跳 + 指数退避
重连沿用既有实现),寻址按 instanceId 查对应链接。
| 帧 | 方向 | 作用 |
|---|---|---|
hello |
双方 | 拨号方自报 instance id |
event |
双方 | 生命周期事件推流 |
msg |
请求方 → 接收方 | 携带 kind(send/reply)、sessionId、text、sender/delivery/resume、reqId |
msg-result |
接收方 → 请求方 | 与 reqId 对应的 SendResult |
query |
请求方 → 接收方 | ping / list / event 发现与事件查询 |
query-result |
接收方 → 请求方 | 与 reqId 对应的查询结果 |
- 出站:
interconnect_send/interconnect_reply/ping/list都发对应帧并等待匹配reqId
的结果(受requestTimeoutMs约束)。到未配置或未联通的对端直接返回unreachable——
没有 HTTP 回退,这是 0.9 的破坏性变化。 - 入站:
msg帧携带send,走deliver逻辑(sender 记录、subagent 封栏等),结果经同一 socket 回msg-result;query帧回query-result。 - 心跳与指数退避重连沿用既有实现。
投递失败的原因
delivered: false 单独一个布尔值无法据以行动,因为不同失败需要不同应对,所以SendResult.reason 会指明是哪一种:
reason |
含义 | 应对 |
|---|---|---|
session-not-live |
对端答复了,但那个 session 没有运行中的 agent | 重试同一个 id 无用;用 interconnect_list 换目标,或带 resume |
unreachable |
没拿到可用答复(传输失败,或鉴权被拒) | 目标 session 可能完好,重试可能成功 |
resume-refused |
请求了唤醒,但对端不允许(allowResume: false) |
再带 resume 也没用 |
resume-failed |
允许唤醒且尝试了,但没得到 live agent(无此持久化 session,或被别的 owner 持有) | 换目标 |
session-owned-by-subagent |
该 session 属于 subagent 路由,投递权在它的父 agent | 通过父 agent 触达,别直接投 |
no-sender-known |
reply 指向的本地 session 从未记录过发送方(它没收到过带 sender 的消息,或对端版本过旧没带 sender) |
先用 interconnect_send 主动建立联系 |
reason 恰好在 delivered 为 false 时出现。
只有真的「尝试唤醒但失败」才是 resume-failed。没装 api-proxy 的部署里,agent lookup
退化成一次 registry 查询、根本没有唤醒能力,这时报 session-not-live——否则会让调用方去重试
一个永远不可能成功的操作。
subagent 会话不可直投
interconnect_send 不会往 subagent 拥有的 session 里投递,interconnect_list 也不会把它们
列出来。那类 session 的投递权属于它的父 agent,从这里 splice 进 inbox 会和父 agent 抢。判定
逻辑镜像 Host 的 hasApiSessionSubagentOwner(@deepseek-ai/dsh-api-session-controller):
Host 在 0.6 之后把这个谓词从 @deepseek-ai/dsh-api-remotes 移走,且没有公开导出——桌面端和npx @deepseek-ai/dsh web 运行时里没有任何可 import 的 Host 绑定,所以这里逐字复制一份
(isSessionOwnedBySubagent,见 src/interconnect/index.ts)。这是安全规则,Host 改动该规则
时必须同步此副本;tests 覆盖 origin=subagent 与 parent-owned 两条封栏分支。
已实测:起一个真实 subagent 后,interconnect_list 不包含它;直接 send 到它的 id 返回session-owned-by-subagent,消息没有进入 inbox。
唤醒离线 session(resume,默认关)
SendPayload.resume: true 让对端唤醒一个已持久化但没有运行 agent 的 session。
默认关闭是有意的。 实测确认:消息投递到 session 后会触发一次完整的 agent 回合——wakeDriver() → kick() → turn() → llm.stream(),即一次计费的模型调用,且 assembly
里带着该 session 的完整工具集。在一个用户没打开、看不到、也无法中断的会话里启动这些,和
「推一下已经开着的会话」不是一个量级,所以必须由发送方显式请求。
两侧都有控制权:
- 发送方按消息决定
resume(默认不唤醒) - 接收方用
Config.allowResume(默认true)一票否决——因为花钱和跑工具的是它那台机器;
拒绝时在跑 lookup 之前就短路,回resume-refused
唤醒不是调本插件的 ctx.agents.resume(),而是走 Host 已配置的 agent lookup
(typert.lookups.get('agent'))。这一点是关键:resume() 返回的 handle 由调用方
context 拥有,实测确认插件 fiber 被 dispose 时会把 resume 出来的 agent 和 session 一起
拆掉(同一调用改用根 ctx 则两者都存活)。交给 Host 的 resolver 之后 owner 是 api-proxy,
而且它会按 session 日志里记录的 preset 重建工具集——不是空壳。
没有 Host lookup 的部署(headless、无 api-proxy 的 profile)会降级为 session-not-live,
不会报错。
唤醒只把消息放进 inbox,是否真的开始处理取决于 delivery:
# 唤醒并让对方实际处理(会起一个计费回合)
interconnect_send(instanceId="peer", sessionId, text, resume=true, delivery="followup")
# 唤醒但不起回合:只写入上下文,等对方下次被唤醒时一起读
interconnect_send(instanceId="peer", sessionId, text, resume=true, delivery="inject")
已实测:resume=true + inject 之后目标从非 live 变 live(interconnect_list 计数 +1),
且该 session 日志里只多一条 agent/inbox/spliced、后面没有 turn/start。
已知限制:磁盘格式过旧的 session 无法唤醒,返回 resume-failed。这不是本插件的限制——
Host 自己的 resume 路径对同一个 session 报 SessionFormatUnsupportedError,同样失败。
投递模式
delivery 的三个取值各自对应一个 Agent 方法,即 (inbox target, wakeup) 组合:
| 模式 | inbox target | 唤醒 | 行为 |
|---|---|---|---|
followup |
next-turn |
是 | 排队成独立一轮,等接收方当前那轮结束 |
steer |
next-step |
是 | 插进运行中那轮的最近 step 边界,不等整轮结束;接收方 idle 时起新一轮 |
inject |
next-step |
否 | 只写入上下文,不唤醒 idle 的 agent,可能一直不被读到 |
紧急程度属于单条消息而非整条链路,所以发送方可以按消息覆盖接收方的默认模式;不带
该字段时沿用接收方 Config.delivery 的配置。SendResult.delivery 回报实际生效的
模式,发送方据此判断覆盖是否被采纳。
配置
interconnect 行的 config(全部可选,下表为默认值):
| 字段 | 默认 | 说明 |
|---|---|---|
instanceId |
'dsh' |
本实例自报的 id,出现在 ping/send/list 的回包里。也作为「对端如何寻址到我」的身份 |
requestTimeoutMs |
10000 |
出站请求超时,上限 60000 |
peers |
{} |
对端路由映射:{ [对端 instanceId]: 我拨向它的 origin },例如隧道端点 http://127.0.0.1:13080。激活时对每个对端自动建持久 WS 链;send/reply/ping/list 都按 instanceId 走对应链接 |
delivery |
'followup' |
入站消息未带 delivery 时的默认投递模式 |
allowResume |
true |
是否允许发送方用 resume 唤醒本机的离线 session |
- id: interconnect
config:
instanceId: my-box
peers:
peer-a: http://127.0.0.1:13080 # 我拨向对端 peer-a 的 origin
peer-b: http://127.0.0.1:13081
delivery: followup
allowResume: false # 拒绝一切唤醒请求
鉴权用的共享密钥不在这里,而是取自 credentials 的 DSH_INTERCONNECT_TOKEN(fail-closed:
没有 token 时端点返回 403)。
安装
本包已发布到 npm:dsh-interconnect。
本仓库是一个 DSH profile bundle(根 package.json 声明 dsh.bundle.patch 指向
根 cordis.patch.yml,后者 insert 三个插件行)。
# 从 npm
dsh plugin --profile <name> add dsh-interconnect
# 或从本地路径(已实测)
dsh plugin --profile <name> add file:/path/to/dsh-interconnect
registry 上的 tarball 自带 lib/*.js 与 lib/types/**/*.d.ts,安装时不跑构建。
dsh plugin add 会把仓库识别为 bundle 并追加进 profile 的 dsh.profile.bundles。这次改动
落在组合层:@deepseek-ai/dsh-base 插入的 hmr 行监听 profile 的 package.json,
manifest 一变就重读 dsh.profile.bundles 并重算 patch 层,所以新增的行会在几秒内插进
运行中的树并激活,不需要重启。已实测:安装前 GET /interconnect/link 返回 404,安装后
6 秒内同一请求返回 403(插件 fail-closed 的应答)。
插件代码的更新属于另一层,必须重启该实例:hmr 的模块监听根在 base 里是空的
(root: [])且跳过 node_modules,已经导入的模块不会重载。升级已装插件、或在 file:
安装下重建 lib/,都落在这一类。没有 profileContext 的启动方式(hmr 行被禁用)也只能靠
重启。
两端实例用相同的 DSH_INTERCONNECT_TOKEN 作为共享密钥,任选一种来源:继承的进程环境变量、$DSH_HOME/.env、或 $DSH_HOME/.credentials.yaml 的 refs 段(该 provider 由@deepseek-ai/dsh-base 挂载;优先级为 继承的环境 > credentials 文档 > 调用目录的 .env >$DSH_HOME/.env):
version: 1
refs:
DSH_INTERCONNECT_TOKEN: <两端相同的共享密钥>
开发
依赖 公开的 DeepSeek Harness monorepo
作为 sibling checkout:package.json 的 devDependencies 用 link:../dsh/... 指向它,
peer 依赖由该 checkout 提供,构建与测试都跑在这份源码上。
ln -s /path/to/deepseek-harness ../dsh
pnpm install --config.auto-install-peers=false # peer @deepseek-ai/dsh-* 由 sibling checkout 提供
pnpm run check # typecheck + test + build
pnpm run build # esbuild → lib/
架构说明
- 三个插件都挂在 host composition:
interconnect是跨 session、跨机器的进程级
服务(有 HTTP/WS 端点),必须 host 级;tool-interconnect和skill-interconnect
也放 host,因为interconnect未做 TypeRT@Remote/Gateway 绑定,放进 agent preset
的 isolate realm 会导致工具/技能行无法 inject 到该服务。 - 服务只 inject
agents与credentials;宿主 webserver 是可选的。有 webserver 时入站
升级路由注册在它上面(webServer经等待式 inject fiber 接入,与组合顺序无关);没有
webserver 的 profile(例如 headless)里服务仍会拨号已配置对端并经这些出站链接投递,即
仅出站模式。dsh.plugin.json的inject与之一致,只有agents、credentials。 ws是运行依赖,由宿主的 node_modules 提供(构建时 external)。
验证
- 164/164 单测通过(服务 + 工具 + skill,含 wire 校验、畸形帧、心跳/池清理、结果帧按 kind 绑定与投影、list 的行数与字节双上限及满页提示等回归用例);
类型检查、构建均干净。 - 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
interconnect_send工具反向回发,均验证通过。 - CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑
pnpm run check。 - 已发布版本:从 registry 下载的 tarball 与本地构建 shasum 一致;干净消费端
解析.、./tool-interconnect、./skill-interconnect入口的类型均通过,负例(把string赋给number)如期报TS2322。 - 投递消息以
source: { kind: 'plugin', plugin: 'dsh-interconnect' }落库,而不是{ kind: 'user' }。负例:把该 source 改回kind: 'user',对应断言转红。
注意source不进模型上下文——只有role和content会,而role是user。
所以这个字段的价值在持久化日志与 UI 归因,接收方的模型本身分辨不出消息来自插件。
同理,reply 的记录与寻址依赖sender,它同样不进模型上下文。 - reply 双向多轮延续在同一测试进程内用真实 WS 双向回环验证(A→B→A→B),不是 mockout。
- 每个行为改动都配负例对照(删掉实现使对应断言转红),而不只是「测试通过」。
例如:去掉peers激活时的自动link(),3 条「自动建链投递 / ping/list / reply 回环」
测试转红;去掉requestTimeoutMs的定时会违反「超时即 unreachable」的契约。 - WS + instanceId 传输在真实 WS 链路上验证:接收方只暴露 upgrade 路由、不暴露任何 HTTP,
一条send仍投递成功——排除了「其实走了 HTTP 回退」的误判(0.9 起根本没有 HTTP)。
许可
MIT,Copyright (c) 2026 Chinesezjc。
Install
Install the catalog once, then DeepSeek Harness can find and install any plugin from this site automatically:
dsh plugin add dshbase-catalog Then say "install dsh-interconnect for me" — your agent finds it in the directory and installs it. Docs: dshbase-catalog · verified packs.
Web profile:
dsh plugin --profile web add dsh-interconnect Headless (CLI) profile:
dsh plugin --profile headless add dsh-interconnect Package
npm: dsh-interconnect · version 0.1.0 · tested on dsh 0.1.0-rc.6
Test report
Verified end-to-end: L1 install + L2 load + L3 runtime Q&A on dsh 0.1.0-rc.6.
When to use it
Give the agent a memory, a knowledge base, or a retrieval layer so it stops forgetting context between sessions.
Who it's for
Users running long projects who want the agent to remember decisions, docs, and preferences without re-explaining.
For developers — extending it
The memory/retrieval backend is the seam — plug a new store, tune what gets distilled, or add citation and audit trails.