Enable WebSocket communication between multiple DSH instances using a shared secret: exchange text messages, push session lifecycle events, and enumerate peer's online sessions.
- Language
- TypeScript
- License
- MIT
- Branch
- main
Install
$ dsh plugin --profile web add dsh-interconnectRun the command above in your terminal to install this plugin via the dsh CLI. You can switch Profile in the top-right corner. New to dsh? Read the beginner tutorial
Install via your agent
Install the DeepSeek Harness plugin Chinesezjc/dsh-interconnect for me: review the repository at https://github.com/Chinesezjc/dsh-interconnect first, then run the install command and verify the plugin loads successfully.
Paste this instruction to the DSH Web GUI assistant — it will install and verify for you.
One-Sentence Positioning
dsh-interconnect is a cross-instance communication plugin on DSH. It enables a DSH instance on one machine to send text messages to another DSH instance on the same machine, another machine, or a different DSH instance on another machine, mutually push session lifecycle events, and enumerate the peer's currently running sessions—all transmissions go through a shared-key authenticated WebSocket persistent link.
Core Capabilities
- Deliver text messages between multiple DSH instances through persistent WebSocket links, addressing by instanceId lookup for the corresponding link
- Real-time push of local session lifecycle events (create/destroy/state change/subagent end) to all configured peers
- Provides four model-visible tools: interconnect_send, interconnect_ping, interconnect_list, interconnect_reply
- Support waking up peer's persisted but not running sessions by message (resume is off by default, receiver can veto with one vote)
- Use sender identity to allow bidirectional multi-round chained reply without re-addressing the peer
- Use shared token + timing-safe verification Bearer authentication, fails closed when token not configured
Technical Implementation
- Language: TypeScript (ES2022 / ESM)
- Key Dependencies:
@deepseek-ai/cordis(Cordis service framework) /@deepseek-ai/dsh-llm(user message construction) /@deepseek-ai/dsh-host-webserver(WebSocket upgrade routing) /ws(WebSocket implementation) /@deepseek-ai/schemastery(Config validation) - Architecture Pattern: Inject two host plugins into host composition via
cordis.patch.yml;interconnectis aServiceclass, injecting three dependencies:webServer / agents / credentials;tool-interconnectregisters four tools toctx.toolssurface viadefineTool - Entry Files:
src/index.ts(root re-export) /src/interconnect/index.ts(host service) /src/tool-interconnect/index.ts(model tools)
Applicable Scenarios
When you need multiple DSH instances to work together across machines (e.g., let an agent on one machine actively assign tasks to a session on another machine, aggregate agent outputs from different instances, or do event synchronization between multiple instances)—install dsh-interconnect, both ends communicate via shared-key WebSocket channel, and models can also actively address and deliver messages using tools; regular single-machine users usually don't need this plugin.
Prerequisites and Compatibility
| Dependency | Minimum Version | Description |
|---|---|---|
| DeepSeek Harness (DSH) | Not declared | Peer dependencies @deepseek-ai/dsh-{agent,api-remotes,credentials,llm,session,subagent,host-apiproxy,host-webserver,tools,invariants} and @deepseek-ai/cordis; this repo uses sibling checkout to link to dsh monorepo source; no explicit engines version constraint |
| Node.js | >=22.0.0 | esbuild compiles to node22, devDep @types/node ^24.0.0 |
| Credential DSH_INTERCONNECT_TOKEN | Required | Shared key; both ends' credentials must set the same value; missing causes server to fail-closed with 403 |
ws package | * (peer) | Provided by host DSH profile's node_modules; pure JS, no native dependencies |
Installation
dsh plugin --profile web add github:Chinesezjc/dsh-interconnect
Configuration
| Config | Type | Description | Default |
|---|---|---|---|
instanceId | string | Self-reported ID of this instance, appears in ping/send/list responses, also how peers address this machine | 'dsh' |
requestTimeoutMs | integer (≤60000) | Timeout threshold for outbound request waiting for result; returns unreachable on timeout | 10000 |
peers | { [instanceId]: origin } | Peer routing mapping; value is the origin to dial the peer from this machine (e.g., http://127.0.0.1:13080); automatically establishes persistent WebSocket link to each peer when activated | {} |
delivery | followup / steer / inject | Default delivery mode when inbound message doesn't carry delivery | 'followup' |
allowResume | boolean | Whether to allow sender to wake up this machine's offline session with resume; receiver can veto with one vote using this | true |
The shared key for authentication is not in this config but comes from DSH credentials'
DSH_INTERCONNECT_TOKEN; at runtime, peers can also be dynamically added/removed viactx.interconnect.subscribe(instanceId, origin)/ctx.interconnect.unsubscribe(instanceId).
FAQ
Q: What can regular users do with this plugin?
A: Mainly for users who want multiple DSH instances to collaborate across machines. For example, let an agent on one machine actively assign tasks to a session on another machine, aggregate running results from different instances, or synchronize session lifecycle events between two instances. Both ends need to install this plugin and configure the same shared key; single-machine users don't need it.
Q: What else needs to be done after installation to enable communication?
A: Configure the same DSH_INTERCONNECT_TOKEN in both ends' DSH credential store (missing causes server to fail-closed with 403), then write the peer's instanceId and reachable origin into local config.peers. When activated, the plugin automatically establishes persistent WebSocket links to each peer—no manual link call needed.
Q: Why do messages sometimes fail to send? What is the reason field for?
A: Because different failures require different responses, the plugin uses reason to distinguish: session-not-live means the peer's session is not running (change target or carry resume); unreachable means transport or authentication failure (can retry); resume-refused / resume-failed related to wake-up; session-owned-by-subagent means that session belongs to subagent routing (go through parent agent); no-sender-known means reply can't find sender record.
Q: Is resume option safe? Why is it off by default?
A: Wake-up triggers a complete agent turn on the peer (wakeDriver → kick → turn → llm.stream), which is a billed model call, and carries the session's full tool set. So it's off by default: sender must explicitly carry resume, receiver can also veto with allowResume: false to avoid remotely triggering billed calls in unattended sessions.
Q: What's the difference between the three delivery modes?
A: followup queues the message as an independent turn, waiting for the receiver's current turn to finish; steer cuts in at the nearest step boundary within a running turn, for urgent messages that can't wait for the whole turn; inject only writes the message to context but doesn't wake the idle agent, which may never read it. Sender can override receiver's default config per message.
Q: Why can't some sessions be seen in interconnect_list?
A: The list only contains live sessions with currently running agents (this is the set send can reach); sessions owned by subagents are also excluded—the delivery right for such sessions belongs to its parent agent, and direct delivery here would compete with the parent agent. The criterion directly reuses Host's hasApiRemoteSubagentOwner, this plugin doesn't reimplement it.
Q: Can old calling methods still work after upgrading to 0.9?
A: No. 0.9 is a breaking major version: removes all HTTP endpoints, send/reply/ping/list all go through /interconnect/link persistent link; addressing changes from baseUrl to instanceId; peers changes to { instanceId: origin } mapping; sender removes baseUrl, becomes addressless identity; returns unreachable directly to unconfigured or unreachable peers, no HTTP fallback.
Learning Curve
Advanced — single instance installation is one command, but to truly enable cross-instance communication, you also need to configure shared key + write peer peers mapping on both ends, and understand the WebSocket persistent link, resume wake-up side effects, three delivery modes, and how peer version and deployment form (subagent owner, headless without api-proxy) affects behavior.
Known Issues and Limitations
- 0.9.0 is a breaking major version: HTTP endpoints all removed, addressing changed to
instanceId,peerschanged to mapping, old calling methods all invalid (CHANGELOGOG.md:11-21) - When peer runs pre-0.9 version without
sender, callinginterconnect_replyon that session from this end returnsno-sender-known(src/interconnect/index.ts:284-288) - Sessions with too old disk format cannot be woken (returns
resume-failed), this is a limitation from Host upstreamresume()path, this plugin doesn't bypass (README.md:166-168) - Deployments without Host
agentlookup (headless / no api-proxy profile) degradesession-not-livewhen callingresume, no error (src/interconnect/index.ts:556-563) - Sessions owned by subagent cannot be directly
sended, returnssession-owned-by-subagent; criterion directly reuses Host'shasApiRemoteSubagentOwner, doesn't reimplement (src/interconnect/index.ts:599-601) - Shared key must be identical on both ends; when not configured,
interconnectserver fails closed with 403, no automatic negotiation or fallback (src/interconnect/index.ts:692-695) - Exceptions thrown by inbound event
ctx.onlisteners are swallowed and logged as warn, to avoid one listener exception crashing the socket's message callback; local listener exceptions may not directly manifest (src/interconnect/index.ts:752-758) - Outbound
send/replywaiting for result is bound byrequestTimeoutMs(default 10s, max 60s), timeout directly returnsunreachable, no guarantee peer actually received (src/interconnect/index.ts:365-371)
跨实例消息互通与事件通知插件,用于 DeepSeek Harness (DSH)。 让一个 DSH 实例能向同一个实例、另一台机器、或另一台机器上的别的 DSH 实例发送消息、探测活性,并在实例之间双向推送事件。
包含两个插件
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 id + 文本,无需再次寻址
用法
寻址(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(sessionId=...)(只需本地 session,目标从记录的 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
不会出现在列表里,也收不到消息。
回复(reply)
send 的线负载带一个 sender 身份(无地址:instanceId + sessionId),收到消息的
instance 会按「本地 session id → 该 sender」记下这份身份。之后那个 session 可以只凭自己的
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 id + 文本,目标从记录的 sender 解析
interconnect_reply(sessionId=B-sess, text="reply")
B 回传:只给本地 session id + 文本,目标从记录的 sender 解析
interconnect_reply(sessionId=B-sess, 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` 帧走 `deliver`/`reply` 逻辑(sender 记录、subagent 封栏、`no-sender-known`
等),结果经同一 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 的 `hasApiRemoteSubagentOwner`(`@deepseek-ai/dsh-api-remotes`),而不是自己
实现一份——这是安全规则,本地副本一定会和上游漂移。
已实测:起一个真实 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`:
```text
# 唤醒并让对方实际处理(会起一个计费回合)
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。重启
web 服务使 host 侧生效。两端实例的 .credentials.yaml(或等价凭据源)设置相同的
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也放 host,因为interconnect未做 TypeRT@Remote/Gateway 绑定,放进 agent preset 的 isolate realm 会导致工具行无法 inject 到该服务。 ws是运行依赖,由宿主的 node_modules 提供(构建时 external)。
验证
- 34/34 单测通过(服务 + 工具);类型检查、构建均干净。
- 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
interconnect_send工具反向回发,均验证通过。 - CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑
pnpm run check。 - 已发布版本:从 registry 下载的 tarball 与本地构建 shasum 一致;干净消费端
解析
.、./tool-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。
Read the usage guide →
Install steps, key points, FAQ and compatibility for this plugin — auto-derived from indexed fields.
Listing badge
[](https://deepseek-plugin.org/plugins/Chinesezjc/dsh-interconnect)Paste this markdown into your GitHub README to link back to this listing. The badge only states the listing — not a security endorsement.