agent-comms 8.15.8 → 8.15.10

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 (184) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/bridges/user/web/server.d.ts.map +1 -1
  4. package/dist/bridges/user/web/server.js +44 -24
  5. package/dist/bridges/user/web/server.js.map +1 -1
  6. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
  7. package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
  8. package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
  9. package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
  10. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
  11. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
  12. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
  13. package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
  14. package/dist/bridges/user/web/web-assets.d.ts +28 -0
  15. package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
  16. package/dist/bridges/user/web/web-assets.js +67 -0
  17. package/dist/bridges/user/web/web-assets.js.map +1 -0
  18. package/package.json +8 -2
  19. package/src/account-cli.ts +195 -0
  20. package/src/account-join-cli.ts +191 -0
  21. package/src/bridges/cc-peer/approval-commands.ts +82 -0
  22. package/src/bridges/cc-peer/bridge.ts +138 -0
  23. package/src/bridges/cc-peer/default-front.ts +58 -0
  24. package/src/bridges/cc-peer/front-controller.ts +160 -0
  25. package/src/bridges/cc-peer/front-relay.ts +191 -0
  26. package/src/bridges/cc-peer/front-runtime.ts +167 -0
  27. package/src/bridges/cc-peer/front.ts +59 -0
  28. package/src/bridges/cc-peer/reply-aliases.ts +90 -0
  29. package/src/bridges/cc-peer/run.ts +100 -0
  30. package/src/bridges/cc-peer/shared-peer.ts +28 -0
  31. package/src/bridges/claude-code/channel.ts +318 -0
  32. package/src/bridges/codex/tool.ts +132 -0
  33. package/src/bridges/mcp/server.ts +137 -0
  34. package/src/bridges/opencode/plugin.ts +130 -0
  35. package/src/bridges/pi/extension.ts +330 -0
  36. package/src/bridges/registry.ts +17 -0
  37. package/src/bridges/user/cli.ts +87 -0
  38. package/src/bridges/user/controller.ts +345 -0
  39. package/src/bridges/user/tui.ts +300 -0
  40. package/src/bridges/user/web/actions.ts +173 -0
  41. package/src/bridges/user/web/contract.ts +222 -0
  42. package/src/bridges/user/web/dist/bundle.js +49 -0
  43. package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
  44. package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
  45. package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
  46. package/src/bridges/user/web/dist/index.html +21 -0
  47. package/src/bridges/user/web/dist/manifest.json +26 -0
  48. package/src/bridges/user/web/dist/mesh-worker.js +64 -0
  49. package/src/bridges/user/web/dist/sw.js +1 -0
  50. package/src/bridges/user/web/event-publisher.ts +114 -0
  51. package/src/bridges/user/web/port-discovery.ts +49 -0
  52. package/src/bridges/user/web/reads.ts +35 -0
  53. package/src/bridges/user/web/router.ts +158 -0
  54. package/src/bridges/user/web/server.ts +782 -0
  55. package/src/bridges/user/web/vite.config.ts +71 -0
  56. package/src/bridges/user/web/web-access.ts +404 -0
  57. package/src/bridges/user/web/web-assets.ts +90 -0
  58. package/src/bridges/user/web/web-beacon.ts +69 -0
  59. package/src/bridges/user/web/web-console-static.ts +90 -0
  60. package/src/cli-main.ts +635 -0
  61. package/src/cli.ts +13 -0
  62. package/src/core/account-bundle.ts +264 -0
  63. package/src/core/account-join.ts +289 -0
  64. package/src/core/account-ledger-crypto.ts +154 -0
  65. package/src/core/account-ledger-store.ts +66 -0
  66. package/src/core/account-ledger.ts +385 -0
  67. package/src/core/agent-details.ts +216 -0
  68. package/src/core/agent-list-format.ts +97 -0
  69. package/src/core/agent-registry.ts +282 -0
  70. package/src/core/agent-table.ts +58 -0
  71. package/src/core/atomic-file.ts +46 -0
  72. package/src/core/bind-retry.ts +38 -0
  73. package/src/core/bridge-mesh.ts +176 -0
  74. package/src/core/bridge.ts +718 -0
  75. package/src/core/capability-ask.ts +162 -0
  76. package/src/core/capability-bubble-up.ts +237 -0
  77. package/src/core/comms-store.ts +130 -0
  78. package/src/core/connection-approval.ts +134 -0
  79. package/src/core/connection-code-tool.ts +92 -0
  80. package/src/core/connection-code.ts +296 -0
  81. package/src/core/coordinator-role.ts +224 -0
  82. package/src/core/data-directory.ts +32 -0
  83. package/src/core/data-frame-routing.ts +309 -0
  84. package/src/core/default-command.ts +12 -0
  85. package/src/core/delegation-policy.ts +41 -0
  86. package/src/core/delivery-engine.ts +649 -0
  87. package/src/core/device-membership-verification.ts +125 -0
  88. package/src/core/device-membership.ts +130 -0
  89. package/src/core/directory-admission.ts +155 -0
  90. package/src/core/discovery-mdns.ts +247 -0
  91. package/src/core/discovery-tailscale.ts +224 -0
  92. package/src/core/discovery.ts +303 -0
  93. package/src/core/display-name.ts +73 -0
  94. package/src/core/dm-grant-actions.ts +72 -0
  95. package/src/core/dm-send-delegation.ts +73 -0
  96. package/src/core/dm-token-verification.ts +76 -0
  97. package/src/core/election-sessions.ts +135 -0
  98. package/src/core/error-reporting.ts +85 -0
  99. package/src/core/first-contact.ts +242 -0
  100. package/src/core/gateway-trust-actions.ts +184 -0
  101. package/src/core/gateway-trust.ts +305 -0
  102. package/src/core/gossip-directory.ts +276 -0
  103. package/src/core/gossip-extensions.ts +123 -0
  104. package/src/core/group-proofs.ts +205 -0
  105. package/src/core/handshake.ts +354 -0
  106. package/src/core/hub-forwarding.ts +24 -0
  107. package/src/core/hub-link.ts +176 -0
  108. package/src/core/hub-session.ts +374 -0
  109. package/src/core/identity-store.ts +715 -0
  110. package/src/core/identity.ts +489 -0
  111. package/src/core/index.ts +65 -0
  112. package/src/core/issuer-grouping.ts +165 -0
  113. package/src/core/issuer-identity-file.ts +187 -0
  114. package/src/core/lan-web-actions.ts +34 -0
  115. package/src/core/lan-web-uis.ts +55 -0
  116. package/src/core/ledger-lock.ts +81 -0
  117. package/src/core/listener-registry.ts +133 -0
  118. package/src/core/loopback-address.ts +22 -0
  119. package/src/core/machine-identity.ts +77 -0
  120. package/src/core/machine-list-groups.ts +53 -0
  121. package/src/core/mcp-registry-retry.ts +69 -0
  122. package/src/core/mcp-registry-version.ts +18 -0
  123. package/src/core/membership-proof.ts +139 -0
  124. package/src/core/membership-proofs.ts +149 -0
  125. package/src/core/mesh-graph-trace-tool.ts +81 -0
  126. package/src/core/mesh-graph.ts +89 -0
  127. package/src/core/mesh-network-actions.ts +183 -0
  128. package/src/core/mesh-store-options.ts +23 -0
  129. package/src/core/mesh-store-shared.ts +134 -0
  130. package/src/core/mesh-store.ts +1114 -0
  131. package/src/core/name-claim.ts +145 -0
  132. package/src/core/naming-actions.ts +90 -0
  133. package/src/core/naming.ts +138 -0
  134. package/src/core/nanoid.ts +20 -0
  135. package/src/core/network-interfaces.ts +24 -0
  136. package/src/core/package-version.ts +34 -0
  137. package/src/core/peer-lifecycle.ts +377 -0
  138. package/src/core/peer-link-error.ts +41 -0
  139. package/src/core/peer-versions.ts +98 -0
  140. package/src/core/pending-connection.ts +53 -0
  141. package/src/core/petnames.ts +121 -0
  142. package/src/core/pgp-keyserver.ts +32 -0
  143. package/src/core/pid-lock-file.ts +122 -0
  144. package/src/core/push-manager.ts +76 -0
  145. package/src/core/random-id.ts +9 -0
  146. package/src/core/receiver-set.ts +42 -0
  147. package/src/core/relay-role.ts +182 -0
  148. package/src/core/relay-selection.ts +120 -0
  149. package/src/core/relay-server.ts +198 -0
  150. package/src/core/relay-uplink.ts +78 -0
  151. package/src/core/request-timeouts.ts +37 -0
  152. package/src/core/room-discovery.ts +48 -0
  153. package/src/core/room-lifecycle.ts +838 -0
  154. package/src/core/room-lookup.ts +41 -0
  155. package/src/core/room-messaging.ts +247 -0
  156. package/src/core/room-notice-log.ts +61 -0
  157. package/src/core/room-path.ts +84 -0
  158. package/src/core/room-protocol.ts +893 -0
  159. package/src/core/room-router.ts +180 -0
  160. package/src/core/room-token-verification.ts +85 -0
  161. package/src/core/room-wire-extensions.ts +60 -0
  162. package/src/core/self-name-claim.ts +67 -0
  163. package/src/core/send-outcome.ts +69 -0
  164. package/src/core/shutdown-signals.ts +57 -0
  165. package/src/core/stale-agent-checker.ts +108 -0
  166. package/src/core/store.ts +704 -0
  167. package/src/core/token-text.ts +40 -0
  168. package/src/core/tool.ts +853 -0
  169. package/src/core/transport-events.ts +73 -0
  170. package/src/core/transport-queries.ts +54 -0
  171. package/src/core/transport.ts +367 -0
  172. package/src/core/types.ts +550 -0
  173. package/src/core/user-identity.ts +290 -0
  174. package/src/core/vapid.ts +81 -0
  175. package/src/core/version-check.ts +147 -0
  176. package/src/core/version-report-actions.ts +79 -0
  177. package/src/core/web-beacon.ts +31 -0
  178. package/src/core/web-push.ts +322 -0
  179. package/src/core/wire-mesh-core-version.ts +25 -0
  180. package/src/core/wire-mesh-identity.ts +36 -0
  181. package/src/core/wire-mesh-transport-options.ts +82 -0
  182. package/src/core/wire-mesh-transport.ts +1007 -0
  183. package/src/core/wire-protocol.ts +153 -0
  184. package/src/core/ws-dial.ts +191 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * In-memory, resumable event publisher for the web UI's subscribeEvents stream.
