dsh-interconnect 0.7.0 → 0.9.0

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
@@ -7,8 +7,9 @@
7
7
 
8
8
  **`interconnect`** —— host 服务(`ctx.interconnect`):
9
9
 
10
- - `send` / `list` / `ping` HTTP 端点(`/interconnect/*`):跨实例、跨机器投递消息、枚举 live session、探测活性
11
- - `/interconnect/link` WebSocket 端点:双向实时事件推流,含心跳与指数退避重连
10
+ - 全走持久 WebSocket 链接:跨实例、跨机器投递消息、枚举 live session、探测活性(`send`/`reply`/`ping`/`list` 经 `/interconnect/link` 的 `msg`/`query` 帧)
11
+ - `/interconnect/link` WebSocket 端点:双向实时事件推流,含心跳与指数退避重连;也承载
12
+ `send`/`reply` 消息(`msg`/`msg-result` 帧,WS 优先 + HTTP 回退)
12
13
  - 事件 fan-out(HTTP + WebSocket),入站事件以 `interconnect/event` 发出
13
14
  - 共享密钥鉴权(`DSH_INTERCONNECT_TOKEN`,bearer,fail-closed,timing-safe 比较)
14
15
 
@@ -21,10 +22,18 @@
21
22
 
22
23
  ## 用法
23
24
 
24
- ### 寻址
25
+ ### 寻址(0.9 起用 instanceId,全走持久链接)
25
26
 
26
- `interconnect_send` 需要一个 session id,而调用方通常并不知道。`interconnect_list` 返回对端
27
- **当前 live** 的 session,每一行的 `sessionId` 在调用时刻都是合法的投递目标:
27
+ 从 0.9 起,**传输只走 WebSocket 持久链接,不再有 HTTP 端点**,寻址参数从 `baseUrl` 改为 `instanceId`:
28
+
29
+ - `interconnect_send(instanceId="peer", sessionId=..., text=...)`
30
+ - `interconnect_ping(instanceId="peer")`
31
+ - `interconnect_list(instanceId="peer")`
32
+ - `interconnect_reply(sessionId=...)`(只需本地 session,目标从记录的 sender 解析)
33
+
34
+ `instanceId` 是 `interconnect` 行 `peers` 映射里的键;真正用来拨号的 origin 由该映射的值给出(例如隧道端点 `http://127.0.0.1:13080`),**instanceId 本身从不出现在线上**,也不参与路由——origin 才是唯一的拨号依据。到**未配置 / 未联通**的对端 `send`/`ping`/`list` 返回 `unreachable`(无 HTTP 回退)。
35
+
36
+ `interconnect_list` 返回对端**当前 live** 的 session,每一行的 `sessionId` 在调用时刻都是合法的投递目标:
28
37
 
29
38
  ```
30
39
  session-264d37b0-… 重构 interconnect 插件 [idle]
@@ -40,27 +49,53 @@ session-b07326da-… [running]
40
49
 
41
50
  ### 回复(`reply`)
42
51
 
43
- `send` 的线负载带一个可选的 `sender` 身份(`baseUrl` + `instanceId` + `sessionId`),收到
44
- 消息的 instance 会按「本地 session id → 该 sender」记下这份身份。之后那个 session 可以只凭
45
- **自己的 session id + 文本**把消息回传给发送方,不用再次给出对端 origin 或远程 session id。
52
+ `send` 的线负载带一个 `sender` 身份(**无地址**:`instanceId` + `sessionId`),收到消息的
53
+ instance 会按「本地 session id → 该 sender」记下这份身份。之后那个 session 可以只凭**自己的
54
+ session id + 文本**把消息回传给发送方,不用再次给出对端 instanceId 或远程 session id——回信
55
+ 走的是本机到那个 instance 的持久链接。
46
56
 
47
57
  ```text
48
- # 源实例 A 指定目标 B 的 session,并带上自己的身份
49
- interconnect_send(baseUrl=B, sessionId=B-sess, text="…", sender={baseUrl:A, instanceId:A, sessionId:A-sess})
58
+ # 源实例 A 指定目标 B 的 session,并带上自己的身份(无 baseUrl)
59
+ interconnect_send(instanceId="b", sessionId=B-sess, text="…", sender={instanceId:A, sessionId:A-sess})
50
60
 
61
+ # B 回传:只给本地 session id + 文本,目标从记录的 sender 解析
62
+ interconnect_reply(sessionId=B-sess, text="reply")
63
+ ```
51
64
  # B 回传:只给本地 session id + 文本,目标从记录的 sender 解析
52
65
  interconnect_reply(sessionId=B-sess, text="reply")
53
66
  ```
54
67
 
