@masons/runtime-broker 0.1.0

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/broker/broker-daemon.d.ts +71 -0
  4. package/dist/broker/broker-daemon.d.ts.map +1 -0
  5. package/dist/broker/broker-daemon.js +837 -0
  6. package/dist/broker/claude-code-spawn-driver.d.ts +14 -0
  7. package/dist/broker/claude-code-spawn-driver.d.ts.map +1 -0
  8. package/dist/broker/claude-code-spawn-driver.js +39 -0
  9. package/dist/broker/closed-endpoint-lookup.d.ts +25 -0
  10. package/dist/broker/closed-endpoint-lookup.d.ts.map +1 -0
  11. package/dist/broker/closed-endpoint-lookup.js +59 -0
  12. package/dist/broker/codex-spawn-driver-stub.d.ts +7 -0
  13. package/dist/broker/codex-spawn-driver-stub.d.ts.map +1 -0
  14. package/dist/broker/codex-spawn-driver-stub.js +13 -0
  15. package/dist/broker/connector-ws.d.ts +47 -0
  16. package/dist/broker/connector-ws.d.ts.map +1 -0
  17. package/dist/broker/connector-ws.js +60 -0
  18. package/dist/broker/control-event-dispatcher.d.ts +21 -0
  19. package/dist/broker/control-event-dispatcher.d.ts.map +1 -0
  20. package/dist/broker/control-event-dispatcher.js +45 -0
  21. package/dist/broker/control-event-types.d.ts +28 -0
  22. package/dist/broker/control-event-types.d.ts.map +1 -0
  23. package/dist/broker/control-event-types.js +1 -0
  24. package/dist/broker/correlation-ring.d.ts +10 -0
  25. package/dist/broker/correlation-ring.d.ts.map +1 -0
  26. package/dist/broker/correlation-ring.js +32 -0
  27. package/dist/broker/discovery-file.d.ts +12 -0
  28. package/dist/broker/discovery-file.d.ts.map +1 -0
  29. package/dist/broker/discovery-file.js +77 -0
  30. package/dist/broker/endpoint-registry.d.ts +53 -0
  31. package/dist/broker/endpoint-registry.d.ts.map +1 -0
  32. package/dist/broker/endpoint-registry.js +83 -0
  33. package/dist/broker/endpoint-state-machine.d.ts +40 -0
  34. package/dist/broker/endpoint-state-machine.d.ts.map +1 -0
  35. package/dist/broker/endpoint-state-machine.js +92 -0
  36. package/dist/broker/entry.d.ts +13 -0
  37. package/dist/broker/entry.d.ts.map +1 -0
  38. package/dist/broker/entry.js +235 -0
  39. package/dist/broker/grace-timer.d.ts +9 -0
  40. package/dist/broker/grace-timer.d.ts.map +1 -0
  41. package/dist/broker/grace-timer.js +34 -0
  42. package/dist/broker/ipc-server.d.ts +79 -0
  43. package/dist/broker/ipc-server.d.ts.map +1 -0
  44. package/dist/broker/ipc-server.js +263 -0
  45. package/dist/broker/logger.d.ts +10 -0
  46. package/dist/broker/logger.d.ts.map +1 -0
  47. package/dist/broker/logger.js +34 -0
  48. package/dist/broker/network-presence-changed-event-types.d.ts +8 -0
  49. package/dist/broker/network-presence-changed-event-types.d.ts.map +1 -0
  50. package/dist/broker/network-presence-changed-event-types.js +1 -0
  51. package/dist/broker/network-presence-emitter.d.ts +22 -0
  52. package/dist/broker/network-presence-emitter.d.ts.map +1 -0
  53. package/dist/broker/network-presence-emitter.js +150 -0
  54. package/dist/broker/network-presence.d.ts +31 -0
  55. package/dist/broker/network-presence.d.ts.map +1 -0
  56. package/dist/broker/network-presence.js +109 -0
  57. package/dist/broker/paths.d.ts +11 -0
  58. package/dist/broker/paths.d.ts.map +1 -0
  59. package/dist/broker/paths.js +30 -0
  60. package/dist/broker/plugin-liveness.d.ts +2 -0
  61. package/dist/broker/plugin-liveness.d.ts.map +1 -0
  62. package/dist/broker/plugin-liveness.js +15 -0
  63. package/dist/broker/received-message-correlation-cache.d.ts +23 -0
  64. package/dist/broker/received-message-correlation-cache.d.ts.map +1 -0
  65. package/dist/broker/received-message-correlation-cache.js +114 -0
  66. package/dist/broker/reconnecting-buffer.d.ts +23 -0
  67. package/dist/broker/reconnecting-buffer.d.ts.map +1 -0
  68. package/dist/broker/reconnecting-buffer.js +107 -0
  69. package/dist/broker/routing-table.d.ts +22 -0
  70. package/dist/broker/routing-table.d.ts.map +1 -0
  71. package/dist/broker/routing-table.js +35 -0
  72. package/dist/broker/runtime-endpoint-port.d.ts +20 -0
  73. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -0
  74. package/dist/broker/runtime-endpoint-port.js +1 -0
  75. package/dist/broker/services-event-client.d.ts +21 -0
  76. package/dist/broker/services-event-client.d.ts.map +1 -0
  77. package/dist/broker/services-event-client.js +221 -0
  78. package/dist/broker/spawn-correlation.d.ts +28 -0
  79. package/dist/broker/spawn-correlation.d.ts.map +1 -0
  80. package/dist/broker/spawn-correlation.js +77 -0
  81. package/dist/broker/spawn-driver.d.ts +27 -0
  82. package/dist/broker/spawn-driver.d.ts.map +1 -0
  83. package/dist/broker/spawn-driver.js +15 -0
  84. package/dist/broker/task-hint-handler.d.ts +21 -0
  85. package/dist/broker/task-hint-handler.d.ts.map +1 -0
  86. package/dist/broker/task-hint-handler.js +33 -0
  87. package/dist/broker/transition-state-retry-queue.d.ts +20 -0
  88. package/dist/broker/transition-state-retry-queue.d.ts.map +1 -0
  89. package/dist/broker/transition-state-retry-queue.js +48 -0
  90. package/dist/broker/undispatched-changed-event-types.d.ts +29 -0
  91. package/dist/broker/undispatched-changed-event-types.d.ts.map +1 -0
  92. package/dist/broker/undispatched-changed-event-types.js +14 -0
  93. package/dist/broker/undispatched-emitter.d.ts +22 -0
  94. package/dist/broker/undispatched-emitter.d.ts.map +1 -0
  95. package/dist/broker/undispatched-emitter.js +149 -0
  96. package/dist/broker/undispatched-inbox.d.ts +26 -0
  97. package/dist/broker/undispatched-inbox.d.ts.map +1 -0
  98. package/dist/broker/undispatched-inbox.js +53 -0
  99. package/dist/broker/version-handshake.d.ts +30 -0
  100. package/dist/broker/version-handshake.d.ts.map +1 -0
  101. package/dist/broker/version-handshake.js +47 -0
  102. package/dist/broker-client/broker-client.d.ts +65 -0
  103. package/dist/broker-client/broker-client.d.ts.map +1 -0
  104. package/dist/broker-client/broker-client.js +165 -0
  105. package/dist/broker-client/lazy-spawn.d.ts +18 -0
  106. package/dist/broker-client/lazy-spawn.d.ts.map +1 -0
  107. package/dist/broker-client/lazy-spawn.js +61 -0
  108. package/dist/config-fs.d.ts +4 -0
  109. package/dist/config-fs.d.ts.map +1 -0
  110. package/dist/config-fs.js +23 -0
  111. package/dist/connector-client.d.ts +65 -0
  112. package/dist/connector-client.d.ts.map +1 -0
  113. package/dist/connector-client.js +364 -0
  114. package/dist/environment-context.d.ts +21 -0
  115. package/dist/environment-context.d.ts.map +1 -0
  116. package/dist/environment-context.js +39 -0
  117. package/dist/platform-client.d.ts +84 -0
  118. package/dist/platform-client.d.ts.map +1 -0
  119. package/dist/platform-client.js +94 -0
  120. package/dist/runtime-endpoint-client.d.ts +74 -0
  121. package/dist/runtime-endpoint-client.d.ts.map +1 -0
  122. package/dist/runtime-endpoint-client.js +163 -0
  123. package/dist/types.d.ts +90 -0
  124. package/dist/types.d.ts.map +1 -0
  125. package/dist/types.js +38 -0
  126. package/dist/version.d.ts +2 -0
  127. package/dist/version.d.ts.map +1 -0
  128. package/dist/version.js +1 -0
  129. package/package.json +60 -0
