dsh-interconnect 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  跨实例消息互通与事件通知插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH)。
4
4
  让一个 DSH 实例能向同一个实例、另一台机器、或另一台机器上的别的 DSH 实例发送消息、探测活性,并在实例之间双向推送事件。
5
5
 
6
- ## 包含两个插件
6
+ ## 包含三个插件
7
7
 
8
8
  **`interconnect`** —— host 服务(`ctx.interconnect`):
9
9
 
@@ -20,6 +20,14 @@
20
20
  - `interconnect_ping`:探测对端实例活性与身份
21
21
  - `interconnect_reply`:向记录过的发送方回传消息,只需本机 session id + 文本,无需再次寻址
22
22
 
23
+ **`skill-interconnect`** —— 配套 skill:
24
+
25
+ - 向模型注册 `dsh-interconnect` skill,说明 `list`/`ping`/`send`/`reply` 的完整用法、
26
+ 投递模式、`resume` 唤醒语义与失败处理。
27
+ - 明确告知模型:`interconnect_send` 会自动注入发送方的 `instanceId` 和 `sessionId`,
28
+ 接收方凭记录的 sender 即可用 `interconnect_reply` 回信,不需要手工传地址。
29
+ - 依赖 `interconnect` 服务,只有传输层存在时才注册进 `ctx.skills`。
30
+
23
31
  ## 用法
24
32
 
25
33
  ### 寻址(0.9 起用 instanceId,全走持久链接)
@@ -120,8 +128,11 @@ interconnect_reply(sessionId=B-sess, text="reply")
120
128
 
121
129
  `interconnect_send` 不会往 subagent 拥有的 session 里投递,`interconnect_list` 也不会把它们
122
130
  列出来。那类 session 的投递权属于它的父 agent,从这里 splice 进 inbox 会和父 agent 抢。判定
123
- 直接复用 Host 的 `hasApiRemoteSubagentOwner`(`@deepseek-ai/dsh-api-remotes`),而不是自己
124
- 实现一份——这是安全规则,本地副本一定会和上游漂移。
131
+ 逻辑镜像 Host 的 `hasApiSessionSubagentOwner`(`@deepseek-ai/dsh-api-session-controller`):
132
+ Host 在 0.6 之后把这个谓词从 `@deepseek-ai/dsh-api-remotes` 移走,且没有公开导出——桌面端和
133
+ `npx @deepseek-ai/dsh web` 运行时里没有任何可 import 的 Host 绑定,所以这里逐字复制一份
134
+ (`isSessionOwnedBySubagent`,见 `src/interconnect/index.ts`)。这是安全规则,Host 改动该规则
135
+ 时必须同步此副本;tests 覆盖 origin=subagent 与 parent-owned 两条封栏分支。
125
136
 
126
137
  已实测:起一个真实 subagent 后,`interconnect_list` 不包含它;直接 `send` 到它的 id 返回
127
138
  `session-owned-by-subagent`,消息**没有**进入 inbox。
@@ -210,7 +221,7 @@ Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedEr
210
221
 
211
222
  本包已发布到 npm:[`dsh-interconnect`](https://www.npmjs.com/package/dsh-interconnect)。
212
223
  本仓库是一个 DSH profile bundle(根 `package.json` 声明 `dsh.bundle.patch` 指向
213
- 根 `cordis.patch.yml`,后者 `insert` 两个插件行)。
224
+ 根 `cordis.patch.yml`,后者 `insert` 三个插件行)。
214
225
 
