@masons/runtime-broker 0.2.0 → 0.2.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 (58) hide show
  1. package/dist/broker/broker-daemon.d.ts +8 -0
  2. package/dist/broker/broker-daemon.d.ts.map +1 -1
  3. package/dist/broker/broker-daemon.js +731 -81
  4. package/dist/broker/connector-ws.d.ts +4 -0
  5. package/dist/broker/connector-ws.d.ts.map +1 -1
  6. package/dist/broker/connector-ws.js +12 -0
  7. package/dist/broker/control-event-dispatcher.d.ts.map +1 -1
  8. package/dist/broker/control-event-dispatcher.js +34 -16
  9. package/dist/broker/control-event-types.d.ts +12 -2
  10. package/dist/broker/control-event-types.d.ts.map +1 -1
  11. package/dist/broker/delivery-cursor-file.d.ts +8 -0
  12. package/dist/broker/delivery-cursor-file.d.ts.map +1 -0
  13. package/dist/broker/delivery-cursor-file.js +71 -0
  14. package/dist/broker/endpoint-registry.d.ts +7 -0
  15. package/dist/broker/endpoint-registry.d.ts.map +1 -1
  16. package/dist/broker/endpoint-registry.js +3 -0
  17. package/dist/broker/entry.d.ts.map +1 -1
  18. package/dist/broker/entry.js +7 -1
  19. package/dist/broker/ipc-server.d.ts +33 -1
  20. package/dist/broker/ipc-server.d.ts.map +1 -1
  21. package/dist/broker/ipc-server.js +8 -1
  22. package/dist/broker/paths.d.ts +1 -0
  23. package/dist/broker/paths.d.ts.map +1 -1
  24. package/dist/broker/paths.js +1 -0
  25. package/dist/broker/reconnecting-buffer.d.ts +3 -0
  26. package/dist/broker/reconnecting-buffer.d.ts.map +1 -1
  27. package/dist/broker/reconnecting-buffer.js +3 -0
  28. package/dist/broker/runtime-endpoint-port.d.ts +4 -0
  29. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -1
  30. package/dist/broker/runtime-inbound-routed-emitter.d.ts +22 -0
  31. package/dist/broker/runtime-inbound-routed-emitter.d.ts.map +1 -0
  32. package/dist/broker/runtime-inbound-routed-emitter.js +147 -0
  33. package/dist/broker/runtime-inbound-routed-event-types.d.ts +10 -0
  34. package/dist/broker/runtime-inbound-routed-event-types.d.ts.map +1 -0
  35. package/dist/broker/runtime-inbound-routed-event-types.js +1 -0
  36. package/dist/broker/runtime-processing-state-event-types.d.ts +26 -0
  37. package/dist/broker/runtime-processing-state-event-types.d.ts.map +1 -0
  38. package/dist/broker/runtime-processing-state-event-types.js +1 -0
  39. package/dist/broker/undispatched-changed-event-types.d.ts +1 -0
  40. package/dist/broker/undispatched-changed-event-types.d.ts.map +1 -1
  41. package/dist/broker/undispatched-inbox.d.ts +1 -0
  42. package/dist/broker/undispatched-inbox.d.ts.map +1 -1
  43. package/dist/broker/version-handshake.js +1 -1
  44. package/dist/broker-client/broker-client.d.ts +31 -0
  45. package/dist/broker-client/broker-client.d.ts.map +1 -1
  46. package/dist/broker-client/broker-client.js +30 -0
  47. package/dist/connector-client.d.ts +5 -0
  48. package/dist/connector-client.d.ts.map +1 -1
  49. package/dist/connector-client.js +47 -5
  50. package/dist/runtime-endpoint-client.d.ts +14 -0
  51. package/dist/runtime-endpoint-client.d.ts.map +1 -1
  52. package/dist/runtime-endpoint-client.js +66 -0
  53. package/dist/types.d.ts +2 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +1 -0
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { spawn as spawnChild } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { basename } from "node:path";
4
4
  import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
5
+ import { readDeliveryCursorFile, writeDeliveryCursorFile, } from "./delivery-cursor-file.js";
5
6
  import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
6
7
  import { EndpointRegistry } from "./endpoint-registry.js";
7
8
  import { DEFAULT_GRACE_MS, transition, } from "./endpoint-state-machine.js";
@@ -13,12 +14,23 @@ import { isPluginAlive as defaultIsPluginAlive } from "./plugin-liveness.js";
13
14
  import { createReceivedMessageCorrelationCache, DEFAULT_TTL_MS as DEFAULT_REPLY_CORRELATION_TTL_MS, } from "./received-message-correlation-cache.js";
14
15
  import { createReconnectingBufferManager, } from "./reconnecting-buffer.js";
15
16
  import { RoutingTable } from "./routing-table.js";
17
+ import { createRuntimeInboundRoutedEmitter, postRuntimeInboundRoutedViaPort, } from "./runtime-inbound-routed-emitter.js";
16
18
  import { createSpawnCorrelationManager, createSpawnRateLimiter, } from "./spawn-correlation.js";
17
19
  import { TASK_HINT_MAX_LENGTH, updateTaskHint } from "./task-hint-handler.js";
18
20
  import { createTransitionStateRetryQueue } from "./transition-state-retry-queue.js";
19
21
  import { CONTENT_PREVIEW_MAX_CODEPOINTS, truncateContentPreview, } from "./undispatched-changed-event-types.js";
20
22
  import { createUndispatchedChangedEmitter, postUndispatchedChangedViaPort, } from "./undispatched-emitter.js";
21
23
  import { createUndispatchedInbox, } from "./undispatched-inbox.js";
