agent-comms 9.1.1 → 9.1.2

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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridges/user/web/request-handling.d.ts +26 -0
  5. package/dist/bridges/user/web/request-handling.d.ts.map +1 -0
  6. package/dist/bridges/user/web/request-handling.js +92 -0
  7. package/dist/bridges/user/web/request-handling.js.map +1 -0
  8. package/dist/bridges/user/web/server.d.ts +4 -0
  9. package/dist/bridges/user/web/server.d.ts.map +1 -1
  10. package/dist/bridges/user/web/server.js +88 -53
  11. package/dist/bridges/user/web/server.js.map +1 -1
  12. package/dist/bridges/user/web/web-access.d.ts.map +1 -1
  13. package/dist/bridges/user/web/web-access.js +14 -1
  14. package/dist/bridges/user/web/web-access.js.map +1 -1
  15. package/dist/bridges/user/web/web-assets.d.ts +2 -1
  16. package/dist/bridges/user/web/web-assets.d.ts.map +1 -1
  17. package/dist/bridges/user/web/web-assets.js +2 -2
  18. package/dist/bridges/user/web/web-assets.js.map +1 -1
  19. package/dist/bridges/user/web/web-console-static.d.ts.map +1 -1
  20. package/dist/bridges/user/web/web-console-static.js +34 -22
  21. package/dist/bridges/user/web/web-console-static.js.map +1 -1
  22. package/dist/core/detached-work.d.ts +14 -0
  23. package/dist/core/detached-work.d.ts.map +1 -0
  24. package/dist/core/detached-work.js +40 -0
  25. package/dist/core/detached-work.js.map +1 -0
  26. package/dist/core/election-sessions.d.ts.map +1 -1
  27. package/dist/core/election-sessions.js +6 -6
  28. package/dist/core/election-sessions.js.map +1 -1
  29. package/dist/core/hub-session.d.ts +10 -1
  30. package/dist/core/hub-session.d.ts.map +1 -1
  31. package/dist/core/hub-session.js +34 -32
  32. package/dist/core/hub-session.js.map +1 -1
  33. package/dist/core/message-limits.d.ts +14 -0
  34. package/dist/core/message-limits.d.ts.map +1 -0
  35. package/dist/core/message-limits.js +22 -0
  36. package/dist/core/message-limits.js.map +1 -0
  37. package/dist/core/peer-lifecycle.d.ts +2 -0
  38. package/dist/core/peer-lifecycle.d.ts.map +1 -1
  39. package/dist/core/peer-lifecycle.js +9 -4
  40. package/dist/core/peer-lifecycle.js.map +1 -1
  41. package/dist/core/pending-connection.d.ts +20 -1
  42. package/dist/core/pending-connection.d.ts.map +1 -1
  43. package/dist/core/pending-connection.js +27 -0
  44. package/dist/core/pending-connection.js.map +1 -1
  45. package/dist/core/relay-server.d.ts +15 -1
  46. package/dist/core/relay-server.d.ts.map +1 -1
  47. package/dist/core/relay-server.js +70 -16
  48. package/dist/core/relay-server.js.map +1 -1
  49. package/dist/core/room-router.d.ts.map +1 -1
  50. package/dist/core/room-router.js +31 -10
  51. package/dist/core/room-router.js.map +1 -1
  52. package/dist/core/tool.d.ts.map +1 -1
  53. package/dist/core/tool.js +3 -0
  54. package/dist/core/tool.js.map +1 -1
  55. package/dist/core/transport-events.d.ts.map +1 -1
  56. package/dist/core/transport-events.js +9 -5
  57. package/dist/core/transport-events.js.map +1 -1
  58. package/dist/core/wire-mesh-transport.d.ts +2 -2
  59. package/dist/core/wire-mesh-transport.d.ts.map +1 -1
  60. package/dist/core/wire-mesh-transport.js +96 -115
  61. package/dist/core/wire-mesh-transport.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/bridges/user/web/request-handling.ts +118 -0
  64. package/src/bridges/user/web/server.ts +109 -54
  65. package/src/bridges/user/web/web-access.ts +14 -1
  66. package/src/bridges/user/web/web-assets.ts +7 -3
  67. package/src/bridges/user/web/web-console-static.ts +34 -26
  68. package/src/core/detached-work.ts +55 -0
  69. package/src/core/election-sessions.ts +8 -4
  70. package/src/core/hub-session.ts +63 -41
  71. package/src/core/message-limits.ts +29 -0
  72. package/src/core/peer-lifecycle.ts +10 -4
  73. package/src/core/pending-connection.ts +48 -1
  74. package/src/core/relay-server.ts +110 -23
  75. package/src/core/room-router.ts +36 -9
  76. package/src/core/tool.ts +3 -0
  77. package/src/core/transport-events.ts +9 -5
  78. package/src/core/wire-mesh-transport.ts +58 -63