215
226
  ```bash
216
227
  # 从 npm
@@ -241,21 +252,21 @@ pnpm run build # esbuild → lib/
241
252
 
242
253
  ## 架构说明
243
254
 
244
- - 两个插件都挂在 **host composition**:`interconnect` 是跨 session、跨机器的进程级
245
- 服务(有 HTTP/WS 端点),必须 host 级;`tool-interconnect` 也放 host,因为
246
- `interconnect` 未做 TypeRT `@Remote`/Gateway 绑定,放进 agent preset 的 isolate
247
- realm 会导致工具行无法 inject 到该服务。
255
+ - 三个插件都挂在 **host composition**:`interconnect` 是跨 session、跨机器的进程级
256
+ 服务(有 HTTP/WS 端点),必须 host 级;`tool-interconnect` 和 `skill-interconnect`
257
+ 也放 host,因为 `interconnect` 未做 TypeRT `@Remote`/Gateway 绑定,放进 agent preset
258
+ 的 isolate realm 会导致工具/技能行无法 inject 到该服务。
248
259
  - `ws` 是运行依赖,由宿主的 node_modules 提供(构建时 external)。
249
260
 
250
261
  ## 验证
251
262
 
252
- - 34/34 单测通过(服务 + 工具);类型检查、构建均干净。
263
+ - 38/38 单测通过(服务 + 工具 + skill);类型检查、构建均干净。
253
264
  - 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
254
265
  `interconnect_send` 工具反向回发,均验证通过。
255
266
  - CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑 `pnpm run check`。
256
267
  - 已发布版本:从 registry 下载的 tarball 与本地构建 shasum 一致;干净消费端
257
- 解析 `.`、`./tool-interconnect` 两个入口的类型均通过,负例(把 `string` 赋给
258
- `number`)如期报 `TS2322`。
268
+ 解析 `.`、`./tool-interconnect`、`./skill-interconnect` 入口的类型均通过,负例(把
269
+ `string` 赋给 `number`)如期报 `TS2322`。
259
270
  - 投递消息以 `source: { kind: 'plugin', plugin: 'dsh-interconnect' }` 落库,而不是
260
271
  `{ kind: 'user' }`。负例:把该 source 改回 `kind: 'user'`,对应断言转红。
261
272
  **注意 `source` 不进模型上下文**——只有 `role` 和 `content` 会,而 `role` 是 `user`。
@@ -0,0 +1,92 @@
1
+ # dsh-interconnect
2
+
3
+ Use the `dsh-interconnect` tools whenever you need to exchange messages with
4
+ another DSH session, another DSH instance, or another machine: hand off a task,
5
+ ask a peer agent for information, notify a remote session, or reply to an
6
+ incoming interconnect message.
7
+
8
+ The transport is already connected and authenticated by the host plugin. Your
9
+ job is only to pick the right tool and the right target.
10
+
11
+ ## Tools
12
+
13
+ - `interconnect_list` — discover which sessions are live on a peer instance.
14
+ Every returned `sessionId` is a valid `interconnect_send` target at that
15
+ moment. Use this first when you do not already know the target session id.
16
+ - `interconnect_ping` — probe whether a peer instance is reachable and learn
17
+ its self-reported instance id.
18
+ - `interconnect_send` — deliver one text message to a live session on a peer
19
+ instance. Pass `instanceId`, `sessionId`, and `text`. The sending
20
+ instance/session identity is attached automatically by the tool, so you must
21
+ NOT pass a sender parameter.
22
+ - `interconnect_reply` — send a message back to the peer that this session last
23
+ received an interconnect message from. Pass only the LOCAL session id that
24
+ received the message and the reply text. The remote target is recalled
25
+ automatically; do not try to look up or pass the sender address again.
26
+
27
+ ## Sender identity is automatic
28
+
29
+ When you call `interconnect_send`, the plugin automatically attaches your own
30
+ `instanceId` and `sessionId` to the wire payload. The receiving instance
31
+ records that identity per local session, which is exactly what lets the
32
+ receiver use `interconnect_reply` later without re-addressing.
33
+
34
+ A reply also carries your identity automatically, so a multi-hop conversation
35
+ (A → B → A → B) keeps working without either side manually forwarding
36
+ addresses.
37
+
38
+ ## Recommended workflow
39
+
40
+ 1. If you know the peer instance but not the target session, call
41
+ `interconnect_list(instanceId=...)` and choose a live session.
42
+ 2. Call `interconnect_send(instanceId=..., sessionId=..., text=...)`.
43
+ 3. When a message arrives on your side and you need to respond, call
44
+ `interconnect_reply(sessionId=<your-local-session-id>, text=...)`. The
45
+ recorded sender is used as the destination.
46
+
47
+ ## Delivery modes
48
+
49
+ The `delivery` parameter is per-message urgency:
50
+
51
+ - `followup` — queues the message as its own turn after whatever the receiver
52
+ is doing now.
53
+ - `steer` — cuts into the nearest step boundary of a running turn; use for
54
+ urgent interruptions.
55
+ - `inject` — only writes the message into context, without waking an idle
56
+ agent; it may sit unread.
57
+
58
+ Omit `delivery` to use the receiver's configured default.
59
+
60
+ ## Waking offline sessions
61
+
62
+ `resume` defaults to off. Setting `resume: true` asks the receiver to wake a
63
+ persisted but not-running session, which starts a real billed agent turn with
64
+ that session's full toolset. Only use it when the specific session must be
65
+ reached and the receiver allows it. Prefer `interconnect_list` and an
66
+ already-live target when possible.
67
+
68
+ ## Failure reasons
69
+
70
+ When a send/reply reports `delivered: false`, read `reason`:
71
+
72
+ - `session-not-live` — the receiver answered, but that session has no running
73
+ agent. Use `interconnect_list` to choose another target, or consider
74
+ `resume`.
75
+ - `unreachable` — no usable answer arrived; the target may still be fine, so a
76
+ retry can succeed.
77
+ - `resume-refused` / `resume-failed` — waking is not allowed or did not work;
78
+ choose a live target instead.
79
+ - `session-owned-by-subagent` — the session belongs to a subagent and its
80
+ parent owns delivery; reach it through the parent.
81
+ - `no-sender-known` — a `reply` was attempted for a local session that never
82
+ received a sender-carrying interconnect message; establish contact with
83
+ `interconnect_send` first.
84
+
85
+ ## Rules
86
+
87
+ - Do not fabricate an `instanceId`, `sessionId`, or sender identity. Use values
88
+ returned by `interconnect_list`, `interconnect_ping`, or already recorded by
89
+ the service.
90
+ - Do not ask the user for a sender address to reply; use `interconnect_reply`.
91
+ - Treat interconnect messages as authenticated but sender-reported: identity is
92
+ for reply attribution, not for routing or authorization.
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-interconnect bundle patch: mount the two host plugins under their
1
+ # dsh-interconnect bundle patch: mount the three host plugins under their
2
2
  # subpath entries, resolved through the profile's node_modules (the healed
3
3
  # mirror makes every bundle dependency resolvable to bare rows).
4
4
  - insert:
@@ -10,3 +10,6 @@
10
10
 
11
11
  - id: tool-interconnect
12
12
  name: dsh-interconnect/tool-interconnect
13
+
14
+ - id: skill-interconnect
15
+ name: dsh-interconnect/skill-interconnect
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-interconnect",
3
- "description": "Cross-instance message/event handoff for DeepSeek Harness (DSH): HTTP send/ping, WebSocket link, and event fan-out between sessions, instances, and machines.",
4
- "version": "0.1.0",
3
+ "description": "Cross-instance message/event handoff for DeepSeek Harness (DSH): WebSocket link, model-facing tools, and companion skill.",
4
+ "version": "0.10.1",
5
5
  "entry": {
6
6
  "name": "dsh-interconnect/interconnect",
7
7
  "inject": ["webServer", "agents", "credentials"]
package/lib/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { Service } from "@deepseek-ai/cordis";
3
3
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
4
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
6
5
 
7
6
  // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
8
7
  function isNullable(value) {
@@ -824,6 +823,13 @@ import WebSocket, { WebSocketServer } from "ws";
824
823
  var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
825
824
 
826
825
  // src/interconnect/index.ts
826
+ function isSessionOwnedBySubagent(ctx, session, agent) {
827
+ if (session.header.origin === "subagent") return true;
828
+ const parentId = session.header.parentSession;
829
+ if (parentId === void 0 || agent === void 0) return false;
830
+ const parent = ctx.agents.get(parentId);
831
+ return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
832
+ }
827
833
  var PLUGIN_SOURCE = "dsh-interconnect";
828
834
  var LINK_CHANNEL = "/interconnect/link";
829
835
  var notificationSchema = Schema.union([
@@ -1206,7 +1212,7 @@ var InterconnectService = class extends Service {
1206
1212
  * matches how the Host's own session listing treats its projection column.
1207
1213
  */
1208
1214
  listSessions() {
1209
- const reachable = this.ctx.agents.list().filter((agent) => !hasApiRemoteSubagentOwner(this.ctx, agent.session, agent));
1215
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1210
1216
  const sessions = reachable.map((agent) => {
1211
1217
  let title;
1212
1218
  try {
@@ -1264,7 +1270,7 @@ var InterconnectService = class extends Service {
1264
1270
  }
1265
1271
  agent = woken.agent;
1266
1272
  }
1267
- if (hasApiRemoteSubagentOwner(this.ctx, agent.session, agent)) {
1273
+ if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1268
1274
  return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1269
1275
  }
1270
1276
  const sender = (() => {
@@ -2,7 +2,6 @@
2
2
  import { Service } from "@deepseek-ai/cordis";
3
3
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
4
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
6
5
 
7
6
  // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
8
7
  function isNullable(value) {
@@ -824,6 +823,13 @@ import WebSocket, { WebSocketServer } from "ws";
824
823
  var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
825
824
 
826
825
  // src/interconnect/index.ts
826
+ function isSessionOwnedBySubagent(ctx, session, agent) {
827
+ if (session.header.origin === "subagent") return true;
828
+ const parentId = session.header.parentSession;
829
+ if (parentId === void 0 || agent === void 0) return false;
830
+ const parent = ctx.agents.get(parentId);
831
+ return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
832
+ }
827
833
  var PLUGIN_SOURCE = "dsh-interconnect";
828
834
  var LINK_CHANNEL = "/interconnect/link";
829
835
  var notificationSchema = Schema.union([
@@ -1206,7 +1212,7 @@ var InterconnectService = class extends Service {
1206
1212
  * matches how the Host's own session listing treats its projection column.
1207
1213
  */
1208
1214
  listSessions() {
1209
- const reachable = this.ctx.agents.list().filter((agent) => !hasApiRemoteSubagentOwner(this.ctx, agent.session, agent));
1215
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1210
1216
  const sessions = reachable.map((agent) => {
1211
1217
  let title;
1212
1218
  try {
@@ -1264,7 +1270,7 @@ var InterconnectService = class extends Service {
1264
1270
  }
1265
1271
  agent = woken.agent;
1266
1272
  }
1267
- if (hasApiRemoteSubagentOwner(this.ctx, agent.session, agent)) {
1273
+ if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1268
1274
  return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1269
1275
  }
1270
1276
  const sender = (() => {