24
+ const REMOTE_SPAWN_CAPABILITY = "remote_spawn_v1";
25
+ const DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS = 30_000;
26
+ const RUNTIME_PROCESSING_RETRY_INITIAL_MS = 1_000;
27
+ const RUNTIME_PROCESSING_RETRY_MAX_MS = 30_000;
28
+ function isBackgroundExchangeMode(value) {
29
+ return value === "resident_endpoint" || value === "adapter_managed_turn";
30
+ }
31
+ function normalizeBackgroundExchangeMode(value) {
32
+ return isBackgroundExchangeMode(value) ? value : "resident_endpoint";
33
+ }
22
34
  function derivePublicDisplayLabel(body) {
23
35
  const parts = [
24
36
  body.kind,
@@ -36,10 +48,17 @@ export async function startBrokerDaemon(opts) {
36
48
  const { paths, asNodeId: _asNodeIdReservedForA3, connector, apiPort, logger, closedEndpointLookup, livenessProbe = defaultIsPluginAlive, spawnDriverRegistry, spawnFn = spawnChild, servicesEventClientFactory, } = opts;
37
49
  const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
38
50
  const presenceGraceMs = opts.presenceGraceMs;
51
+ const runtimeAssignmentAcceptanceTimeoutMs = opts.runtimeAssignmentAcceptanceTimeoutMs ??
52
+ DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS;
39
53
  const postControlAck = opts.postControlAck ??
40
54
  (async () => {
41
55
  });
42
56
  void _asNodeIdReservedForA3;
57
+ const persistedDeliveryCursor = readDeliveryCursorFile(paths.deliveryCursorFile) ?? 0;
58
+ connector.setDeliveryCursor(persistedDeliveryCursor);
59
+ logger.info("delivery_cursor_loaded", {
60
+ lastKnownSeq: persistedDeliveryCursor,
61
+ });
43
62
  logger.info("connecting_to_connector");
44
63
  await connector.connect();
45
64
  logger.info("connector_connected");
@@ -55,6 +74,8 @@ export async function startBrokerDaemon(opts) {
55
74
  const reconnectingBuffers = createReconnectingBufferManager(paths.buffersDir);
56
75
  const graceTimers = createGraceTimerManager();
57
76
  const controlEventDispatcher = createControlEventDispatcher();
77
+ const runtimeAssignments = new Map();
78
+ const runtimeAssignmentByEndpointCorrelation = new Map();
58
79
  const spawnCorrelation = createSpawnCorrelationManager({
59
80
  timeoutMs: opts.spawnCorrelationTimeoutMs,
60
81
  });
@@ -86,7 +107,8 @@ export async function startBrokerDaemon(opts) {
86
107
  }
87
108
  }, replyCorrelationSweepInterval);
88
109
  replyCorrelationSweepTimer.unref?.();
89
- const undispatchedEmitter = createUndispatchedChangedEmitter(opts.undispatchedChangedPost ?? postUndispatchedChangedViaPort(apiPort), logger, {
110
+ const postUndispatchedChanged = opts.undispatchedChangedPost ?? postUndispatchedChangedViaPort(apiPort);
111
+ const undispatchedEmitter = createUndispatchedChangedEmitter(postUndispatchedChanged, logger, {
90
112
  capacity: opts.undispatchedChangedCapacity,
91
113
  backoffInitialMs: opts.undispatchedChangedBackoffInitialMs,
92
114
  backoffMaxMs: opts.undispatchedChangedBackoffMaxMs,
@@ -105,6 +127,144 @@ export async function startBrokerDaemon(opts) {
105
127
  const emitNetworkPresence = (event) => {
106
128
  networkPresenceEmitter.enqueue(event);
107
129
  };
130
+ const postRuntimeInboundRouted = opts.runtimeInboundRoutedPost ?? postRuntimeInboundRoutedViaPort(apiPort);
131
+ const runtimeInboundRoutedEmitter = createRuntimeInboundRoutedEmitter(postRuntimeInboundRouted, logger, {
132
+ capacity: opts.runtimeInboundRoutedCapacity,
133
+ backoffInitialMs: opts.runtimeInboundRoutedBackoffInitialMs,
134
+ backoffMaxMs: opts.runtimeInboundRoutedBackoffMaxMs,
135
+ maxRetries: opts.runtimeInboundRoutedMaxRetries,
136
+ });
137
+ const runtimeAssignmentKey = (sourceMessageId, assignmentId) => `${sourceMessageId}\u0000${assignmentId}`;
138
+ const endpointCorrelationKey = (endpointId, correlationId) => `${endpointId}\u0000${correlationId}`;
139
+ const isBackgroundExchangeTarget = (entry) => Boolean(entry.runtime_capabilities?.includes("background_exchange_v1") &&
140
+ isBackgroundExchangeMode(entry.background_exchange_mode));
141
+ const rememberRuntimeAssignmentCorrelation = (endpointId, correlationId, assignmentKey) => {
142
+ if (typeof correlationId !== "string" || correlationId.length === 0) {
143
+ return;
144
+ }
145
+ runtimeAssignmentByEndpointCorrelation.set(endpointCorrelationKey(endpointId, correlationId), assignmentKey);
146
+ };
147
+ const forgetRuntimeAssignment = (key) => {
148
+ const assignment = runtimeAssignments.get(key);
149
+ if (assignment?.acceptanceTimer) {
150
+ clearTimeout(assignment.acceptanceTimer);
151
+ }
152
+ if (assignment?.replyEmittedRetryTimer) {
153
+ clearTimeout(assignment.replyEmittedRetryTimer);
154
+ }
155
+ runtimeAssignments.delete(key);
156
+ for (const [correlationKey, assignmentKey] of Array.from(runtimeAssignmentByEndpointCorrelation.entries())) {
157
+ if (assignmentKey === key) {
158
+ runtimeAssignmentByEndpointCorrelation.delete(correlationKey);
159
+ }
160
+ }
161
+ };
162
+ const scheduleAcceptanceTimeout = (key) => {
163
+ if (runtimeAssignmentAcceptanceTimeoutMs <= 0)
164
+ return;
165
+ const assignment = runtimeAssignments.get(key);
166
+ if (!assignment || assignment.accepted || assignment.acceptanceTimer) {
167
+ return;
168
+ }
169
+ const timer = setTimeout(() => {
170
+ const current = runtimeAssignments.get(key);
171
+ if (!current || current.accepted)
172
+ return;
173
+ logger.warn("runtime_assignment_acceptance_timeout", {
174
+ undispatched_id: current.context.undispatchedId,
175
+ source_message_id: current.context.sourceMessageId,
176
+ target_endpoint_id: current.context.targetRef.projection_endpoint_id,
177
+ });
178
+ void restorePendingAssignmentsForEndpoint(current.context.targetRef.projection_endpoint_id, "adapter_acceptance_timeout");
179
+ }, runtimeAssignmentAcceptanceTimeoutMs);
180
+ timer.unref?.();
181
+ assignment.acceptanceTimer = timer;
182
+ };
183
+ const emitRuntimeProcessingState = async (event) => {
184
+ if (!apiPort.emitRuntimeProcessingState) {
185
+ return {
186
+ ok: false,
187
+ terminal: true,
188
+ detail: "runtime_processing_state_port_not_configured",
189
+ };
190
+ }
191
+ try {
192
+ return await apiPort.emitRuntimeProcessingState(event);
193
+ }
194
+ catch (err) {
195
+ return {
196
+ ok: false,
197
+ terminal: false,
198
+ detail: err instanceof Error ? err.message : String(err),
199
+ };
200
+ }
201
+ };
202
+ const directPostUndispatched = async (event, opts = {}) => {
203
+ let outcome;
204
+ try {
205
+ outcome = await postUndispatchedChanged(event);
206
+ }
207
+ catch (err) {
208
+ outcome = {
209
+ ok: false,
210
+ terminal: false,
211
+ detail: err instanceof Error ? err.message : String(err),
212
+ };
213
+ }
214
+ if (outcome.ok)
215
+ return true;
216
+ logger.warn("undispatched_changed_direct_post_failed", {
217
+ action: event.action,
218
+ undispatched_id: event.undispatched_id,
219
+ terminal: outcome.terminal,
220
+ status: outcome.status,
221
+ detail: outcome.detail,
222
+ });
223
+ if (!outcome.terminal && opts.queueOnTransientFailure !== false) {
224
+ undispatchedEmitter.enqueue(event);
225
+ }
226
+ return false;
227
+ };
228
+ const directPostRuntimeInboundRouted = async (event, opts = {}) => {
229
+ let outcome;
230
+ try {
231
+ outcome = await postRuntimeInboundRouted(event);
232
+ }
233
+ catch (err) {
234
+ outcome = {
235
+ ok: false,
236
+ terminal: false,
237
+ detail: err instanceof Error ? err.message : String(err),
238
+ };
239
+ }
240
+ if (outcome.ok)
241
+ return true;
242
+ logger.warn("runtime_inbound_routed_direct_post_failed", {
243
+ source_message_id: event.source_message_id,
244
+ routed_to_endpoint_id: event.routed_to_endpoint_id,
245
+ terminal: outcome.terminal,
246
+ status: outcome.status,
247
+ detail: outcome.detail,
248
+ });
249
+ if (!outcome.terminal && opts.queueOnTransientFailure !== false) {
250
+ runtimeInboundRoutedEmitter.enqueue(event);
251
+ }
252
+ return false;
253
+ };
254
+ const markDeliverySeqAccepted = (seq) => {
255
+ if (typeof seq !== "number")
256
+ return true;
257
+ const before = connector.getDeliveryCursor();
258
+ const prepared = connector.prepareDeliverySeqAccepted(seq);
259
+ if (typeof prepared === "number" && prepared !== before) {
260
+ writeDeliveryCursorFile(paths.deliveryCursorFile, prepared);
261
+ }
262
+ const after = connector.markDeliverySeqAccepted(seq);
263
+ if (after !== prepared) {
264
+ throw new Error("delivery cursor changed during durable commit");
265
+ }
266
+ return true;
267
+ };
108
268
  const recordInboundCorrelation = (endpoint_id, metadata) => {
109
269
  const c = metadata && typeof metadata.correlation_id === "string"
110
270
  ? metadata.correlation_id
@@ -113,6 +273,127 @@ export async function startBrokerDaemon(opts) {
113
273
  return;
114
274
  replyCorrelationCache.record(endpoint_id, c);
115
275
  };
276
+ const restorePendingAssignmentsForEndpoint = (endpoint_id, reason) => {
277
+ const tasks = [];
278
+ for (const [key, assignment] of Array.from(runtimeAssignments.entries())) {
279
+ if (assignment.context.targetRef.projection_endpoint_id !== endpoint_id ||
280
+ assignment.accepted) {
281
+ continue;
282
+ }
283
+ if (assignment.acceptanceTimer) {
284
+ clearTimeout(assignment.acceptanceTimer);
285
+ assignment.acceptanceTimer = undefined;
286
+ }
287
+ const readded = undispatchedInbox.tryAdd(assignment.message);
288
+ if (!readded) {
289
+ logger.error("runtime_assignment_reinsert_failed_at_capacity", {
290
+ undispatched_id: assignment.context.undispatchedId,
291
+ source_message_id: assignment.context.sourceMessageId,
292
+ target_endpoint_id: endpoint_id,
293
+ inbox_size: undispatchedInbox.size(),
294
+ inbox_capacity: undispatchedInbox.capacity(),
295
+ });
296
+ scheduleAcceptanceTimeout(key);
297
+ continue;
298
+ }
299
+ const task = emitRuntimeProcessingState({
300
+ type: "runtime_processing_state",
301
+ version: 1,
302
+ source_message_id: assignment.context.sourceMessageId,
303
+ assignment_id: assignment.context.assignmentId,
304
+ state_event_id: `${assignment.context.assignmentId}:target_lost`,
305
+ state_sequence: 2,
306
+ target_ref: assignment.context.targetRef,
307
+ state: "target_lost_before_acceptance",
308
+ occurred_at: assignment.assignedAt + 1,
309
+ undispatched_id: assignment.context.undispatchedId,
310
+ reason_code: reason,
311
+ }).then((outcome) => {
312
+ if (outcome.ok) {
313
+ forgetRuntimeAssignment(key);
314
+ return;
315
+ }
316
+ logger.warn("runtime_assignment_target_lost_emit_failed", {
317
+ undispatched_id: assignment.context.undispatchedId,
318
+ source_message_id: assignment.context.sourceMessageId,
319
+ target_endpoint_id: endpoint_id,
320
+ status: outcome.status,
321
+ terminal: outcome.terminal,
322
+ detail: outcome.detail,
323
+ });
324
+ scheduleAcceptanceTimeout(key);
325
+ });
326
+ tasks.push(task);
327
+ }
328
+ return Promise.allSettled(tasks).then(() => undefined);
329
+ };
330
+ const emitReplyEmittedWithRetry = (assignmentKey, event, attempt = 0) => {
331
+ const assignment = runtimeAssignments.get(assignmentKey);
332
+ if (!assignment)
333
+ return;
334
+ assignment.replyEmittedEvent = event;
335
+ if (assignment.replyEmittedRetryTimer) {
336
+ clearTimeout(assignment.replyEmittedRetryTimer);
337
+ assignment.replyEmittedRetryTimer = undefined;
338
+ }
339
+ void emitRuntimeProcessingState(event).then((outcome) => {
340
+ const current = runtimeAssignments.get(assignmentKey);
341
+ if (!current)
342
+ return;
343
+ if (outcome.ok) {
344
+ forgetRuntimeAssignment(assignmentKey);
345
+ return;
346
+ }
347
+ logger.warn("runtime_reply_emitted_projection_failed", {
348
+ source_message_id: current.context.sourceMessageId,
349
+ assignment_id: current.context.assignmentId,
350
+ status: outcome.status,
351
+ terminal: outcome.terminal,
352
+ detail: outcome.detail,
353
+ });
354
+ if (outcome.terminal)
355
+ return;
356
+ const delay = Math.min(RUNTIME_PROCESSING_RETRY_INITIAL_MS * 2 ** attempt, RUNTIME_PROCESSING_RETRY_MAX_MS);
357
+ const timer = setTimeout(() => {
358
+ emitReplyEmittedWithRetry(assignmentKey, event, attempt + 1);
359
+ }, delay);
360
+ timer.unref?.();
361
+ current.replyEmittedRetryTimer = timer;
362
+ });
363
+ };
364
+ const flushRuntimeAssignmentsOnShutdown = async () => {
365
+ const endpointIds = new Set();
366
+ for (const assignment of runtimeAssignments.values()) {
367
+ endpointIds.add(assignment.context.targetRef.projection_endpoint_id);
368
+ }
369
+ for (const [key, assignment] of Array.from(runtimeAssignments.entries())) {
370
+ if (assignment.replyEmittedEvent) {
371
+ const outcome = await emitRuntimeProcessingState(assignment.replyEmittedEvent);
372
+ if (outcome.ok) {
373
+ forgetRuntimeAssignment(key);
374
+ continue;
375
+ }
376
+ logger.warn("runtime_reply_emitted_shutdown_flush_failed", {
377
+ source_message_id: assignment.context.sourceMessageId,
378
+ assignment_id: assignment.context.assignmentId,
379
+ status: outcome.status,
380
+ terminal: outcome.terminal,
381
+ detail: outcome.detail,
382
+ });
383
+ }
384
+ }
385
+ for (const endpointId of endpointIds) {
386
+ await restorePendingAssignmentsForEndpoint(endpointId, "broker_shutdown");
387
+ }
388
+ if (runtimeAssignments.size > 0) {
389
+ logger.warn("runtime_assignments_dropped_on_shutdown", {
390
+ size: runtimeAssignments.size,
391
+ });
392
+ }
393
+ for (const key of Array.from(runtimeAssignments.keys())) {
394
+ forgetRuntimeAssignment(key);
395
+ }
396
+ };
116
397
  const channels = new Map();
117
398
  let networkPresence = "offline";
118
399
  let presenceGraceTimer;
@@ -275,25 +556,46 @@ export async function startBrokerDaemon(opts) {
275
556
  break;
276
557
  case "drain_buffer": {
277
558
  const buf = reconnectingBuffers.forEndpoint(endpoint_id);
278
- const drained = buf.drain();
559
+ const drained = buf.read();
279
560
  const e = registry.get(endpoint_id);
561
+ let allPushed = true;
280
562
  if (e) {
281
563
  for (const msg of drained) {
282
- pushToPlugin(e.ipc_ws, {
564
+ const pushed = pushToPlugin(e.ipc_ws, {
283
565
  event: "message_received",
284
566
  from: msg.envelope.from,
285
567
  content: msg.envelope.content,
286
568
  contentType: msg.envelope.contentType,
287
569
  metadata: msg.envelope.metadata,
570
+ ...(msg.envelope.sourceMessageId !== undefined && {
571
+ sourceMessageId: msg.envelope.sourceMessageId,
572
+ }),
573
+ ...(msg.envelope.runtimeAssignment !== undefined && {
574
+ runtimeAssignment: msg.envelope.runtimeAssignment,
575
+ }),
288
576
  });
577
+ if (!pushed) {
578
+ allPushed = false;
579
+ break;
580
+ }
289
581
  }
290
582
  }
583
+ if (allPushed) {
584
+ buf.delete();
585
+ }
586
+ else {
587
+ logger.warn("reconnecting_buffer_drain_failed", {
588
+ endpoint_id,
589
+ buffered_count: drained.length,
590
+ });
591
+ }
291
592
  break;
292
593
  }
293
594
  case "delete_buffer":
294
595
  reconnectingBuffers.cleanup(endpoint_id);
295
596
  break;
296
597
  case "remove_from_registry":
598
+ void restorePendingAssignmentsForEndpoint(endpoint_id, "endpoint_lost");
297
599
  clearHeartbeat(endpoint_id);
298
600
  registry.unregister(endpoint_id);
299
601
  replyCorrelationCache.forgetEndpoint(endpoint_id);
@@ -304,20 +606,49 @@ export async function startBrokerDaemon(opts) {
304
606
  }
305
607
  };
306
608
  controlEventDispatcher.on("dispatch_undispatched", async (event) => {
307
- const taken = undispatchedInbox.take(event.undispatched_id);
609
+ const targetEndpointId = event.target_ref?.projection_endpoint_id ?? event.target_endpoint_id;
610
+ if (!targetEndpointId) {
611
+ return {
612
+ ok: false,
613
+ detail: "dispatch target_ref missing",
614
+ };
615
+ }
616
+ const assignmentKey = runtimeAssignmentKey(event.source_message_id, event.idempotency_key);
617
+ const existingAssignment = runtimeAssignments.get(assignmentKey);
618
+ const taken = undispatchedInbox.take(event.undispatched_id) ??
619
+ (!existingAssignment?.accepted &&
620
+ existingAssignment?.context.undispatchedId === event.undispatched_id &&
621
+ existingAssignment.context.targetRef.projection_endpoint_id ===
622
+ targetEndpointId
623
+ ? existingAssignment.message
624
+ : undefined);
308
625
  if (!taken) {
309
626
  return {
310
627
  ok: false,
311
628
  detail: `unknown undispatched_id: ${event.undispatched_id}`,
312
629
  };
313
630
  }
314
- const target = registry.get(event.target_endpoint_id);
631
+ if (taken.source_message_id === undefined) {
632
+ undispatchedInbox.tryAdd(taken);
633
+ return {
634
+ ok: false,
635
+ detail: "dispatch source_message_id missing from inbox row",
636
+ };
637
+ }
638
+ if (event.source_message_id !== taken.source_message_id) {
639
+ undispatchedInbox.tryAdd(taken);
640
+ return {
641
+ ok: false,
642
+ detail: `source_message_id mismatch: ${event.source_message_id}`,
643
+ };
644
+ }
645
+ const target = registry.get(targetEndpointId);
315
646
  if (!target) {
316
647
  const readded = undispatchedInbox.tryAdd(taken);
317
648
  if (!readded) {
318
649
  logger.error("dispatch_reinsert_failed_at_capacity", {
319
650
  undispatched_id: event.undispatched_id,
320
- target_endpoint_id: event.target_endpoint_id,
651
+ target_endpoint_id: targetEndpointId,
321
652
  inbox_size: undispatchedInbox.size(),
322
653
  inbox_capacity: undispatchedInbox.capacity(),
323
654
  });
@@ -326,14 +657,82 @@ export async function startBrokerDaemon(opts) {
326
657
  version: 1,
327
658
  action: "remove",
328
659
  undispatched_id: event.undispatched_id,
660
+ ...(taken.source_message_id !== undefined && {
661
+ source_message_id: taken.source_message_id,
662
+ }),
329
663
  remove_reason: "lost_at_capacity_during_redispatch_bounce",
330
664
  });
331
665
  }
332
666
  return {
333
667
  ok: false,
334
- detail: `target endpoint not found: ${event.target_endpoint_id}`,
668
+ detail: `target endpoint not found: ${targetEndpointId}`,
669
+ };
670
+ }
671
+ const targetRef = event.target_ref ?? {
672
+ kind: "projection_endpoint",
673
+ projection_endpoint_id: targetEndpointId,
674
+ background_exchange_mode: normalizeBackgroundExchangeMode(target.background_exchange_mode),
675
+ };
676
+ if (!target.runtime_capabilities?.includes("background_exchange_v1") ||
677
+ target.background_exchange_mode !== targetRef.background_exchange_mode) {
678
+ undispatchedInbox.tryAdd(taken);
679
+ return {
680
+ ok: false,
681
+ detail: `target endpoint is not background-exchange capable: ${targetEndpointId}`,
335
682
  };
336
683
  }
684
+ const assignedAt = event.emitted_at;
685
+ const processingEvent = {
686
+ type: "runtime_processing_state",
687
+ version: 1,
688
+ source_message_id: event.source_message_id,
689
+ assignment_id: event.idempotency_key,
690
+ state_event_id: `${event.idempotency_key}:assigned`,
691
+ state_sequence: 1,
692
+ target_ref: targetRef,
693
+ state: "assigned_to_runtime_target",
694
+ occurred_at: assignedAt,
695
+ undispatched_id: event.undispatched_id,
696
+ };
697
+ const processingResult = await emitRuntimeProcessingState(processingEvent);
698
+ if (!processingResult.ok) {
699
+ undispatchedInbox.tryAdd(taken);
700
+ logger.warn("runtime_processing_state_emit_failed", {
701
+ undispatched_id: event.undispatched_id,
702
+ source_message_id: event.source_message_id,
703
+ target_endpoint_id: targetEndpointId,
704
+ status: processingResult.status,
705
+ terminal: processingResult.terminal,
706
+ detail: processingResult.detail,
707
+ });
708
+ return {
709
+ ok: false,
710
+ detail: processingResult.detail ??
711
+ "runtime_processing_state_assignment_failed",
712
+ };
713
+ }
714
+ const runtimeAssignment = {
715
+ undispatchedId: event.undispatched_id,
716
+ sourceMessageId: event.source_message_id,
717
+ assignmentId: event.idempotency_key,
718
+ targetRef,
719
+ deliveryIntent: "external_handoff",
720
+ ...(typeof taken.metadata.correlation_id === "string" && {
721
+ replyCorrelationId: taken.metadata.correlation_id,
722
+ }),
723
+ };
724
+ const storedAssignment = existingAssignment ?? {
725
+ context: runtimeAssignment,
726
+ message: taken,
727
+ accepted: false,
728
+ assignedAt,
729
+ };
730
+ storedAssignment.context = runtimeAssignment;
731
+ storedAssignment.message = taken;
732
+ storedAssignment.assignedAt = assignedAt;
733
+ runtimeAssignments.set(assignmentKey, storedAssignment);
734
+ scheduleAcceptanceTimeout(assignmentKey);
735
+ rememberRuntimeAssignmentCorrelation(target.endpoint_id, taken.metadata.correlation_id, assignmentKey);
337
736
  const stamped = {
338
737
  event: "message_received",
339
738
  from: taken.sender_address,
@@ -342,38 +741,50 @@ export async function startBrokerDaemon(opts) {
342
741
  metadata: {
343
742
  ...taken.metadata,
344
743
  dispatched_from: event.undispatched_id,
345
- dispatched_by: "passport",
744
+ delivery_intent: "external_handoff",
346
745
  },
746
+ sourceMessageId: taken.source_message_id,
747
+ runtimeAssignment,
347
748
  };
348
749
  recordInboundCorrelation(target.endpoint_id, stamped.metadata);
349
750
  if (target.state === "active") {
350
- pushToPlugin(target.ipc_ws, stamped);
751
+ if (!pushToPlugin(target.ipc_ws, stamped)) {
752
+ await restorePendingAssignmentsForEndpoint(target.endpoint_id, "ipc_push_failed");
753
+ return {
754
+ ok: false,
755
+ detail: `target endpoint not writable: ${targetEndpointId}`,
756
+ };
757
+ }
351
758
  }
352
759
  else {
353
- const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
354
- buf.append({
355
- id: randomUUID(),
356
- arrived_at: new Date().toISOString(),
357
- envelope: {
358
- from: taken.sender_address,
359
- content: taken.content,
360
- contentType: taken.content_type,
361
- metadata: stamped.metadata,
362
- },
363
- });
760
+ try {
761
+ const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
762
+ buf.append({
763
+ id: randomUUID(),
764
+ arrived_at: new Date().toISOString(),
765
+ envelope: {
766
+ from: taken.sender_address,
767
+ content: taken.content,
768
+ contentType: taken.content_type,
769
+ metadata: stamped.metadata,
770
+ sourceMessageId: stamped.sourceMessageId,
771
+ runtimeAssignment,
772
+ },
773
+ });
774
+ }
775
+ catch (err) {
776
+ await restorePendingAssignmentsForEndpoint(target.endpoint_id, "reconnecting_buffer_failed");
777
+ return {
778
+ ok: false,
779
+ detail: err instanceof Error ? err.message : String(err),
780
+ };
781
+ }
364
782
  }
365
783
  logger.info("undispatched_dispatched", {
366
784
  undispatched_id: event.undispatched_id,
367
- target_endpoint_id: event.target_endpoint_id,
785
+ target_endpoint_id: targetEndpointId,
368
786
  target_state: target.state,
369
787
  });
370
- emitUndispatched({
371
- type: "undispatched_changed",
372
- version: 1,
373
- action: "dispatch",
374
- undispatched_id: event.undispatched_id,
375
- dispatched_to_endpoint_id: event.target_endpoint_id,
376
- });
377
788
  return { ok: true };
378
789
  });
379
790
  controlEventDispatcher.on("spawn_request", async (event) => {
@@ -461,6 +872,17 @@ export async function startBrokerDaemon(opts) {
461
872
  throw new BrokerHttpError(400, "ipc_channel_missing", `no open IPC channel for plugin_pid=${body.plugin_pid}; ` +
462
873
  "open WS /v1/stream with x-plugin-pid header first");
463
874
  }
875
+ const spawnDriver = spawnDriverRegistry?.lookup(body.kind);
876
+ const spawnAvailability = spawnDriver
877
+ ? await spawnDriver.isAvailable().catch(() => ({ available: false }))
878
+ : null;
879
+ const runtimeCapabilitiesSet = new Set(body.runtime_capabilities?.filter((capability) => capability !== REMOTE_SPAWN_CAPABILITY) ?? []);
880
+ if (spawnAvailability?.available) {
881
+ runtimeCapabilitiesSet.add(REMOTE_SPAWN_CAPABILITY);
882
+ }
883
+ const runtimeCapabilities = runtimeCapabilitiesSet.size > 0
884
+ ? Array.from(runtimeCapabilitiesSet)
885
+ : undefined;
464
886
  const apiResp = await apiPort.register({
465
887
  runtime_kind: body.kind,
466
888
  endpoint_nonce: randomUUID(),
@@ -471,12 +893,18 @@ export async function startBrokerDaemon(opts) {
471
893
  tracking_ref: body.tracking_ref,
472
894
  session_name: body.session_name,
473
895
  task_hint: body.task_hint,
896
+ runtime_capabilities: runtimeCapabilities,
897
+ execution_surface: body.execution_surface,
898
+ background_exchange_mode: body.background_exchange_mode,
474
899
  });
475
900
  registry.register({
476
901
  endpoint_id: apiResp.endpoint_id,
477
902
  agent_id: body.agent_id,
478
903
  plugin_pid: body.plugin_pid,
479
904
  ipc_ws: boundWs,
905
+ runtime_capabilities: runtimeCapabilities,
906
+ execution_surface: body.execution_surface,
907
+ background_exchange_mode: body.background_exchange_mode,
480
908
  display_metadata: {
481
909
  session_name: body.session_name,
482
910
  kind: body.kind,
@@ -593,8 +1021,86 @@ export async function startBrokerDaemon(opts) {
593
1021
  }
594
1022
  throw err;
595
1023
  }
1024
+ if (inReplyTo) {
1025
+ const assignmentKey = runtimeAssignmentByEndpointCorrelation.get(endpointCorrelationKey(body.endpoint_id, inReplyTo));
1026
+ const assignment = assignmentKey !== undefined
1027
+ ? runtimeAssignments.get(assignmentKey)
1028
+ : undefined;
1029
+ if (assignmentKey !== undefined && assignment?.accepted) {
1030
+ emitReplyEmittedWithRetry(assignmentKey, {
1031
+ type: "runtime_processing_state",
1032
+ version: 1,
1033
+ source_message_id: assignment.context.sourceMessageId,
1034
+ assignment_id: assignment.context.assignmentId,
1035
+ state_event_id: `${assignment.context.assignmentId}:reply:${ack.messageId}`,
1036
+ state_sequence: Number.MAX_SAFE_INTEGER,
1037
+ target_ref: assignment.context.targetRef,
1038
+ state: "reply_emitted",
1039
+ occurred_at: Date.now(),
1040
+ undispatched_id: assignment.context.undispatchedId,
1041
+ outbound_message_id: ack.messageId,
1042
+ });
1043
+ }
1044
+ }
596
1045
  return { messageId: ack.messageId, status: ack.status };
597
1046
  },
1047
+ async reportProcessingState(body) {
1048
+ const entry = registry.get(body.endpoint_id);
1049
+ if (!entry) {
1050
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
1051
+ }
1052
+ if (entry.plugin_pid !== body.plugin_pid) {
1053
+ throw new BrokerHttpError(403, "endpoint_pid_mismatch", "plugin_pid does not own endpoint_id");
1054
+ }
1055
+ const key = runtimeAssignmentKey(body.source_message_id, body.assignment_id);
1056
+ const assignment = runtimeAssignments.get(key);
1057
+ if (!assignment) {
1058
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1059
+ }
1060
+ if (assignment.context.targetRef.projection_endpoint_id !== body.endpoint_id) {
1061
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1062
+ }
1063
+ if (body.state === "reply_emitted") {
1064
+ throw new BrokerHttpError(403, "reply_emitted_broker_owned", "reply_emitted is emitted by the broker after outbound wire send");
1065
+ }
1066
+ const event = {
1067
+ type: "runtime_processing_state",
1068
+ version: 1,
1069
+ source_message_id: body.source_message_id,
1070
+ assignment_id: body.assignment_id,
1071
+ state_event_id: body.state_event_id,
1072
+ state_sequence: body.state_sequence,
1073
+ target_ref: assignment.context.targetRef,
1074
+ state: body.state,
1075
+ occurred_at: body.occurred_at ?? Date.now(),
1076
+ undispatched_id: body.undispatched_id ?? assignment.context.undispatchedId,
1077
+ ...(body.retryable !== undefined && { retryable: body.retryable }),
1078
+ ...(body.reason_code !== undefined && {
1079
+ reason_code: body.reason_code,
1080
+ }),
1081
+ ...(body.detail !== undefined && { detail: body.detail }),
1082
+ ...(body.reply_request_id !== undefined && {
1083
+ reply_request_id: body.reply_request_id,
1084
+ }),
1085
+ ...(body.outbound_message_id !== undefined && {
1086
+ outbound_message_id: body.outbound_message_id,
1087
+ }),
1088
+ };
1089
+ const outcome = await emitRuntimeProcessingState(event);
1090
+ if (!outcome.ok) {
1091
+ throw new BrokerHttpError(outcome.terminal ? 409 : 503, "runtime_processing_state_rejected", outcome.detail ?? "runtime processing state was not accepted");
1092
+ }
1093
+ if (body.state === "adapter_accepted") {
1094
+ assignment.accepted = true;
1095
+ if (assignment.acceptanceTimer) {
1096
+ clearTimeout(assignment.acceptanceTimer);
1097
+ assignment.acceptanceTimer = undefined;
1098
+ }
1099
+ }
1100
+ if (body.state === "processing_failed" || body.state === "expired") {
1101
+ forgetRuntimeAssignment(key);
1102
+ }
1103
+ },
598
1104
  async reattachEndpoint(endpoint_id, plugin_pid, _ipcWsHint) {
599
1105
  const ipcWs = channels.get(plugin_pid);
600
1106
  if (!ipcWs) {
@@ -617,18 +1123,37 @@ export async function startBrokerDaemon(opts) {
617
1123
  }
618
1124
  bound.add(endpoint_id);
619
1125
  const buf = reconnectingBuffers.forEndpoint(endpoint_id);
620
- const drained = buf.drain();
1126
+ const drained = buf.read();
621
1127
  graceTimers.cancel(endpoint_id);
622
1128
  registry.markActive(endpoint_id, ipcWs);
1129
+ let allPushed = true;
623
1130
  for (const msg of drained) {
624
- pushToPlugin(ipcWs, {
1131
+ const pushed = pushToPlugin(ipcWs, {
625
1132
  event: "message_received",
626
1133
  from: msg.envelope.from,
627
1134
  content: msg.envelope.content,
628
1135
  contentType: msg.envelope.contentType,
629
1136
  metadata: msg.envelope.metadata,
1137
+ ...(msg.envelope.sourceMessageId !== undefined && {
1138
+ sourceMessageId: msg.envelope.sourceMessageId,
1139
+ }),
1140
+ ...(msg.envelope.runtimeAssignment !== undefined && {
1141
+ runtimeAssignment: msg.envelope.runtimeAssignment,
1142
+ }),
630
1143
  });
1144
+ if (!pushed) {
1145
+ allPushed = false;
1146
+ break;
1147
+ }
631
1148
  }
1149
+ if (!allPushed) {
1150
+ handleTransition(endpoint_id, {
1151
+ type: "ipc_close",
1152
+ plugin_alive: livenessProbe(plugin_pid),
1153
+ });
1154
+ throw new BrokerHttpError(503, "reconnecting_buffer_drain_failed", "failed to push buffered messages to reattached endpoint", { retryable: true });
1155
+ }
1156
+ buf.delete();
632
1157
  emitTransition(endpoint_id, "active", "reattach");
633
1158
  logger.info("endpoint_reattached", {
634
1159
  endpoint_id,
@@ -641,13 +1166,21 @@ export async function startBrokerDaemon(opts) {
641
1166
  return undispatchedInbox.list();
642
1167
  },
643
1168
  async dispatch(undispatched_id, target_endpoint_id) {
1169
+ const pending = undispatchedInbox.get(undispatched_id);
1170
+ const target = registry.get(target_endpoint_id);
644
1171
  const result = await controlEventDispatcher.dispatch({
645
1172
  type: "dispatch_undispatched",
646
1173
  version: 1,
647
1174
  idempotency_key: `local-${randomUUID()}`,
648
1175
  emitted_at: Date.now(),
649
1176
  undispatched_id,
650
- target_endpoint_id,
1177
+ source_message_id: pending?.source_message_id ?? undispatched_id,
1178
+ target_ref: {
1179
+ kind: "projection_endpoint",
1180
+ projection_endpoint_id: target_endpoint_id,
1181
+ background_exchange_mode: normalizeBackgroundExchangeMode(target?.background_exchange_mode),
1182
+ },
1183
+ delivery_intent: "external_handoff",
651
1184
  });
652
1185
  if (!result.ok) {
653
1186
  throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
@@ -724,42 +1257,140 @@ export async function startBrokerDaemon(opts) {
724
1257
  }
725
1258
  },
726
1259
  });
727
- connector.on("message_received", async ({ payload }) => {
1260
+ const inboundProcessing = new Set();
1261
+ const blockedInboundSeqs = new Set();
1262
+ let unsequencedInboundBlocked = false;
1263
+ const handleInboundMessage = async (payload) => {
728
1264
  const meta = payload.metadata ?? {};
729
1265
  const target_endpoint_id = typeof meta.target_endpoint_id === "string"
730
1266
  ? meta.target_endpoint_id
731
1267
  : undefined;
732
1268
  const correlation_id = typeof meta.correlation_id === "string" ? meta.correlation_id : undefined;
1269
+ const addUndispatchedFromPayload = async (opts) => {
1270
+ const undispatched = {
1271
+ id: payload.messageId ?? randomUUID(),
1272
+ ...(payload.messageId !== undefined && {
1273
+ source_message_id: payload.messageId,
1274
+ }),
1275
+ arrived_at: new Date().toISOString(),
1276
+ sender_address: payload.from,
1277
+ content: payload.content,
1278
+ content_type: payload.contentType,
1279
+ metadata: meta,
1280
+ reason: opts.reason,
1281
+ original_target_endpoint_id: opts.originalTargetEndpointId,
1282
+ original_correlation_id: opts.originalCorrelationId,
1283
+ };
1284
+ const added = undispatchedInbox.tryAdd(undispatched);
1285
+ if (!added) {
1286
+ logger.warn("undispatched_inbox_at_capacity", {
1287
+ from: payload.from,
1288
+ size: undispatchedInbox.size(),
1289
+ capacity: undispatchedInbox.capacity(),
1290
+ });
1291
+ return false;
1292
+ }
1293
+ logger.info("undispatched_added", {
1294
+ id: undispatched.id,
1295
+ reason: opts.reason,
1296
+ });
1297
+ const wireReason = undispatched.reason === "unaddressed"
1298
+ ? "no_target_endpoint"
1299
+ : undispatched.reason;
1300
+ const posted = await directPostUndispatched({
1301
+ type: "undispatched_changed",
1302
+ version: 1,
1303
+ action: "add",
1304
+ undispatched_id: undispatched.id,
1305
+ ...(undispatched.source_message_id !== undefined && {
1306
+ source_message_id: undispatched.source_message_id,
1307
+ }),
1308
+ sender_address: undispatched.sender_address,
1309
+ content_preview: truncateContentPreview(undispatched.content, CONTENT_PREVIEW_MAX_CODEPOINTS),
1310
+ reason: wireReason,
1311
+ ...(opts.originalTargetEndpointId !== undefined && {
1312
+ target_endpoint_id_hint: opts.originalTargetEndpointId,
1313
+ }),
1314
+ ...(typeof correlation_id === "string" && {
1315
+ in_reply_to: correlation_id,
1316
+ }),
1317
+ arrived_at: Date.parse(undispatched.arrived_at),
1318
+ }, { queueOnTransientFailure: typeof payload.seq !== "number" });
1319
+ if (!posted)
1320
+ return false;
1321
+ return markDeliverySeqAccepted(payload.seq);
1322
+ };
733
1323
  const decision = router.route({ target_endpoint_id, correlation_id });
734
1324
  if (decision.kind === "endpoint") {
1325
+ const isCorrelationContinuation = typeof correlation_id === "string" &&
1326
+ decision.entry.correlation_ring.has(correlation_id);
1327
+ if (isBackgroundExchangeTarget(decision.entry) &&
1328
+ !isCorrelationContinuation) {
1329
+ return addUndispatchedFromPayload({
1330
+ reason: "unaddressed",
1331
+ originalTargetEndpointId: decision.entry.endpoint_id,
1332
+ originalCorrelationId: correlation_id,
1333
+ });
1334
+ }
735
1335
  recordInboundCorrelation(decision.entry.endpoint_id, meta);
736
1336
  if (decision.entry.state === "active") {
737
- pushToPlugin(decision.entry.ipc_ws, {
1337
+ const pushed = pushToPlugin(decision.entry.ipc_ws, {
738
1338
  event: "message_received",
739
1339
  from: payload.from,
740
1340
  content: payload.content,
741
1341
  contentType: payload.contentType,
742
1342
  metadata: meta,
1343
+ ...(payload.messageId !== undefined && {
1344
+ sourceMessageId: payload.messageId,
1345
+ }),
743
1346
  });
1347
+ if (!pushed)
1348
+ return false;
744
1349
  }
745
1350
  else {
746
1351
  const buf = reconnectingBuffers.forEndpoint(decision.entry.endpoint_id);
747
- buf.append({
748
- id: randomUUID(),
749
- arrived_at: new Date().toISOString(),
750
- envelope: {
751
- from: payload.from,
752
- content: payload.content,
753
- contentType: payload.contentType,
754
- metadata: meta,
755
- },
756
- });
1352
+ try {
1353
+ buf.append({
1354
+ id: payload.messageId ?? randomUUID(),
1355
+ arrived_at: new Date().toISOString(),
1356
+ envelope: {
1357
+ from: payload.from,
1358
+ content: payload.content,
1359
+ contentType: payload.contentType,
1360
+ metadata: meta,
1361
+ ...(payload.messageId !== undefined && {
1362
+ sourceMessageId: payload.messageId,
1363
+ }),
1364
+ },
1365
+ });
1366
+ }
1367
+ catch (err) {
1368
+ logger.warn("buffer_for_reconnecting_endpoint_failed", {
1369
+ endpoint_id: decision.entry.endpoint_id,
1370
+ err: err instanceof Error ? err.message : String(err),
1371
+ });
1372
+ return false;
1373
+ }
757
1374
  logger.info("buffered_for_reconnecting_endpoint", {
758
1375
  endpoint_id: decision.entry.endpoint_id,
759
1376
  buffer_size: buf.size(),
760
1377
  });
761
1378
  }
762
- return;
1379
+ if (payload.messageId !== undefined) {
1380
+ const posted = await directPostRuntimeInboundRouted({
1381
+ type: "runtime_inbound_routed",
1382
+ version: 1,
1383
+ source_message_id: payload.messageId,
1384
+ routed_to_endpoint_id: decision.entry.endpoint_id,
1385
+ route_kind: decision.entry.state === "active"
1386
+ ? "active_endpoint"
1387
+ : "reconnecting_endpoint",
1388
+ routed_at: Date.now(),
1389
+ }, { queueOnTransientFailure: typeof payload.seq !== "number" });
1390
+ if (!posted && typeof payload.seq === "number")
1391
+ return false;
1392
+ }
1393
+ return markDeliverySeqAccepted(payload.seq);
763
1394
  }
764
1395
  let reason = "unaddressed";
765
1396
  let originalTarget;
@@ -778,49 +1409,59 @@ export async function startBrokerDaemon(opts) {
778
1409
  });
779
1410
  }
780
1411
  }
781
- const undispatched = {
782
- id: randomUUID(),
783
- arrived_at: new Date().toISOString(),
784
- sender_address: payload.from,
785
- content: payload.content,
786
- content_type: payload.contentType,
787
- metadata: meta,
1412
+ return addUndispatchedFromPayload({
788
1413
  reason,
789
- original_target_endpoint_id: originalTarget,
790
- original_correlation_id: correlation_id,
791
- };
792
- const added = undispatchedInbox.tryAdd(undispatched);
793
- if (!added) {
794
- logger.warn("undispatched_inbox_at_capacity", {
1414
+ originalTargetEndpointId: originalTarget,
1415
+ originalCorrelationId: correlation_id,
1416
+ });
1417
+ };
1418
+ connector.on("message_received", ({ payload }) => {
1419
+ let task;
1420
+ task = handleInboundMessage(payload)
1421
+ .then((accepted) => {
1422
+ if (typeof payload.seq === "number") {
1423
+ if (accepted)
1424
+ blockedInboundSeqs.delete(payload.seq);
1425
+ else
1426
+ blockedInboundSeqs.add(payload.seq);
1427
+ return;
1428
+ }
1429
+ if (accepted)
1430
+ unsequencedInboundBlocked = false;
1431
+ else
1432
+ unsequencedInboundBlocked = true;
1433
+ })
1434
+ .catch((err) => {
1435
+ if (typeof payload.seq === "number") {
1436
+ blockedInboundSeqs.add(payload.seq);
1437
+ }
1438
+ else {
1439
+ unsequencedInboundBlocked = true;
1440
+ }
1441
+ logger.error("inbound_message_handling_failed", {
1442
+ err: err instanceof Error ? err.message : String(err),
1443
+ messageId: payload.messageId,
795
1444
  from: payload.from,
796
- size: undispatchedInbox.size(),
797
- capacity: undispatchedInbox.capacity(),
798
1445
  });
1446
+ })
1447
+ .finally(() => {
1448
+ inboundProcessing.delete(task);
1449
+ });
1450
+ inboundProcessing.add(task);
1451
+ });
1452
+ connector.on("delivery_pending", async ({ payload }) => {
1453
+ if (inboundProcessing.size > 0) {
1454
+ await Promise.allSettled([...inboundProcessing]);
799
1455
  }
800
- else {
801
- logger.info("undispatched_added", { id: undispatched.id, reason });
802
- const wireReason = undispatched.reason === "unaddressed"
803
- ? "no_target_endpoint"
804
- : undispatched.reason;
805
- emitUndispatched({
806
- type: "undispatched_changed",
807
- version: 1,
808
- action: "add",
809
- undispatched_id: undispatched.id,
810
- sender_address: undispatched.sender_address,
811
- content_preview: truncateContentPreview(undispatched.content, CONTENT_PREVIEW_MAX_CODEPOINTS),
812
- reason: wireReason,
813
- ...(undispatched.original_target_endpoint_id !== undefined && {
814
- target_endpoint_id_hint: undispatched.original_target_endpoint_id,
815
- }),
816
- ...(typeof correlation_id === "string" && {
817
- in_reply_to: correlation_id,
818
- }),
819
- arrived_at: Date.parse(undispatched.arrived_at),
1456
+ if (unsequencedInboundBlocked || blockedInboundSeqs.size > 0) {
1457
+ logger.warn("delivery_ack_deferred_until_replay", {
1458
+ upTo: payload.upTo,
1459
+ count: payload.count,
1460
+ blockedSeqs: [...blockedInboundSeqs].sort((a, b) => a - b),
1461
+ unsequencedBlocked: unsequencedInboundBlocked,
820
1462
  });
1463
+ return;
821
1464
  }
822
- });
823
- connector.on("delivery_pending", ({ payload }) => {
824
1465
  connector.ackDelivery(payload.upTo);
825
1466
  });
826
1467
  writeDiscoveryFile(paths.discoveryFile, {
@@ -887,6 +1528,14 @@ export async function startBrokerDaemon(opts) {
887
1528
  });
888
1529
  }
889
1530
  await networkPresenceEmitter.shutdown();
1531
+ if (runtimeInboundRoutedEmitter.size() > 0) {
1532
+ logger.warn("runtime_inbound_routed_queue_dropped_on_shutdown", {
1533
+ size: runtimeInboundRoutedEmitter.size(),
1534
+ reason,
1535
+ });
1536
+ }
1537
+ await runtimeInboundRoutedEmitter.shutdown();
1538
+ await flushRuntimeAssignmentsOnShutdown();
890
1539
  const endpoints = registry.list();
891
1540
  await Promise.all(endpoints.map(async (entry) => {
892
1541
  try {
@@ -923,6 +1572,7 @@ export async function startBrokerDaemon(opts) {
923
1572
  retryQueueSize: () => retryQueue.size(),
924
1573
  undispatchedChangedQueueSize: () => undispatchedEmitter.size(),
925
1574
  networkPresenceChangedQueueSize: () => networkPresenceEmitter.size(),
1575
+ runtimeInboundRoutedQueueSize: () => runtimeInboundRoutedEmitter.size(),
926
1576
  replyCorrelationCacheSize: (endpoint_id) => replyCorrelationCache.sizeForEndpoint(endpoint_id),
927
1577
  };
928
1578
  }