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.
@@ -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.15",
4
+ "version": "0.11.17",
5
5
  "entry": {
6
6
  "name": "dsh-interconnect/interconnect",
7
7
  "inject": ["agents", "credentials"]
package/lib/index.js CHANGED
@@ -835,8 +835,10 @@ 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 MAX_INSTANCE_ID_CHARS = 256;
838
839
  var MAX_REQUEST_ID_CHARS = 256;
839
840
  var MAX_LISTED_SESSIONS = 100;
841
+ var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
840
842
  var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
841
843
  var notificationSchema = Schema.union([
842
844
  Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
@@ -927,7 +929,7 @@ var linkFrameSchema = Schema.union([
927
929
  var InterconnectService = class extends Service {
928
930
  static inject = ["agents", "credentials"];
929
931
  static Config = Schema.object({
930
- instanceId: Schema.string().default("dsh"),
932
+ instanceId: Schema.string().max(MAX_INSTANCE_ID_CHARS).default("dsh"),
931
933
  requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
932
934
  peers: Schema.dict(Schema.string()).default({}),
933
935
  delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
@@ -937,7 +939,6 @@ var InterconnectService = class extends Service {
937
939
  requestTimeoutMs;
938
940
  delivery;
939
941
  allowResume;
940
- subscriptions = [];
941
942
  server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
942
943
  sockets = /* @__PURE__ */ new Set();
943
944
  /** Outbound peer links keyed by the peer's `instanceId`. */
@@ -994,28 +995,28 @@ var InterconnectService = class extends Service {
994
995
  socket.ping();
995
996
  }
996
997
  }, 3e4);
997
- this.subscriptions.push(ctx.on("agent/status", ({ agent, status }) => {
998
+ ctx.on("agent/status", ({ agent, status }) => {
998
999
  this.fanout({ kind: "agent/status", sessionId: String(agent.session.id), status });
999
- }));
1000
- this.subscriptions.push(ctx.on("agent/created", ({ agent }) => {
1000
+ });
1001
+ ctx.on("agent/created", ({ agent }) => {
1001
1002
  this.fanout({ kind: "agent/created", sessionId: String(agent.session.id) });
1002
- }));
1003
- this.subscriptions.push(ctx.on("agent/disposed", ({ agent }) => {
1003
+ });
1004
+ ctx.on("agent/disposed", ({ agent }) => {
1004
1005
  this.fanout({ kind: "agent/disposed", sessionId: String(agent.session.id) });
1005
- }));
1006
- this.subscriptions.push(ctx.on("session/created", (session) => {
1006
+ });
1007
+ ctx.on("session/created", (session) => {
1007
1008
  const parentSessionId = session.header.parentSession === void 0 ? void 0 : String(session.header.parentSession);
1008
1009
  this.fanout({
1009
1010
  kind: "session/created",
1010
1011
  sessionId: String(session.id),
1011
1012
  ...parentSessionId === void 0 ? {} : { parentSessionId }
1012
1013
  });
1013
- }));
1014
- this.subscriptions.push(ctx.on("session/disposed", (session) => {
1014
+ });
1015
+ ctx.on("session/disposed", (session) => {
1015
1016
  this.senders.delete(String(session.id));
1016
1017
  this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
1017
- }));
1018
- this.subscriptions.push(ctx.on("subagent/end", (info) => {
1018
+ });
1019
+ ctx.on("subagent/end", (info) => {
1019
1020
  if (!info.local) return;
1020
1021
  this.fanout({
1021
1022
  kind: "subagent/end",
@@ -1023,10 +1024,7 @@ var InterconnectService = class extends Service {
1023
1024
  childSessionId: String(info.id),
1024
1025
  stopReason: info.stopReason
1025
1026
  });
1026
- }));
1027
- ctx.effect(() => () => {
1028
- for (const dispose of this.subscriptions.splice(0)) dispose();
1029
- }, "interconnect: event subscriptions");
1027
+ });
1030
1028
  ctx.effect(() => () => {
1031
1029
  if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
1032
1030
  for (const state of this.linkStates.values()) state.close();
@@ -1123,22 +1121,30 @@ var InterconnectService = class extends Service {
1123
1121
  * Send a `msg` frame over the live link to a peer and resolve with its
1124
1122
  * correlated `msg-result`. Undefined when the peer has no live link here
1125
1123
  * (not configured, or the link is down) — sends can only address configured+
1126
- * connected peers by design.
1124
+ * connected peers by design — and a `message-too-large` result when the
1125
+ * encoded frame would not fit the link.
1127
1126
  */
1128
1127
  async msgRequest(instanceId, kind, payload) {
1129
1128
  const state = this.linkStates.get(instanceId);
1130
1129
  if (state === void 0 || !state.writable()) return void 0;
1131
1130
  const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
1132
- const message = {
1133
- kind,
1134
- sessionId: payload.sessionId,
1135
- text: payload.text,
1136
- ...payload.sender === void 0 ? {} : { sender: payload.sender },
1137
- ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1138
- ...payload.resume === void 0 ? {} : { resume: payload.resume }
1131
+ const frame = {
1132
+ type: "msg",
1133
+ reqId,
1134
+ message: {
1135
+ kind,
1136
+ sessionId: payload.sessionId,
1137
+ text: payload.text,
1138
+ ...payload.sender === void 0 ? {} : { sender: payload.sender },
1139
+ ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1140
+ ...payload.resume === void 0 ? {} : { resume: payload.resume }
1141
+ }
1139
1142
  };
1143
+ if (!frameFitsLink(frame)) {
1144
+ return { delivered: false, instance: instanceId, reason: "message-too-large" };
1145
+ }
1140
1146
  return this.waitForResult(reqId, "msg", (failure) => {
1141
- const wrote = state.sendFrame({ type: "msg", reqId, message });
1147
+ const wrote = state.sendFrame(frame);
1142
1148
  if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
1143
1149
  });
1144
1150
  }
@@ -1208,10 +1214,12 @@ var InterconnectService = class extends Service {
1208
1214
  * dialed link plus the peer's inbound one), so one event must leave over one
1209
1215
  * of them. Which one is decided by local ownership, not by the peer: a dialed
1210
1216
  * socket is a configured peer's link and always sends. An inbound socket is
1211
- * skipped only when a dialed link to the id that socket itself announced is
1212
- * open, so an announcement can suppress nothing but a duplicate the
1213
- * controlled link already carries — an impostor cannot drop an event for a
1214
- * peer, and without a dialed link every inbound socket receives it.
1217
+ * skipped only when an open dialed link covers the id that socket itself
1218
+ * announced — either because `Config.peers` lists that peer under the id, or
1219
+ * because the peer announced the id over that dialed link — so an
1220
+ * announcement can suppress nothing but a duplicate the controlled link
1221
+ * already carries. An impostor cannot drop an event for a peer, and without a
1222
+ * dialed link every inbound socket receives it.
1215
1223
  *
1216
1224
  * That holds once a peer's announcement has been seen. Between this
1217
1225
  * instance's dialed link opening and the peer's `hello` arriving on its
@@ -1230,7 +1238,10 @@ var InterconnectService = class extends Service {
1230
1238
  continue;
1231
1239
  }
1232
1240
  const dialedPeer = this.dialedPeerOf.get(socket);
1233
- if (dialedPeer !== void 0) covered.add(dialedPeer);
1241
+ if (dialedPeer === void 0) continue;
1242
+ covered.add(dialedPeer);
1243
+ const announced = this.peerOf.get(socket);
1244
+ if (announced !== void 0) covered.add(announced);
1234
1245
  }
1235
1246
  for (const socket of this.sockets) {
1236
1247
  if (this.dialedPeerOf.get(socket) === void 0) {
@@ -1262,12 +1273,7 @@ var InterconnectService = class extends Service {
1262
1273
  if (query.kind === "ping") {
1263
1274
  result = { pong: true, instance: this.instanceId };
1264
1275
  } else if (query.kind === "list") {
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);
1276
+ result = this.listSessions(this.listRowsBudgetBytes(reqId));
1271
1277
  } else {
1272
1278
  const payload = {
1273
1279
  sender: this.peerOf.get(socket) ?? "unknown-peer",
@@ -1299,10 +1305,11 @@ var InterconnectService = class extends Service {
1299
1305
  * Bytes one `list` answer may spend on its `sessions` rows: the frame cap
1300
1306
  * minus the serialized envelope that answer will carry. The envelope holds
1301
1307
  * the peer-supplied `reqId` and this instance's own id, so it is measured per
1302
- * answer instead of reserved with a fixed constant.
1308
+ * answer instead of reserved with a fixed constant. Both ids are bounded
1309
+ * ({@link MAX_REQUEST_ID_CHARS} and {@link MAX_INSTANCE_ID_CHARS} characters),
1310
+ * so the envelope always fits and the budget is always positive.
1303
1311
  * @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.
1312
+ * @returns the byte budget for the rows.
1306
1313
  */
1307
1314
  listRowsBudgetBytes(reqId) {
1308
1315
  const envelope = {
@@ -835,8 +835,10 @@ 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 MAX_INSTANCE_ID_CHARS = 256;
838
839
  var MAX_REQUEST_ID_CHARS = 256;
839
840
  var MAX_LISTED_SESSIONS = 100;
841
+ var frameFitsLink = (frame) => Buffer.byteLength(JSON.stringify(frame), "utf8") <= MAX_LINK_FRAME_BYTES;
840
842
  var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
841
843
  var notificationSchema = Schema.union([
842
844
  Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
@@ -927,7 +929,7 @@ var linkFrameSchema = Schema.union([
927
929
  var InterconnectService = class extends Service {
928
930
  static inject = ["agents", "credentials"];
929
931
  static Config = Schema.object({
930
- instanceId: Schema.string().default("dsh"),
932
+ instanceId: Schema.string().max(MAX_INSTANCE_ID_CHARS).default("dsh"),
931
933
  requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
932
934
  peers: Schema.dict(Schema.string()).default({}),
933
935
  delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
@@ -937,7 +939,6 @@ var InterconnectService = class extends Service {
937
939
  requestTimeoutMs;
938
940
  delivery;
939
941
  allowResume;
940
- subscriptions = [];
941
942
  server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
942
943
  sockets = /* @__PURE__ */ new Set();
943
944
  /** Outbound peer links keyed by the peer's `instanceId`. */
@@ -994,28 +995,28 @@ var InterconnectService = class extends Service {
994
995
  socket.ping();
995
996
  }
996
997
  }, 3e4);
997
- this.subscriptions.push(ctx.on("agent/status", ({ agent, status }) => {
998
+ ctx.on("agent/status", ({ agent, status }) => {
998
999
  this.fanout({ kind: "agent/status", sessionId: String(agent.session.id), status });
999
- }));
1000
- this.subscriptions.push(ctx.on("agent/created", ({ agent }) => {
1000
+ });
1001
+ ctx.on("agent/created", ({ agent }) => {
1001
1002
  this.fanout({ kind: "agent/created", sessionId: String(agent.session.id) });
1002
- }));
1003
- this.subscriptions.push(ctx.on("agent/disposed", ({ agent }) => {
1003
+ });
1004
+ ctx.on("agent/disposed", ({ agent }) => {
1004
1005
  this.fanout({ kind: "agent/disposed", sessionId: String(agent.session.id) });
1005
- }));
1006
- this.subscriptions.push(ctx.on("session/created", (session) => {
1006
+ });
1007
+ ctx.on("session/created", (session) => {
1007
1008
  const parentSessionId = session.header.parentSession === void 0 ? void 0 : String(session.header.parentSession);
1008
1009
  this.fanout({
1009
1010
  kind: "session/created",
1010
1011
  sessionId: String(session.id),
1011
1012
  ...parentSessionId === void 0 ? {} : { parentSessionId }
1012
1013
  });
1013
- }));
1014
- this.subscriptions.push(ctx.on("session/disposed", (session) => {
1014
+ });
1015
+ ctx.on("session/disposed", (session) => {
1015
1016
  this.senders.delete(String(session.id));
1016
1017
  this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
1017
- }));
1018
- this.subscriptions.push(ctx.on("subagent/end", (info) => {
1018
+ });
1019
+ ctx.on("subagent/end", (info) => {
1019
1020
  if (!info.local) return;
1020
1021
  this.fanout({
1021
1022
  kind: "subagent/end",
@@ -1023,10 +1024,7 @@ var InterconnectService = class extends Service {
1023
1024
  childSessionId: String(info.id),
1024
1025
  stopReason: info.stopReason
1025
1026
  });
1026
- }));
1027
- ctx.effect(() => () => {
1028
- for (const dispose of this.subscriptions.splice(0)) dispose();
1029
- }, "interconnect: event subscriptions");
1027
+ });
1030
1028
  ctx.effect(() => () => {
1031
1029
  if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
1032
1030
  for (const state of this.linkStates.values()) state.close();
@@ -1123,22 +1121,30 @@ var InterconnectService = class extends Service {
1123
1121
  * Send a `msg` frame over the live link to a peer and resolve with its
1124
1122
  * correlated `msg-result`. Undefined when the peer has no live link here
1125
1123
  * (not configured, or the link is down) — sends can only address configured+
1126
- * connected peers by design.
1124
+ * connected peers by design — and a `message-too-large` result when the
1125
+ * encoded frame would not fit the link.
1127
1126
  */
1128
1127
  async msgRequest(instanceId, kind, payload) {
1129
1128
  const state = this.linkStates.get(instanceId);
1130
1129
  if (state === void 0 || !state.writable()) return void 0;
1131
1130
  const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
1132
- const message = {
1133
- kind,
1134
- sessionId: payload.sessionId,
1135
- text: payload.text,
1136
- ...payload.sender === void 0 ? {} : { sender: payload.sender },
1137
- ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1138
- ...payload.resume === void 0 ? {} : { resume: payload.resume }
1131
+ const frame = {
1132
+ type: "msg",
1133
+ reqId,
1134
+ message: {
1135
+ kind,
1136
+ sessionId: payload.sessionId,
1137
+ text: payload.text,
1138
+ ...payload.sender === void 0 ? {} : { sender: payload.sender },
1139
+ ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1140
+ ...payload.resume === void 0 ? {} : { resume: payload.resume }
1141
+ }
1139
1142
  };
1143
+ if (!frameFitsLink(frame)) {
1144
+ return { delivered: false, instance: instanceId, reason: "message-too-large" };
1145
+ }
1140
1146
  return this.waitForResult(reqId, "msg", (failure) => {
1141
- const wrote = state.sendFrame({ type: "msg", reqId, message });
1147
+ const wrote = state.sendFrame(frame);
1142
1148
  if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
1143
1149
  });
1144
1150
  }
@@ -1208,10 +1214,12 @@ var InterconnectService = class extends Service {
1208
1214
  * dialed link plus the peer's inbound one), so one event must leave over one
1209
1215
  * of them. Which one is decided by local ownership, not by the peer: a dialed
1210
1216
  * socket is a configured peer's link and always sends. An inbound socket is
1211
- * skipped only when a dialed link to the id that socket itself announced is
1212
- * open, so an announcement can suppress nothing but a duplicate the
1213
- * controlled link already carries — an impostor cannot drop an event for a
1214
- * peer, and without a dialed link every inbound socket receives it.
1217
+ * skipped only when an open dialed link covers the id that socket itself
1218
+ * announced — either because `Config.peers` lists that peer under the id, or
1219
+ * because the peer announced the id over that dialed link — so an
1220
+ * announcement can suppress nothing but a duplicate the controlled link
1221
+ * already carries. An impostor cannot drop an event for a peer, and without a
1222
+ * dialed link every inbound socket receives it.
1215
1223
  *
1216
1224
  * That holds once a peer's announcement has been seen. Between this
1217
1225
  * instance's dialed link opening and the peer's `hello` arriving on its
@@ -1230,7 +1238,10 @@ var InterconnectService = class extends Service {
1230
1238
  continue;
1231
1239
  }
1232
1240
  const dialedPeer = this.dialedPeerOf.get(socket);
1233
- if (dialedPeer !== void 0) covered.add(dialedPeer);
1241
+ if (dialedPeer === void 0) continue;
1242
+ covered.add(dialedPeer);
1243
+ const announced = this.peerOf.get(socket);
1244
+ if (announced !== void 0) covered.add(announced);
1234
1245
  }
1235
1246
  for (const socket of this.sockets) {
1236
1247
  if (this.dialedPeerOf.get(socket) === void 0) {
@@ -1262,12 +1273,7 @@ var InterconnectService = class extends Service {
1262
1273
  if (query.kind === "ping") {
1263
1274
  result = { pong: true, instance: this.instanceId };
1264
1275
  } else if (query.kind === "list") {
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);
1276
+ result = this.listSessions(this.listRowsBudgetBytes(reqId));
1271
1277
  } else {
1272
1278
  const payload = {
1273
1279
  sender: this.peerOf.get(socket) ?? "unknown-peer",
@@ -1299,10 +1305,11 @@ var InterconnectService = class extends Service {
1299
1305
  * Bytes one `list` answer may spend on its `sessions` rows: the frame cap
1300
1306
  * minus the serialized envelope that answer will carry. The envelope holds
1301
1307
  * the peer-supplied `reqId` and this instance's own id, so it is measured per
1302
- * answer instead of reserved with a fixed constant.
1308
+ * answer instead of reserved with a fixed constant. Both ids are bounded
1309
+ * ({@link MAX_REQUEST_ID_CHARS} and {@link MAX_INSTANCE_ID_CHARS} characters),
1310
+ * so the envelope always fits and the budget is always positive.
1303
1311
  * @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.
1312
+ * @returns the byte budget for the rows.
1306
1313
  */
1307
1314
  listRowsBudgetBytes(reqId) {
1308
1315
  const envelope = {