dsh-interconnect 0.5.0 → 0.6.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 +80 -13
- package/lib/index.js +54 -10
- package/lib/interconnect/index.js +54 -10
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +20 -2
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +19 -1
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +30 -2
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
**`interconnect`** —— host 服务(`ctx.interconnect`):
|
|
9
9
|
|
|
10
|
-
- `send` / `ping` HTTP 端点(`/interconnect
|
|
10
|
+
- `send` / `list` / `ping` HTTP 端点(`/interconnect/*`):跨实例、跨机器投递消息、枚举 live session、探测活性
|
|
11
11
|
- `/interconnect/link` WebSocket 端点:双向实时事件推流,含心跳与指数退避重连
|
|
12
12
|
- 事件 fan-out(HTTP + WebSocket),入站事件以 `interconnect/event` 发出
|
|
13
13
|
- 共享密钥鉴权(`DSH_INTERCONNECT_TOKEN`,bearer,fail-closed,timing-safe 比较)
|
|
14
14
|
|
|
15
15
|
**`tool-interconnect`** —— 模型可见工具:
|
|
16
16
|
|
|
17
|
-
- `interconnect_send`:向对端实例的指定 session
|
|
17
|
+
- `interconnect_send`:向对端实例的指定 session 投递消息;可选 `delivery` 选投递模式、`resume` 唤醒离线 session
|
|
18
18
|
- `interconnect_list`:列出对端实例的 live session(id + 标题 + 状态),用于在不预先知道 session id 时寻址
|
|
19
19
|
- `interconnect_ping`:探测对端实例活性与身份
|
|
20
20
|
|
|
21
|
+
## 用法
|
|
22
|
+
|
|
21
23
|
### 寻址
|
|
22
24
|
|
|
23
25
|
`interconnect_send` 需要一个 session id,而调用方通常并不知道。`interconnect_list` 返回对端
|
|
@@ -35,22 +37,59 @@ session-b07326da-… [running]
|
|
|
35
37
|
只列 live session 是有意的:`send` 能到达的正好是这些。对端存在但没有运行 agent 的 session
|
|
36
38
|
不会出现在列表里,也收不到消息。
|
|
37
39
|
|
|
38
|
-
###
|
|
40
|
+
### 投递失败的原因
|
|
39
41
|
|
|
40
|
-
`delivered: false`
|
|
42
|
+
`delivered: false` 单独一个布尔值无法据以行动,因为不同失败需要不同应对,所以
|
|
41
43
|
`SendResult.reason` 会指明是哪一种:
|
|
42
44
|
|
|
43
45
|
| `reason` | 含义 | 应对 |
|
|
44
46
|
|---|---|---|
|
|
45
|
-
| `session-not-live` | 对端**答复了**,但那个 session 没有运行中的 agent | 重试同一个 id 无用;用 `interconnect_list`
|
|
47
|
+
| `session-not-live` | 对端**答复了**,但那个 session 没有运行中的 agent | 重试同一个 id 无用;用 `interconnect_list` 换目标,或带 `resume` |
|
|
46
48
|
| `unreachable` | 没拿到可用答复(传输失败,或鉴权被拒) | 目标 session 可能完好,重试可能成功 |
|
|
49
|
+
| `resume-refused` | 请求了唤醒,但对端不允许(`allowResume: false`) | 再带 `resume` 也没用 |
|
|
50
|
+
| `resume-failed` | 允许唤醒且尝试了,但没得到 live agent(无此持久化 session,或被别的 owner 持有) | 换目标 |
|
|
47
51
|
|
|
48
52
|
`reason` 恰好在 `delivered` 为 false 时出现。
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
### 唤醒离线 session(`resume`,默认关)
|
|
55
|
+
|
|
56
|
+
`SendPayload.resume: true` 让对端唤醒一个已持久化但没有运行 agent 的 session。
|
|
57
|
+
|
|
58
|
+
**默认关闭是有意的。** 实测确认:消息投递到 session 后会触发一次**完整的 agent 回合**——
|
|
59
|
+
`wakeDriver()` → `kick()` → `turn()` → `llm.stream()`,即一次计费的模型调用,且 assembly
|
|
60
|
+
里带着该 session 的完整工具集。在一个用户没打开、看不到、也无法中断的会话里启动这些,和
|
|
61
|
+
「推一下已经开着的会话」不是一个量级,所以必须由发送方显式请求。
|
|
62
|
+
|
|
63
|
+
两侧都有控制权:
|
|
64
|
+
|
|
65
|
+
- **发送方**按消息决定 `resume`(默认不唤醒)
|
|
66
|
+
- **接收方**用 `Config.allowResume`(默认 `true`)一票否决——因为花钱和跑工具的是它那台机器;
|
|
67
|
+
拒绝时在跑 lookup 之前就短路,回 `resume-refused`
|
|
68
|
+
|
|
69
|
+
唤醒**不是**调本插件的 `ctx.agents.resume()`,而是走 Host 已配置的 `agent` lookup
|
|
70
|
+
(`typert.lookups.get('agent')`)。这一点是关键:`resume()` 返回的 handle 由**调用方
|
|
71
|
+
context** 拥有,实测确认插件 fiber 被 dispose 时会把 resume 出来的 agent 和 session 一起
|
|
72
|
+
拆掉(同一调用改用根 ctx 则两者都存活)。交给 Host 的 resolver 之后 owner 是 api-proxy,
|
|
73
|
+
而且它会按 session 日志里记录的 preset 重建工具集——不是空壳。
|
|
74
|
+
|
|
75
|
+
没有 Host lookup 的部署(headless、无 api-proxy 的 profile)会降级为 `session-not-live`,
|
|
76
|
+
不会报错。
|
|
77
|
+
|
|
78
|
+
唤醒**只把消息放进 inbox**,是否真的开始处理取决于 `delivery`:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
# 唤醒并让对方实际处理(会起一个计费回合)
|
|
82
|
+
interconnect_send(baseUrl, sessionId, text, resume=true, delivery="followup")
|
|
83
|
+
|
|
84
|
+
# 唤醒但不起回合:只写入上下文,等对方下次被唤醒时一起读
|
|
85
|
+
interconnect_send(baseUrl, sessionId, text, resume=true, delivery="inject")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
已实测:`resume=true` + `inject` 之后目标从非 live 变 live(`interconnect_list` 计数 +1),
|
|
89
|
+
且该 session 日志里只多一条 `agent/inbox/spliced`、**后面没有 `turn/start`**。
|
|
90
|
+
|
|
91
|
+
**已知限制**:磁盘格式过旧的 session 无法唤醒,返回 `resume-failed`。这不是本插件的限制——
|
|
92
|
+
Host 自己的 resume 路径对同一个 session 报 `SessionFormatUnsupportedError`,同样失败。
|
|
54
93
|
|
|
55
94
|
### 投递模式
|
|
56
95
|
|
|
@@ -66,6 +105,30 @@ session 一起拆掉(用根 ctx 调用则不会——ownership 跟随调用时
|
|
|
66
105
|
该字段时沿用接收方 `Config.delivery` 的配置。`SendResult.delivery` 回报实际生效的
|
|
67
106
|
模式,发送方据此判断覆盖是否被采纳。
|
|
68
107
|
|
|
108
|
+
## 配置
|
|
109
|
+
|
|
110
|
+
`interconnect` 行的 `config`(全部可选,下表为默认值):
|
|
111
|
+
|
|
112
|
+
| 字段 | 默认 | 说明 |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `instanceId` | `'dsh'` | 本实例自报的 id,出现在 `ping`/`send`/`list` 的回包里。仅诊断用,**不作为路由依据** |
|
|
115
|
+
| `requestTimeoutMs` | `10000` | 出站请求超时,上限 60000 |
|
|
116
|
+
| `peers` | `[]` | 启动时的事件 fan-out 目标;运行时可用 `subscribe()` 增补 |
|
|
117
|
+
| `delivery` | `'followup'` | 入站消息未带 `delivery` 时的默认投递模式 |
|
|
118
|
+
| `allowResume` | `true` | 是否允许发送方用 `resume` 唤醒本机的离线 session |
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
- id: interconnect
|
|
122
|
+
config:
|
|
123
|
+
instanceId: my-box
|
|
124
|
+
peers: ['http://peer-host:3080']
|
|
125
|
+
delivery: followup
|
|
126
|
+
allowResume: false # 拒绝一切唤醒请求
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
鉴权用的共享密钥不在这里,而是取自 credentials 的 `DSH_INTERCONNECT_TOKEN`(fail-closed:
|
|
130
|
+
没有 token 时端点返回 403)。
|
|
131
|
+
|
|
69
132
|
## 安装
|
|
70
133
|
|
|
71
134
|
本包已发布到 npm:[`dsh-interconnect`](https://www.npmjs.com/package/dsh-interconnect)。
|
|
@@ -109,16 +172,20 @@ pnpm run build # esbuild → lib/
|
|
|
109
172
|
|
|
110
173
|
## 验证
|
|
111
174
|
|
|
112
|
-
-
|
|
175
|
+
- 50/50 单测通过(服务 + 工具);类型检查、构建均干净。
|
|
113
176
|
- 已在两台机器之间实测双向互通:消息投递、WebSocket 事件推流、以及 agent 经
|
|
114
177
|
`interconnect_send` 工具反向回发,均验证通过。
|
|
115
178
|
- CI(GitHub Actions):clone 公开 DSH 仓库作为 sibling,跑 `pnpm run check`。
|
|
116
179
|
- 已发布版本:从 registry 下载的 tarball 与本地构建 shasum 一致;干净消费端
|
|
117
180
|
解析 `.`、`./tool-interconnect` 两个入口的类型均通过,负例(把 `string` 赋给
|
|
118
181
|
`number`)如期报 `TS2322`。
|
|
119
|
-
-
|
|
120
|
-
`{ kind: 'user' }
|
|
121
|
-
|
|
182
|
+
- 投递消息以 `source: { kind: 'plugin', plugin: 'dsh-interconnect' }` 落库,而不是
|
|
183
|
+
`{ kind: 'user' }`。负例:把该 source 改回 `kind: 'user'`,对应断言转红。
|
|
184
|
+
**注意 `source` 不进模型上下文**——只有 `role` 和 `content` 会,而 `role` 是 `user`。
|
|
185
|
+
所以这个字段的价值在持久化日志与 UI 归因,接收方的**模型本身分辨不出**消息来自插件。
|
|
186
|
+
- 每个行为改动都配负例对照(删掉实现使对应断言转红),而不只是「测试通过」。
|
|
187
|
+
例如:去掉请求处理器的 `await` 会让 25 个测试转红;去掉 `resume` 的发送方 opt-in
|
|
188
|
+
门或接收方否决门,各让 1 个转红。
|
|
122
189
|
|
|
123
190
|
## 许可
|
|
124
191
|
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@deepseek-ai/dsh-host-apiproxy/api";
|
|
8
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
9
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
|
+
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
10
11
|
|
|
11
12
|
// ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
|
|
12
13
|
function isNullable(value) {
|
|
@@ -848,7 +849,8 @@ var linkFrameSchema = Schema.union([
|
|
|
848
849
|
var sendPayloadSchema = Schema.object({
|
|
849
850
|
sessionId: Schema.string(),
|
|
850
851
|
text: Schema.string(),
|
|
851
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")])
|
|
852
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
+
resume: Schema.boolean()
|
|
852
854
|
});
|
|
853
855
|
var eventPayloadSchema = Schema.object({
|
|
854
856
|
sender: Schema.string(),
|
|
@@ -860,11 +862,13 @@ var InterconnectService = class extends Service {
|
|
|
860
862
|
instanceId: Schema.string().default("dsh"),
|
|
861
863
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
862
864
|
peers: Schema.array(Schema.string()).default([]),
|
|
863
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup")
|
|
865
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
866
|
+
allowResume: Schema.boolean().default(true)
|
|
864
867
|
});
|
|
865
868
|
instanceId;
|
|
866
869
|
requestTimeoutMs;
|
|
867
870
|
delivery;
|
|
871
|
+
allowResume;
|
|
868
872
|
peers = /* @__PURE__ */ new Set();
|
|
869
873
|
subscriptions = [];
|
|
870
874
|
server = new WebSocketServer({ noServer: true });
|
|
@@ -878,6 +882,7 @@ var InterconnectService = class extends Service {
|
|
|
878
882
|
this.instanceId = config.instanceId;
|
|
879
883
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
880
884
|
this.delivery = config.delivery ?? "followup";
|
|
885
|
+
this.allowResume = config.allowResume ?? true;
|
|
881
886
|
for (const peer of config.peers ?? []) this.peers.add(trimBase(peer));
|
|
882
887
|
const route = {
|
|
883
888
|
kind: "prefix",
|
|
@@ -949,7 +954,8 @@ var InterconnectService = class extends Service {
|
|
|
949
954
|
const payload = {
|
|
950
955
|
sessionId: request.sessionId,
|
|
951
956
|
text: request.text,
|
|
952
|
-
...request.delivery === void 0 ? {} : { delivery: request.delivery }
|
|
957
|
+
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
958
|
+
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
953
959
|
};
|
|
954
960
|
const result = await this.post(request.baseUrl, "send", payload);
|
|
955
961
|
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
@@ -1088,7 +1094,7 @@ var InterconnectService = class extends Service {
|
|
|
1088
1094
|
return;
|
|
1089
1095
|
}
|
|
1090
1096
|
try {
|
|
1091
|
-
const value = this.runEndpoint(endpoint, parsed.data.payload);
|
|
1097
|
+
const value = await this.runEndpoint(endpoint, parsed.data.payload);
|
|
1092
1098
|
writeFull(res, rpcId, { ok: true, value });
|
|
1093
1099
|
} catch (error) {
|
|
1094
1100
|
if (error instanceof UnknownEndpointError) {
|
|
@@ -1113,7 +1119,7 @@ var InterconnectService = class extends Service {
|
|
|
1113
1119
|
}
|
|
1114
1120
|
}
|
|
1115
1121
|
/** Dispatch one authenticated business call. */
|
|
1116
|
-
runEndpoint(endpoint, payload) {
|
|
1122
|
+
async runEndpoint(endpoint, payload) {
|
|
1117
1123
|
if (endpoint === "ping") {
|
|
1118
1124
|
return { pong: true, instance: this.instanceId };
|
|
1119
1125
|
}
|
|
@@ -1157,7 +1163,8 @@ var InterconnectService = class extends Service {
|
|
|
1157
1163
|
* matches how the Host's own session listing treats its projection column.
|
|
1158
1164
|
*/
|
|
1159
1165
|
listSessions() {
|
|
1160
|
-
const
|
|
1166
|
+
const reachable = this.ctx.agents.list().filter((agent) => !hasApiRemoteSubagentOwner(this.ctx, agent.session, agent));
|
|
1167
|
+
const sessions = reachable.map((agent) => {
|
|
1161
1168
|
let title;
|
|
1162
1169
|
try {
|
|
1163
1170
|
const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
|
|
@@ -1174,11 +1181,48 @@ var InterconnectService = class extends Service {
|
|
|
1174
1181
|
});
|
|
1175
1182
|
return { sessions, instance: this.instanceId };
|
|
1176
1183
|
}
|
|
1177
|
-
/**
|
|
1178
|
-
|
|
1179
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* Resolve a session that is not currently live, when the sender asked to wake
|
|
1186
|
+
* it. Returns the agent, or the reason it stays undelivered.
|
|
1187
|
+
*
|
|
1188
|
+
* The resume is delegated to the Host's configured `agent` lookup rather than
|
|
1189
|
+
* calling `ctx.agents.resume()` here, and that is the whole point: a handle
|
|
1190
|
+
* from `resume()` is owned by the CALLING context, so resuming on this
|
|
1191
|
+
* plugin's fiber would tear the session down again the moment the plugin
|
|
1192
|
+
* unloads (measured: the same call through the root context leaves it alive).
|
|
1193
|
+
* The Host's resolver owns it instead, and it also composes the preset the
|
|
1194
|
+
* session recorded — so a woken agent comes back with the toolset its history
|
|
1195
|
+
* was produced under, not an empty one.
|
|
1196
|
+
*/
|
|
1197
|
+
async wake(payload) {
|
|
1198
|
+
if (payload.resume !== true) return { reason: "session-not-live" };
|
|
1199
|
+
if (!this.allowResume) return { reason: "resume-refused" };
|
|
1200
|
+
const lookup = this.ctx.get("typert")?.lookups.get("agent");
|
|
1201
|
+
if (lookup === void 0) return { reason: "session-not-live" };
|
|
1202
|
+
try {
|
|
1203
|
+
const resolved = await lookup.resolve(payload.sessionId);
|
|
1204
|
+
if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
|
|
1205
|
+
return { agent: resolved };
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
this.ctx.logger.info(`interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1208
|
+
return { reason: "resume-failed" };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Deliver one message to a live local session, waking a persisted one only
|
|
1213
|
+
* when the sender asked and this receiver allows it.
|
|
1214
|
+
*/
|
|
1215
|
+
async deliver(payload) {
|
|
1216
|
+
let agent = this.ctx.agents.get(payload.sessionId);
|
|
1180
1217
|
if (agent === void 0) {
|
|
1181
|
-
|
|
1218
|
+
const woken = await this.wake(payload);
|
|
1219
|
+
if ("reason" in woken) {
|
|
1220
|
+
return { delivered: false, instance: this.instanceId, reason: woken.reason };
|
|
1221
|
+
}
|
|
1222
|
+
agent = woken.agent;
|
|
1223
|
+
}
|
|
1224
|
+
if (hasApiRemoteSubagentOwner(this.ctx, agent.session, agent)) {
|
|
1225
|
+
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1182
1226
|
}
|
|
1183
1227
|
const message = createUserMessage({
|
|
1184
1228
|
source: {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@deepseek-ai/dsh-host-apiproxy/api";
|
|
8
8
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
9
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
|
+
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
10
11
|
|
|
11
12
|
// ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
|
|
12
13
|
function isNullable(value) {
|
|
@@ -848,7 +849,8 @@ var linkFrameSchema = Schema.union([
|
|
|
848
849
|
var sendPayloadSchema = Schema.object({
|
|
849
850
|
sessionId: Schema.string(),
|
|
850
851
|
text: Schema.string(),
|
|
851
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")])
|
|
852
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
+
resume: Schema.boolean()
|
|
852
854
|
});
|
|
853
855
|
var eventPayloadSchema = Schema.object({
|
|
854
856
|
sender: Schema.string(),
|
|
@@ -860,11 +862,13 @@ var InterconnectService = class extends Service {
|
|
|
860
862
|
instanceId: Schema.string().default("dsh"),
|
|
861
863
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
862
864
|
peers: Schema.array(Schema.string()).default([]),
|
|
863
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup")
|
|
865
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
866
|
+
allowResume: Schema.boolean().default(true)
|
|
864
867
|
});
|
|
865
868
|
instanceId;
|
|
866
869
|
requestTimeoutMs;
|
|
867
870
|
delivery;
|
|
871
|
+
allowResume;
|
|
868
872
|
peers = /* @__PURE__ */ new Set();
|
|
869
873
|
subscriptions = [];
|
|
870
874
|
server = new WebSocketServer({ noServer: true });
|
|
@@ -878,6 +882,7 @@ var InterconnectService = class extends Service {
|
|
|
878
882
|
this.instanceId = config.instanceId;
|
|
879
883
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
880
884
|
this.delivery = config.delivery ?? "followup";
|
|
885
|
+
this.allowResume = config.allowResume ?? true;
|
|
881
886
|
for (const peer of config.peers ?? []) this.peers.add(trimBase(peer));
|
|
882
887
|
const route = {
|
|
883
888
|
kind: "prefix",
|
|
@@ -949,7 +954,8 @@ var InterconnectService = class extends Service {
|
|
|
949
954
|
const payload = {
|
|
950
955
|
sessionId: request.sessionId,
|
|
951
956
|
text: request.text,
|
|
952
|
-
...request.delivery === void 0 ? {} : { delivery: request.delivery }
|
|
957
|
+
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
958
|
+
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
953
959
|
};
|
|
954
960
|
const result = await this.post(request.baseUrl, "send", payload);
|
|
955
961
|
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
@@ -1088,7 +1094,7 @@ var InterconnectService = class extends Service {
|
|
|
1088
1094
|
return;
|
|
1089
1095
|
}
|
|
1090
1096
|
try {
|
|
1091
|
-
const value = this.runEndpoint(endpoint, parsed.data.payload);
|
|
1097
|
+
const value = await this.runEndpoint(endpoint, parsed.data.payload);
|
|
1092
1098
|
writeFull(res, rpcId, { ok: true, value });
|
|
1093
1099
|
} catch (error) {
|
|
1094
1100
|
if (error instanceof UnknownEndpointError) {
|
|
@@ -1113,7 +1119,7 @@ var InterconnectService = class extends Service {
|
|
|
1113
1119
|
}
|
|
1114
1120
|
}
|
|
1115
1121
|
/** Dispatch one authenticated business call. */
|
|
1116
|
-
runEndpoint(endpoint, payload) {
|
|
1122
|
+
async runEndpoint(endpoint, payload) {
|
|
1117
1123
|
if (endpoint === "ping") {
|
|
1118
1124
|
return { pong: true, instance: this.instanceId };
|
|
1119
1125
|
}
|
|
@@ -1157,7 +1163,8 @@ var InterconnectService = class extends Service {
|
|
|
1157
1163
|
* matches how the Host's own session listing treats its projection column.
|
|
1158
1164
|
*/
|
|
1159
1165
|
listSessions() {
|
|
1160
|
-
const
|
|
1166
|
+
const reachable = this.ctx.agents.list().filter((agent) => !hasApiRemoteSubagentOwner(this.ctx, agent.session, agent));
|
|
1167
|
+
const sessions = reachable.map((agent) => {
|
|
1161
1168
|
let title;
|
|
1162
1169
|
try {
|
|
1163
1170
|
const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
|
|
@@ -1174,11 +1181,48 @@ var InterconnectService = class extends Service {
|
|
|
1174
1181
|
});
|
|
1175
1182
|
return { sessions, instance: this.instanceId };
|
|
1176
1183
|
}
|
|
1177
|
-
/**
|
|
1178
|
-
|
|
1179
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* Resolve a session that is not currently live, when the sender asked to wake
|
|
1186
|
+
* it. Returns the agent, or the reason it stays undelivered.
|
|
1187
|
+
*
|
|
1188
|
+
* The resume is delegated to the Host's configured `agent` lookup rather than
|
|
1189
|
+
* calling `ctx.agents.resume()` here, and that is the whole point: a handle
|
|
1190
|
+
* from `resume()` is owned by the CALLING context, so resuming on this
|
|
1191
|
+
* plugin's fiber would tear the session down again the moment the plugin
|
|
1192
|
+
* unloads (measured: the same call through the root context leaves it alive).
|
|
1193
|
+
* The Host's resolver owns it instead, and it also composes the preset the
|
|
1194
|
+
* session recorded — so a woken agent comes back with the toolset its history
|
|
1195
|
+
* was produced under, not an empty one.
|
|
1196
|
+
*/
|
|
1197
|
+
async wake(payload) {
|
|
1198
|
+
if (payload.resume !== true) return { reason: "session-not-live" };
|
|
1199
|
+
if (!this.allowResume) return { reason: "resume-refused" };
|
|
1200
|
+
const lookup = this.ctx.get("typert")?.lookups.get("agent");
|
|
1201
|
+
if (lookup === void 0) return { reason: "session-not-live" };
|
|
1202
|
+
try {
|
|
1203
|
+
const resolved = await lookup.resolve(payload.sessionId);
|
|
1204
|
+
if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
|
|
1205
|
+
return { agent: resolved };
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
this.ctx.logger.info(`interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1208
|
+
return { reason: "resume-failed" };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Deliver one message to a live local session, waking a persisted one only
|
|
1213
|
+
* when the sender asked and this receiver allows it.
|
|
1214
|
+
*/
|
|
1215
|
+
async deliver(payload) {
|
|
1216
|
+
let agent = this.ctx.agents.get(payload.sessionId);
|
|
1180
1217
|
if (agent === void 0) {
|
|
1181
|
-
|
|
1218
|
+
const woken = await this.wake(payload);
|
|
1219
|
+
if ("reason" in woken) {
|
|
1220
|
+
return { delivered: false, instance: this.instanceId, reason: woken.reason };
|
|
1221
|
+
}
|
|
1222
|
+
agent = woken.agent;
|
|
1223
|
+
}
|
|
1224
|
+
if (hasApiRemoteSubagentOwner(this.ctx, agent.session, agent)) {
|
|
1225
|
+
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1182
1226
|
}
|
|
1183
1227
|
const message = createUserMessage({
|
|
1184
1228
|
source: {
|