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
|
@@ -80,6 +80,8 @@ When a send/reply reports `delivered: false`, read `reason`:
|
|
|
80
80
|
choose a live target instead.
|
|
81
81
|
- `session-owned-by-subagent` — the session belongs to a subagent and its
|
|
82
82
|
parent owns delivery; reach it through the parent.
|
|
83
|
+
- `message-too-large` — the message is larger than the peer link can carry
|
|
84
|
+
(1 MiB once encoded); shorten the text instead of retrying it unchanged.
|
|
83
85
|
- `no-sender-known` — a `reply` was attempted for a local session that never
|
|
84
86
|
received a sender-carrying interconnect message, or whose recorded sender was
|
|
85
87
|
forgotten by a receiver restart; establish contact with `interconnect_send`
|
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.16",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-interconnect/interconnect",
|
|
7
7
|
"inject": ["agents", "credentials"]
|
package/lib/index.js
CHANGED
|
@@ -835,9 +835,9 @@ 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;
|
|
840
|
+
var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
|
|
841
841
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
842
842
|
var notificationSchema = Schema.union([
|
|
843
843
|
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
@@ -920,10 +920,10 @@ var msgResultSchema = Schema.union([
|
|
|
920
920
|
var linkFrameSchema = Schema.union([
|
|
921
921
|
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
922
922
|
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() })
|
|
923
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
|
|
924
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
|
|
925
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
|
|
926
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
|
|
927
927
|
]);
|
|
928
928
|
var InterconnectService = class extends Service {
|
|
929
929
|
static inject = ["agents", "credentials"];
|
|
@@ -1124,22 +1124,30 @@ var InterconnectService = class extends Service {
|
|
|
1124
1124
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1125
1125
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1126
1126
|
* (not configured, or the link is down) — sends can only address configured+
|
|
1127
|
-
* 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.
|
|
1128
1129
|
*/
|
|
1129
1130
|
async msgRequest(instanceId, kind, payload) {
|
|
1130
1131
|
const state = this.linkStates.get(instanceId);
|
|
1131
1132
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1132
1133
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
+
}
|
|
1140
1145
|
};
|
|
1146
|
+
if (!frameFitsLink(frame)) {
|
|
1147
|
+
return { delivered: false, instance: instanceId, reason: "message-too-large" };
|
|
1148
|
+
}
|
|
1141
1149
|
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1142
|
-
const wrote = state.sendFrame(
|
|
1150
|
+
const wrote = state.sendFrame(frame);
|
|
1143
1151
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1144
1152
|
});
|
|
1145
1153
|
}
|
|
@@ -1263,7 +1271,12 @@ var InterconnectService = class extends Service {
|
|
|
1263
1271
|
if (query.kind === "ping") {
|
|
1264
1272
|
result = { pong: true, instance: this.instanceId };
|
|
1265
1273
|
} else if (query.kind === "list") {
|
|
1266
|
-
|
|
1274
|
+
const rowBudget = this.listRowsBudgetBytes(reqId);
|
|
1275
|
+
if (rowBudget < 0) {
|
|
1276
|
+
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
result = this.listSessions(rowBudget);
|
|
1267
1280
|
} else {
|
|
1268
1281
|
const payload = {
|
|
1269
1282
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1291,32 +1304,57 @@ var InterconnectService = class extends Service {
|
|
|
1291
1304
|
this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
|
|
1292
1305
|
});
|
|
1293
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1309
|
+
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1310
|
+
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1311
|
+
* answer instead of reserved with a fixed constant.
|
|
1312
|
+
* @param reqId - the request id the answer has to echo.
|
|
1313
|
+
* @returns the byte budget for the rows, negative when even an empty answer
|
|
1314
|
+
* exceeds the cap and so cannot be sent at all.
|
|
1315
|
+
*/
|
|
1316
|
+
listRowsBudgetBytes(reqId) {
|
|
1317
|
+
const envelope = {
|
|
1318
|
+
type: "query-result",
|
|
1319
|
+
reqId,
|
|
1320
|
+
result: { sessions: [], instance: this.instanceId }
|
|
1321
|
+
};
|
|
1322
|
+
return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
1323
|
+
}
|
|
1294
1324
|
/**
|
|
1295
1325
|
* Summarize the live local sessions so a sender can discover valid targets
|
|
1296
1326
|
* 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
|
-
*
|
|
1327
|
+
* because `send` can reach exactly those. The answer stops at
|
|
1328
|
+
* {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
|
|
1329
|
+
* envelope leaves, whichever comes first: that budget is what keeps the
|
|
1330
|
+
* complete `query-result` frame inside the link's cap, because a title's
|
|
1331
|
+
* length comes from the session-title projection's own bound rather than
|
|
1332
|
+
* from anything this listing controls. A title that does not fit the
|
|
1333
|
+
* remaining budget is dropped so its target stays listed; a row that does not
|
|
1334
|
+
* fit even untitled ends the listing, which keeps the answer a prefix of the
|
|
1335
|
+
* registration order instead of a set with gaps.
|
|
1300
1336
|
*
|
|
1301
1337
|
* Title and status are best-effort: the title projection is an optional
|
|
1302
1338
|
* service, and a receiver without it still returns the ids. A projection that
|
|
1303
1339
|
* throws degrades that one row rather than failing the whole listing, which
|
|
1304
1340
|
* matches how the Host's own session listing treats its projection column.
|
|
1341
|
+
* @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
|
|
1342
|
+
* @returns the bounded listing.
|
|
1305
1343
|
*/
|
|
1306
|
-
listSessions() {
|
|
1344
|
+
listSessions(rowBudgetBytes) {
|
|
1307
1345
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1308
1346
|
const sessions = [];
|
|
1309
1347
|
let used = 0;
|
|
1310
1348
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
1311
1349
|
const row = this.sessionRow(agent);
|
|
1312
1350
|
const sized = used + rowBytes(row);
|
|
1313
|
-
if (sized >
|
|
1351
|
+
if (sized > rowBudgetBytes) {
|
|
1314
1352
|
const untitled = {
|
|
1315
1353
|
sessionId: row.sessionId,
|
|
1316
1354
|
...row.status === void 0 ? {} : { status: row.status }
|
|
1317
1355
|
};
|
|
1318
1356
|
const untitledSized = used + rowBytes(untitled);
|
|
1319
|
-
if (untitledSized >
|
|
1357
|
+
if (untitledSized > rowBudgetBytes) break;
|
|
1320
1358
|
sessions.push(untitled);
|
|
1321
1359
|
used = untitledSized;
|
|
1322
1360
|
continue;
|
|
@@ -835,9 +835,9 @@ 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;
|
|
840
|
+
var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
|
|
841
841
|
var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
842
842
|
var notificationSchema = Schema.union([
|
|
843
843
|
Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
|
|
@@ -920,10 +920,10 @@ var msgResultSchema = Schema.union([
|
|
|
920
920
|
var linkFrameSchema = Schema.union([
|
|
921
921
|
Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
|
|
922
922
|
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() })
|
|
923
|
+
Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
|
|
924
|
+
Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
|
|
925
|
+
Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
|
|
926
|
+
Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
|
|
927
927
|
]);
|
|
928
928
|
var InterconnectService = class extends Service {
|
|
929
929
|
static inject = ["agents", "credentials"];
|
|
@@ -1124,22 +1124,30 @@ var InterconnectService = class extends Service {
|
|
|
1124
1124
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1125
1125
|
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1126
1126
|
* (not configured, or the link is down) — sends can only address configured+
|
|
1127
|
-
* 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.
|
|
1128
1129
|
*/
|
|
1129
1130
|
async msgRequest(instanceId, kind, payload) {
|
|
1130
1131
|
const state = this.linkStates.get(instanceId);
|
|
1131
1132
|
if (state === void 0 || !state.writable()) return void 0;
|
|
1132
1133
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
+
}
|
|
1140
1145
|
};
|
|
1146
|
+
if (!frameFitsLink(frame)) {
|
|
1147
|
+
return { delivered: false, instance: instanceId, reason: "message-too-large" };
|
|
1148
|
+
}
|
|
1141
1149
|
return this.waitForResult(reqId, "msg", (failure) => {
|
|
1142
|
-
const wrote = state.sendFrame(
|
|
1150
|
+
const wrote = state.sendFrame(frame);
|
|
1143
1151
|
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1144
1152
|
});
|
|
1145
1153
|
}
|
|
@@ -1263,7 +1271,12 @@ var InterconnectService = class extends Service {
|
|
|
1263
1271
|
if (query.kind === "ping") {
|
|
1264
1272
|
result = { pong: true, instance: this.instanceId };
|
|
1265
1273
|
} else if (query.kind === "list") {
|
|
1266
|
-
|
|
1274
|
+
const rowBudget = this.listRowsBudgetBytes(reqId);
|
|
1275
|
+
if (rowBudget < 0) {
|
|
1276
|
+
this.ctx.logger.warn("interconnect: list answer cannot fit the link frame cap; not answering");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
result = this.listSessions(rowBudget);
|
|
1267
1280
|
} else {
|
|
1268
1281
|
const payload = {
|
|
1269
1282
|
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
@@ -1291,32 +1304,57 @@ var InterconnectService = class extends Service {
|
|
|
1291
1304
|
this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
|
|
1292
1305
|
});
|
|
1293
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Bytes one `list` answer may spend on its `sessions` rows: the frame cap
|
|
1309
|
+
* minus the serialized envelope that answer will carry. The envelope holds
|
|
1310
|
+
* the peer-supplied `reqId` and this instance's own id, so it is measured per
|
|
1311
|
+
* answer instead of reserved with a fixed constant.
|
|
1312
|
+
* @param reqId - the request id the answer has to echo.
|
|
1313
|
+
* @returns the byte budget for the rows, negative when even an empty answer
|
|
1314
|
+
* exceeds the cap and so cannot be sent at all.
|
|
1315
|
+
*/
|
|
1316
|
+
listRowsBudgetBytes(reqId) {
|
|
1317
|
+
const envelope = {
|
|
1318
|
+
type: "query-result",
|
|
1319
|
+
reqId,
|
|
1320
|
+
result: { sessions: [], instance: this.instanceId }
|
|
1321
|
+
};
|
|
1322
|
+
return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
1323
|
+
}
|
|
1294
1324
|
/**
|
|
1295
1325
|
* Summarize the live local sessions so a sender can discover valid targets
|
|
1296
1326
|
* 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
|
-
*
|
|
1327
|
+
* because `send` can reach exactly those. The answer stops at
|
|
1328
|
+
* {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
|
|
1329
|
+
* envelope leaves, whichever comes first: that budget is what keeps the
|
|
1330
|
+
* complete `query-result` frame inside the link's cap, because a title's
|
|
1331
|
+
* length comes from the session-title projection's own bound rather than
|
|
1332
|
+
* from anything this listing controls. A title that does not fit the
|
|
1333
|
+
* remaining budget is dropped so its target stays listed; a row that does not
|
|
1334
|
+
* fit even untitled ends the listing, which keeps the answer a prefix of the
|
|
1335
|
+
* registration order instead of a set with gaps.
|
|
1300
1336
|
*
|
|
1301
1337
|
* Title and status are best-effort: the title projection is an optional
|
|
1302
1338
|
* service, and a receiver without it still returns the ids. A projection that
|
|
1303
1339
|
* throws degrades that one row rather than failing the whole listing, which
|
|
1304
1340
|
* matches how the Host's own session listing treats its projection column.
|
|
1341
|
+
* @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
|
|
1342
|
+
* @returns the bounded listing.
|
|
1305
1343
|
*/
|
|
1306
|
-
listSessions() {
|
|
1344
|
+
listSessions(rowBudgetBytes) {
|
|
1307
1345
|
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
|
|
1308
1346
|
const sessions = [];
|
|
1309
1347
|
let used = 0;
|
|
1310
1348
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
1311
1349
|
const row = this.sessionRow(agent);
|
|
1312
1350
|
const sized = used + rowBytes(row);
|
|
1313
|
-
if (sized >
|
|
1351
|
+
if (sized > rowBudgetBytes) {
|
|
1314
1352
|
const untitled = {
|
|
1315
1353
|
sessionId: row.sessionId,
|
|
1316
1354
|
...row.status === void 0 ? {} : { status: row.status }
|
|
1317
1355
|
};
|
|
1318
1356
|
const untitledSized = used + rowBytes(untitled);
|
|
1319
|
-
if (untitledSized >
|
|
1357
|
+
if (untitledSized > rowBudgetBytes) break;
|
|
1320
1358
|
sessions.push(untitled);
|
|
1321
1359
|
used = untitledSized;
|
|
1322
1360
|
continue;
|