@@ -0,0 +1,837 @@
1
+ import { spawn as spawnChild } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
4
+ import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
5
+ import { EndpointRegistry } from "./endpoint-registry.js";
6
+ import { DEFAULT_GRACE_MS, transition, } from "./endpoint-state-machine.js";
7
+ import { createGraceTimerManager } from "./grace-timer.js";
8
+ import { BrokerHttpError, pushToPlugin, startIPCServer, } from "./ipc-server.js";
9
+ import { transitionPresence, } from "./network-presence.js";
10
+ import { createNetworkPresenceEmitter, postNetworkPresenceChangedViaPort, } from "./network-presence-emitter.js";
11
+ import { isPluginAlive as defaultIsPluginAlive } from "./plugin-liveness.js";
12
+ import { createReceivedMessageCorrelationCache, DEFAULT_TTL_MS as DEFAULT_REPLY_CORRELATION_TTL_MS, } from "./received-message-correlation-cache.js";
13
+ import { createReconnectingBufferManager, } from "./reconnecting-buffer.js";
14
+ import { RoutingTable } from "./routing-table.js";
15
+ import { createSpawnCorrelationManager, createSpawnRateLimiter, } from "./spawn-correlation.js";
16
+ import { TASK_HINT_MAX_LENGTH, updateTaskHint } from "./task-hint-handler.js";
17
+ import { createTransitionStateRetryQueue } from "./transition-state-retry-queue.js";
18
+ import { CONTENT_PREVIEW_MAX_CODEPOINTS, truncateContentPreview, } from "./undispatched-changed-event-types.js";
19
+ import { createUndispatchedChangedEmitter, postUndispatchedChangedViaPort, } from "./undispatched-emitter.js";
20
+ import { createUndispatchedInbox, } from "./undispatched-inbox.js";
21
+ export async function startBrokerDaemon(opts) {
22
+ const { paths, asNodeId: _asNodeIdReservedForA3, connector, apiPort, logger, closedEndpointLookup, livenessProbe = defaultIsPluginAlive, spawnDriverRegistry, spawnFn = spawnChild, servicesEventClientFactory, } = opts;
23
+ const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
24
+ const presenceGraceMs = opts.presenceGraceMs;
25
+ const postControlAck = opts.postControlAck ??
26
+ (async () => {
27
+ });
28
+ void _asNodeIdReservedForA3;
29
+ logger.info("connecting_to_connector");
30
+ await connector.connect();
31
+ logger.info("connector_connected");
32
+ const registry = new EndpointRegistry();
33
+ const router = new RoutingTable(registry, {
34
+ onCollision: (correlation_id, count) => {
35
+ logger.warn("correlation_id_collision", { correlation_id, count });
36
+ },
37
+ });
38
+ const undispatchedInbox = createUndispatchedInbox({
39
+ capacity: opts.inboxCapacity,
40
+ });
41
+ const reconnectingBuffers = createReconnectingBufferManager(paths.buffersDir);
42
+ const graceTimers = createGraceTimerManager();
43
+ const controlEventDispatcher = createControlEventDispatcher();
44
+ const spawnCorrelation = createSpawnCorrelationManager({
45
+ timeoutMs: opts.spawnCorrelationTimeoutMs,
46
+ });
47
+ const spawnRateLimiter = createSpawnRateLimiter({
48
+ maxInWindow: opts.spawnRateLimitMaxInWindow,
49
+ windowMs: opts.spawnRateLimitWindowMs,
50
+ });
51
+ const retryQueue = createTransitionStateRetryQueue({
52
+ capacity: opts.retryQueueCapacity,
53
+ onDrop: (dropped) => {
54
+ logger.warn("transition_state_retry_queue_overflow", {
55
+ endpoint_id: dropped.endpoint_id,
56
+ state: dropped.params.state,
57
+ reason: dropped.params.reason,
58
+ enqueued_at: dropped.enqueued_at,
59
+ });
60
+ },
61
+ });
62
+ const replyCorrelationCache = createReceivedMessageCorrelationCache({
63
+ ttlMs: opts.replyCorrelationTtlMs,
64
+ capPerEndpoint: opts.replyCorrelationCapPerEndpoint,
65
+ });
66
+ const replyCorrelationTtl = opts.replyCorrelationTtlMs ?? DEFAULT_REPLY_CORRELATION_TTL_MS;
67
+ const replyCorrelationSweepInterval = Math.max(Math.floor(replyCorrelationTtl / 4), 60_000);
68
+ const replyCorrelationSweepTimer = setInterval(() => {
69
+ const evicted = replyCorrelationCache.sweep();
70
+ if (evicted > 0) {
71
+ logger.info("reply_correlation_cache_sweep", { evicted });
72
+ }
73
+ }, replyCorrelationSweepInterval);
74
+ replyCorrelationSweepTimer.unref?.();
75
+ const undispatchedEmitter = createUndispatchedChangedEmitter(opts.undispatchedChangedPost ?? postUndispatchedChangedViaPort(apiPort), logger, {
76
+ capacity: opts.undispatchedChangedCapacity,
77
+ backoffInitialMs: opts.undispatchedChangedBackoffInitialMs,
78
+ backoffMaxMs: opts.undispatchedChangedBackoffMaxMs,
79
+ maxRetries: opts.undispatchedChangedMaxRetries,
80
+ });
81
+ const emitUndispatched = (event) => {
82
+ undispatchedEmitter.enqueue(event);
83
+ };
84
+ const networkPresenceEmitter = createNetworkPresenceEmitter(opts.networkPresenceChangedPost ??
85
+ postNetworkPresenceChangedViaPort(apiPort), logger, {
86
+ capacity: opts.networkPresenceChangedCapacity,
87
+ backoffInitialMs: opts.networkPresenceChangedBackoffInitialMs,
88
+ backoffMaxMs: opts.networkPresenceChangedBackoffMaxMs,
89
+ maxRetries: opts.networkPresenceChangedMaxRetries,
90
+ });
91
+ const emitNetworkPresence = (event) => {
92
+ networkPresenceEmitter.enqueue(event);
93
+ };
94
+ const recordInboundCorrelation = (endpoint_id, metadata) => {
95
+ const c = metadata && typeof metadata.correlation_id === "string"
96
+ ? metadata.correlation_id
97
+ : undefined;
98
+ if (!c)
99
+ return;
100
+ replyCorrelationCache.record(endpoint_id, c);
101
+ };
102
+ let networkPresence = "offline";
103
+ let presenceGraceTimer;
104
+ const applyPresenceTransition = (event) => {
105
+ const result = transitionPresence(networkPresence, event, {
106
+ graceMs: presenceGraceMs,
107
+ });
108
+ if (result.ignored)
109
+ return;
110
+ networkPresence = result.next;
111
+ for (const effect of result.effects) {
112
+ switch (effect.type) {
113
+ case "start_grace_timer":
114
+ if (presenceGraceTimer)
115
+ clearTimeout(presenceGraceTimer);
116
+ presenceGraceTimer = setTimeout(() => {
117
+ applyPresenceTransition({ type: "presence_grace_expired" });
118
+ }, effect.deadline_ms);
119
+ presenceGraceTimer.unref?.();
120
+ break;
121
+ case "cancel_grace_timer":
122
+ if (presenceGraceTimer) {
123
+ clearTimeout(presenceGraceTimer);
124
+ presenceGraceTimer = undefined;
125
+ }
126
+ break;
127
+ case "emit_presence":
128
+ logger.info("network_presence_changed", {
129
+ presence: effect.presence,
130
+ reason: effect.reason,
131
+ });
132
+ emitNetworkPresence({
133
+ type: "network_presence_changed",
134
+ version: 1,
135
+ state: effect.presence,
136
+ ts: Date.now(),
137
+ reason: effect.reason,
138
+ });
139
+ if (effect.presence === "online") {
140
+ void retryQueue.flush(async (entry) => {
141
+ await apiPort.transitionState(entry.endpoint_id, entry.params);
142
+ });
143
+ }
144
+ break;
145
+ }
146
+ }
147
+ };
148
+ applyPresenceTransition({ type: "connector_connected" });
149
+ connector.on("connected", () => {
150
+ applyPresenceTransition({ type: "connector_connected" });
151
+ });
152
+ connector.on("disconnected", () => {
153
+ applyPresenceTransition({ type: "connector_disconnected" });
154
+ });
155
+ const channels = new Map();
156
+ const wsEndpoints = new WeakMap();
157
+ const displacedWs = new WeakSet();
158
+ const emitTransition = (endpoint_id, state, reason) => {
159
+ const params = {
160
+ state,
161
+ reason,
162
+ ts: new Date().toISOString(),
163
+ };
164
+ if (networkPresence === "online") {
165
+ void apiPort.transitionState(endpoint_id, params);
166
+ return;
167
+ }
168
+ const accepted = retryQueue.enqueue(endpoint_id, params);
169
+ logger.info("transition_state_deferred", {
170
+ endpoint_id,
171
+ state,
172
+ reason,
173
+ presence: networkPresence,
174
+ queued: accepted,
175
+ retry_queue_size: retryQueue.size(),
176
+ });
177
+ };
178
+ const handleTransition = (endpoint_id, event) => {
179
+ const entry = registry.get(endpoint_id);
180
+ if (!entry)
181
+ return;
182
+ const result = transition(entry.state, event, { graceMs });
183
+ if (result.ignored) {
184
+ logger.info("state_event_ignored", {
185
+ endpoint_id,
186
+ current: entry.state,
187
+ event,
188
+ });
189
+ return;
190
+ }
191
+ for (const effect of result.effects) {
192
+ switch (effect.type) {
193
+ case "start_grace_timer": {
194
+ const deadline = new Date(Date.now() + effect.deadline_ms);
195
+ registry.markReconnecting(endpoint_id, deadline);
196
+ graceTimers.start(endpoint_id, effect.deadline_ms, () => {
197
+ handleTransition(endpoint_id, { type: "grace_expired" });
198
+ });
199
+ break;
200
+ }
201
+ case "cancel_grace_timer":
202
+ graceTimers.cancel(endpoint_id);
203
+ break;
204
+ case "emit_state":
205
+ emitTransition(endpoint_id, effect.state, effect.reason);
206
+ break;
207
+ case "drain_buffer": {
208
+ const buf = reconnectingBuffers.forEndpoint(endpoint_id);
209
+ const drained = buf.drain();
210
+ const e = registry.get(endpoint_id);
211
+ if (e) {
212
+ for (const msg of drained) {
213
+ pushToPlugin(e.ipc_ws, {
214
+ event: "message_received",
215
+ from: msg.envelope.from,
216
+ content: msg.envelope.content,
217
+ contentType: msg.envelope.contentType,
218
+ metadata: msg.envelope.metadata,
219
+ });
220
+ }
221
+ }
222
+ break;
223
+ }
224
+ case "delete_buffer":
225
+ reconnectingBuffers.cleanup(endpoint_id);
226
+ break;
227
+ case "remove_from_registry":
228
+ registry.unregister(endpoint_id);
229
+ replyCorrelationCache.forgetEndpoint(endpoint_id);
230
+ void apiPort.unregister(endpoint_id).catch(() => {
231
+ });
232
+ break;
233
+ }
234
+ }
235
+ };
236
+ controlEventDispatcher.on("dispatch_undispatched", async (event) => {
237
+ const taken = undispatchedInbox.take(event.undispatched_id);
238
+ if (!taken) {
239
+ return {
240
+ ok: false,
241
+ detail: `unknown undispatched_id: ${event.undispatched_id}`,
242
+ };
243
+ }
244
+ const target = registry.get(event.target_endpoint_id);
245
+ if (!target) {
246
+ const readded = undispatchedInbox.tryAdd(taken);
247
+ if (!readded) {
248
+ logger.error("dispatch_reinsert_failed_at_capacity", {
249
+ undispatched_id: event.undispatched_id,
250
+ target_endpoint_id: event.target_endpoint_id,
251
+ inbox_size: undispatchedInbox.size(),
252
+ inbox_capacity: undispatchedInbox.capacity(),
253
+ });
254
+ emitUndispatched({
255
+ type: "undispatched_changed",
256
+ version: 1,
257
+ action: "remove",
258
+ undispatched_id: event.undispatched_id,
259
+ remove_reason: "lost_at_capacity_during_redispatch_bounce",
260
+ });
261
+ }
262
+ return {
263
+ ok: false,
264
+ detail: `target endpoint not found: ${event.target_endpoint_id}`,
265
+ };
266
+ }
267
+ const stamped = {
268
+ event: "message_received",
269
+ from: taken.sender_address,
270
+ content: taken.content,
271
+ contentType: taken.content_type,
272
+ metadata: {
273
+ ...taken.metadata,
274
+ dispatched_from: event.undispatched_id,
275
+ dispatched_by: "passport",
276
+ },
277
+ };
278
+ recordInboundCorrelation(target.endpoint_id, stamped.metadata);
279
+ if (target.state === "active") {
280
+ pushToPlugin(target.ipc_ws, stamped);
281
+ }
282
+ else {
283
+ const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
284
+ buf.append({
285
+ id: randomUUID(),
286
+ arrived_at: new Date().toISOString(),
287
+ envelope: {
288
+ from: taken.sender_address,
289
+ content: taken.content,
290
+ contentType: taken.content_type,
291
+ metadata: stamped.metadata,
292
+ },
293
+ });
294
+ }
295
+ logger.info("undispatched_dispatched", {
296
+ undispatched_id: event.undispatched_id,
297
+ target_endpoint_id: event.target_endpoint_id,
298
+ target_state: target.state,
299
+ });
300
+ emitUndispatched({
301
+ type: "undispatched_changed",
302
+ version: 1,
303
+ action: "dispatch",
304
+ undispatched_id: event.undispatched_id,
305
+ dispatched_to_endpoint_id: event.target_endpoint_id,
306
+ });
307
+ return { ok: true };
308
+ });
309
+ controlEventDispatcher.on("spawn_request", async (event) => {
310
+ if (!spawnRateLimiter.tryConsume()) {
311
+ return {
312
+ ok: false,
313
+ detail: "spawn_rate_limited",
314
+ };
315
+ }
316
+ if (!spawnDriverRegistry) {
317
+ return {
318
+ ok: false,
319
+ detail: "spawn_driver_registry_not_configured",
320
+ };
321
+ }
322
+ const driver = spawnDriverRegistry.lookup(event.runtime_kind);
323
+ if (!driver) {
324
+ return {
325
+ ok: false,
326
+ detail: `runtime_kind_unsupported: ${event.runtime_kind}`,
327
+ };
328
+ }
329
+ const avail = await driver.isAvailable();
330
+ if (!avail.available) {
331
+ return {
332
+ ok: false,
333
+ detail: avail.reason ?? "driver_unavailable",
334
+ };
335
+ }
336
+ const cmd = driver.buildSpawnCommand({
337
+ prompt: event.prompt,
338
+ spawn_token: event.idempotency_key,
339
+ });
340
+ let child;
341
+ try {
342
+ child = spawnFn(cmd.binary, cmd.args, {
343
+ detached: true,
344
+ stdio: "ignore",
345
+ windowsHide: true,
346
+ env: { ...process.env, ...(cmd.env ?? {}) },
347
+ cwd: cmd.cwd,
348
+ });
349
+ child.unref();
350
+ }
351
+ catch (err) {
352
+ return {
353
+ ok: false,
354
+ detail: `spawn_failed: ${err instanceof Error ? err.message : String(err)}`,
355
+ };
356
+ }
357
+ spawnCorrelation.track(event.idempotency_key, () => {
358
+ logger.warn("spawn_correlation_timeout", {
359
+ spawn_token: event.idempotency_key,
360
+ runtime_kind: event.runtime_kind,
361
+ });
362
+ void postControlAck({
363
+ idempotency_key: event.idempotency_key,
364
+ status: "failed",
365
+ detail: "spawn_correlation_timeout",
366
+ });
367
+ });
368
+ logger.info("spawn_request_initiated", {
369
+ spawn_token: event.idempotency_key,
370
+ runtime_kind: event.runtime_kind,
371
+ pid: child.pid,
372
+ });
373
+ return { ok: true, ack_hint: "received" };
374
+ });
375
+ controlEventDispatcher.on("force_unregister", async (event) => {
376
+ const entry = registry.get(event.endpoint_id);
377
+ if (!entry) {
378
+ return { ok: true };
379
+ }
380
+ logger.info("force_unregister_received", {
381
+ endpoint_id: event.endpoint_id,
382
+ reason: event.reason,
383
+ });
384
+ handleTransition(event.endpoint_id, { type: "explicit_unregister" });
385
+ return { ok: true };
386
+ });
387
+ const handlers = {
388
+ async registerEndpoint(body, _ipcWs) {
389
+ const boundWs = channels.get(body.plugin_pid);
390
+ if (!boundWs) {
391
+ throw new BrokerHttpError(400, "ipc_channel_missing", `no open IPC channel for plugin_pid=${body.plugin_pid}; ` +
392
+ "open WS /v1/stream with x-plugin-pid header first");
393
+ }
394
+ const apiResp = await apiPort.register({
395
+ runtime_kind: body.kind,
396
+ endpoint_nonce: randomUUID(),
397
+ display_label: body.session_name ??
398
+ `${body.kind}-${String(body.plugin_pid).slice(-4)}`,
399
+ workspace: body.workspace
400
+ ? { root: body.workspace, cwd: body.workspace }
401
+ : undefined,
402
+ tracking_ref: body.tracking_ref,
403
+ });
404
+ registry.register({
405
+ endpoint_id: apiResp.endpoint_id,
406
+ agent_id: body.agent_id,
407
+ plugin_pid: body.plugin_pid,
408
+ ipc_ws: boundWs,
409
+ display_metadata: {
410
+ session_name: body.session_name,
411
+ kind: body.kind,
412
+ started_at: body.started_at ?? new Date().toISOString(),
413
+ workspace: body.workspace,
414
+ tracking_ref: body.tracking_ref,
415
+ task_hint: body.task_hint,
416
+ },
417
+ });
418
+ let bound = wsEndpoints.get(boundWs);
419
+ if (!bound) {
420
+ bound = new Set();
421
+ wsEndpoints.set(boundWs, bound);
422
+ }
423
+ bound.add(apiResp.endpoint_id);
424
+ emitTransition(apiResp.endpoint_id, "active", "register");
425
+ if (typeof body.spawn_token === "string") {
426
+ const pending = spawnCorrelation.consume(body.spawn_token);
427
+ if (pending) {
428
+ logger.info("endpoint_spawned_correlation_resolved", {
429
+ endpoint_id: apiResp.endpoint_id,
430
+ spawn_token: body.spawn_token,
431
+ });
432
+ void postControlAck({
433
+ idempotency_key: pending.spawn_token,
434
+ status: "applied",
435
+ });
436
+ }
437
+ else {
438
+ logger.warn("endpoint_register_unknown_spawn_token", {
439
+ endpoint_id: apiResp.endpoint_id,
440
+ spawn_token: body.spawn_token,
441
+ });
442
+ }
443
+ }
444
+ logger.info("endpoint_registered", {
445
+ endpoint_id: apiResp.endpoint_id,
446
+ plugin_pid: body.plugin_pid,
447
+ kind: body.kind,
448
+ });
449
+ return { endpoint_id: apiResp.endpoint_id };
450
+ },
451
+ async heartbeatEndpoint(endpoint_id) {
452
+ const entry = registry.get(endpoint_id);
453
+ if (!entry) {
454
+ throw new BrokerHttpError(404, "endpoint_unknown", `unknown endpoint_id: ${endpoint_id}`);
455
+ }
456
+ await apiPort.heartbeat(endpoint_id);
457
+ },
458
+ async unregisterEndpoint(endpoint_id) {
459
+ const entry = registry.get(endpoint_id);
460
+ if (!entry)
461
+ return;
462
+ handleTransition(endpoint_id, { type: "explicit_unregister" });
463
+ },
464
+ async send(body) {
465
+ const entry = registry.get(body.endpoint_id);
466
+ if (!entry) {
467
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
468
+ }
469
+ if (entry.state !== "active") {
470
+ throw new BrokerHttpError(409, "endpoint_not_active", `endpoint ${body.endpoint_id} is ${entry.state}; outbound send is only allowed from Active`);
471
+ }
472
+ const incoming = body.metadata ?? {};
473
+ if ("correlation_id" in incoming) {
474
+ throw new BrokerHttpError(400, "correlation_id_forbidden", "broker is the sole issuer of correlation_id; do not pass it from the Plugin");
475
+ }
476
+ if ("target_endpoint_id" in incoming) {
477
+ throw new BrokerHttpError(400, "target_endpoint_id_forbidden", "target_endpoint_id is a reply-routing key, not an outbound input");
478
+ }
479
+ if ("source_endpoint_id" in incoming &&
480
+ incoming.source_endpoint_id !== body.endpoint_id) {
481
+ throw new BrokerHttpError(403, "source_endpoint_id_mismatch", "metadata.source_endpoint_id must equal body.endpoint_id");
482
+ }
483
+ const inReplyTo = typeof incoming.in_reply_to === "string"
484
+ ? incoming.in_reply_to
485
+ : undefined;
486
+ let correlationId;
487
+ if (inReplyTo) {
488
+ const resolved = replyCorrelationCache.resolve(body.endpoint_id, inReplyTo);
489
+ if (resolved.ok) {
490
+ correlationId = inReplyTo;
491
+ }
492
+ else {
493
+ logger.warn("reply_aware_fallback", {
494
+ endpoint_id: body.endpoint_id,
495
+ in_reply_to: inReplyTo,
496
+ reason: resolved.reason,
497
+ });
498
+ correlationId = randomUUID();
499
+ }
500
+ }
501
+ else {
502
+ correlationId = randomUUID();
503
+ }
504
+ entry.correlation_ring.add(correlationId);
505
+ const metadata = {
506
+ ...incoming,
507
+ correlation_id: correlationId,
508
+ source_endpoint_id: body.endpoint_id,
509
+ require_live: body.require_live ?? false,
510
+ };
511
+ const ack = await connector.send(body.to, body.content, body.contentType ?? "text", metadata);
512
+ return { messageId: ack.messageId, status: ack.status };
513
+ },
514
+ async reattachEndpoint(endpoint_id, plugin_pid, _ipcWsHint) {
515
+ const ipcWs = channels.get(plugin_pid);
516
+ if (!ipcWs) {
517
+ throw new BrokerHttpError(400, "ipc_channel_missing", `no open IPC channel for plugin_pid=${plugin_pid}; open WS /v1/stream first`);
518
+ }
519
+ const status = registry.getReattachStatus(endpoint_id, plugin_pid);
520
+ if (!status.ok) {
521
+ if (status.reason === "endpoint_unknown") {
522
+ throw new BrokerHttpError(404, "endpoint_unknown", `unknown endpoint_id: ${endpoint_id}`);
523
+ }
524
+ if (status.reason === "reattach_pid_mismatch") {
525
+ throw new BrokerHttpError(403, "reattach_pid_mismatch", "plugin_pid does not match the registered endpoint");
526
+ }
527
+ throw new BrokerHttpError(409, "endpoint_not_reconnecting", "endpoint is currently active; reattach is only valid from reconnecting");
528
+ }
529
+ let bound = wsEndpoints.get(ipcWs);
530
+ if (!bound) {
531
+ bound = new Set();
532
+ wsEndpoints.set(ipcWs, bound);
533
+ }
534
+ bound.add(endpoint_id);
535
+ const buf = reconnectingBuffers.forEndpoint(endpoint_id);
536
+ const drained = buf.drain();
537
+ graceTimers.cancel(endpoint_id);
538
+ registry.markActive(endpoint_id, ipcWs);
539
+ for (const msg of drained) {
540
+ pushToPlugin(ipcWs, {
541
+ event: "message_received",
542
+ from: msg.envelope.from,
543
+ content: msg.envelope.content,
544
+ contentType: msg.envelope.contentType,
545
+ metadata: msg.envelope.metadata,
546
+ });
547
+ }
548
+ emitTransition(endpoint_id, "active", "reattach");
549
+ logger.info("endpoint_reattached", {
550
+ endpoint_id,
551
+ plugin_pid,
552
+ reconnecting_buffer_count: drained.length,
553
+ });
554
+ return { restored: true, reconnecting_buffer_count: drained.length };
555
+ },
556
+ async listUndispatched() {
557
+ return undispatchedInbox.list();
558
+ },
559
+ async dispatch(undispatched_id, target_endpoint_id) {
560
+ const result = await controlEventDispatcher.dispatch({
561
+ type: "dispatch_undispatched",
562
+ version: 1,
563
+ idempotency_key: `local-${randomUUID()}`,
564
+ emitted_at: Date.now(),
565
+ undispatched_id,
566
+ target_endpoint_id,
567
+ });
568
+ if (!result.ok) {
569
+ throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
570
+ }
571
+ },
572
+ async setTaskHint(body) {
573
+ const result = updateTaskHint({ registry }, {
574
+ endpoint_id: body.endpoint_id,
575
+ plugin_pid: body.plugin_pid,
576
+ task_hint: body.task_hint,
577
+ });
578
+ if (!result.ok) {
579
+ const status = result.code === "endpoint_unknown"
580
+ ? 404
581
+ : result.code === "ownership_mismatch"
582
+ ? 403
583
+ : 400;
584
+ throw new BrokerHttpError(status, result.code, result.message);
585
+ }
586
+ logger.info("task_hint_updated", {
587
+ endpoint_id: body.endpoint_id,
588
+ truncated: result.truncated,
589
+ length: result.effective.length,
590
+ max: TASK_HINT_MAX_LENGTH,
591
+ });
592
+ },
593
+ };
594
+ const bearerToken = mintBearerToken();
595
+ const ipc = await startIPCServer({
596
+ bearerToken,
597
+ handlers,
598
+ logger,
599
+ onChannelOpened: (plugin_pid, ws) => {
600
+ const prior = channels.get(plugin_pid);
601
+ if (prior && prior !== ws) {
602
+ displacedWs.add(prior);
603
+ try {
604
+ prior.close(1000, "replaced");
605
+ }
606
+ catch {
607
+ }
608
+ }
609
+ channels.set(plugin_pid, ws);
610
+ },
611
+ onChannelClosed: (plugin_pid, ws) => {
612
+ if (channels.get(plugin_pid) === ws) {
613
+ channels.delete(plugin_pid);
614
+ }
615
+ if (displacedWs.has(ws))
616
+ return;
617
+ const bound = wsEndpoints.get(ws);
618
+ if (!bound)
619
+ return;
620
+ const alive = livenessProbe(plugin_pid);
621
+ for (const endpoint_id of bound) {
622
+ handleTransition(endpoint_id, {
623
+ type: "ipc_close",
624
+ plugin_alive: alive,
625
+ });
626
+ }
627
+ },
628
+ });
629
+ connector.on("message_received", async ({ payload }) => {
630
+ const meta = payload.metadata ?? {};
631
+ const target_endpoint_id = typeof meta.target_endpoint_id === "string"
632
+ ? meta.target_endpoint_id
633
+ : undefined;
634
+ const correlation_id = typeof meta.correlation_id === "string" ? meta.correlation_id : undefined;
635
+ const decision = router.route({ target_endpoint_id, correlation_id });
636
+ if (decision.kind === "endpoint") {
637
+ recordInboundCorrelation(decision.entry.endpoint_id, meta);
638
+ if (decision.entry.state === "active") {
639
+ pushToPlugin(decision.entry.ipc_ws, {
640
+ event: "message_received",
641
+ from: payload.from,
642
+ content: payload.content,
643
+ contentType: payload.contentType,
644
+ metadata: meta,
645
+ });
646
+ }
647
+ else {
648
+ const buf = reconnectingBuffers.forEndpoint(decision.entry.endpoint_id);
649
+ buf.append({
650
+ id: randomUUID(),
651
+ arrived_at: new Date().toISOString(),
652
+ envelope: {
653
+ from: payload.from,
654
+ content: payload.content,
655
+ contentType: payload.contentType,
656
+ metadata: meta,
657
+ },
658
+ });
659
+ logger.info("buffered_for_reconnecting_endpoint", {
660
+ endpoint_id: decision.entry.endpoint_id,
661
+ buffer_size: buf.size(),
662
+ });
663
+ }
664
+ return;
665
+ }
666
+ let reason = "unaddressed";
667
+ let originalTarget;
668
+ if (target_endpoint_id && closedEndpointLookup) {
669
+ try {
670
+ const result = await closedEndpointLookup.lookup(target_endpoint_id, opts.asNodeId);
671
+ if (result.exists && result.closed) {
672
+ reason = "closed_session_continuation";
673
+ originalTarget = target_endpoint_id;
674
+ }
675
+ }
676
+ catch (err) {
677
+ logger.warn("closed_lookup_failed", {
678
+ target_endpoint_id,
679
+ err: err instanceof Error ? err.message : String(err),
680
+ });
681
+ }
682
+ }
683
+ const undispatched = {
684
+ id: randomUUID(),
685
+ arrived_at: new Date().toISOString(),
686
+ sender_address: payload.from,
687
+ content: payload.content,
688
+ content_type: payload.contentType,
689
+ metadata: meta,
690
+ reason,
691
+ original_target_endpoint_id: originalTarget,
692
+ original_correlation_id: correlation_id,
693
+ };
694
+ const added = undispatchedInbox.tryAdd(undispatched);
695
+ if (!added) {
696
+ logger.warn("undispatched_inbox_at_capacity", {
697
+ from: payload.from,
698
+ size: undispatchedInbox.size(),
699
+ capacity: undispatchedInbox.capacity(),
700
+ });
701
+ }
702
+ else {
703
+ logger.info("undispatched_added", { id: undispatched.id, reason });
704
+ const wireReason = undispatched.reason === "unaddressed"
705
+ ? "no_target_endpoint"
706
+ : undispatched.reason;
707
+ emitUndispatched({
708
+ type: "undispatched_changed",
709
+ version: 1,
710
+ action: "add",
711
+ undispatched_id: undispatched.id,
712
+ sender_address: undispatched.sender_address,
713
+ content_preview: truncateContentPreview(undispatched.content, CONTENT_PREVIEW_MAX_CODEPOINTS),
714
+ reason: wireReason,
715
+ ...(undispatched.original_target_endpoint_id !== undefined && {
716
+ target_endpoint_id_hint: undispatched.original_target_endpoint_id,
717
+ }),
718
+ ...(typeof correlation_id === "string" && {
719
+ in_reply_to: correlation_id,
720
+ }),
721
+ arrived_at: Date.parse(undispatched.arrived_at),
722
+ });
723
+ }
724
+ });
725
+ connector.on("delivery_pending", ({ payload }) => {
726
+ connector.ackDelivery(payload.upTo);
727
+ });
728
+ writeDiscoveryFile(paths.discoveryFile, {
729
+ version: 1,
730
+ pid: process.pid,
731
+ startedAt: new Date().toISOString(),
732
+ ipcUrl: ipc.ipcUrl,
733
+ bearerToken,
734
+ });
735
+ logger.info("discovery_written", { path: paths.discoveryFile });
736
+ let servicesEventClient;
737
+ if (servicesEventClientFactory) {
738
+ servicesEventClient = servicesEventClientFactory({
739
+ dispatcher: controlEventDispatcher,
740
+ logger,
741
+ });
742
+ try {
743
+ await servicesEventClient.start();
744
+ }
745
+ catch (err) {
746
+ logger.warn("services_event_client_start_failed", {
747
+ err: err instanceof Error ? err.message : String(err),
748
+ });
749
+ }
750
+ }
751
+ let shutdownStarted = false;
752
+ const shutdown = async (reason) => {
753
+ if (shutdownStarted)
754
+ return;
755
+ shutdownStarted = true;
756
+ logger.info("shutdown_begin", { reason });
757
+ if (servicesEventClient) {
758
+ try {
759
+ await servicesEventClient.stop();
760
+ }
761
+ catch {
762
+ }
763
+ }
764
+ graceTimers.cancelAll();
765
+ spawnCorrelation.cancelAll();
766
+ if (presenceGraceTimer) {
767
+ clearTimeout(presenceGraceTimer);
768
+ presenceGraceTimer = undefined;
769
+ }
770
+ clearInterval(replyCorrelationSweepTimer);
771
+ if (retryQueue.size() > 0) {
772
+ logger.warn("retry_queue_dropped_on_shutdown", {
773
+ size: retryQueue.size(),
774
+ reason,
775
+ });
776
+ }
777
+ if (undispatchedEmitter.size() > 0) {
778
+ logger.warn("undispatched_changed_queue_dropped_on_shutdown", {
779
+ size: undispatchedEmitter.size(),
780
+ reason,
781
+ });
782
+ }
783
+ await undispatchedEmitter.shutdown();
784
+ if (networkPresenceEmitter.size() > 0) {
785
+ logger.warn("network_presence_changed_queue_dropped_on_shutdown", {
786
+ size: networkPresenceEmitter.size(),
787
+ reason,
788
+ });
789
+ }
790
+ await networkPresenceEmitter.shutdown();
791
+ const endpoints = registry.list();
792
+ await Promise.all(endpoints.map(async (entry) => {
793
+ try {
794
+ await apiPort.unregister(entry.endpoint_id);
795
+ }
796
+ catch {
797
+ }
798
+ }));
799
+ deleteDiscoveryFile(paths.discoveryFile);
800
+ try {
801
+ await ipc.close();
802
+ }
803
+ catch (err) {
804
+ logger.warn("ipc_close_error", {
805
+ err: err instanceof Error ? err.message : String(err),
806
+ });
807
+ }
808
+ try {
809
+ connector.disconnect();
810
+ }
811
+ catch {
812
+ }
813
+ };
814
+ return {
815
+ bearerToken,
816
+ ipcUrl: ipc.ipcUrl,
817
+ shutdown,
818
+ registrySize: () => registry.size(),
819
+ state: (endpoint_id) => registry.get(endpoint_id)?.state,
820
+ undispatchedInbox,
821
+ controlEventDispatcher,
822
+ networkPresence: () => networkPresence,
823
+ pendingSpawnCount: () => spawnCorrelation.size(),
824
+ retryQueueSize: () => retryQueue.size(),
825
+ undispatchedChangedQueueSize: () => undispatchedEmitter.size(),
826
+ networkPresenceChangedQueueSize: () => networkPresenceEmitter.size(),
827
+ replyCorrelationCacheSize: (endpoint_id) => replyCorrelationCache.sizeForEndpoint(endpoint_id),
828
+ };
829
+ }
830
+ export function readPluginPidHeader(req) {
831
+ const raw = req.headers["x-plugin-pid"];
832
+ const str = Array.isArray(raw) ? raw[0] : raw;
833
+ if (!str)
834
+ return null;
835
+ const n = Number.parseInt(str, 10);
836
+ return Number.isFinite(n) && n > 0 ? n : null;
837
+ }