dsh-interconnect 0.11.14 → 0.11.16
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 +60 -22
- package/lib/interconnect/index.js +60 -22
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +66 -23
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +23 -4
- 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 +6 -1
|
@@ -838,9 +838,9 @@ 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
|
|
842
|
-
var MAX_LIST_ROWS_BYTES = MAX_LINK_FRAME_BYTES - LIST_FRAME_ENVELOPE_BYTES;
|
|
841
|
+
var MAX_REQUEST_ID_CHARS = 256;
|
|
843
842
|
var MAX_LISTED_SESSIONS = 100;
|
|
843
|
+
var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
|
|
844
844
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
845
845
|
var notificationSchema = Schema.union([
|
|
846
846
|
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
@@ -923,10 +923,10 @@ var msgResultSchema = Schema.union([
|
|
|
923
923
|
var linkFrameSchema = Schema.union([
|
|
924
924
|
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
925
925
|
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
926
|
-
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
927
|
-
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
928
|
-
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
929
|
-
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
926
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
|
|
927
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
|
|
928
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
|
|
929
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
|
|
930
930
|
]);
|
|
931
931
|
var InterconnectService = class extends Service {
|
|
932
932
|
static inject = ["agents", "credentials"];
|
|
@@ -1127,22 +1127,30 @@ var InterconnectService = class extends Service {
|
|
|
1127
1127
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1128
1128
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1129
1129
|
* (not configured, or the link is down) — sends can only address configured+
|
|
1130
|
-
* connected peers by design
|
|
1130
|
+
* connected peers by design — and a `message-too-large` result when the
|
|
1131
|
+
* encoded frame would not fit the link.
|
|
1131
1132
|
*/
|
|
1132
1133
|
async msgRequest(instanceId, kind, payload) {
|
|
1133
1134
|
const state = this.linkStates.get(instanceId);
|
|
1134
1135
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1135
1136
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1137
|
+
const frame = {
|
|
1138
|
+
type: "msg",
|
|
1139
|
+
reqId,
|
|
1140
|
+
message: {
|
|
1141
|
+
kind,
|
|
1142
|
+
sessionId: payload.sessionId,
|
|
1143
|
+
text: payload.text,
|
|
1144
|
+
...payload.sender === void 0 ? {} : { sender: payload.sender },
|
|
1145
|
+
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1146
|
+
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1147
|
+
}
|
|
1143
1148
|
};
|
|
1149
|
+
if (!frameFitsLink(frame)) {
|
|
1150
|
+
return { delivered: false, instance: instanceId, reason: "message-too-large" };
|
|
1151
|
+
}
|
|
1144
1152
|
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1145
|
-
const wrote = state.sendFrame(
|
|
1153
|
+
const wrote = state.sendFrame(frame);
|
|
1146
1154
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1147
1155
|
});
|
|
1148
1156
|
}
|
|
@@ -1266,7 +1274,12 @@ var InterconnectService = class extends Service {
|
|
|
1266
1274
|
if (query.kind === "ping") {
|
|
1267
1275
|
result = { pong: true, instance: this.instanceId };
|
|
1268
1276
|
} else if (query.kind === "list") {
|
|
1269
|
-
|
|
1277
|
+
const rowBudget = this.listRowsBudgetBytes(reqId);
|
|
1278
|
+
if (rowBudget < 0) {
|
|
1279
|
+
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
result = this.listSessions(rowBudget);
|
|
1270
1283
|
} else {
|
|
1271
1284
|
const payload = {
|
|
1272
1285
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1294,32 +1307,57 @@ var InterconnectService = class extends Service {
|
|
|
1294
1307
|
this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
|
|
1295
1308
|
});
|
|
1296
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1312
|
+
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1313
|
+
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1314
|
+
* answer instead of reserved with a fixed constant.
|
|
1315
|
+
* @param reqId - the request id the answer has to echo.
|
|
1316
|
+
* @returns the byte budget for the rows, negative when even an empty answer
|
|
1317
|
+
* exceeds the cap and so cannot be sent at all.
|
|
1318
|
+
*/
|
|
1319
|
+
listRowsBudgetBytes(reqId) {
|
|
1320
|
+
const envelope = {
|
|
1321
|
+
type: "query-result",
|
|
1322
|
+
reqId,
|
|
1323
|
+
result: { sessions: [], instance: this.instanceId }
|
|
1324
|
+
};
|
|
1325
|
+
return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
1326
|
+
}
|
|
1297
1327
|
/**
|
|
1298
1328
|
* Summarize the live local sessions so a sender can discover valid targets
|
|
1299
1329
|
* instead of having to know a session id already. Only live agents are listed
|
|
1300
|
-
* because `send` can reach exactly those
|
|
1301
|
-
* {@link MAX_LISTED_SESSIONS} rows
|
|
1302
|
-
*
|
|
1330
|
+
* because `send` can reach exactly those. The answer stops at
|
|
1331
|
+
* {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
|
|
1332
|
+
* envelope leaves, whichever comes first: that budget is what keeps the
|
|
1333
|
+
* complete `query-result` frame inside the link's cap, because a title's
|
|
1334
|
+
* length comes from the session-title projection's own bound rather than
|
|
1335
|
+
* from anything this listing controls. A title that does not fit the
|
|
1336
|
+
* remaining budget is dropped so its target stays listed; a row that does not
|
|
1337
|
+
* fit even untitled ends the listing, which keeps the answer a prefix of the
|
|
1338
|
+
* registration order instead of a set with gaps.
|
|
1303
1339
|
*
|
|
1304
1340
|
* Title and status are best-effort: the title projection is an optional
|
|
1305
1341
|
* service, and a receiver without it still returns the ids. A projection that
|
|
1306
1342
|
* throws degrades that one row rather than failing the whole listing, which
|
|
1307
1343
|
* matches how the Host's own session listing treats its projection column.
|
|
1344
|
+
* @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
|
|
1345
|
+
* @returns the bounded listing.
|
|
1308
1346
|
*/
|
|
1309
|
-
listSessions() {
|
|
1347
|
+
listSessions(rowBudgetBytes) {
|
|
1310
1348
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1311
1349
|
const sessions = [];
|
|
1312
1350
|
let used = 0;
|
|
1313
1351
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
1314
1352
|
const row = this.sessionRow(agent);
|
|
1315
1353
|
const sized = used + rowBytes(row);
|
|
1316
|
-
if (sized >
|
|
1354
|
+
if (sized > rowBudgetBytes) {
|
|
1317
1355
|
const untitled = {
|
|
1318
1356
|
sessionId: row.sessionId,
|
|
1319
1357
|
...row.status === void 0 ? {} : { status: row.status }
|
|
1320
1358
|
};
|
|
1321
1359
|
const untitledSized = used + rowBytes(untitled);
|
|
1322
|
-
if (untitledSized >
|
|
1360
|
+
if (untitledSized > rowBudgetBytes) break;
|
|
1323
1361
|
sessions.push(untitled);
|
|
1324
1362
|
used = untitledSized;
|
|
1325
1363
|
continue;
|
|
@@ -1763,7 +1801,7 @@ function apply(ctx) {
|
|
|
1763
1801
|
text: {
|
|
1764
1802
|
type: "string",
|
|
1765
1803
|
required: true,
|
|
1766
|
-
description:
|
|
1804
|
+
description: 'Message text delivered to the peer session. At most 1 MiB once encoded: a larger message is refused with reason "message-too-large".'
|
|
1767
1805
|
},
|
|
1768
1806
|
delivery: {
|
|
1769
1807
|
type: "string",
|
|
@@ -1794,6 +1832,8 @@ function apply(ctx) {
|
|
|
1794
1832
|
return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no persisted session under that id, or the wake itself failed)`;
|
|
1795
1833
|
case "session-owned-by-subagent":
|
|
1796
1834
|
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`;
|
|
1835
|
+
case "message-too-large":
|
|
1836
|
+
return "not delivered: the message is too large for the peer link (limit 1 MiB); shorten the text and try again";
|
|
1797
1837
|
default:
|
|
1798
1838
|
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");
|
|
1799
1839
|
}
|
|
@@ -1944,6 +1984,9 @@ function apply(ctx) {
|
|
|
1944
1984
|
if (value.reason === "no-sender-known") {
|
|
1945
1985
|
return "not delivered: no sender identity is known to reply to";
|
|
1946
1986
|
}
|
|
1987
|
+
if (value.reason === "message-too-large") {
|
|
1988
|
+
return "not delivered: the reply is too large for the peer link (limit 1 MiB); shorten the text and try again";
|
|
1989
|
+
}
|
|
1947
1990
|
if (value.reason === "session-owned-by-subagent") {
|
|
1948
1991
|
return "not delivered: the recorded sender's session belongs to a subagent and its parent agent owns delivery";
|
|
1949
1992
|
}
|