@@ -8,6 +8,7 @@
8
8
  * connect_request's accept/reject flow maps onto sendManageRequest's own request/response round trip directly, rather than a separate pair of connect_accepted/connect_rejected messages: manage-request-frame already tolerates arbitrary latency between a request and its response, so "waiting for a human to approve" is simply a manage-response that hasn't been sent yet, not a protocol gap needing its own mechanism. connect_request therefore holds its own IncomingManageRequest open (rather than responding immediately) until acceptConnection/rejectConnection is actually called.
9
9
  */
10
10
 
11
+ import { consumeEach, reportRejection } from "./detached-work.js";
11
12
  import { createTlsTransport } from "wire-mesh-core/adapters/tls-transport";
12
13
  import {
13
14
  ROOM_JOIN_APPROVAL_TIMEOUT_MS,
@@ -44,8 +45,7 @@ import { RelayRole } from "./relay-role.js";
44
45
  import { ALL_INTERFACES_HOST } from "./relay-server.js";
45
46
  import {
46
47
  DEFAULT_PENDING_CONNECTION_TIMEOUT_MS,
47
- expirePendingConnection,
48
- type ConnectionDecision,
48
+ holdConnectRequest,
49
49
  type PendingConnection,
50
50
  } from "./pending-connection.js";
51
51
  import {
@@ -435,18 +435,20 @@ export class WireMeshTransport implements MeshTransport {
435
435
  ): void {
436
436
  // Watched from the start, so any claim this session gossips before it introduces itself is read and dropped rather than held back and replayed once it is trusted.
437
437
  this.electionSessions.watch(session, handle);
438
- void (async () => {
439
- let approved = false;
440
- for await (const request of session.incomingManageRequests) {
441
- if (this.shutDown) break;
438
+ let approved = false;
439
+ // A request that cannot be handled is reported and the next is still read; the loop ends only where the session itself is being ended, and says so with "stop".
440
+ void consumeEach(
441
+ session.incomingManageRequests,
442
+ async (request) => {
443
+ if (this.shutDown) return "stop";
442
444
  if (approved) {
443
445
  await this.roomRouter.handleLocalRequest(request, handle);
444
- continue;
446
+ return "continue";
445
447
  }
446
448
  const message = extractMessage(request.command);
447
449
  if (message === undefined) {
448
450
  await request.respond({ result: "ok" }).catch(() => undefined);
449
- continue;
451
+ return "continue";
450
452
  }
451
453
  if (message.method === "introduce") {
452
454
  this.trackSession(handle.id, session);
@@ -458,48 +460,36 @@ export class WireMeshTransport implements MeshTransport {
458
460
  });
459
461
  await request.respond({ result: "ok" }).catch(() => undefined);
460
462
  approved = true;
461
- continue;
463
+ return "continue";
462
464
  }
463
465
  if (message.method === "connect_request") {
464
- // Held open deliberately (see this file's own header comment). Answered later by acceptConnection/rejectConnection, or by expirePendingConnection if neither happens within pendingConnectionTimeoutMs.
465
- const decision = await new Promise<ConnectionDecision>((resolve) => {
466
- this.pendingConnections.set(handle.id, {
467
- respond: request.respond,
468
- dataPort: message.dataPort,
469
- name: message.name,
470
- fingerprint: message.fingerprint,
471
- policy: handle.policy,
472
- session,
473
- resolve,
474
- // unref()'d so a stray pending connection (this timer failing to be cleared through some path not yet covered) can never by itself keep the process alive for up to pendingConnectionTimeoutMs after everything else is done -- confirmed necessary directly: shutdown() originally cleared every pendingConnections entry without clearing its timer, and the whole test process hung for the full default timeout before exiting.
475
- timeoutHandle: setTimeout(() => {
476
- expirePendingConnection(this.pendingConnections, handle.id);
477
- }, this.pendingConnectionTimeoutMs).unref(),
478
- });
479
- this.events.onConnectionRequest(handle, {
480
- peerId: handle.id,
481
- dataPort: message.dataPort,
482
- name: message.name,
483
- fingerprint: message.fingerprint,
484
- });
485
- });
466
+ // Held open deliberately (see this file's own header comment).
467
+ const decision = await holdConnectRequest(
468
+ {
469
+ pendingConnections: this.pendingConnections,
470
+ timeoutMs: this.pendingConnectionTimeoutMs,
471
+ events: this.events,
472
+ },
473
+ { request, message, handle, session },
474
+ );
486
475
  if (decision === "reject") {
487
476
  // Rejection can arrive via rejectConnection (session still open) or via shutdown/disconnect (session already closing) -- catch rather than assume which.
488
477
  await session.close().catch(() => undefined);
489
- return;
478
+ return "stop";
490
479
  }
491
480
  // acceptConnection has already called trackSession synchronously, before firing onIntroduction -- nothing left to do here beyond trusting subsequent requests on this same session.
492
481
  approved = true;
493
- continue;
482
+ return "continue";
494
483
  }
495
484
  // Any other message before introduce/connect_request is a protocol violation from an unapproved peer -- refuse and terminate rather than route it.
496
485
  await request
497
486
  .respond({ result: "error", code: "not_approved" })
498
487
  .catch(() => undefined);
499
488
  await session.close().catch(() => undefined);
500
- return;
501
- }
502
- })();
489
+ return "stop";
490
+ },
491
+ () => this.events.onError,
492
+ );
503
493
  }
504
494
 
505
495
  /** Opens a mesh session over an already-established connection, wired to this transport's own frame dispatch and advertised addresses. Every session this transport owns is built exactly this way, whether the connection was accepted by a listener or dialled at a peer, at the coordinator, or at a remote gateway, so the wiring lives here rather than being restated at each call site. Resolves undefined, with the session closed, when shutdown() has begun by the time the session is open, whether it began before the connection arrived or while the session was opening: shutdown() closes only the sessions it can see, so one opened after it started would stay open, and its peer would never learn that this side had gone (agent-comms#372). */
@@ -527,18 +517,28 @@ export class WireMeshTransport implements MeshTransport {
527
517
  machineLocal: boolean,
528
518
  ): void {
529
519
  this.roomRouter.drainSession(session, handle);
530
- this.drainRevocationAnnouncements(session);
520
+ // Reads every revocation-entry this session's peer announces, for as long as the session lives, handing each one to MeshStore via onRevocationAnnounce, one call per entry, matching revocationAnnouncements' own per-entry flattening of a revocation-announce frame's entries array. A gossiped revocation is not a manage-request and has no verb for roomRouter to dispatch, so it has its own independent drain.
521
+ // A revocation that cannot be handled is reported and the next one is still read: one bad entry must not end revocation intake on this session.
522
+ void consumeEach(
523
+ session.revocationAnnouncements,
524
+ (entry) => {
525
+ this.events.onRevocationAnnounce(entry);
526
+ return "continue";
527
+ },
528
+ () => this.events.onError,
529
+ );
531
530
  this.electionSessions.watch(session, handle);
532
531
  if (machineLocal) this.electionSessions.enrol(session, handle);
533
532
  }
534
533
 
535
- /** Reads every revocation-entry this session's peer announces, for as long as the session lives, handing each one to MeshStore via onRevocationAnnounce -- one call per entry, matching revocationAnnouncements' own per-entry flattening of a revocation-announce frame's entries array. */
536
- private drainRevocationAnnouncements(session: AcceptedMeshSession): void {
537
- void (async () => {
538
- for await (const entry of session.revocationAnnouncements) {
539
- this.events.onRevocationAnnounce(entry);
540
- }
541
- })();
534
+ /** The callback a listener calls with each connection it accepts, handling it without awaiting it: a failure goes to the transport's error handler, or stderr, and is never left as an unhandled rejection that would end the process. */
535
+ private accepting(accept: Readonly<AcceptOptions>) {
536
+ return (connection: Readonly<Connection>): void => {
537
+ reportRejection(
538
+ this.handleAcceptedConnection(connection, accept),
539
+ () => this.events.onError,
540
+ );
541
+ };
542
542
  }
543
543
 
544
544
  private watchForDisconnect(
@@ -546,10 +546,12 @@ export class WireMeshTransport implements MeshTransport {
546
546
  handle: Readonly<ConnectionHandle>,
547
547
  deviceIdHex: string,
548
548
  ): void {
549
- void (async () => {
550
- // The presence this session last reported. Every session event carries the whole directory, including events this side causes by sending a frame and the final closed event, so the peer's advert repeats long after the peer sent it. Only a change is news: re-reporting a stale advert would mark a departed peer's agent running again after the coordinator has set it offline, and nothing would ever correct that.
551
- let reportedPresence: AgentStatus | undefined;
552
- for await (const event of session.events) {
549
+ // The presence this session last reported. Every session event carries the whole directory, including events this side causes by sending a frame and the final closed event, so the peer's advert repeats long after the peer sent it. Only a change is news: re-reporting a stale advert would mark a departed peer's agent running again after the coordinator has set it offline, and nothing would ever correct that.
550
+ let reportedPresence: AgentStatus | undefined;
551
+ // A session event that cannot be handled is reported and the next is still read, so the closed event that ends this session's bookkeeping is never missed.
552
+ void consumeEach(
553
+ session.events,
554
+ (event) => {
553
555
  this.knownDevices.mergeLocal(event.directory);
554
556
  const presence = findPresenceAdvert(deviceIdHex, event.directory);
555
557
  if (presence !== undefined && presence !== reportedPresence) {
@@ -576,10 +578,12 @@ export class WireMeshTransport implements MeshTransport {
576
578
  if (wasTracked && !this.shutDown) {
577
579
  this.events.onPeerDisconnected(handle);
578
580
  }
579
- return;
581
+ return "stop";
580
582
  }
581
- }
582
- })();
583
+ return "continue";
584
+ },
585
+ () => this.events.onError,
586
+ );
583
587
  }
584
588
 
585
589
  // -----------------------------------------------------------------------
@@ -589,9 +593,7 @@ export class WireMeshTransport implements MeshTransport {
589
593
  async startDataServer(): Promise<void> {
590
594
  this.dataListener = await this.wireTransport.listen(
591
595
  `${COORDINATOR_HOST}:0`,
592
- (connection) => {
593
- void this.handleAcceptedConnection(connection, DATA_SERVER_ACCEPT);
594
- },
596
+ this.accepting(DATA_SERVER_ACCEPT),
595
597
  );
596
598
  this._dataPort = listenerPort(this.dataListener);
597
599
  }
@@ -642,9 +644,7 @@ export class WireMeshTransport implements MeshTransport {
642
644
  coordinatorListeners: this.coordinatorListeners,
643
645
  host,
644
646
  port,
645
- onAccepted: (connection) => {
646
- void this.handleAcceptedConnection(connection, DEFAULT_LISTENER_ACCEPT);
647
- },
647
+ onAccepted: this.accepting(DEFAULT_LISTENER_ACCEPT),
648
648
  isShuttingDown: () => this.isShuttingDown(),
649
649
  });
650
650
  this._isCoordinator = true;
@@ -907,12 +907,7 @@ export class WireMeshTransport implements MeshTransport {
907
907
  host,
908
908
  port,
909
909
  policy,
910
- onAccepted: (connection) => {
911
- void this.handleAcceptedConnection(
912
- connection,
913
- operatorListenerAccept(policy),
914
- );
915
- },
910
+ onAccepted: this.accepting(operatorListenerAccept(policy)),
916
911
  });
917
912
  }
918
913