dsh-interconnect 0.11.15 → 0.11.17
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/assets/dsh-interconnect.md +2 -0
- package/dsh.plugin.json +1 -1
- package/lib/index.js +47 -40
- package/lib/interconnect/index.js +47 -40
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +53 -41
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +12 -9
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +4 -1
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -838,8 +838,10 @@ var PLUGIN_SOURCE = "dsh-interconnect";
|
|
|
838
838
|
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
839
839
|
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
840
840
|
var LINK_CHANNEL = "/interconnect/link";
|
|
841
|
+
var MAX_INSTANCE_ID_CHARS = 256;
|
|
841
842
|
var MAX_REQUEST_ID_CHARS = 256;
|
|
842
843
|
var MAX_LISTED_SESSIONS = 100;
|
|
844
|
+
var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
|
|
843
845
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
844
846
|
var notificationSchema = Schema.union([
|
|
845
847
|
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
@@ -930,7 +932,7 @@ var linkFrameSchema = Schema.union([
|
|
|
930
932
|
var InterconnectService = class extends Service {
|
|
931
933
|
static inject = ["agents", "credentials"];
|
|
932
934
|
static Config = Schema.object({
|
|
933
|
-
instanceId: Schema.string().default("dsh"),
|
|
935
|
+
instanceId: Schema.string().max(MAX_INSTANCE_ID_CHARS).default("dsh"),
|
|
934
936
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
935
937
|
peers: Schema.dict(Schema.string()).default({}),
|
|
936
938
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
@@ -940,7 +942,6 @@ var InterconnectService = class extends Service {
|
|
|
940
942
|
requestTimeoutMs;
|
|
941
943
|
delivery;
|
|
942
944
|
allowResume;
|
|
943
|
-
subscriptions = [];
|
|
944
945
|
server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
|
|
945
946
|
sockets = /* @__PURE__ */ new Set();
|
|
946
947
|
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
@@ -997,28 +998,28 @@ var InterconnectService = class extends Service {
|
|
|
997
998
|
socket.ping();
|
|
998
999
|
}
|
|
999
1000
|
}, 3e4);
|
|
1000
|
-
|
|
1001
|
+
ctx.on("agent/status", ({ agent, status }) => {
|
|
1001
1002
|
this.fanout({ kind: "agent/status", sessionId: String(agent.session.id), status });
|
|
1002
|
-
})
|
|
1003
|
-
|
|
1003
|
+
});
|
|
1004
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
1004
1005
|
this.fanout({ kind: "agent/created", sessionId: String(agent.session.id) });
|
|
1005
|
-
})
|
|
1006
|
-
|
|
1006
|
+
});
|
|
1007
|
+
ctx.on("agent/disposed", ({ agent }) => {
|
|
1007
1008
|
this.fanout({ kind: "agent/disposed", sessionId: String(agent.session.id) });
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1009
|
+
});
|
|
1010
|
+
ctx.on("session/created", (session) => {
|
|
1010
1011
|
const parentSessionId = session.header.parentSession === void 0 ? void 0 : String(session.header.parentSession);
|
|
1011
1012
|
this.fanout({
|
|
1012
1013
|
kind: "session/created",
|
|
1013
1014
|
sessionId: String(session.id),
|
|
1014
1015
|
...parentSessionId === void 0 ? {} : { parentSessionId }
|
|
1015
1016
|
});
|
|
1016
|
-
})
|
|
1017
|
-
|
|
1017
|
+
});
|
|
1018
|
+
ctx.on("session/disposed", (session) => {
|
|
1018
1019
|
this.senders.delete(String(session.id));
|
|
1019
1020
|
this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1021
|
+
});
|
|
1022
|
+
ctx.on("subagent/end", (info) => {
|
|
1022
1023
|
if (!info.local) return;
|
|
1023
1024
|
this.fanout({
|
|
1024
1025
|
kind: "subagent/end",
|
|
@@ -1026,10 +1027,7 @@ var InterconnectService = class extends Service {
|
|
|
1026
1027
|
childSessionId: String(info.id),
|
|
1027
1028
|
stopReason: info.stopReason
|
|
1028
1029
|
});
|
|
1029
|
-
})
|
|
1030
|
-
ctx.effect(() => () => {
|
|
1031
|
-
for (const dispose of this.subscriptions.splice(0)) dispose();
|
|
1032
|
-
}, "interconnect: event subscriptions");
|
|
1030
|
+
});
|
|
1033
1031
|
ctx.effect(() => () => {
|
|
1034
1032
|
if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
|
|
1035
1033
|
for (const state of this.linkStates.values()) state.close();
|
|
@@ -1126,22 +1124,30 @@ var InterconnectService = class extends Service {
|
|
|
1126
1124
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1127
1125
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1128
1126
|
* (not configured, or the link is down) — sends can only address configured+
|
|
1129
|
-
* connected peers by design
|
|
1127
|
+
* connected peers by design — and a `message-too-large` result when the
|
|
1128
|
+
* encoded frame would not fit the link.
|
|
1130
1129
|
*/
|
|
1131
1130
|
async msgRequest(instanceId, kind, payload) {
|
|
1132
1131
|
const state = this.linkStates.get(instanceId);
|
|
1133
1132
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1134
1133
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1134
|
+
const frame = {
|
|
1135
|
+
type: "msg",
|
|
1136
|
+
reqId,
|
|
1137
|
+
message: {
|
|
1138
|
+
kind,
|
|
1139
|
+
sessionId: payload.sessionId,
|
|
1140
|
+
text: payload.text,
|
|
1141
|
+
...payload.sender === void 0 ? {} : { sender: payload.sender },
|
|
1142
|
+
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1143
|
+
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1144
|
+
}
|
|
1142
1145
|
};
|
|
1146
|
+
if (!frameFitsLink(frame)) {
|
|
1147
|
+
return { delivered: false, instance: instanceId, reason: "message-too-large" };
|
|
1148
|
+
}
|
|
1143
1149
|
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1144
|
-
const wrote = state.sendFrame(
|
|
1150
|
+
const wrote = state.sendFrame(frame);
|
|
1145
1151
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1146
1152
|
});
|
|
1147
1153
|
}
|
|
@@ -1211,10 +1217,12 @@ var InterconnectService = class extends Service {
|
|
|
1211
1217
|
* dialed link plus the peer's inbound one), so one event must leave over one
|
|
1212
1218
|
* of them. Which one is decided by local ownership, not by the peer: a dialed
|
|
1213
1219
|
* socket is a configured peer's link and always sends. An inbound socket is
|
|
1214
|
-
* skipped only when
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1220
|
+
* skipped only when an open dialed link covers the id that socket itself
|
|
1221
|
+
* announced — either because `Config.peers` lists that peer under the id, or
|
|
1222
|
+
* because the peer announced the id over that dialed link — so an
|
|
1223
|
+
* announcement can suppress nothing but a duplicate the controlled link
|
|
1224
|
+
* already carries. An impostor cannot drop an event for a peer, and without a
|
|
1225
|
+
* dialed link every inbound socket receives it.
|
|
1218
1226
|
*
|
|
1219
1227
|
* That holds once a peer's announcement has been seen. Between this
|
|
1220
1228
|
* instance's dialed link opening and the peer's `hello` arriving on its
|
|
@@ -1233,7 +1241,10 @@ var InterconnectService = class extends Service {
|
|
|
1233
1241
|
continue;
|
|
1234
1242
|
}
|
|
1235
1243
|
const dialedPeer = this.dialedPeerOf.get(socket);
|
|
1236
|
-
if (dialedPeer
|
|
1244
|
+
if (dialedPeer === void 0) continue;
|
|
1245
|
+
covered.add(dialedPeer);
|
|
1246
|
+
const announced = this.peerOf.get(socket);
|
|
1247
|
+
if (announced !== void 0) covered.add(announced);
|
|
1237
1248
|
}
|
|
1238
1249
|
for (const socket of this.sockets) {
|
|
1239
1250
|
if (this.dialedPeerOf.get(socket) === void 0) {
|
|
@@ -1265,12 +1276,7 @@ var InterconnectService = class extends Service {
|
|
|
1265
1276
|
if (query.kind === "ping") {
|
|
1266
1277
|
result = { pong: true, instance: this.instanceId };
|
|
1267
1278
|
} else if (query.kind === "list") {
|
|
1268
|
-
|
|
1269
|
-
if (rowBudget < 0) {
|
|
1270
|
-
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
result = this.listSessions(rowBudget);
|
|
1279
|
+
result = this.listSessions(this.listRowsBudgetBytes(reqId));
|
|
1274
1280
|
} else {
|
|
1275
1281
|
const payload = {
|
|
1276
1282
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1302,10 +1308,11 @@ var InterconnectService = class extends Service {
|
|
|
1302
1308
|
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1303
1309
|
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1304
1310
|
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1305
|
-
* answer instead of reserved with a fixed constant.
|
|
1311
|
+
* answer instead of reserved with a fixed constant. Both ids are bounded
|
|
1312
|
+
* ({@link MAX_REQUEST_ID_CHARS} and {@link MAX_INSTANCE_ID_CHARS} characters),
|
|
1313
|
+
* so the envelope always fits and the budget is always positive.
|
|
1306
1314
|
* @param reqId - the request id the answer has to echo.
|
|
1307
|
-
* @returns the byte budget for the rows
|
|
1308
|
-
* exceeds the cap and so cannot be sent at all.
|
|
1315
|
+
* @returns the byte budget for the rows.
|
|
1309
1316
|
*/
|
|
1310
1317
|
listRowsBudgetBytes(reqId) {
|
|
1311
1318
|
const envelope = {
|
|
@@ -1792,7 +1799,7 @@ function apply(ctx) {
|
|
|
1792
1799
|
text: {
|
|
1793
1800
|
type: "string",
|
|
1794
1801
|
required: true,
|
|
1795
|
-
description:
|
|
1802
|
+
description: 'Message text delivered to the peer session. At most 1 MiB once encoded: a larger message is refused with reason "message-too-large".'
|
|
1796
1803
|
},
|
|
1797
1804
|
delivery: {
|
|
1798
1805
|
type: "string",
|
|
@@ -1823,6 +1830,8 @@ function apply(ctx) {
|
|
|
1823
1830
|
return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no persisted session under that id, or the wake itself failed)`;
|
|
1824
1831
|
case "session-owned-by-subagent":
|
|
1825
1832
|
return `not delivered: "${_args.sessionId}" is a subagent's session on ${value.instance} \u2014 its parent agent owns delivery, so reach it through that parent`;
|
|
1833
|
+
case "message-too-large":
|
|
1834
|
+
return "not delivered: the message is too large for the peer link (limit 1 MiB); shorten the text and try again";
|
|
1826
1835
|
default:
|
|
1827
1836
|
return `not delivered: no live session "${_args.sessionId}" on ${value.instance}` + (_args.resume === true ? " \u2014 use interconnect_list to see which sessions are live there" : " \u2014 use interconnect_list to see which sessions are live there, or set resume to wake this one");
|
|
1828
1837
|
}
|
|
@@ -1973,6 +1982,9 @@ function apply(ctx) {
|
|
|
1973
1982
|
if (value.reason === "no-sender-known") {
|
|
1974
1983
|
return "not delivered: no sender identity is known to reply to";
|
|
1975
1984
|
}
|
|
1985
|
+
if (value.reason === "message-too-large") {
|
|
1986
|
+
return "not delivered: the reply is too large for the peer link (limit 1 MiB); shorten the text and try again";
|
|
1987
|
+
}
|
|
1976
1988
|
if (value.reason === "session-owned-by-subagent") {
|
|
1977
1989
|
return "not delivered: the recorded sender's session belongs to a subagent and its parent agent owns delivery";
|
|
1978
1990
|
}
|