dsh-interconnect 0.11.14 → 0.11.15
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/dsh.plugin.json +1 -1
- package/lib/index.js +42 -13
- package/lib/interconnect/index.js +42 -13
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +42 -13
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +21 -3
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/package.json +6 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-interconnect",
|
|
3
3
|
"description": "Cross-instance message/event handoff for DeepSeek Harness (DSH): WebSocket link, model-facing tools, and companion skill.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.15",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-interconnect/interconnect",
|
|
7
7
|
"inject": ["agents", "credentials"]
|
package/lib/index.js
CHANGED
|
@@ -835,8 +835,7 @@ var PLUGIN_SOURCE = "dsh-interconnect";
|
|
|
835
835
|
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
836
836
|
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
837
837
|
var LINK_CHANNEL = "/interconnect/link";
|
|
838
|
-
var
|
|
839
|
-
var MAX_LIST_ROWS_BYTES = MAX_LINK_FRAME_BYTES - LIST_FRAME_ENVELOPE_BYTES;
|
|
838
|
+
var MAX_REQUEST_ID_CHARS = 256;
|
|
840
839
|
var MAX_LISTED_SESSIONS = 100;
|
|
841
840
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
842
841
|
var notificationSchema = Schema.union([
|
|
@@ -920,10 +919,10 @@ var msgResultSchema = Schema.union([
|
|
|
920
919
|
var linkFrameSchema = Schema.union([
|
|
921
920
|
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
922
921
|
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
923
|
-
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
924
|
-
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
925
|
-
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
926
|
-
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
922
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
|
|
923
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
|
|
924
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
|
|
925
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
|
|
927
926
|
]);
|
|
928
927
|
var InterconnectService = class extends Service {
|
|
929
928
|
static inject = ["agents", "credentials"];
|
|
@@ -1263,7 +1262,12 @@ var InterconnectService = class extends Service {
|
|
|
1263
1262
|
if (query.kind === "ping") {
|
|
1264
1263
|
result = { pong: true, instance: this.instanceId };
|
|
1265
1264
|
} else if (query.kind === "list") {
|
|
1266
|
-
|
|
1265
|
+
const rowBudget = this.listRowsBudgetBytes(reqId);
|
|
1266
|
+
if (rowBudget < 0) {
|
|
1267
|
+
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
result = this.listSessions(rowBudget);
|
|
1267
1271
|
} else {
|
|
1268
1272
|
const payload = {
|
|
1269
1273
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1291,32 +1295,57 @@ var InterconnectService = class extends Service {
|
|
|
1291
1295
|
this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
|
|
1292
1296
|
});
|
|
1293
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1300
|
+
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1301
|
+
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1302
|
+
* answer instead of reserved with a fixed constant.
|
|
1303
|
+
* @param reqId - the request id the answer has to echo.
|
|
1304
|
+
* @returns the byte budget for the rows, negative when even an empty answer
|
|
1305
|
+
* exceeds the cap and so cannot be sent at all.
|
|
1306
|
+
*/
|
|
1307
|
+
listRowsBudgetBytes(reqId) {
|
|
1308
|
+
const envelope = {
|
|
1309
|
+
type: "query-result",
|
|
1310
|
+
reqId,
|
|
1311
|
+
result: { sessions: [], instance: this.instanceId }
|
|
1312
|
+
};
|
|
1313
|
+
return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
1314
|
+
}
|
|
1294
1315
|
/**
|
|
1295
1316
|
* Summarize the live local sessions so a sender can discover valid targets
|
|
1296
1317
|
* instead of having to know a session id already. Only live agents are listed
|
|
1297
|
-
* because `send` can reach exactly those
|
|
1298
|
-
* {@link MAX_LISTED_SESSIONS} rows
|
|
1299
|
-
*
|
|
1318
|
+
* because `send` can reach exactly those. The answer stops at
|
|
1319
|
+
* {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
|
|
1320
|
+
* envelope leaves, whichever comes first: that budget is what keeps the
|
|
1321
|
+
* complete `query-result` frame inside the link's cap, because a title's
|
|
1322
|
+
* length comes from the session-title projection's own bound rather than
|
|
1323
|
+
* from anything this listing controls. A title that does not fit the
|
|
1324
|
+
* remaining budget is dropped so its target stays listed; a row that does not
|
|
1325
|
+
* fit even untitled ends the listing, which keeps the answer a prefix of the
|
|
1326
|
+
* registration order instead of a set with gaps.
|
|
1300
1327
|
*
|
|
1301
1328
|
* Title and status are best-effort: the title projection is an optional
|
|
1302
1329
|
* service, and a receiver without it still returns the ids. A projection that
|
|
1303
1330
|
* throws degrades that one row rather than failing the whole listing, which
|
|
1304
1331
|
* matches how the Host's own session listing treats its projection column.
|
|
1332
|
+
* @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
|
|
1333
|
+
* @returns the bounded listing.
|
|
1305
1334
|
*/
|
|
1306
|
-
listSessions() {
|
|
1335
|
+
listSessions(rowBudgetBytes) {
|
|
1307
1336
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1308
1337
|
const sessions = [];
|
|
1309
1338
|
let used = 0;
|
|
1310
1339
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
1311
1340
|
const row = this.sessionRow(agent);
|
|
1312
1341
|
const sized = used + rowBytes(row);
|
|
1313
|
-
if (sized >
|
|
1342
|
+
if (sized > rowBudgetBytes) {
|
|
1314
1343
|
const untitled = {
|
|
1315
1344
|
sessionId: row.sessionId,
|
|
1316
1345
|
...row.status === void 0 ? {} : { status: row.status }
|
|
1317
1346
|
};
|
|
1318
1347
|
const untitledSized = used + rowBytes(untitled);
|
|
1319
|
-
if (untitledSized >
|
|
1348
|
+
if (untitledSized > rowBudgetBytes) break;
|
|
1320
1349
|
sessions.push(untitled);
|
|
1321
1350
|
used = untitledSized;
|
|
1322
1351
|
continue;
|
|
@@ -835,8 +835,7 @@ var PLUGIN_SOURCE = "dsh-interconnect";
|
|
|
835
835
|
var MAX_LINK_FRAME_BYTES = 1024 * 1024;
|
|
836
836
|
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
837
837
|
var LINK_CHANNEL = "/interconnect/link";
|
|
838
|
-
var
|
|
839
|
-
var MAX_LIST_ROWS_BYTES = MAX_LINK_FRAME_BYTES - LIST_FRAME_ENVELOPE_BYTES;
|
|
838
|
+
var MAX_REQUEST_ID_CHARS = 256;
|
|
840
839
|
var MAX_LISTED_SESSIONS = 100;
|
|
841
840
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
842
841
|
var notificationSchema = Schema.union([
|
|
@@ -920,10 +919,10 @@ var msgResultSchema = Schema.union([
|
|
|
920
919
|
var linkFrameSchema = Schema.union([
|
|
921
920
|
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
922
921
|
Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
|
|
923
|
-
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
|
|
924
|
-
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
|
|
925
|
-
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
|
|
926
|
-
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
|
|
922
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
|
|
923
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
|
|
924
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
|
|
925
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
|
|
927
926
|
]);
|
|
928
927
|
var InterconnectService = class extends Service {
|
|
929
928
|
static inject = ["agents", "credentials"];
|
|
@@ -1263,7 +1262,12 @@ var InterconnectService = class extends Service {
|
|
|
1263
1262
|
if (query.kind === "ping") {
|
|
1264
1263
|
result = { pong: true, instance: this.instanceId };
|
|
1265
1264
|
} else if (query.kind === "list") {
|
|
1266
|
-
|
|
1265
|
+
const rowBudget = this.listRowsBudgetBytes(reqId);
|
|
1266
|
+
if (rowBudget < 0) {
|
|
1267
|
+
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
result = this.listSessions(rowBudget);
|
|
1267
1271
|
} else {
|
|
1268
1272
|
const payload = {
|
|
1269
1273
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1291,32 +1295,57 @@ var InterconnectService = class extends Service {
|
|
|
1291
1295
|
this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
|
|
1292
1296
|
});
|
|
1293
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1300
|
+
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1301
|
+
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1302
|
+
* answer instead of reserved with a fixed constant.
|
|
1303
|
+
* @param reqId - the request id the answer has to echo.
|
|
1304
|
+
* @returns the byte budget for the rows, negative when even an empty answer
|
|
1305
|
+
* exceeds the cap and so cannot be sent at all.
|
|
1306
|
+
*/
|
|
1307
|
+
listRowsBudgetBytes(reqId) {
|
|
1308
|
+
const envelope = {
|
|
1309
|
+
type: "query-result",
|
|
1310
|
+
reqId,
|
|
1311
|
+
result: { sessions: [], instance: this.instanceId }
|
|
1312
|
+
};
|
|
1313
|
+
return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
1314
|
+
}
|
|
1294
1315
|
/**
|
|
1295
1316
|
* Summarize the live local sessions so a sender can discover valid targets
|
|
1296
1317
|
* instead of having to know a session id already. Only live agents are listed
|
|
1297
|
-
* because `send` can reach exactly those
|
|
1298
|
-
* {@link MAX_LISTED_SESSIONS} rows
|
|
1299
|
-
*
|
|
1318
|
+
* because `send` can reach exactly those. The answer stops at
|
|
1319
|
+
* {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
|
|
1320
|
+
* envelope leaves, whichever comes first: that budget is what keeps the
|
|
1321
|
+
* complete `query-result` frame inside the link's cap, because a title's
|
|
1322
|
+
* length comes from the session-title projection's own bound rather than
|
|
1323
|
+
* from anything this listing controls. A title that does not fit the
|
|
1324
|
+
* remaining budget is dropped so its target stays listed; a row that does not
|
|
1325
|
+
* fit even untitled ends the listing, which keeps the answer a prefix of the
|
|
1326
|
+
* registration order instead of a set with gaps.
|
|
1300
1327
|
*
|
|
1301
1328
|
* Title and status are best-effort: the title projection is an optional
|
|
1302
1329
|
* service, and a receiver without it still returns the ids. A projection that
|
|
1303
1330
|
* throws degrades that one row rather than failing the whole listing, which
|
|
1304
1331
|
* matches how the Host's own session listing treats its projection column.
|
|
1332
|
+
* @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
|
|
1333
|
+
* @returns the bounded listing.
|
|
1305
1334
|
*/
|
|
1306
|
-
listSessions() {
|
|
1335
|
+
listSessions(rowBudgetBytes) {
|
|
1307
1336
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1308
1337
|
const sessions = [];
|
|
1309
1338
|
let used = 0;
|
|
1310
1339
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
1311
1340
|
const row = this.sessionRow(agent);
|
|
1312
1341
|
const sized = used + rowBytes(row);
|
|
1313
|
-
if (sized >
|
|
1342
|
+
if (sized > rowBudgetBytes) {
|
|
1314
1343
|
const untitled = {
|
|
1315
1344
|
sessionId: row.sessionId,
|
|
1316
1345
|
...row.status === void 0 ? {} : { status: row.status }
|
|
1317
1346
|
};
|
|
1318
1347
|
const untitledSized = used + rowBytes(untitled);
|
|
1319
|
-
if (untitledSized >
|
|
1348
|
+
if (untitledSized > rowBudgetBytes) break;
|
|
1320
1349
|
sessions.push(untitled);
|
|
1321
1350
|
used = untitledSized;
|
|
1322
1351
|
continue;
|