55
68
  - `sender` 是**自报**的,只用于 reply 归因与寻址,**不是**路由或鉴权依据——连接本身仍由
56
69
  共享密钥在每个 origin 上独立鉴权。
57
- - 回复的消息也带 `sender`(当本机配置了 `selfBaseUrl`),所以对话可双向多轮延续。
70
+ - 回复的消息也带 `sender`(本机恒带,不再需要配置 origin),所以对话可双向多轮延续。
58
71
  - 只有当入站 send **带了 `sender`** 时 reply 才有目标;对端版本没带、或本 session 从未收过
59
72
  互联消息时,`reply` 返回 `delivered: false, reason: "no-sender-known"`。
60
73
  - `sender` **不进模型上下文**(和 `source` 一样只落到持久化日志与 UI 归因)——这条消息的
61
74
  内容字面就是 wire 上传来的 `text`,模型看到的仍是普通 user 文本,只是不带任何结构化的
62
75
  发送方标记。
63
76
 
77
+ ### 消息信道:全走 WS(`msg` / `query` 帧)
78
+
79
+ 从 0.9 起**不再有任何 HTTP 端点**——`send`/`reply`/`ping`/`list` 全部在持久 WebSocket 链路
80
+ (`/interconnect/link`)上完成。`peers` 映射在激活时**自动 `link()` 每个对端**(心跳 + 指数退避
81
+ 重连沿用既有实现),寻址按 `instanceId` 查对应链接。
82
+
83
+ | 帧 | 方向 | 作用 |
84
+ |---|---|---|
85
+ | `hello` | 双方 | 拨号方自报 instance id |
86
+ | `event` | 双方 | 生命周期事件推流 |
87
+ | `msg` | 请求方 → 接收方 | 携带 `kind`(`send`/`reply`)、`sessionId`、`text`、`sender`/`delivery`/`resume`、`reqId` |
88
+ | `msg-result` | 接收方 → 请求方 | 与 `reqId` 对应的 `SendResult` |
89
+ | `query` | 请求方 → 接收方 | `ping` / `list` / `event` 发现与事件查询 |
90
+ | `query-result` | 接收方 → 请求方 | 与 `reqId` 对应的查询结果 |
91
+
92
+ - **出站**:`interconnect_send`/`interconnect_reply`/`ping`/`list` 都发对应帧并等待匹配 `reqId`
93
+ 的结果(受 `requestTimeoutMs` 约束)。到**未配置或未联通**的对端直接返回 `unreachable`——
94
+ **没有 HTTP 回退**,这是 0.9 的破坏性变化。
95
+ - **入站**:`msg` 帧走 `deliver`/`reply` 逻辑(sender 记录、subagent 封栏、`no-sender-known`
96
+ 等),结果经同一 socket 回 `msg-result`;`query` 帧回 `query-result`。
97
+ - 心跳与指数退避重连沿用既有实现。
98
+
64
99
  ### 投递失败的原因
65
100
 
66
101
  `delivered: false` 单独一个布尔值无法据以行动,因为不同失败需要不同应对,所以
@@ -73,7 +108,7 @@ interconnect_reply(sessionId=B-sess, text="reply")
73
108
  | `resume-refused` | 请求了唤醒,但对端不允许(`allowResume: false`) | 再带 `resume` 也没用 |
74
109
  | `resume-failed` | 允许唤醒且尝试了,但没得到 live agent(无此持久化 session,或被别的 owner 持有) | 换目标 |
75
110
  | `session-owned-by-subagent` | 该 session 属于 subagent 路由,投递权在它的父 agent | 通过父 agent 触达,别直接投 |
76
- | `no-sender-known` | `reply` 指向的本地 session 从未记录过发送方(它没收到过带 `sender` 的消息,或对端版本过旧没带 `sender`) | 先用 `interconnect_send` 主动建立联系,或确认对端配置了 `selfBaseUrl` |
111
+ | `no-sender-known` | `reply` 指向的本地 session 从未记录过发送方(它没收到过带 `sender` 的消息,或对端版本过旧没带 `sender`) | 先用 `interconnect_send` 主动建立联系 |
77
112
 
78
113
  `reason` 恰好在 `delivered` 为 false 时出现。
79
114
 
@@ -119,10 +154,10 @@ context** 拥有,实测确认插件 fiber 被 dispose 时会把 resume 出来
119
154
 
120
155
  ```text
121
156
  # 唤醒并让对方实际处理(会起一个计费回合)
122
- interconnect_send(baseUrl, sessionId, text, resume=true, delivery="followup")
157
+ interconnect_send(instanceId="peer", sessionId, text, resume=true, delivery="followup")
123
158
 
124
159
  # 唤醒但不起回合:只写入上下文,等对方下次被唤醒时一起读
125
- interconnect_send(baseUrl, sessionId, text, resume=true, delivery="inject")
160
+ interconnect_send(instanceId="peer", sessionId, text, resume=true, delivery="inject")
126
161
  ```