3
+ *
4
+ * oRPC v2 beta (`\@orpc/server`, `\@orpc/client`, `\@orpc/contract` at 2.0.0-beta.35 -- confirmed against the actual published tarballs, not just docs) ships no MemoryPublisher/EventPublisher class. This is a hand-written equivalent built on the primitives that do exist: a plain async generator (satisfies AsyncIteratorObject on its own) and withEventMeta (from `\@standard-server/core`, re-exported by `\@orpc/server`) to tag each published event with a monotonic id oRPC's own RetryLinkPlugin already knows how to read back as lastEventId on reconnect.
5
+ *
6
+ * One instance per WebServerHandle -- not a module-level singleton -- so multiple concurrent web server instances (multiple bridges each starting their own web UI) never share replay buffers.
7
+ */
8
+
9
+ import { withEventMeta } from "@orpc/server";
10
+ import type { MeshEvent } from "./contract.js";
11
+
12
+ /** A plain function call is opaque to TS's control-flow narrowing, unlike a direct `signal?.aborted` comparison -- narrowing that across an `await` (the abort can genuinely flip mid-wait) produces a false "these types have no overlap" error at the re-check below the await. */
13
+ function isAborted(signal: AbortSignal | undefined): boolean {
14
+ return signal?.aborted === true;
15
+ }
16
+
17
+ export interface MeshEventPublisherOptions {
18
+ /** How long a published event stays available for replay by a resuming subscriber. */
19
+ retentionMs?: number;
20
+ }
21
+
22
+ const DEFAULT_RETENTION_MS = 60_000;
23
+
24
+ interface BufferedEvent {
25
+ id: string;
26
+ event: MeshEvent;
27
+ publishedAt: number;
28
+ }
29
+
30
+ export interface MeshEventSubscribeOptions {
31
+ signal?: AbortSignal;
32
+ lastEventId?: string;
33
+ }
34
+
35
+ export class MeshEventPublisher {
36
+ private seq = 0;
37
+ private readonly buffer: BufferedEvent[] = [];
38
+ private readonly listeners = new Set<(event: MeshEvent) => void>();
39
+ private readonly retentionMs: number;
40
+
41
+ constructor(options: Readonly<MeshEventPublisherOptions> = {}) {
42
+ this.retentionMs = options.retentionMs ?? DEFAULT_RETENTION_MS;
43
+ }
44
+
45
+ publish(event: MeshEvent): void {
46
+ this.prune();
47
+ const id = String(++this.seq);
48
+ const tagged = withEventMeta(event, { id });
49
+ this.buffer.push({ id, event: tagged, publishedAt: Date.now() });
50
+ for (const listener of this.listeners) listener(tagged);
51
+ }
52
+
53
+ private prune(): void {
54
+ const cutoff = Date.now() - this.retentionMs;
55
+ while (
56
+ this.buffer.length > 0 &&
57
+ (this.buffer[0]?.publishedAt ?? Infinity) < cutoff
58
+ ) {
59
+ this.buffer.shift();
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Replays buffered events after `lastEventId` (or every buffered event, if a resuming subscriber's `lastEventId` has already aged out of the retention window), then yields new events as they're published until `signal` aborts.
65
+ */
66
+ async *subscribe(
67
+ options: Readonly<MeshEventSubscribeOptions> = {},
68
+ ): AsyncGenerator<MeshEvent, void, void> {
69
+ const { signal, lastEventId } = options;
70
+ this.prune();
71
+
72
+ if (lastEventId !== undefined) {
73
+ const startIndex = this.buffer.findIndex((b) => b.id === lastEventId);
74
+ const replay =
75
+ startIndex === -1 ? this.buffer : this.buffer.slice(startIndex + 1);
76
+ for (const buffered of replay) {
77
+ if (isAborted(signal)) return;
78
+ yield buffered.event;
79
+ }
80
+ }
81
+
82
+ const queue: MeshEvent[] = [];
83
+ let wake: (() => void) | undefined;
84
+ const listener = (event: MeshEvent): void => {
85
+ queue.push(event);
86
+ wake?.();
87
+ };
88
+ this.listeners.add(listener);
89
+
90
+ try {
91
+ for (;;) {
92
+ if (isAborted(signal)) return;
93
+ if (queue.length === 0) {
94
+ await new Promise<void>((resolve) => {
95
+ wake = resolve;
96
+ signal?.addEventListener(
97
+ "abort",
98
+ () => {
99
+ resolve();
100
+ },
101
+ { once: true },
102
+ );
103
+ });
104
+ wake = undefined;
105
+ if (isAborted(signal)) return;
106
+ }
107
+ const next = queue.shift();
108
+ if (next !== undefined) yield next;
109
+ }
110
+ } finally {
111
+ this.listeners.delete(listener);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Port discovery — walk up from a base port to find a free one.
3
+ *
4
+ * Extracted from server.ts so the logic can be unit-tested without
5
+ * loading the full server module (which reads built assets at import time).
6
+ */
7
+
8
+ import * as http from "node:http";
9
+
10
+ const WEB_HOST = "127.0.0.1";
11
+
12
+ /** Maximum number of ports to try when searching for a free one. */
13
+ export const DEFAULT_MAX_ATTEMPTS = 10;
14
+
15
+ /**
16
+ * Try binding sequentially from `base`.
17
+ * Returns the first port that succeeds, or undefined if all are taken.
18
+ */
19
+ export async function findFreePort(
20
+ base: number,
21
+ maxAttempts: number = DEFAULT_MAX_ATTEMPTS,
22
+ ): Promise<number | undefined> {
23
+ return new Promise((resolve) => {
24
+ let attempts = 0;
25
+
26
+ function tryPort(port: number): void {
27
+ if (attempts >= maxAttempts) {
28
+ resolve(undefined);
29
+ return;
30
+ }
31
+ attempts++;
32
+
33
+ const probe = http.createServer();
34
+ probe.on("error", () => {
35
+ probe.close();
36
+ tryPort(port + 1);
37
+ });
38
+ probe.listen(port, WEB_HOST, () => {
39
+ const addr = probe.address();
40
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
41
+ probe.close(() => {
42
+ resolve(actualPort);
43
+ });
44
+ });
45
+ }
46
+
47
+ tryPort(base);
48
+ });
49
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Plain read functions -- one per structured one-shot read (agent-comms#206), extracted so both the REST handlers in server.ts and the new oRPC procedures in router.ts call the same implementation instead of two copies drifting apart, the same de-duplication actions.ts already established for mutations.
3
+ *
4
+ * Deliberately excludes agents/rooms: those are already fully covered by subscribeEvents' state_sync/state_patch stream (router.ts), so there is no REST-vs-oRPC pair to de-duplicate for them.
5
+ */
6
+
7
+ import type { RoomMessage } from "../../../core/types.js";
8
+ import type { ChatController } from "../controller.js";
9
+ import type { DisplayNameParts } from "../../../core/display-name.js";
10
+ import type { MeshGraph, MeshTraceResult } from "./contract.js";
11
+
12
+ export async function getRoomMessagesRead(
13
+ controller: ChatController,
14
+ input: Readonly<{ room: string; since?: string | undefined }>,
15
+ ): Promise<RoomMessage[]> {
16
+ return controller.getRoomMessages(input.room, input.since);
17
+ }
18
+
19
+ export function getMeshGraphRead(controller: ChatController): MeshGraph {
20
+ return controller.meshStore.meshGraph();
21
+ }
22
+
23
+ /** Every id this bridge's viewer has a name for (a petname or a known self name), for the dashboard to label ids by the shared display convention (agent-comms#345). */
24
+ export async function getDisplayNamesRead(
25
+ controller: ChatController,
26
+ ): Promise<DisplayNameParts[]> {
27
+ return controller.meshStore.naming.nameParts(controller.agentId);
28
+ }
29
+
30
+ export async function getMeshTraceRead(
31
+ controller: ChatController,
32
+ input: Readonly<{ target: string; timeoutMs?: number | undefined }>,
33
+ ): Promise<MeshTraceResult> {
34
+ return controller.meshStore.meshTrace(input.target, input.timeoutMs);
35
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * oRPC router -- implements meshContract against a real ChatController.
3
+ *
4
+ * Every mutating procedure is a one-line passthrough to the plain action functions in actions.ts, the same functions the legacy REST /api/action dispatcher calls, so both paths share one implementation. subscribeEvents is the one genuinely new procedure: it unifies the old chat socket's delivery frames and the old mesh socket's state_sync/state_update methods into a single resumable stream, backed by a per-handle MeshEventPublisher.
5
+ */
6
+
7
+ import { implement } from "@orpc/server";
8
+ import type { PushManager } from "../../../core/push-manager.js";
9
+ import type { ChatController } from "../controller.js";
10
+ import {
11
+ createRoomAction,
12
+ declineInviteAction,
13
+ destroyRoomAction,
14
+ dmAction,
15
+ inviteAction,
16
+ joinRoomAction,
17
+ kickAction,
18
+ leaveRoomAction,
19
+ listAgentsAction,
20
+ listRoomsAction,
21
+ pushSubscribeAction,
22
+ pushUnsubscribeAction,
23
+ readRoomAction,
24
+ renameAgentAction,
25
+ sendAction,
26
+ } from "./actions.js";
27
+ import { meshContract } from "./contract.js";
28
+ import type { SerialisedState } from "./contract.js";
29
+ import type { MeshEventPublisher } from "./event-publisher.js";
30
+ import {
31
+ getMeshGraphRead,
32
+ getDisplayNamesRead,
33
+ getMeshTraceRead,
34
+ getRoomMessagesRead,
35
+ } from "./reads.js";
36
+
37
+ export interface MeshRouterContext {
38
+ controller: ChatController;
39
+ publisher: MeshEventPublisher;
40
+ pushManager: PushManager | undefined;
41
+ }
42
+
43
+ /** Drops deliveryQueues -- mesh-internal replay state for offline agents, not something a browser tab needs to see -- from a MeshStore snapshot before it goes out as a state_sync event. */
44
+ function toBrowserState(state: {
45
+ agents: SerialisedState["agents"];
46
+ rooms: SerialisedState["rooms"];
47
+ messages: SerialisedState["messages"];
48
+ dms: SerialisedState["dms"];
49
+ }): SerialisedState {
50
+ return {
51
+ agents: state.agents,
52
+ rooms: state.rooms,
53
+ messages: state.messages,
54
+ dms: state.dms,
55
+ };
56
+ }
57
+
58
+ const impl = implement(meshContract).$context<MeshRouterContext>();
59
+
60
+ export const meshRouter = {
61
+ send: impl.send.handler(async ({ context, input }) =>
62
+ sendAction(context.controller, input),
63
+ ),
64
+
65
+ dm: impl.dm.handler(async ({ context, input }) =>
66
+ dmAction(context.controller, input),
67
+ ),
68
+
69
+ joinRoom: impl.joinRoom.handler(async ({ context, input }) =>
70
+ joinRoomAction(context.controller, input),
71
+ ),
72
+
73
+ leaveRoom: impl.leaveRoom.handler(async ({ context, input }) =>
74
+ leaveRoomAction(context.controller, input),
75
+ ),
76
+
77
+ createRoom: impl.createRoom.handler(async ({ context, input }) =>
78
+ createRoomAction(context.controller, input),
79
+ ),
80
+
81
+ listRooms: impl.listRooms.handler(async ({ context }) =>
82
+ listRoomsAction(context.controller),
83
+ ),
84
+
85
+ listAgents: impl.listAgents.handler(async ({ context }) =>
86
+ listAgentsAction(context.controller),
87
+ ),
88
+
89
+ readRoom: impl.readRoom.handler(async ({ context, input }) =>
90
+ readRoomAction(context.controller, input),
91
+ ),
92
+
93
+ destroyRoom: impl.destroyRoom.handler(async ({ context, input }) =>
94
+ destroyRoomAction(context.controller, input),
95
+ ),
96
+
97
+ invite: impl.invite.handler(async ({ context, input }) =>
98
+ inviteAction(context.controller, input),
99
+ ),
100
+
101
+ declineInvite: impl.declineInvite.handler(async ({ context, input }) =>
102
+ declineInviteAction(context.controller, input),
103
+ ),
104
+
105
+ kick: impl.kick.handler(async ({ context, input }) =>
106
+ kickAction(context.controller, input),
107
+ ),
108
+
109
+ renameAgent: impl.renameAgent.handler(async ({ context, input }) =>
110
+ renameAgentAction(context.controller, input),
111
+ ),
112
+
113
+ pushSubscribe: impl.pushSubscribe.handler(({ context, input }) =>
114
+ pushSubscribeAction(context.controller, context.pushManager, input),
115
+ ),
116
+
117
+ pushUnsubscribe: impl.pushUnsubscribe.handler(({ context, input }) =>
118
+ pushUnsubscribeAction(context.controller, context.pushManager, input),
119
+ ),
120
+
121
+ getRoomMessages: impl.getRoomMessages.handler(async ({ context, input }) =>
122
+ getRoomMessagesRead(context.controller, input),
123
+ ),
124
+
125
+ getMeshGraph: impl.getMeshGraph.handler(({ context }) =>
126
+ getMeshGraphRead(context.controller),
127
+ ),
128
+
129
+ getDisplayNames: impl.getDisplayNames.handler(async ({ context }) =>
130
+ getDisplayNamesRead(context.controller),
131
+ ),
132
+
133
+ getMeshTrace: impl.getMeshTrace.handler(async ({ context, input }) =>
134
+ getMeshTraceRead(context.controller, input),
135
+ ),
136
+
137
+ subscribeEvents: impl.subscribeEvents.handler(async function* ({
138
+ context,
139
+ input,
140
+ signal,
141
+ lastEventId,
142
+ }) {
143
+ // lastEventId is set two ways: oRPC's own RetryLinkPlugin threads it automatically on a mid-stream reconnect (the protocol-level `lastEventId` above); a tab that reconnects from cold (a fresh subscribeEvents call, e.g. after being closed and reopened) has to pass it explicitly as input instead, since there's no in-flight call for the protocol layer to resume.
144
+ const resumeFrom = lastEventId ?? input.lastEventId;
145
+
146
+ if (resumeFrom === undefined) {
147
+ yield {
148
+ kind: "state_sync" as const,
149
+ state: toBrowserState(context.controller.meshStore.serialise()),
150
+ };
151
+ }
152
+
153
+ yield* context.publisher.subscribe({
154
+ ...(signal ? { signal } : {}),
155
+ ...(resumeFrom !== undefined ? { lastEventId: resumeFrom } : {}),
156
+ });
157
+ }),
158
+ };