dsh-interconnect 0.11.13 → 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.
@@ -838,8 +838,7 @@ 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;
844
843
  var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
845
844
  var notificationSchema = Schema.union([
@@ -923,10 +922,10 @@ var msgResultSchema = Schema.union([
923
922
  var linkFrameSchema = Schema.union([
924
923
  Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
925
924
  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() })
925
+ Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), message: messageSchema.required() }),
926
+ Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: msgResultSchema.required() }),
927
+ Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), query: querySchema.required() }),
928
+ Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().max(MAX_REQUEST_ID_CHARS).required(), result: queryResultSchema.required() })
930
929
  ]);
931
930
  var InterconnectService = class extends Service {
932
931
  static inject = ["agents", "credentials"];
@@ -1216,6 +1215,12 @@ var InterconnectService = class extends Service {
1216
1215
  * open, so an announcement can suppress nothing but a duplicate the
1217
1216
  * controlled link already carries — an impostor cannot drop an event for a
1218
1217
  * peer, and without a dialed link every inbound socket receives it.
1218
+ *
1219
+ * That holds once a peer's announcement has been seen. Between this
1220
+ * instance's dialed link opening and the peer's `hello` arriving on its
1221
+ * inbound socket, the inbound socket is not yet attributable, so an event
1222
+ * emitted in that window leaves over both sockets and reaches that peer
1223
+ * twice. The duplicate is transient and never a dropped event.
1219
1224
  */
1220
1225
  broadcast(notification) {
1221
1226
  if (this.sockets.size === 0) return;
@@ -1260,7 +1265,12 @@ var InterconnectService = class extends Service {
1260
1265
  if (query.kind === "ping") {
1261
1266
  result = { pong: true, instance: this.instanceId };
1262
1267
  } else if (query.kind === "list") {
1263
- result = this.listSessions();
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);
1264
1274
  } else {
1265
1275
  const payload = {
1266
1276
  sender: this.peerOf.get(socket) ?? "unknown-peer",
@@ -1288,32 +1298,57 @@ var InterconnectService = class extends Service {
1288
1298
  this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
1289
1299
  });
1290
1300
  }
1301
+ /**
1302
+ * Bytes one `list` answer may spend on its `sessions` rows: the frame cap
1303
+ * minus the serialized envelope that answer will carry. The envelope holds
1304
+ * the peer-supplied `reqId` and this instance's own id, so it is measured per
1305
+ * answer instead of reserved with a fixed constant.
1306
+ * @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.
1309
+ */
1310
+ listRowsBudgetBytes(reqId) {
1311
+ const envelope = {
1312
+ type: "query-result",
1313
+ reqId,
1314
+ result: { sessions: [], instance: this.instanceId }
1315
+ };
1316
+ return MAX_LINK_FRAME_BYTES - Buffer.byteLength(JSON.stringify(envelope), "utf8");
1317
+ }
1291
1318
  /**
1292
1319
  * Summarize the live local sessions so a sender can discover valid targets
1293
1320
  * instead of having to know a session id already. Only live agents are listed
1294
- * because `send` can reach exactly those, and the answer carries at most
1295
- * {@link MAX_LISTED_SESSIONS} rows so a large live set cannot push the
1296
- * `query-result` frame past the link's frame cap.
1321
+ * because `send` can reach exactly those. The answer stops at
1322
+ * {@link MAX_LISTED_SESSIONS} rows or at the row budget the answer's own
1323
+ * envelope leaves, whichever comes first: that budget is what keeps the
1324
+ * complete `query-result` frame inside the link's cap, because a title's
1325
+ * length comes from the session-title projection's own bound rather than
1326
+ * from anything this listing controls. A title that does not fit the
1327
+ * remaining budget is dropped so its target stays listed; a row that does not
1328
+ * fit even untitled ends the listing, which keeps the answer a prefix of the
1329
+ * registration order instead of a set with gaps.
1297
1330
  *
1298
1331
  * Title and status are best-effort: the title projection is an optional
1299
1332
  * service, and a receiver without it still returns the ids. A projection that
1300
1333
  * throws degrades that one row rather than failing the whole listing, which
1301
1334
  * matches how the Host's own session listing treats its projection column.
1335
+ * @param rowBudgetBytes - bytes the `sessions` rows may occupy, envelope excluded.
1336
+ * @returns the bounded listing.
1302
1337
  */
1303
- listSessions() {
1338
+ listSessions(rowBudgetBytes) {
1304
1339
  const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1305
1340
  const sessions = [];
1306
1341
  let used = 0;
1307
1342
  for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
1308
1343
  const row = this.sessionRow(agent);
1309
1344
  const sized = used + rowBytes(row);
1310
- if (sized > MAX_LIST_ROWS_BYTES) {
1345
+ if (sized > rowBudgetBytes) {
1311
1346
  const untitled = {
1312
1347
  sessionId: row.sessionId,
1313
1348
  ...row.status === void 0 ? {} : { status: row.status }
1314
1349
  };
1315
1350
  const untitledSized = used + rowBytes(untitled);
1316
- if (untitledSized > MAX_LIST_ROWS_BYTES) break;
1351
+ if (untitledSized > rowBudgetBytes) break;
1317
1352
  sessions.push(untitled);
1318
1353
  used = untitledSized;
1319
1354
  continue;
@@ -1785,7 +1820,7 @@ function apply(ctx) {
1785
1820
  case "resume-refused":
1786
1821
  return `not delivered: ${value.instance} does not allow waking persisted sessions`;
1787
1822
  case "resume-failed":
1788
- return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no such persisted session, or another owner holds it)`;
1823
+ return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no persisted session under that id, or the wake itself failed)`;
1789
1824
  case "session-owned-by-subagent":
1790
1825
  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`;
1791
1826
  default: