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.
@@ -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 LIST_FRAME_ENVELOPE_BYTES = 4096;
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 message = {
1137
- kind,
1138
- sessionId: payload.sessionId,
1139
- text: payload.text,
1140
- ...payload.sender === void 0 ? {} : { sender: payload.sender },
1141
- ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1142
- ...payload.resume === void 0 ? {} : { resume: payload.resume }
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({ type: "msg", reqId, message });
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
- result = this.listSessions();
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, and the answer carries at most
1301
- * {@link MAX_LISTED_SESSIONS} rows so a large live set cannot push the
1302
- * `query-result` frame past the link's frame cap.
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 > MAX_LIST_ROWS_BYTES) {
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 > MAX_LIST_ROWS_BYTES) break;
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: "Message text delivered to the peer session."
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
  }