@masons/agent-network 0.5.28 → 0.5.30

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 (72) hide show
  1. package/dist/broker/broker-daemon.d.ts +34 -0
  2. package/dist/broker/broker-daemon.d.ts.map +1 -0
  3. package/dist/broker/broker-daemon.js +471 -0
  4. package/dist/broker/closed-endpoint-lookup.d.ts +25 -0
  5. package/dist/broker/closed-endpoint-lookup.d.ts.map +1 -0
  6. package/dist/broker/closed-endpoint-lookup.js +59 -0
  7. package/dist/broker/connector-ws.d.ts +47 -0
  8. package/dist/broker/connector-ws.d.ts.map +1 -0
  9. package/dist/broker/connector-ws.js +60 -0
  10. package/dist/broker/control-event-dispatcher.d.ts +20 -0
  11. package/dist/broker/control-event-dispatcher.d.ts.map +1 -0
  12. package/dist/broker/control-event-dispatcher.js +45 -0
  13. package/dist/broker/control-event-types.d.ts +30 -0
  14. package/dist/broker/control-event-types.d.ts.map +1 -0
  15. package/dist/broker/control-event-types.js +1 -0
  16. package/dist/broker/correlation-ring.d.ts +10 -0
  17. package/dist/broker/correlation-ring.d.ts.map +1 -0
  18. package/dist/broker/correlation-ring.js +32 -0
  19. package/dist/broker/discovery-file.d.ts +12 -0
  20. package/dist/broker/discovery-file.d.ts.map +1 -0
  21. package/dist/broker/discovery-file.js +77 -0
  22. package/dist/broker/endpoint-registry.d.ts +53 -0
  23. package/dist/broker/endpoint-registry.d.ts.map +1 -0
  24. package/dist/broker/endpoint-registry.js +83 -0
  25. package/dist/broker/endpoint-state-machine.d.ts +40 -0
  26. package/dist/broker/endpoint-state-machine.d.ts.map +1 -0
  27. package/dist/broker/endpoint-state-machine.js +92 -0
  28. package/dist/broker/entry.d.ts +5 -0
  29. package/dist/broker/entry.d.ts.map +1 -0
  30. package/dist/broker/entry.js +123 -0
  31. package/dist/broker/grace-timer.d.ts +9 -0
  32. package/dist/broker/grace-timer.d.ts.map +1 -0
  33. package/dist/broker/grace-timer.js +34 -0
  34. package/dist/broker/ipc-server.d.ts +72 -0
  35. package/dist/broker/ipc-server.d.ts.map +1 -0
  36. package/dist/broker/ipc-server.js +243 -0
  37. package/dist/broker/logger.d.ts +10 -0
  38. package/dist/broker/logger.d.ts.map +1 -0
  39. package/dist/broker/logger.js +34 -0
  40. package/dist/broker/paths.d.ts +11 -0
  41. package/dist/broker/paths.d.ts.map +1 -0
  42. package/dist/broker/paths.js +30 -0
  43. package/dist/broker/plugin-liveness.d.ts +2 -0
  44. package/dist/broker/plugin-liveness.d.ts.map +1 -0
  45. package/dist/broker/plugin-liveness.js +15 -0
  46. package/dist/broker/reconnecting-buffer.d.ts +23 -0
  47. package/dist/broker/reconnecting-buffer.d.ts.map +1 -0
  48. package/dist/broker/reconnecting-buffer.js +107 -0
  49. package/dist/broker/routing-table.d.ts +22 -0
  50. package/dist/broker/routing-table.d.ts.map +1 -0
  51. package/dist/broker/routing-table.js +35 -0
  52. package/dist/broker/runtime-endpoint-port.d.ts +8 -0
  53. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -0
  54. package/dist/broker/runtime-endpoint-port.js +1 -0
  55. package/dist/broker/undispatched-inbox.d.ts +26 -0
  56. package/dist/broker/undispatched-inbox.d.ts.map +1 -0
  57. package/dist/broker/undispatched-inbox.js +53 -0
  58. package/dist/broker/version-handshake.d.ts +30 -0
  59. package/dist/broker/version-handshake.d.ts.map +1 -0
  60. package/dist/broker/version-handshake.js +47 -0
  61. package/dist/broker-client/broker-client.d.ts +64 -0
  62. package/dist/broker-client/broker-client.d.ts.map +1 -0
  63. package/dist/broker-client/broker-client.js +164 -0
  64. package/dist/broker-client/lazy-spawn.d.ts +18 -0
  65. package/dist/broker-client/lazy-spawn.d.ts.map +1 -0
  66. package/dist/broker-client/lazy-spawn.js +61 -0
  67. package/dist/runtime-endpoint-client.d.ts +9 -2
  68. package/dist/runtime-endpoint-client.d.ts.map +1 -1
  69. package/dist/runtime-endpoint-client.js +15 -2
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/package.json +11 -2
@@ -0,0 +1,34 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { ClosedEndpointLookup } from "./closed-endpoint-lookup.js";
3
+ import type { ConnectorWS } from "./connector-ws.js";
4
+ import { type ControlEventDispatcher } from "./control-event-dispatcher.js";
5
+ import { type EndpointState } from "./endpoint-state-machine.js";
6
+ import type { BrokerLogger } from "./logger.js";
7
+ import type { BrokerPaths } from "./paths.js";
8
+ import { type BufferedMessage } from "./reconnecting-buffer.js";
9
+ import type { RuntimeEndpointPort } from "./runtime-endpoint-port.js";
10
+ import { type UndispatchedInbox } from "./undispatched-inbox.js";
11
+ export interface BrokerDaemonOptions {
12
+ paths: BrokerPaths;
13
+ asNodeId: string;
14
+ connector: ConnectorWS;
15
+ apiPort: RuntimeEndpointPort;
16
+ logger: BrokerLogger;
17
+ closedEndpointLookup?: ClosedEndpointLookup;
18
+ inboxCapacity?: number;
19
+ graceMs?: number;
20
+ livenessProbe?: (plugin_pid: number) => boolean;
21
+ }
22
+ export interface RunningBroker {
23
+ bearerToken: string;
24
+ ipcUrl: string;
25
+ shutdown(reason: string): Promise<void>;
26
+ registrySize(): number;
27
+ state(endpoint_id: string): EndpointState | undefined;
28
+ undispatchedInbox: UndispatchedInbox;
29
+ controlEventDispatcher: ControlEventDispatcher;
30
+ }
31
+ export declare function startBrokerDaemon(opts: BrokerDaemonOptions): Promise<RunningBroker>;
32
+ export declare function readPluginPidHeader(req: IncomingMessage): number | null;
33
+ export type { BufferedMessage };
34
+ //# sourceMappingURL=broker-daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-daemon.d.ts","sourceRoot":"","sources":["../../src/broker/broker-daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,6BAA6B,CAAC;AAarC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,WAAW,CAAC;IACvB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,YAAY,CAAC;IAGrB,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAE5C,aAAa,CAAC,EAAE,MAAM,CAAC;IAGvB,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;CACjD;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,YAAY,IAAI,MAAM,CAAC;IAGvB,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAC;IAEtD,iBAAiB,EAAE,iBAAiB,CAAC;IAErC,sBAAsB,EAAE,sBAAsB,CAAC;CAChD;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,aAAa,CAAC,CAqnBxB;AAGD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAMvE;AAED,YAAY,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,471 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
3
+ import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
4
+ import { EndpointRegistry } from "./endpoint-registry.js";
5
+ import { DEFAULT_GRACE_MS, transition, } from "./endpoint-state-machine.js";
6
+ import { createGraceTimerManager } from "./grace-timer.js";
7
+ import { BrokerHttpError, pushToPlugin, startIPCServer, } from "./ipc-server.js";
8
+ import { isPluginAlive as defaultIsPluginAlive } from "./plugin-liveness.js";
9
+ import { createReconnectingBufferManager, } from "./reconnecting-buffer.js";
10
+ import { RoutingTable } from "./routing-table.js";
11
+ import { createUndispatchedInbox, } from "./undispatched-inbox.js";
12
+ export async function startBrokerDaemon(opts) {
13
+ const { paths, asNodeId: _asNodeIdReservedForA3, connector, apiPort, logger, closedEndpointLookup, livenessProbe = defaultIsPluginAlive, } = opts;
14
+ const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
15
+ void _asNodeIdReservedForA3;
16
+ logger.info("connecting_to_connector");
17
+ await connector.connect();
18
+ logger.info("connector_connected");
19
+ const registry = new EndpointRegistry();
20
+ const router = new RoutingTable(registry, {
21
+ onCollision: (correlation_id, count) => {
22
+ logger.warn("correlation_id_collision", { correlation_id, count });
23
+ },
24
+ });
25
+ const undispatchedInbox = createUndispatchedInbox({
26
+ capacity: opts.inboxCapacity,
27
+ });
28
+ const reconnectingBuffers = createReconnectingBufferManager(paths.buffersDir);
29
+ const graceTimers = createGraceTimerManager();
30
+ const controlEventDispatcher = createControlEventDispatcher();
31
+ const channels = new Map();
32
+ const wsEndpoints = new WeakMap();
33
+ const displacedWs = new WeakSet();
34
+ const emitTransition = (endpoint_id, state, reason) => {
35
+ void apiPort.transitionState(endpoint_id, {
36
+ state,
37
+ reason,
38
+ ts: new Date().toISOString(),
39
+ });
40
+ };
41
+ const handleTransition = (endpoint_id, event) => {
42
+ const entry = registry.get(endpoint_id);
43
+ if (!entry)
44
+ return;
45
+ const result = transition(entry.state, event, { graceMs });
46
+ if (result.ignored) {
47
+ logger.info("state_event_ignored", {
48
+ endpoint_id,
49
+ current: entry.state,
50
+ event,
51
+ });
52
+ return;
53
+ }
54
+ for (const effect of result.effects) {
55
+ switch (effect.type) {
56
+ case "start_grace_timer": {
57
+ const deadline = new Date(Date.now() + effect.deadline_ms);
58
+ registry.markReconnecting(endpoint_id, deadline);
59
+ graceTimers.start(endpoint_id, effect.deadline_ms, () => {
60
+ handleTransition(endpoint_id, { type: "grace_expired" });
61
+ });
62
+ break;
63
+ }
64
+ case "cancel_grace_timer":
65
+ graceTimers.cancel(endpoint_id);
66
+ break;
67
+ case "emit_state":
68
+ emitTransition(endpoint_id, effect.state, effect.reason);
69
+ break;
70
+ case "drain_buffer": {
71
+ const buf = reconnectingBuffers.forEndpoint(endpoint_id);
72
+ const drained = buf.drain();
73
+ const e = registry.get(endpoint_id);
74
+ if (e) {
75
+ for (const msg of drained) {
76
+ pushToPlugin(e.ipc_ws, {
77
+ event: "message_received",
78
+ from: msg.envelope.from,
79
+ content: msg.envelope.content,
80
+ contentType: msg.envelope.contentType,
81
+ metadata: msg.envelope.metadata,
82
+ });
83
+ }
84
+ }
85
+ break;
86
+ }
87
+ case "delete_buffer":
88
+ reconnectingBuffers.cleanup(endpoint_id);
89
+ break;
90
+ case "remove_from_registry":
91
+ registry.unregister(endpoint_id);
92
+ void apiPort.unregister(endpoint_id).catch(() => {
93
+ });
94
+ break;
95
+ }
96
+ }
97
+ };
98
+ controlEventDispatcher.on("dispatch_undispatched", async (event) => {
99
+ const taken = undispatchedInbox.take(event.undispatched_id);
100
+ if (!taken) {
101
+ return {
102
+ ok: false,
103
+ detail: `unknown undispatched_id: ${event.undispatched_id}`,
104
+ };
105
+ }
106
+ const target = registry.get(event.target_endpoint_id);
107
+ if (!target) {
108
+ const readded = undispatchedInbox.tryAdd(taken);
109
+ if (!readded) {
110
+ logger.error("dispatch_reinsert_failed_at_capacity", {
111
+ undispatched_id: event.undispatched_id,
112
+ target_endpoint_id: event.target_endpoint_id,
113
+ inbox_size: undispatchedInbox.size(),
114
+ inbox_capacity: undispatchedInbox.capacity(),
115
+ });
116
+ }
117
+ return {
118
+ ok: false,
119
+ detail: `target endpoint not found: ${event.target_endpoint_id}`,
120
+ };
121
+ }
122
+ const stamped = {
123
+ event: "message_received",
124
+ from: taken.sender_address,
125
+ content: taken.content,
126
+ contentType: taken.content_type,
127
+ metadata: {
128
+ ...taken.metadata,
129
+ dispatched_from: event.undispatched_id,
130
+ dispatched_by: "passport",
131
+ },
132
+ };
133
+ if (target.state === "active") {
134
+ pushToPlugin(target.ipc_ws, stamped);
135
+ }
136
+ else {
137
+ const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
138
+ buf.append({
139
+ id: randomUUID(),
140
+ arrived_at: new Date().toISOString(),
141
+ envelope: {
142
+ from: taken.sender_address,
143
+ content: taken.content,
144
+ contentType: taken.content_type,
145
+ metadata: stamped.metadata,
146
+ },
147
+ });
148
+ }
149
+ logger.info("undispatched_dispatched", {
150
+ undispatched_id: event.undispatched_id,
151
+ target_endpoint_id: event.target_endpoint_id,
152
+ target_state: target.state,
153
+ });
154
+ return { ok: true };
155
+ });
156
+ const handlers = {
157
+ async registerEndpoint(body, _ipcWs) {
158
+ const boundWs = channels.get(body.plugin_pid);
159
+ if (!boundWs) {
160
+ throw new BrokerHttpError(400, "ipc_channel_missing", `no open IPC channel for plugin_pid=${body.plugin_pid}; ` +
161
+ "open WS /v1/stream with x-plugin-pid header first");
162
+ }
163
+ const apiResp = await apiPort.register({
164
+ runtime_kind: body.kind,
165
+ endpoint_nonce: randomUUID(),
166
+ display_label: body.session_name ??
167
+ `${body.kind}-${String(body.plugin_pid).slice(-4)}`,
168
+ workspace: body.workspace
169
+ ? { root: body.workspace, cwd: body.workspace }
170
+ : undefined,
171
+ tracking_ref: body.tracking_ref,
172
+ });
173
+ registry.register({
174
+ endpoint_id: apiResp.endpoint_id,
175
+ agent_id: body.agent_id,
176
+ plugin_pid: body.plugin_pid,
177
+ ipc_ws: boundWs,
178
+ display_metadata: {
179
+ session_name: body.session_name,
180
+ kind: body.kind,
181
+ started_at: body.started_at ?? new Date().toISOString(),
182
+ workspace: body.workspace,
183
+ tracking_ref: body.tracking_ref,
184
+ task_hint: body.task_hint,
185
+ },
186
+ });
187
+ let bound = wsEndpoints.get(boundWs);
188
+ if (!bound) {
189
+ bound = new Set();
190
+ wsEndpoints.set(boundWs, bound);
191
+ }
192
+ bound.add(apiResp.endpoint_id);
193
+ emitTransition(apiResp.endpoint_id, "active", "register");
194
+ logger.info("endpoint_registered", {
195
+ endpoint_id: apiResp.endpoint_id,
196
+ plugin_pid: body.plugin_pid,
197
+ kind: body.kind,
198
+ });
199
+ return { endpoint_id: apiResp.endpoint_id };
200
+ },
201
+ async heartbeatEndpoint(endpoint_id) {
202
+ const entry = registry.get(endpoint_id);
203
+ if (!entry) {
204
+ throw new BrokerHttpError(404, "endpoint_unknown", `unknown endpoint_id: ${endpoint_id}`);
205
+ }
206
+ await apiPort.heartbeat(endpoint_id);
207
+ },
208
+ async unregisterEndpoint(endpoint_id) {
209
+ const entry = registry.get(endpoint_id);
210
+ if (!entry)
211
+ return;
212
+ handleTransition(endpoint_id, { type: "explicit_unregister" });
213
+ },
214
+ async send(body) {
215
+ const entry = registry.get(body.endpoint_id);
216
+ if (!entry) {
217
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
218
+ }
219
+ if (entry.state !== "active") {
220
+ throw new BrokerHttpError(409, "endpoint_not_active", `endpoint ${body.endpoint_id} is ${entry.state}; outbound send is only allowed from Active`);
221
+ }
222
+ const incoming = body.metadata ?? {};
223
+ if ("correlation_id" in incoming) {
224
+ throw new BrokerHttpError(400, "correlation_id_forbidden", "broker is the sole issuer of correlation_id; do not pass it from the Plugin");
225
+ }
226
+ if ("target_endpoint_id" in incoming) {
227
+ throw new BrokerHttpError(400, "target_endpoint_id_forbidden", "target_endpoint_id is a reply-routing key, not an outbound input");
228
+ }
229
+ if ("source_endpoint_id" in incoming &&
230
+ incoming.source_endpoint_id !== body.endpoint_id) {
231
+ throw new BrokerHttpError(403, "source_endpoint_id_mismatch", "metadata.source_endpoint_id must equal body.endpoint_id");
232
+ }
233
+ const correlationId = randomUUID();
234
+ entry.correlation_ring.add(correlationId);
235
+ const metadata = {
236
+ ...incoming,
237
+ correlation_id: correlationId,
238
+ source_endpoint_id: body.endpoint_id,
239
+ require_live: body.require_live ?? false,
240
+ };
241
+ const ack = await connector.send(body.to, body.content, body.contentType ?? "text", metadata);
242
+ return { messageId: ack.messageId, status: ack.status };
243
+ },
244
+ async reattachEndpoint(endpoint_id, plugin_pid, _ipcWsHint) {
245
+ const ipcWs = channels.get(plugin_pid);
246
+ if (!ipcWs) {
247
+ throw new BrokerHttpError(400, "ipc_channel_missing", `no open IPC channel for plugin_pid=${plugin_pid}; open WS /v1/stream first`);
248
+ }
249
+ const status = registry.getReattachStatus(endpoint_id, plugin_pid);
250
+ if (!status.ok) {
251
+ if (status.reason === "endpoint_unknown") {
252
+ throw new BrokerHttpError(404, "endpoint_unknown", `unknown endpoint_id: ${endpoint_id}`);
253
+ }
254
+ if (status.reason === "reattach_pid_mismatch") {
255
+ throw new BrokerHttpError(403, "reattach_pid_mismatch", "plugin_pid does not match the registered endpoint");
256
+ }
257
+ throw new BrokerHttpError(409, "endpoint_not_reconnecting", "endpoint is currently active; reattach is only valid from reconnecting");
258
+ }
259
+ let bound = wsEndpoints.get(ipcWs);
260
+ if (!bound) {
261
+ bound = new Set();
262
+ wsEndpoints.set(ipcWs, bound);
263
+ }
264
+ bound.add(endpoint_id);
265
+ const buf = reconnectingBuffers.forEndpoint(endpoint_id);
266
+ const drained = buf.drain();
267
+ graceTimers.cancel(endpoint_id);
268
+ registry.markActive(endpoint_id, ipcWs);
269
+ for (const msg of drained) {
270
+ pushToPlugin(ipcWs, {
271
+ event: "message_received",
272
+ from: msg.envelope.from,
273
+ content: msg.envelope.content,
274
+ contentType: msg.envelope.contentType,
275
+ metadata: msg.envelope.metadata,
276
+ });
277
+ }
278
+ emitTransition(endpoint_id, "active", "reattach");
279
+ logger.info("endpoint_reattached", {
280
+ endpoint_id,
281
+ plugin_pid,
282
+ reconnecting_buffer_count: drained.length,
283
+ });
284
+ return { restored: true, reconnecting_buffer_count: drained.length };
285
+ },
286
+ async listUndispatched() {
287
+ return undispatchedInbox.list();
288
+ },
289
+ async dispatch(undispatched_id, target_endpoint_id) {
290
+ const result = await controlEventDispatcher.dispatch({
291
+ type: "dispatch_undispatched",
292
+ version: 1,
293
+ idempotency_key: `local-${randomUUID()}`,
294
+ undispatched_id,
295
+ target_endpoint_id,
296
+ });
297
+ if (!result.ok) {
298
+ throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
299
+ }
300
+ },
301
+ };
302
+ const bearerToken = mintBearerToken();
303
+ const ipc = await startIPCServer({
304
+ bearerToken,
305
+ handlers,
306
+ logger,
307
+ onChannelOpened: (plugin_pid, ws) => {
308
+ const prior = channels.get(plugin_pid);
309
+ if (prior && prior !== ws) {
310
+ displacedWs.add(prior);
311
+ try {
312
+ prior.close(1000, "replaced");
313
+ }
314
+ catch {
315
+ }
316
+ }
317
+ channels.set(plugin_pid, ws);
318
+ },
319
+ onChannelClosed: (plugin_pid, ws) => {
320
+ if (channels.get(plugin_pid) === ws) {
321
+ channels.delete(plugin_pid);
322
+ }
323
+ if (displacedWs.has(ws))
324
+ return;
325
+ const bound = wsEndpoints.get(ws);
326
+ if (!bound)
327
+ return;
328
+ const alive = livenessProbe(plugin_pid);
329
+ for (const endpoint_id of bound) {
330
+ handleTransition(endpoint_id, {
331
+ type: "ipc_close",
332
+ plugin_alive: alive,
333
+ });
334
+ }
335
+ },
336
+ });
337
+ connector.on("message_received", async ({ payload }) => {
338
+ const meta = payload.metadata ?? {};
339
+ const target_endpoint_id = typeof meta.target_endpoint_id === "string"
340
+ ? meta.target_endpoint_id
341
+ : undefined;
342
+ const correlation_id = typeof meta.correlation_id === "string" ? meta.correlation_id : undefined;
343
+ const decision = router.route({ target_endpoint_id, correlation_id });
344
+ if (decision.kind === "endpoint") {
345
+ if (decision.entry.state === "active") {
346
+ pushToPlugin(decision.entry.ipc_ws, {
347
+ event: "message_received",
348
+ from: payload.from,
349
+ content: payload.content,
350
+ contentType: payload.contentType,
351
+ metadata: meta,
352
+ });
353
+ }
354
+ else {
355
+ const buf = reconnectingBuffers.forEndpoint(decision.entry.endpoint_id);
356
+ buf.append({
357
+ id: randomUUID(),
358
+ arrived_at: new Date().toISOString(),
359
+ envelope: {
360
+ from: payload.from,
361
+ content: payload.content,
362
+ contentType: payload.contentType,
363
+ metadata: meta,
364
+ },
365
+ });
366
+ logger.info("buffered_for_reconnecting_endpoint", {
367
+ endpoint_id: decision.entry.endpoint_id,
368
+ buffer_size: buf.size(),
369
+ });
370
+ }
371
+ return;
372
+ }
373
+ let reason = "unaddressed";
374
+ let originalTarget;
375
+ if (target_endpoint_id && closedEndpointLookup) {
376
+ try {
377
+ const result = await closedEndpointLookup.lookup(target_endpoint_id, opts.asNodeId);
378
+ if (result.exists && result.closed) {
379
+ reason = "closed_session_continuation";
380
+ originalTarget = target_endpoint_id;
381
+ }
382
+ }
383
+ catch (err) {
384
+ logger.warn("closed_lookup_failed", {
385
+ target_endpoint_id,
386
+ err: err instanceof Error ? err.message : String(err),
387
+ });
388
+ }
389
+ }
390
+ const undispatched = {
391
+ id: randomUUID(),
392
+ arrived_at: new Date().toISOString(),
393
+ sender_address: payload.from,
394
+ content: payload.content,
395
+ content_type: payload.contentType,
396
+ metadata: meta,
397
+ reason,
398
+ original_target_endpoint_id: originalTarget,
399
+ original_correlation_id: correlation_id,
400
+ };
401
+ const added = undispatchedInbox.tryAdd(undispatched);
402
+ if (!added) {
403
+ logger.warn("undispatched_inbox_at_capacity", {
404
+ from: payload.from,
405
+ size: undispatchedInbox.size(),
406
+ capacity: undispatchedInbox.capacity(),
407
+ });
408
+ }
409
+ else {
410
+ logger.info("undispatched_added", { id: undispatched.id, reason });
411
+ }
412
+ });
413
+ connector.on("delivery_pending", ({ payload }) => {
414
+ connector.ackDelivery(payload.upTo);
415
+ });
416
+ writeDiscoveryFile(paths.discoveryFile, {
417
+ version: 1,
418
+ pid: process.pid,
419
+ startedAt: new Date().toISOString(),
420
+ ipcUrl: ipc.ipcUrl,
421
+ bearerToken,
422
+ });
423
+ logger.info("discovery_written", { path: paths.discoveryFile });
424
+ let shutdownStarted = false;
425
+ const shutdown = async (reason) => {
426
+ if (shutdownStarted)
427
+ return;
428
+ shutdownStarted = true;
429
+ logger.info("shutdown_begin", { reason });
430
+ graceTimers.cancelAll();
431
+ const endpoints = registry.list();
432
+ await Promise.all(endpoints.map(async (entry) => {
433
+ try {
434
+ await apiPort.unregister(entry.endpoint_id);
435
+ }
436
+ catch {
437
+ }
438
+ }));
439
+ deleteDiscoveryFile(paths.discoveryFile);
440
+ try {
441
+ await ipc.close();
442
+ }
443
+ catch (err) {
444
+ logger.warn("ipc_close_error", {
445
+ err: err instanceof Error ? err.message : String(err),
446
+ });
447
+ }
448
+ try {
449
+ connector.disconnect();
450
+ }
451
+ catch {
452
+ }
453
+ };
454
+ return {
455
+ bearerToken,
456
+ ipcUrl: ipc.ipcUrl,
457
+ shutdown,
458
+ registrySize: () => registry.size(),
459
+ state: (endpoint_id) => registry.get(endpoint_id)?.state,
460
+ undispatchedInbox,
461
+ controlEventDispatcher,
462
+ };
463
+ }
464
+ export function readPluginPidHeader(req) {
465
+ const raw = req.headers["x-plugin-pid"];
466
+ const str = Array.isArray(raw) ? raw[0] : raw;
467
+ if (!str)
468
+ return null;
469
+ const n = Number.parseInt(str, 10);
470
+ return Number.isFinite(n) && n > 0 ? n : null;
471
+ }
@@ -0,0 +1,25 @@
1
+ import type { PlatformClientConfig } from "../platform-client.js";
2
+ import { type OwnedRuntimeEndpointView } from "../runtime-endpoint-client.js";
3
+ export interface ClosedEndpointInfo {
4
+ exists: true;
5
+ closed: boolean;
6
+ view: OwnedRuntimeEndpointView;
7
+ }
8
+ export interface UnknownEndpoint {
9
+ exists: false;
10
+ }
11
+ export type LookupResult = ClosedEndpointInfo | UnknownEndpoint;
12
+ export interface ClosedEndpointLookup {
13
+ lookup(endpoint_id: string, asNodeId: string): Promise<LookupResult>;
14
+ invalidate(endpoint_id: string): void;
15
+ size(): number;
16
+ }
17
+ export interface ClosedEndpointLookupOptions {
18
+ cfg: PlatformClientConfig;
19
+ runtimeKey: string;
20
+ cacheSize?: number;
21
+ cacheTtlMs?: number;
22
+ fetcher?: (cfg: PlatformClientConfig, runtimeKey: string, endpointId: string, asNodeId: string) => Promise<OwnedRuntimeEndpointView>;
23
+ }
24
+ export declare function createClosedEndpointLookup(opts: ClosedEndpointLookupOptions): ClosedEndpointLookup;
25
+ //# sourceMappingURL=closed-endpoint-lookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed-endpoint-lookup.d.ts","sourceRoot":"","sources":["../../src/broker/closed-endpoint-lookup.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IAEjC,MAAM,EAAE,IAAI,CAAC;IAEb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,wBAAwB,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,KAAK,CAAC;CACf;AAED,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAIrE,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAEtC,IAAI,IAAI,MAAM,CAAC;CAChB;AAaD,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,oBAAoB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,EAAE,CACR,GAAG,EAAE,oBAAoB,EACzB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,wBAAwB,CAAC,CAAC;CACxC;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,2BAA2B,GAChC,oBAAoB,CA6DtB"}
@@ -0,0 +1,59 @@
1
+ import { getOwnedRuntimeEndpoint, } from "../runtime-endpoint-client.js";
2
+ const DEFAULT_CACHE_SIZE = 64;
3
+ const DEFAULT_CACHE_TTL_MS = 5_000;
4
+ export function createClosedEndpointLookup(opts) {
5
+ const cacheSize = opts.cacheSize ?? DEFAULT_CACHE_SIZE;
6
+ const ttl = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
7
+ const fetcher = opts.fetcher ?? getOwnedRuntimeEndpoint;
8
+ const cache = new Map();
9
+ const evict = () => {
10
+ if (cache.size <= cacheSize)
11
+ return;
12
+ const oldest = cache.keys().next();
13
+ if (!oldest.done)
14
+ cache.delete(oldest.value);
15
+ };
16
+ return {
17
+ async lookup(endpoint_id, asNodeId) {
18
+ const now = Date.now();
19
+ const hit = cache.get(endpoint_id);
20
+ if (hit && hit.expires_at > now) {
21
+ cache.delete(endpoint_id);
22
+ cache.set(endpoint_id, hit);
23
+ return hit.result;
24
+ }
25
+ let result;
26
+ try {
27
+ const view = await fetcher(opts.cfg, opts.runtimeKey, endpoint_id, asNodeId);
28
+ result = {
29
+ exists: true,
30
+ closed: typeof view.retired_at === "string" && view.retired_at.length > 0,
31
+ view,
32
+ };
33
+ }
34
+ catch (err) {
35
+ if (isPlatformApi404(err)) {
36
+ result = { exists: false };
37
+ }
38
+ else {
39
+ return { exists: false };
40
+ }
41
+ }
42
+ cache.set(endpoint_id, { result, expires_at: now + ttl });
43
+ evict();
44
+ return result;
45
+ },
46
+ invalidate(endpoint_id) {
47
+ cache.delete(endpoint_id);
48
+ },
49
+ size() {
50
+ return cache.size;
51
+ },
52
+ };
53
+ }
54
+ function isPlatformApi404(err) {
55
+ if (typeof err !== "object" || err === null)
56
+ return false;
57
+ const status = err.status;
58
+ return status === 404;
59
+ }
@@ -0,0 +1,47 @@
1
+ import { ConnectorError } from "../connector-client.js";
2
+ import type { AddressedMessageEvent, DeliveryPendingEvent, DeliveryStatusEvent, SendAckEvent, StructuredErrorEvent } from "../types.js";
3
+ export type ConnectorWSEvent = {
4
+ event: "message_received";
5
+ payload: AddressedMessageEvent;
6
+ } | {
7
+ event: "send_ack";
8
+ payload: SendAckEvent;
9
+ } | {
10
+ event: "delivery_pending";
11
+ payload: DeliveryPendingEvent;
12
+ } | {
13
+ event: "delivery_status";
14
+ payload: DeliveryStatusEvent;
15
+ } | {
16
+ event: "structured_error";
17
+ payload: StructuredErrorEvent;
18
+ } | {
19
+ event: "connected";
20
+ } | {
21
+ event: "disconnected";
22
+ } | {
23
+ event: "error";
24
+ payload: Error;
25
+ };
26
+ type ListenerFor<E extends ConnectorWSEvent["event"]> = (e: Extract<ConnectorWSEvent, {
27
+ event: E;
28
+ }>) => void;
29
+ export interface ConnectorWSOptions {
30
+ url: string;
31
+ token: string;
32
+ }
33
+ export declare class ConnectorWS {
34
+ private readonly client;
35
+ private readonly bus;
36
+ constructor(opts: ConnectorWSOptions);
37
+ connect(): Promise<void>;
38
+ disconnect(): void;
39
+ send(to: string, content: string, contentType?: string, metadata?: Record<string, unknown>): Promise<SendAckEvent>;
40
+ ackDelivery(upTo: string): boolean;
41
+ on<E extends ConnectorWSEvent["event"]>(event: E, listener: ListenerFor<E>): this;
42
+ off<E extends ConnectorWSEvent["event"]>(event: E, listener: ListenerFor<E>): this;
43
+ _emitForTesting(e: ConnectorWSEvent): void;
44
+ private wireForwarders;
45
+ }
46
+ export { ConnectorError };
47
+ //# sourceMappingURL=connector-ws.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector-ws.d.ts","sourceRoot":"","sources":["../../src/broker/connector-ws.ts"],"names":[],"mappings":"AAaA,OAAO,EAAmB,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,GAC7D;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAC5D;IAAE,KAAK,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,GAC1D;IAAE,KAAK,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAC5D;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,GACtB;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GACzB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvC,KAAK,WAAW,CAAC,CAAC,SAAS,gBAAgB,CAAC,OAAO,CAAC,IAAI,CACtD,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC,KACvC,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsB;gBAE9B,IAAI,EAAE,kBAAkB;IAMpC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,UAAU,IAAI,IAAI;IASlB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,WAAW,SAAS,EACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,YAAY,CAAC;IAKxB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIlC,EAAE,CAAC,CAAC,SAAS,gBAAgB,CAAC,OAAO,CAAC,EACpC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,GACvB,IAAI;IAKP,GAAG,CAAC,CAAC,SAAS,gBAAgB,CAAC,OAAO,CAAC,EACrC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,GACvB,IAAI;IAMS,eAAe,CAAC,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAI1D,OAAO,CAAC,cAAc;CA0BvB;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}