127
162
 
128
163
  已实测:`resume=true` + `inject` 之后目标从非 live 变 live(`interconnect_list` 计数 +1),
@@ -151,10 +186,9 @@ Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedEr
151
186
 
152
187
  | 字段 | 默认 | 说明 |
153
188
  |---|---|---|
154
- | `instanceId` | `'dsh'` | 本实例自报的 id,出现在 `ping`/`send`/`list` 的回包里。仅诊断用,**不作为路由依据** |
155
- | `selfBaseUrl` | 无 | 本实例对外可达的 origin(如 `http://my-box:3080`)。设置后,出站 `send`/`reply` 会带上本实例的 `sender` 身份,使对端能回信;不设置则不归因、对端 `reply` 报 `no-sender-known` |
189
+ | `instanceId` | `'dsh'` | 本实例自报的 id,出现在 `ping`/`send`/`list` 的回包里。也作为「对端如何寻址到我」的身份 |
156
190
  | `requestTimeoutMs` | `10000` | 出站请求超时,上限 60000 |
157
- | `peers` | `[]` | 启动时的事件 fan-out 目标;运行时可用 `subscribe()` 增补 |
191
+ | `peers` | `{}` | **对端路由映射**:`{ [对端 instanceId]: 我拨向它的 origin }`,例如隧道端点 `http://127.0.0.1:13080`。激活时对每个对端自动建持久 WS 链;`send`/`reply`/`ping`/`list` 都按 instanceId 走对应链接 |
158
192
  | `delivery` | `'followup'` | 入站消息未带 `delivery` 时的默认投递模式 |
159
193
  | `allowResume` | `true` | 是否允许发送方用 `resume` 唤醒本机的离线 session |
160
194
 
@@ -162,8 +196,9 @@ Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedEr
162
196
  - id: interconnect
163
197
  config:
164
198
  instanceId: my-box
165
- selfBaseUrl: http://my-box:3080 # 让对端能 reply 回本机
166
- peers: ['http://peer-host:3080']
199
+ peers:
200
+ peer-a: http://127.0.0.1:13080 # 我拨向对端 peer-a 的 origin
201
+ peer-b: http://127.0.0.1:13081
167
202
  delivery: followup
168
203
  allowResume: false # 拒绝一切唤醒请求
169
204
  ```
@@ -214,7 +249,7 @@ pnpm run build # esbuild → lib/
214
249
 
215
250
  ## 验证
216
251
 
217
- - 67/67 单测通过(服务 + 工具);类型检查、构建均干净。
252
+ - 34/34 单测通过(服务 + 工具);类型检查、构建均干净。
218
253
  - 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
219
254
  `interconnect_send` 工具反向回发,均验证通过。
220
255
  - CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑 `pnpm run check`。
@@ -226,11 +261,12 @@ pnpm run build # esbuild → lib/
226
261
  **注意 `source` 不进模型上下文**——只有 `role` 和 `content` 会,而 `role` 是 `user`。
227
262
  所以这个字段的价值在持久化日志与 UI 归因,接收方的**模型本身分辨不出**消息来自插件。
228
263
  同理,reply 的记录与寻址依赖 `sender`,它同样不进模型上下文。
229
- - reply 双向多轮延续在**同一测试进程内用真实 HTTP 双向回环**验证(A→B→A→B),不是 mockout。
264
+ - reply 双向多轮延续在**同一测试进程内用真实 WS 双向回环**验证(A→B→A→B),不是 mockout。
230
265
  - 每个行为改动都配负例对照(删掉实现使对应断言转红),而不只是「测试通过」。
231
- 例如:去掉请求处理器的 `await` 会让 25 个测试转红;去掉 `resume` 的发送方 opt-in
232
- 门或接收方否决门,各让 1 个转红;去掉入站 send 的 sender 记录(`senders.set`),
233
- 两条 reply 回环测试转红。
266
+ 例如:去掉 `peers` 激活时的自动 `link()`,3 条「自动建链投递 / ping/list / reply 回环」
267
+ 测试转红;去掉 `requestTimeoutMs` 的定时会违反「超时即 unreachable」的契约。
268
+ - WS + instanceId 传输在**真实 WS 链路**上验证:接收方只暴露 upgrade 路由、不暴露任何 HTTP,
269
+ 一条 `send` 仍投递成功——排除了「其实走了 HTTP 回退」的误判(0.9 起根本没有 HTTP)。
234
270
 
235
271
  ## 许可
236
272