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.
@@ -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
- this.subscriptions.push(ctx.on("agent/status", ({ agent, status }) => {
1001
+ ctx.on("agent/status", ({ agent, status }) => {
1001
1002
  this.fanout({ kind: "agent/status", sessionId: String(agent.session.id), status });
1002
- }));
1003
- this.subscriptions.push(ctx.on("agent/created", ({ agent }) => {
1003
+ });
1004
+ ctx.on("agent/created", ({ agent }) => {
1004
1005
  this.fanout({ kind: "agent/created", sessionId: String(agent.session.id) });
1005
- }));
1006
- this.subscriptions.push(ctx.on("agent/disposed", ({ agent }) => {
1006
+ });
1007
+ ctx.on("agent/disposed", ({ agent }) => {
1007
1008
  this.fanout({ kind: "agent/disposed", sessionId: String(agent.session.id) });
1008
- }));
1009
- this.subscriptions.push(ctx.on("session/created", (session) => {
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
- this.subscriptions.push(ctx.on("session/disposed", (session) => {
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
- this.subscriptions.push(ctx.on("subagent/end", (info) => {
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 message = {
1136
- kind,
1137
- sessionId: payload.sessionId,
1138
- text: payload.text,
1139
- ...payload.sender === void 0 ? {} : { sender: payload.sender },
1140
- ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1141
- ...payload.resume === void 0 ? {} : { resume: payload.resume }
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({ type: "msg", reqId, message });
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 a dialed link to the id that socket itself announced is
1215
- * open, so an announcement can suppress nothing but a duplicate the
1216
- * controlled link already carries — an impostor cannot drop an event for a
1217
- * peer, and without a dialed link every inbound socket receives it.
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 !== void 0) covered.add(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
- const rowBudget = this.listRowsBudgetBytes(reqId);
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, negative when even an empty answer
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: "Message text delivered to the peer session."
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
  }