@masons/runtime-broker 0.2.1 → 0.2.5

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 (39) hide show
  1. package/dist/broker/broker-daemon.d.ts +1 -0
  2. package/dist/broker/broker-daemon.d.ts.map +1 -1
  3. package/dist/broker/broker-daemon.js +821 -115
  4. package/dist/broker/control-event-dispatcher.d.ts.map +1 -1
  5. package/dist/broker/control-event-dispatcher.js +34 -16
  6. package/dist/broker/control-event-types.d.ts +16 -2
  7. package/dist/broker/control-event-types.d.ts.map +1 -1
  8. package/dist/broker/endpoint-registry.d.ts +9 -0
  9. package/dist/broker/endpoint-registry.d.ts.map +1 -1
  10. package/dist/broker/endpoint-registry.js +4 -0
  11. package/dist/broker/entry.d.ts.map +1 -1
  12. package/dist/broker/entry.js +4 -1
  13. package/dist/broker/ipc-server.d.ts +44 -0
  14. package/dist/broker/ipc-server.d.ts.map +1 -1
  15. package/dist/broker/ipc-server.js +12 -0
  16. package/dist/broker/reconnecting-buffer.d.ts +2 -0
  17. package/dist/broker/reconnecting-buffer.d.ts.map +1 -1
  18. package/dist/broker/reconnecting-buffer.js +3 -0
  19. package/dist/broker/runtime-endpoint-port.d.ts +2 -0
  20. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -1
  21. package/dist/broker/runtime-processing-state-event-types.d.ts +30 -0
  22. package/dist/broker/runtime-processing-state-event-types.d.ts.map +1 -0
  23. package/dist/broker/runtime-processing-state-event-types.js +1 -0
  24. package/dist/broker/version-handshake.d.ts +1 -0
  25. package/dist/broker/version-handshake.d.ts.map +1 -1
  26. package/dist/broker/version-handshake.js +1 -0
  27. package/dist/broker-client/broker-client.d.ts +53 -1
  28. package/dist/broker-client/broker-client.d.ts.map +1 -1
  29. package/dist/broker-client/broker-client.js +72 -0
  30. package/dist/broker-client/lazy-spawn.d.ts.map +1 -1
  31. package/dist/connector-client.d.ts +8 -0
  32. package/dist/connector-client.d.ts.map +1 -1
  33. package/dist/connector-client.js +17 -3
  34. package/dist/runtime-endpoint-client.d.ts +23 -2
  35. package/dist/runtime-endpoint-client.d.ts.map +1 -1
  36. package/dist/runtime-endpoint-client.js +36 -0
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { spawn as spawnChild } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { basename } from "node:path";
4
+ import { ConnectorClientUnavailableError, ConnectorSendAckTimeoutError, } from "../connector-client.js";
4
5
  import { createControlEventDispatcher, } from "./control-event-dispatcher.js";
5
6
  import { readDeliveryCursorFile, writeDeliveryCursorFile, } from "./delivery-cursor-file.js";
6
7
  import { deleteDiscoveryFile, mintBearerToken, writeDiscoveryFile, } from "./discovery-file.js";
@@ -22,6 +23,29 @@ import { CONTENT_PREVIEW_MAX_CODEPOINTS, truncateContentPreview, } from "./undis
22
23
  import { createUndispatchedChangedEmitter, postUndispatchedChangedViaPort, } from "./undispatched-emitter.js";
23
24
  import { createUndispatchedInbox, } from "./undispatched-inbox.js";
24
25
  const REMOTE_SPAWN_CAPABILITY = "remote_spawn_v1";
26
+ const DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS = 30_000;
27
+ const RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS = 24 * 60 * 60 * 1000;
28
+ const RUNTIME_PROCESSING_RETRY_INITIAL_MS = 1_000;
29
+ const RUNTIME_PROCESSING_RETRY_MAX_MS = 30_000;
30
+ function isBackgroundExchangeMode(value) {
31
+ return value === "resident_endpoint" || value === "adapter_managed_turn";
32
+ }
33
+ function buildLocalDispatchTargetRef(targetEndpointId, target) {
34
+ if (target?.background_exchange_mode === "adapter_managed_turn" &&
35
+ target.runtime_session_id) {
36
+ return {
37
+ kind: "projection_endpoint",
38
+ projection_endpoint_id: targetEndpointId,
39
+ runtime_session_id: target.runtime_session_id,
40
+ background_exchange_mode: "adapter_managed_turn",
41
+ };
42
+ }
43
+ return {
44
+ kind: "projection_endpoint",
45
+ projection_endpoint_id: targetEndpointId,
46
+ background_exchange_mode: "resident_endpoint",
47
+ };
48
+ }
25
49
  function derivePublicDisplayLabel(body) {
26
50
  const parts = [
27
51
  body.kind,
@@ -39,6 +63,8 @@ export async function startBrokerDaemon(opts) {
39
63
  const { paths, asNodeId: _asNodeIdReservedForA3, connector, apiPort, logger, closedEndpointLookup, livenessProbe = defaultIsPluginAlive, spawnDriverRegistry, spawnFn = spawnChild, servicesEventClientFactory, } = opts;
40
64
  const graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
41
65
  const presenceGraceMs = opts.presenceGraceMs;
66
+ const runtimeAssignmentAcceptanceTimeoutMs = opts.runtimeAssignmentAcceptanceTimeoutMs ??
67
+ DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS;
42
68
  const postControlAck = opts.postControlAck ??
43
69
  (async () => {
44
70
  });
@@ -63,6 +89,10 @@ export async function startBrokerDaemon(opts) {
63
89
  const reconnectingBuffers = createReconnectingBufferManager(paths.buffersDir);
64
90
  const graceTimers = createGraceTimerManager();
65
91
  const controlEventDispatcher = createControlEventDispatcher();
92
+ const runtimeAssignments = new Map();
93
+ const runtimeAssignmentByEndpointCorrelation = new Map();
94
+ const runtimeAssignmentReplyRecords = new Map();
95
+ const runtimeAssignmentReplyInFlight = new Map();
66
96
  const spawnCorrelation = createSpawnCorrelationManager({
67
97
  timeoutMs: opts.spawnCorrelationTimeoutMs,
68
98
  });
@@ -121,6 +151,91 @@ export async function startBrokerDaemon(opts) {
121
151
  backoffMaxMs: opts.runtimeInboundRoutedBackoffMaxMs,
122
152
  maxRetries: opts.runtimeInboundRoutedMaxRetries,
123
153
  });
154
+ const runtimeAssignmentKey = (sourceMessageId, assignmentId) => `${sourceMessageId}\u0000${assignmentId}`;
155
+ const runtimeAssignmentReplyKey = (sourceMessageId, assignmentId, replyRequestId) => `${sourceMessageId}\u0000${assignmentId}\u0000${replyRequestId}`;
156
+ const runtimeAssignmentReplyFingerprint = (content, contentType) => JSON.stringify([content, contentType ?? "text"]);
157
+ const pruneRuntimeAssignmentReplyRecords = (now = Date.now()) => {
158
+ for (const [key, record] of runtimeAssignmentReplyRecords.entries()) {
159
+ const [sourceMessageId, assignmentId] = key.split("\u0000");
160
+ if (sourceMessageId !== undefined &&
161
+ assignmentId !== undefined &&
162
+ runtimeAssignments.has(runtimeAssignmentKey(sourceMessageId, assignmentId))) {
163
+ continue;
164
+ }
165
+ if (record.expiresAt <= now) {
166
+ runtimeAssignmentReplyRecords.delete(key);
167
+ }
168
+ }
169
+ };
170
+ const endpointCorrelationKey = (endpointId, correlationId) => `${endpointId}\u0000${correlationId}`;
171
+ const isBackgroundExchangeTarget = (entry) => Boolean(entry.runtime_capabilities?.includes("background_exchange_v1") &&
172
+ isBackgroundExchangeMode(entry.background_exchange_mode));
173
+ const rememberRuntimeAssignmentCorrelation = (endpointId, correlationId, assignmentKey) => {
174
+ if (typeof correlationId !== "string" || correlationId.length === 0) {
175
+ return;
176
+ }
177
+ runtimeAssignmentByEndpointCorrelation.set(endpointCorrelationKey(endpointId, correlationId), assignmentKey);
178
+ };
179
+ const forgetRuntimeAssignment = (key) => {
180
+ const assignment = runtimeAssignments.get(key);
181
+ if (assignment?.acceptanceTimer) {
182
+ clearTimeout(assignment.acceptanceTimer);
183
+ }
184
+ if (assignment?.replyEmittedRetryTimer) {
185
+ clearTimeout(assignment.replyEmittedRetryTimer);
186
+ }
187
+ runtimeAssignments.delete(key);
188
+ for (const [correlationKey, assignmentKey] of Array.from(runtimeAssignmentByEndpointCorrelation.entries())) {
189
+ if (assignmentKey === key) {
190
+ runtimeAssignmentByEndpointCorrelation.delete(correlationKey);
191
+ }
192
+ }
193
+ for (const replyKey of Array.from(runtimeAssignmentReplyInFlight.keys())) {
194
+ if (replyKey.startsWith(`${key}\u0000`)) {
195
+ runtimeAssignmentReplyInFlight.delete(replyKey);
196
+ }
197
+ }
198
+ };
199
+ const scheduleAcceptanceTimeout = (key) => {
200
+ if (runtimeAssignmentAcceptanceTimeoutMs <= 0)
201
+ return;
202
+ const assignment = runtimeAssignments.get(key);
203
+ if (!assignment || assignment.accepted || assignment.acceptanceTimer) {
204
+ return;
205
+ }
206
+ const timer = setTimeout(() => {
207
+ const current = runtimeAssignments.get(key);
208
+ if (!current || current.accepted)
209
+ return;
210
+ logger.warn("runtime_assignment_acceptance_timeout", {
211
+ undispatched_id: current.context.undispatchedId,
212
+ source_message_id: current.context.sourceMessageId,
213
+ target_endpoint_id: current.context.targetRef.projection_endpoint_id,
214
+ });
215
+ void restorePendingAssignmentsForEndpoint(current.context.targetRef.projection_endpoint_id, "adapter_acceptance_timeout");
216
+ }, runtimeAssignmentAcceptanceTimeoutMs);
217
+ timer.unref?.();
218
+ assignment.acceptanceTimer = timer;
219
+ };
220
+ const emitRuntimeProcessingState = async (event) => {
221
+ if (!apiPort.emitRuntimeProcessingState) {
222
+ return {
223
+ ok: false,
224
+ terminal: true,
225
+ detail: "runtime_processing_state_port_not_configured",
226
+ };
227
+ }
228
+ try {
229
+ return await apiPort.emitRuntimeProcessingState(event);
230
+ }
231
+ catch (err) {
232
+ return {
233
+ ok: false,
234
+ terminal: false,
235
+ detail: err instanceof Error ? err.message : String(err),
236
+ };
237
+ }
238
+ };
124
239
  const directPostUndispatched = async (event, opts = {}) => {
125
240
  let outcome;
126
241
  try {
@@ -195,6 +310,153 @@ export async function startBrokerDaemon(opts) {
195
310
  return;
196
311
  replyCorrelationCache.record(endpoint_id, c);
197
312
  };
313
+ const restorePendingAssignmentsForEndpoint = (endpoint_id, reason) => {
314
+ const tasks = [];
315
+ for (const [key, assignment] of Array.from(runtimeAssignments.entries())) {
316
+ if (assignment.context.targetRef.projection_endpoint_id !== endpoint_id ||
317
+ assignment.accepted) {
318
+ continue;
319
+ }
320
+ if (assignment.acceptanceTimer) {
321
+ clearTimeout(assignment.acceptanceTimer);
322
+ assignment.acceptanceTimer = undefined;
323
+ }
324
+ const readded = undispatchedInbox.tryAdd(assignment.message);
325
+ if (!readded) {
326
+ logger.error("runtime_assignment_reinsert_failed_at_capacity", {
327
+ undispatched_id: assignment.context.undispatchedId,
328
+ source_message_id: assignment.context.sourceMessageId,
329
+ target_endpoint_id: endpoint_id,
330
+ inbox_size: undispatchedInbox.size(),
331
+ inbox_capacity: undispatchedInbox.capacity(),
332
+ });
333
+ scheduleAcceptanceTimeout(key);
334
+ continue;
335
+ }
336
+ const task = emitRuntimeProcessingState({
337
+ type: "runtime_processing_state",
338
+ version: 1,
339
+ source_message_id: assignment.context.sourceMessageId,
340
+ assignment_id: assignment.context.assignmentId,
341
+ state_event_id: `${assignment.context.assignmentId}:target_lost`,
342
+ state_sequence: 2,
343
+ target_ref: assignment.context.targetRef,
344
+ state: "target_lost_before_acceptance",
345
+ occurred_at: assignment.assignedAt + 1,
346
+ undispatched_id: assignment.context.undispatchedId,
347
+ reason_code: reason,
348
+ }).then((outcome) => {
349
+ if (outcome.ok) {
350
+ forgetRuntimeAssignment(key);
351
+ return;
352
+ }
353
+ logger.warn("runtime_assignment_target_lost_emit_failed", {
354
+ undispatched_id: assignment.context.undispatchedId,
355
+ source_message_id: assignment.context.sourceMessageId,
356
+ target_endpoint_id: endpoint_id,
357
+ status: outcome.status,
358
+ terminal: outcome.terminal,
359
+ detail: outcome.detail,
360
+ });
361
+ scheduleAcceptanceTimeout(key);
362
+ });
363
+ tasks.push(task);
364
+ }
365
+ return Promise.allSettled(tasks).then(() => undefined);
366
+ };
367
+ const restorePendingAssignmentBeforeAcceptance = (key, assignment, reason) => {
368
+ if (assignment.acceptanceTimer) {
369
+ clearTimeout(assignment.acceptanceTimer);
370
+ assignment.acceptanceTimer = undefined;
371
+ }
372
+ const readded = undispatchedInbox.tryAdd(assignment.message);
373
+ if (!readded) {
374
+ logger.error("runtime_assignment_reinsert_failed_at_capacity", {
375
+ undispatched_id: assignment.context.undispatchedId,
376
+ source_message_id: assignment.context.sourceMessageId,
377
+ target_endpoint_id: assignment.context.targetRef.projection_endpoint_id,
378
+ inbox_size: undispatchedInbox.size(),
379
+ inbox_capacity: undispatchedInbox.capacity(),
380
+ reason,
381
+ });
382
+ scheduleAcceptanceTimeout(key);
383
+ return false;
384
+ }
385
+ forgetRuntimeAssignment(key);
386
+ return true;
387
+ };
388
+ const emitReplyEmittedWithRetry = (assignmentKey, event, attempt = 0) => {
389
+ const assignment = runtimeAssignments.get(assignmentKey);
390
+ if (!assignment)
391
+ return;
392
+ assignment.replyEmittedEvent = event;
393
+ if (assignment.replyEmittedRetryTimer) {
394
+ clearTimeout(assignment.replyEmittedRetryTimer);
395
+ assignment.replyEmittedRetryTimer = undefined;
396
+ }
397
+ void emitRuntimeProcessingState(event).then((outcome) => {
398
+ const current = runtimeAssignments.get(assignmentKey);
399
+ if (!current)
400
+ return;
401
+ if (outcome.ok) {
402
+ forgetRuntimeAssignment(assignmentKey);
403
+ return;
404
+ }
405
+ logger.warn("runtime_reply_emitted_projection_failed", {
406
+ source_message_id: current.context.sourceMessageId,
407
+ assignment_id: current.context.assignmentId,
408
+ status: outcome.status,
409
+ terminal: outcome.terminal,
410
+ detail: outcome.detail,
411
+ });
412
+ if (outcome.terminal) {
413
+ forgetRuntimeAssignment(assignmentKey);
414
+ return;
415
+ }
416
+ const delay = Math.min(RUNTIME_PROCESSING_RETRY_INITIAL_MS * 2 ** attempt, RUNTIME_PROCESSING_RETRY_MAX_MS);
417
+ const timer = setTimeout(() => {
418
+ emitReplyEmittedWithRetry(assignmentKey, event, attempt + 1);
419
+ }, delay);
420
+ timer.unref?.();
421
+ current.replyEmittedRetryTimer = timer;
422
+ });
423
+ };
424
+ const flushRuntimeAssignmentsOnShutdown = async () => {
425
+ const endpointIds = new Set();
426
+ for (const assignment of runtimeAssignments.values()) {
427
+ endpointIds.add(assignment.context.targetRef.projection_endpoint_id);
428
+ }
429
+ for (const [key, assignment] of Array.from(runtimeAssignments.entries())) {
430
+ if (assignment.replyEmittedEvent) {
431
+ const outcome = await emitRuntimeProcessingState(assignment.replyEmittedEvent);
432
+ if (outcome.ok) {
433
+ forgetRuntimeAssignment(key);
434
+ continue;
435
+ }
436
+ logger.warn("runtime_reply_emitted_shutdown_flush_failed", {
437
+ source_message_id: assignment.context.sourceMessageId,
438
+ assignment_id: assignment.context.assignmentId,
439
+ status: outcome.status,
440
+ terminal: outcome.terminal,
441
+ detail: outcome.detail,
442
+ });
443
+ if (outcome.terminal) {
444
+ forgetRuntimeAssignment(key);
445
+ }
446
+ }
447
+ }
448
+ for (const endpointId of endpointIds) {
449
+ await restorePendingAssignmentsForEndpoint(endpointId, "broker_shutdown");
450
+ }
451
+ if (runtimeAssignments.size > 0) {
452
+ logger.warn("runtime_assignments_dropped_on_shutdown", {
453
+ size: runtimeAssignments.size,
454
+ });
455
+ }
456
+ for (const key of Array.from(runtimeAssignments.keys())) {
457
+ forgetRuntimeAssignment(key);
458
+ }
459
+ };
198
460
  const channels = new Map();
199
461
  let networkPresence = "offline";
200
462
  let presenceGraceTimer;
@@ -357,11 +619,12 @@ export async function startBrokerDaemon(opts) {
357
619
  break;
358
620
  case "drain_buffer": {
359
621
  const buf = reconnectingBuffers.forEndpoint(endpoint_id);
360
- const drained = buf.drain();
622
+ const drained = buf.read();
361
623
  const e = registry.get(endpoint_id);
624
+ let allPushed = true;
362
625
  if (e) {
363
626
  for (const msg of drained) {
364
- pushToPlugin(e.ipc_ws, {
627
+ const pushed = pushToPlugin(e.ipc_ws, {
365
628
  event: "message_received",
366
629
  from: msg.envelope.from,
367
630
  content: msg.envelope.content,
@@ -370,15 +633,32 @@ export async function startBrokerDaemon(opts) {
370
633
  ...(msg.envelope.sourceMessageId !== undefined && {
371
634
  sourceMessageId: msg.envelope.sourceMessageId,
372
635
  }),
636
+ ...(msg.envelope.runtimeAssignment !== undefined && {
637
+ runtimeAssignment: msg.envelope.runtimeAssignment,
638
+ }),
373
639
  });
640
+ if (!pushed) {
641
+ allPushed = false;
642
+ break;
643
+ }
374
644
  }
375
645
  }
646
+ if (allPushed) {
647
+ buf.delete();
648
+ }
649
+ else {
650
+ logger.warn("reconnecting_buffer_drain_failed", {
651
+ endpoint_id,
652
+ buffered_count: drained.length,
653
+ });
654
+ }
376
655
  break;
377
656
  }
378
657
  case "delete_buffer":
379
658
  reconnectingBuffers.cleanup(endpoint_id);
380
659
  break;
381
660
  case "remove_from_registry":
661
+ void restorePendingAssignmentsForEndpoint(endpoint_id, "endpoint_lost");
382
662
  clearHeartbeat(endpoint_id);
383
663
  registry.unregister(endpoint_id);
384
664
  replyCorrelationCache.forgetEndpoint(endpoint_id);
@@ -389,20 +669,49 @@ export async function startBrokerDaemon(opts) {
389
669
  }
390
670
  };
391
671
  controlEventDispatcher.on("dispatch_undispatched", async (event) => {
392
- const taken = undispatchedInbox.take(event.undispatched_id);
672
+ const targetEndpointId = event.target_ref?.projection_endpoint_id ?? event.target_endpoint_id;
673
+ if (!targetEndpointId) {
674
+ return {
675
+ ok: false,
676
+ detail: "dispatch target_ref missing",
677
+ };
678
+ }
679
+ const assignmentKey = runtimeAssignmentKey(event.source_message_id, event.idempotency_key);
680
+ const existingAssignment = runtimeAssignments.get(assignmentKey);
681
+ const taken = undispatchedInbox.take(event.undispatched_id) ??
682
+ (!existingAssignment?.accepted &&
683
+ existingAssignment?.context.undispatchedId === event.undispatched_id &&
684
+ existingAssignment.context.targetRef.projection_endpoint_id ===
685
+ targetEndpointId
686
+ ? existingAssignment.message
687
+ : undefined);
393
688
  if (!taken) {
394
689
  return {
395
690
  ok: false,
396
691
  detail: `unknown undispatched_id: ${event.undispatched_id}`,
397
692
  };
398
693
  }
399
- const target = registry.get(event.target_endpoint_id);
694
+ if (taken.source_message_id === undefined) {
695
+ undispatchedInbox.tryAdd(taken);
696
+ return {
697
+ ok: false,
698
+ detail: "dispatch source_message_id missing from inbox row",
699
+ };
700
+ }
701
+ if (event.source_message_id !== taken.source_message_id) {
702
+ undispatchedInbox.tryAdd(taken);
703
+ return {
704
+ ok: false,
705
+ detail: `source_message_id mismatch: ${event.source_message_id}`,
706
+ };
707
+ }
708
+ const target = registry.get(targetEndpointId);
400
709
  if (!target) {
401
710
  const readded = undispatchedInbox.tryAdd(taken);
402
711
  if (!readded) {
403
712
  logger.error("dispatch_reinsert_failed_at_capacity", {
404
713
  undispatched_id: event.undispatched_id,
405
- target_endpoint_id: event.target_endpoint_id,
714
+ target_endpoint_id: targetEndpointId,
406
715
  inbox_size: undispatchedInbox.size(),
407
716
  inbox_capacity: undispatchedInbox.capacity(),
408
717
  });
@@ -419,9 +728,94 @@ export async function startBrokerDaemon(opts) {
419
728
  }
420
729
  return {
421
730
  ok: false,
422
- detail: `target endpoint not found: ${event.target_endpoint_id}`,
731
+ detail: `target endpoint not found: ${targetEndpointId}`,
732
+ };
733
+ }
734
+ let targetRef = event.target_ref;
735
+ if (!targetRef) {
736
+ if (target.background_exchange_mode === "resident_endpoint") {
737
+ targetRef = {
738
+ kind: "projection_endpoint",
739
+ projection_endpoint_id: targetEndpointId,
740
+ background_exchange_mode: "resident_endpoint",
741
+ };
742
+ }
743
+ else if (target.background_exchange_mode === "adapter_managed_turn" &&
744
+ target.runtime_session_id) {
745
+ targetRef = {
746
+ kind: "projection_endpoint",
747
+ projection_endpoint_id: targetEndpointId,
748
+ runtime_session_id: target.runtime_session_id,
749
+ background_exchange_mode: "adapter_managed_turn",
750
+ };
751
+ }
752
+ }
753
+ if (!targetRef ||
754
+ !target.runtime_capabilities?.includes("background_exchange_v1") ||
755
+ target.background_exchange_mode !==
756
+ targetRef.background_exchange_mode ||
757
+ (targetRef.background_exchange_mode === "adapter_managed_turn" &&
758
+ target.runtime_session_id !== targetRef.runtime_session_id)) {
759
+ undispatchedInbox.tryAdd(taken);
760
+ return {
761
+ ok: false,
762
+ detail: `target endpoint is not background-exchange capable: ${targetEndpointId}`,
423
763
  };
424
764
  }
765
+ const assignedAt = event.emitted_at;
766
+ const processingEvent = {
767
+ type: "runtime_processing_state",
768
+ version: 1,
769
+ source_message_id: event.source_message_id,
770
+ assignment_id: event.idempotency_key,
771
+ state_event_id: `${event.idempotency_key}:assigned`,
772
+ state_sequence: 1,
773
+ target_ref: targetRef,
774
+ state: "assigned_to_runtime_target",
775
+ occurred_at: assignedAt,
776
+ undispatched_id: event.undispatched_id,
777
+ };
778
+ const processingResult = await emitRuntimeProcessingState(processingEvent);
779
+ if (!processingResult.ok) {
780
+ undispatchedInbox.tryAdd(taken);
781
+ logger.warn("runtime_processing_state_emit_failed", {
782
+ undispatched_id: event.undispatched_id,
783
+ source_message_id: event.source_message_id,
784
+ target_endpoint_id: targetEndpointId,
785
+ status: processingResult.status,
786
+ terminal: processingResult.terminal,
787
+ detail: processingResult.detail,
788
+ });
789
+ return {
790
+ ok: false,
791
+ detail: processingResult.detail ??
792
+ "runtime_processing_state_assignment_failed",
793
+ };
794
+ }
795
+ const runtimeAssignment = {
796
+ undispatchedId: event.undispatched_id,
797
+ sourceMessageId: event.source_message_id,
798
+ assignmentId: event.idempotency_key,
799
+ targetRef,
800
+ deliveryIntent: "external_handoff",
801
+ ...(typeof taken.metadata.correlation_id === "string" && {
802
+ replyCorrelationId: taken.metadata.correlation_id,
803
+ }),
804
+ };
805
+ const storedAssignment = existingAssignment ?? {
806
+ context: runtimeAssignment,
807
+ message: taken,
808
+ accepted: false,
809
+ assignedAt,
810
+ lastStateSequence: processingEvent.state_sequence,
811
+ };
812
+ storedAssignment.context = runtimeAssignment;
813
+ storedAssignment.message = taken;
814
+ storedAssignment.assignedAt = assignedAt;
815
+ storedAssignment.lastStateSequence = Math.max(storedAssignment.lastStateSequence, processingEvent.state_sequence);
816
+ runtimeAssignments.set(assignmentKey, storedAssignment);
817
+ scheduleAcceptanceTimeout(assignmentKey);
818
+ rememberRuntimeAssignmentCorrelation(target.endpoint_id, taken.metadata.correlation_id, assignmentKey);
425
819
  const stamped = {
426
820
  event: "message_received",
427
821
  from: taken.sender_address,
@@ -430,47 +824,50 @@ export async function startBrokerDaemon(opts) {
430
824
  metadata: {
431
825
  ...taken.metadata,
432
826
  dispatched_from: event.undispatched_id,
433
- dispatched_by: "passport",
827
+ delivery_intent: "external_handoff",
434
828
  },
435
- ...(taken.source_message_id !== undefined && {
436
- sourceMessageId: taken.source_message_id,
437
- }),
829
+ sourceMessageId: taken.source_message_id,
830
+ runtimeAssignment,
438
831
  };
439
832
  recordInboundCorrelation(target.endpoint_id, stamped.metadata);
440
833
  if (target.state === "active") {
441
- pushToPlugin(target.ipc_ws, stamped);
834
+ if (!pushToPlugin(target.ipc_ws, stamped)) {
835
+ await restorePendingAssignmentsForEndpoint(target.endpoint_id, "ipc_push_failed");
836
+ return {
837
+ ok: false,
838
+ detail: `target endpoint not writable: ${targetEndpointId}`,
839
+ };
840
+ }
442
841
  }
443
842
  else {
444
- const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
445
- buf.append({
446
- id: randomUUID(),
447
- arrived_at: new Date().toISOString(),
448
- envelope: {
449
- from: taken.sender_address,
450
- content: taken.content,
451
- contentType: taken.content_type,
452
- metadata: stamped.metadata,
453
- ...(stamped.sourceMessageId !== undefined && {
843
+ try {
844
+ const buf = reconnectingBuffers.forEndpoint(target.endpoint_id);
845
+ buf.append({
846
+ id: randomUUID(),
847
+ arrived_at: new Date().toISOString(),
848
+ envelope: {
849
+ from: taken.sender_address,
850
+ content: taken.content,
851
+ contentType: taken.content_type,
852
+ metadata: stamped.metadata,
454
853
  sourceMessageId: stamped.sourceMessageId,
455
- }),
456
- },
457
- });
854
+ runtimeAssignment,
855
+ },
856
+ });
857
+ }
858
+ catch (err) {
859
+ await restorePendingAssignmentsForEndpoint(target.endpoint_id, "reconnecting_buffer_failed");
860
+ return {
861
+ ok: false,
862
+ detail: err instanceof Error ? err.message : String(err),
863
+ };
864
+ }
458
865
  }
459
866
  logger.info("undispatched_dispatched", {
460
867
  undispatched_id: event.undispatched_id,
461
- target_endpoint_id: event.target_endpoint_id,
868
+ target_endpoint_id: targetEndpointId,
462
869
  target_state: target.state,
463
870
  });
464
- emitUndispatched({
465
- type: "undispatched_changed",
466
- version: 1,
467
- action: "dispatch",
468
- undispatched_id: event.undispatched_id,
469
- ...(taken.source_message_id !== undefined && {
470
- source_message_id: taken.source_message_id,
471
- }),
472
- dispatched_to_endpoint_id: event.target_endpoint_id,
473
- });
474
871
  return { ok: true };
475
872
  });
476
873
  controlEventDispatcher.on("spawn_request", async (event) => {
@@ -562,9 +959,14 @@ export async function startBrokerDaemon(opts) {
562
959
  const spawnAvailability = spawnDriver
563
960
  ? await spawnDriver.isAvailable().catch(() => ({ available: false }))
564
961
  : null;
565
- const runtimeCapabilities = spawnAvailability?.available
566
- ? [REMOTE_SPAWN_CAPABILITY]
962
+ const runtimeCapabilitiesSet = new Set(body.runtime_capabilities?.filter((capability) => capability !== REMOTE_SPAWN_CAPABILITY) ?? []);
963
+ if (spawnAvailability?.available) {
964
+ runtimeCapabilitiesSet.add(REMOTE_SPAWN_CAPABILITY);
965
+ }
966
+ const runtimeCapabilities = runtimeCapabilitiesSet.size > 0
967
+ ? Array.from(runtimeCapabilitiesSet)
567
968
  : undefined;
969
+ const runtimeSessionId = body.runtime_session_id?.trim() || undefined;
568
970
  const apiResp = await apiPort.register({
569
971
  runtime_kind: body.kind,
570
972
  endpoint_nonce: randomUUID(),
@@ -576,12 +978,19 @@ export async function startBrokerDaemon(opts) {
576
978
  session_name: body.session_name,
577
979
  task_hint: body.task_hint,
578
980
  runtime_capabilities: runtimeCapabilities,
981
+ execution_surface: body.execution_surface,
982
+ runtime_session_id: runtimeSessionId,
983
+ background_exchange_mode: body.background_exchange_mode,
579
984
  });
580
985
  registry.register({
581
986
  endpoint_id: apiResp.endpoint_id,
582
987
  agent_id: body.agent_id,
583
988
  plugin_pid: body.plugin_pid,
584
989
  ipc_ws: boundWs,
990
+ runtime_capabilities: runtimeCapabilities,
991
+ execution_surface: body.execution_surface,
992
+ runtime_session_id: runtimeSessionId,
993
+ background_exchange_mode: body.background_exchange_mode,
585
994
  display_metadata: {
586
995
  session_name: body.session_name,
587
996
  kind: body.kind,
@@ -691,15 +1100,262 @@ export async function startBrokerDaemon(opts) {
691
1100
  }
692
1101
  catch (err) {
693
1102
  const msg = err instanceof Error ? err.message : String(err);
694
- if (msg === "WebSocket not connected" || msg === "SEND_ACK timeout") {
1103
+ if (err instanceof ConnectorClientUnavailableError) {
695
1104
  throw new BrokerHttpError(503, "connector_unavailable", msg, {
696
1105
  retryable: true,
697
1106
  });
698
1107
  }
1108
+ if (err instanceof ConnectorSendAckTimeoutError) {
1109
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
1110
+ }
699
1111
  throw err;
700
1112
  }
701
1113
  return { messageId: ack.messageId, status: ack.status };
702
1114
  },
1115
+ async sendRuntimeAssignmentReply(body) {
1116
+ const entry = registry.get(body.endpoint_id);
1117
+ if (!entry) {
1118
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
1119
+ }
1120
+ if (entry.plugin_pid !== body.plugin_pid) {
1121
+ throw new BrokerHttpError(403, "endpoint_pid_mismatch", "plugin_pid does not own endpoint_id");
1122
+ }
1123
+ if (entry.state !== "active") {
1124
+ throw new BrokerHttpError(409, "endpoint_not_active", `endpoint ${body.endpoint_id} is ${entry.state}; assignment reply is only allowed from Active`);
1125
+ }
1126
+ if (!body.reply_request_id) {
1127
+ throw new BrokerHttpError(400, "reply_request_id_required", "reply_request_id is required for assignment replies");
1128
+ }
1129
+ const assignmentKey = runtimeAssignmentKey(body.source_message_id, body.assignment_id);
1130
+ const replyKey = runtimeAssignmentReplyKey(body.source_message_id, body.assignment_id, body.reply_request_id);
1131
+ pruneRuntimeAssignmentReplyRecords();
1132
+ const requestFingerprint = runtimeAssignmentReplyFingerprint(body.content, body.contentType);
1133
+ const assignment = runtimeAssignments.get(assignmentKey);
1134
+ const existingRecord = runtimeAssignmentReplyRecords.get(replyKey);
1135
+ const inFlight = runtimeAssignmentReplyInFlight.get(replyKey);
1136
+ if (assignment) {
1137
+ if (assignment.context.targetRef.projection_endpoint_id !==
1138
+ body.endpoint_id) {
1139
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1140
+ }
1141
+ if (!assignment.accepted) {
1142
+ throw new BrokerHttpError(409, "runtime_assignment_not_accepted", "assignment replies require adapter_accepted first");
1143
+ }
1144
+ }
1145
+ else if (existingRecord || inFlight) {
1146
+ const owner = existingRecord ?? inFlight;
1147
+ if (owner?.endpointId !== body.endpoint_id ||
1148
+ owner.pluginPid !== body.plugin_pid) {
1149
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1150
+ }
1151
+ }
1152
+ else {
1153
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1154
+ }
1155
+ if (existingRecord &&
1156
+ existingRecord.requestFingerprint !== requestFingerprint) {
1157
+ throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
1158
+ }
1159
+ if (existingRecord?.outcomeUnknown) {
1160
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", existingRecord.outcomeUnknown.message, { retryable: false });
1161
+ }
1162
+ if (existingRecord?.result)
1163
+ return existingRecord.result;
1164
+ if (inFlight) {
1165
+ if (inFlight.requestFingerprint !== requestFingerprint) {
1166
+ throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
1167
+ }
1168
+ return inFlight.task;
1169
+ }
1170
+ if (!assignment) {
1171
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1172
+ }
1173
+ const task = (async () => {
1174
+ let record = runtimeAssignmentReplyRecords.get(replyKey);
1175
+ if (!record) {
1176
+ const replyRequestedSequence = assignment.lastStateSequence + 1;
1177
+ const replyRequestedEvent = {
1178
+ type: "runtime_processing_state",
1179
+ version: 1,
1180
+ source_message_id: assignment.context.sourceMessageId,
1181
+ assignment_id: assignment.context.assignmentId,
1182
+ state_event_id: `${assignment.context.assignmentId}:reply_requested:${body.reply_request_id}`,
1183
+ state_sequence: replyRequestedSequence,
1184
+ target_ref: assignment.context.targetRef,
1185
+ state: "reply_requested",
1186
+ occurred_at: Date.now(),
1187
+ undispatched_id: assignment.context.undispatchedId,
1188
+ reply_request_id: body.reply_request_id,
1189
+ };
1190
+ const replyRequested = await emitRuntimeProcessingState(replyRequestedEvent);
1191
+ if (!replyRequested.ok) {
1192
+ if (replyRequested.terminal && replyRequested.status === 403) {
1193
+ logger.warn("runtime_reply_requested_projection_skipped", {
1194
+ source_message_id: assignment.context.sourceMessageId,
1195
+ assignment_id: assignment.context.assignmentId,
1196
+ status: replyRequested.status,
1197
+ detail: replyRequested.detail,
1198
+ });
1199
+ }
1200
+ else {
1201
+ throw new BrokerHttpError(replyRequested.terminal ? 409 : 503, "runtime_reply_request_rejected", replyRequested.detail ??
1202
+ "runtime reply request was not accepted");
1203
+ }
1204
+ }
1205
+ record = {
1206
+ replyRequestedEvent,
1207
+ endpointId: body.endpoint_id,
1208
+ pluginPid: body.plugin_pid,
1209
+ requestFingerprint,
1210
+ wireMessageId: randomUUID(),
1211
+ expiresAt: Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS,
1212
+ };
1213
+ runtimeAssignmentReplyRecords.set(replyKey, record);
1214
+ assignment.lastStateSequence = replyRequestedSequence;
1215
+ }
1216
+ const correlationId = typeof assignment.context.replyCorrelationId === "string" &&
1217
+ assignment.context.replyCorrelationId.length > 0
1218
+ ? assignment.context.replyCorrelationId
1219
+ : randomUUID();
1220
+ entry.correlation_ring.add(correlationId);
1221
+ const remoteMessageId = assignment.message.metadata.message_id;
1222
+ const metadata = {
1223
+ message_id: record.wireMessageId,
1224
+ ...(typeof remoteMessageId === "string" &&
1225
+ remoteMessageId.length > 0 && { in_reply_to: remoteMessageId }),
1226
+ correlation_id: correlationId,
1227
+ source_endpoint_id: body.endpoint_id,
1228
+ require_live: false,
1229
+ };
1230
+ let ack;
1231
+ try {
1232
+ ack = await connector.send(assignment.message.sender_address, body.content, body.contentType ?? "text", metadata);
1233
+ }
1234
+ catch (err) {
1235
+ const msg = err instanceof Error ? err.message : String(err);
1236
+ if (err instanceof ConnectorClientUnavailableError) {
1237
+ throw new BrokerHttpError(503, "connector_unavailable", msg, {
1238
+ retryable: true,
1239
+ });
1240
+ }
1241
+ if (err instanceof ConnectorSendAckTimeoutError) {
1242
+ record.outcomeUnknown = { message: msg };
1243
+ record.expiresAt =
1244
+ Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
1245
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
1246
+ }
1247
+ throw err;
1248
+ }
1249
+ const result = {
1250
+ messageId: record.wireMessageId,
1251
+ status: ack.status,
1252
+ };
1253
+ record.result = result;
1254
+ record.expiresAt = Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
1255
+ const replyEmittedSequence = assignment.lastStateSequence + 1;
1256
+ emitReplyEmittedWithRetry(assignmentKey, {
1257
+ type: "runtime_processing_state",
1258
+ version: 1,
1259
+ source_message_id: assignment.context.sourceMessageId,
1260
+ assignment_id: assignment.context.assignmentId,
1261
+ state_event_id: `${assignment.context.assignmentId}:reply_emitted:${body.reply_request_id}`,
1262
+ state_sequence: replyEmittedSequence,
1263
+ target_ref: assignment.context.targetRef,
1264
+ state: "reply_emitted",
1265
+ occurred_at: Date.now(),
1266
+ undispatched_id: assignment.context.undispatchedId,
1267
+ reply_request_id: body.reply_request_id,
1268
+ outbound_message_id: record.wireMessageId,
1269
+ });
1270
+ assignment.lastStateSequence = replyEmittedSequence;
1271
+ return result;
1272
+ })();
1273
+ runtimeAssignmentReplyInFlight.set(replyKey, {
1274
+ endpointId: body.endpoint_id,
1275
+ pluginPid: body.plugin_pid,
1276
+ requestFingerprint,
1277
+ task,
1278
+ });
1279
+ try {
1280
+ return await task;
1281
+ }
1282
+ finally {
1283
+ runtimeAssignmentReplyInFlight.delete(replyKey);
1284
+ }
1285
+ },
1286
+ async reportProcessingState(body) {
1287
+ const entry = registry.get(body.endpoint_id);
1288
+ if (!entry) {
1289
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
1290
+ }
1291
+ if (entry.plugin_pid !== body.plugin_pid) {
1292
+ throw new BrokerHttpError(403, "endpoint_pid_mismatch", "plugin_pid does not own endpoint_id");
1293
+ }
1294
+ const key = runtimeAssignmentKey(body.source_message_id, body.assignment_id);
1295
+ const assignment = runtimeAssignments.get(key);
1296
+ if (!assignment) {
1297
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1298
+ }
1299
+ if (assignment.context.targetRef.projection_endpoint_id !== body.endpoint_id) {
1300
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1301
+ }
1302
+ if (body.state === "reply_requested" || body.state === "reply_emitted") {
1303
+ throw new BrokerHttpError(403, `${body.state}_broker_owned`, `${body.state} is emitted by the broker assignment reply path`);
1304
+ }
1305
+ const event = {
1306
+ type: "runtime_processing_state",
1307
+ version: 1,
1308
+ source_message_id: body.source_message_id,
1309
+ assignment_id: body.assignment_id,
1310
+ state_event_id: body.state_event_id,
1311
+ state_sequence: body.state_sequence,
1312
+ target_ref: assignment.context.targetRef,
1313
+ state: body.state,
1314
+ occurred_at: body.occurred_at ?? Date.now(),
1315
+ undispatched_id: body.undispatched_id ?? assignment.context.undispatchedId,
1316
+ ...(body.retryable !== undefined && { retryable: body.retryable }),
1317
+ ...(body.reason_code !== undefined && {
1318
+ reason_code: body.reason_code,
1319
+ }),
1320
+ ...(body.detail !== undefined && { detail: body.detail }),
1321
+ ...(body.reply_request_id !== undefined && {
1322
+ reply_request_id: body.reply_request_id,
1323
+ }),
1324
+ ...(body.outbound_message_id !== undefined && {
1325
+ outbound_message_id: body.outbound_message_id,
1326
+ }),
1327
+ };
1328
+ const outcome = await emitRuntimeProcessingState(event);
1329
+ if (!outcome.ok) {
1330
+ throw new BrokerHttpError(outcome.terminal ? 409 : 503, "runtime_processing_state_rejected", outcome.detail ?? "runtime processing state was not accepted");
1331
+ }
1332
+ assignment.lastStateSequence = Math.max(assignment.lastStateSequence, body.state_sequence);
1333
+ if (body.state === "adapter_accepted") {
1334
+ assignment.accepted = true;
1335
+ if (assignment.acceptanceTimer) {
1336
+ clearTimeout(assignment.acceptanceTimer);
1337
+ assignment.acceptanceTimer = undefined;
1338
+ }
1339
+ }
1340
+ if (!assignment.accepted &&
1341
+ (body.state === "processing_failed" ||
1342
+ body.state === "waiting_for_runtime_capability" ||
1343
+ body.state === "target_lost_before_acceptance")) {
1344
+ if (body.state === "processing_failed" && body.retryable !== true) {
1345
+ if (assignment.acceptanceTimer) {
1346
+ clearTimeout(assignment.acceptanceTimer);
1347
+ assignment.acceptanceTimer = undefined;
1348
+ }
1349
+ forgetRuntimeAssignment(key);
1350
+ return;
1351
+ }
1352
+ restorePendingAssignmentBeforeAcceptance(key, assignment, body.state);
1353
+ return;
1354
+ }
1355
+ if (body.state === "processing_failed" || body.state === "expired") {
1356
+ forgetRuntimeAssignment(key);
1357
+ }
1358
+ },
703
1359
  async reattachEndpoint(endpoint_id, plugin_pid, _ipcWsHint) {
704
1360
  const ipcWs = channels.get(plugin_pid);
705
1361
  if (!ipcWs) {
@@ -722,11 +1378,12 @@ export async function startBrokerDaemon(opts) {
722
1378
  }
723
1379
  bound.add(endpoint_id);
724
1380
  const buf = reconnectingBuffers.forEndpoint(endpoint_id);
725
- const drained = buf.drain();
1381
+ const drained = buf.read();
726
1382
  graceTimers.cancel(endpoint_id);
727
1383
  registry.markActive(endpoint_id, ipcWs);
1384
+ let allPushed = true;
728
1385
  for (const msg of drained) {
729
- pushToPlugin(ipcWs, {
1386
+ const pushed = pushToPlugin(ipcWs, {
730
1387
  event: "message_received",
731
1388
  from: msg.envelope.from,
732
1389
  content: msg.envelope.content,
@@ -735,8 +1392,23 @@ export async function startBrokerDaemon(opts) {
735
1392
  ...(msg.envelope.sourceMessageId !== undefined && {
736
1393
  sourceMessageId: msg.envelope.sourceMessageId,
737
1394
  }),
1395
+ ...(msg.envelope.runtimeAssignment !== undefined && {
1396
+ runtimeAssignment: msg.envelope.runtimeAssignment,
1397
+ }),
738
1398
  });
1399
+ if (!pushed) {
1400
+ allPushed = false;
1401
+ break;
1402
+ }
739
1403
  }
1404
+ if (!allPushed) {
1405
+ handleTransition(endpoint_id, {
1406
+ type: "ipc_close",
1407
+ plugin_alive: livenessProbe(plugin_pid),
1408
+ });
1409
+ throw new BrokerHttpError(503, "reconnecting_buffer_drain_failed", "failed to push buffered messages to reattached endpoint", { retryable: true });
1410
+ }
1411
+ buf.delete();
740
1412
  emitTransition(endpoint_id, "active", "reattach");
741
1413
  logger.info("endpoint_reattached", {
742
1414
  endpoint_id,
@@ -749,13 +1421,17 @@ export async function startBrokerDaemon(opts) {
749
1421
  return undispatchedInbox.list();
750
1422
  },
751
1423
  async dispatch(undispatched_id, target_endpoint_id) {
1424
+ const pending = undispatchedInbox.get(undispatched_id);
1425
+ const target = registry.get(target_endpoint_id);
752
1426
  const result = await controlEventDispatcher.dispatch({
753
1427
  type: "dispatch_undispatched",
754
1428
  version: 1,
755
1429
  idempotency_key: `local-${randomUUID()}`,
756
1430
  emitted_at: Date.now(),
757
1431
  undispatched_id,
758
- target_endpoint_id,
1432
+ source_message_id: pending?.source_message_id ?? undispatched_id,
1433
+ target_ref: buildLocalDispatchTargetRef(target_endpoint_id, target),
1434
+ delivery_intent: "external_handoff",
759
1435
  });
760
1436
  if (!result.ok) {
761
1437
  throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
@@ -841,21 +1517,71 @@ export async function startBrokerDaemon(opts) {
841
1517
  ? meta.target_endpoint_id
842
1518
  : undefined;
843
1519
  const correlation_id = typeof meta.correlation_id === "string" ? meta.correlation_id : undefined;
1520
+ const addUndispatchedFromPayload = async (opts) => {
1521
+ const undispatched = {
1522
+ id: payload.messageId ?? randomUUID(),
1523
+ ...(payload.messageId !== undefined && {
1524
+ source_message_id: payload.messageId,
1525
+ }),
1526
+ arrived_at: new Date().toISOString(),
1527
+ sender_address: payload.from,
1528
+ content: payload.content,
1529
+ content_type: payload.contentType,
1530
+ metadata: meta,
1531
+ reason: opts.reason,
1532
+ original_target_endpoint_id: opts.originalTargetEndpointId,
1533
+ original_correlation_id: opts.originalCorrelationId,
1534
+ };
1535
+ const added = undispatchedInbox.tryAdd(undispatched);
1536
+ if (!added) {
1537
+ logger.warn("undispatched_inbox_at_capacity", {
1538
+ from: payload.from,
1539
+ size: undispatchedInbox.size(),
1540
+ capacity: undispatchedInbox.capacity(),
1541
+ });
1542
+ return false;
1543
+ }
1544
+ logger.info("undispatched_added", {
1545
+ id: undispatched.id,
1546
+ reason: opts.reason,
1547
+ });
1548
+ const wireReason = undispatched.reason === "unaddressed"
1549
+ ? "no_target_endpoint"
1550
+ : undispatched.reason;
1551
+ const posted = await directPostUndispatched({
1552
+ type: "undispatched_changed",
1553
+ version: 1,
1554
+ action: "add",
1555
+ undispatched_id: undispatched.id,
1556
+ ...(undispatched.source_message_id !== undefined && {
1557
+ source_message_id: undispatched.source_message_id,
1558
+ }),
1559
+ sender_address: undispatched.sender_address,
1560
+ content_preview: truncateContentPreview(undispatched.content, CONTENT_PREVIEW_MAX_CODEPOINTS),
1561
+ reason: wireReason,
1562
+ ...(opts.originalTargetEndpointId !== undefined && {
1563
+ target_endpoint_id_hint: opts.originalTargetEndpointId,
1564
+ }),
1565
+ ...(typeof correlation_id === "string" && {
1566
+ in_reply_to: correlation_id,
1567
+ }),
1568
+ arrived_at: Date.parse(undispatched.arrived_at),
1569
+ }, { queueOnTransientFailure: typeof payload.seq !== "number" });
1570
+ if (!posted)
1571
+ return false;
1572
+ return markDeliverySeqAccepted(payload.seq);
1573
+ };
844
1574
  const decision = router.route({ target_endpoint_id, correlation_id });
845
1575
  if (decision.kind === "endpoint") {
846
- if (payload.messageId !== undefined) {
847
- const posted = await directPostRuntimeInboundRouted({
848
- type: "runtime_inbound_routed",
849
- version: 1,
850
- source_message_id: payload.messageId,
851
- routed_to_endpoint_id: decision.entry.endpoint_id,
852
- route_kind: decision.entry.state === "active"
853
- ? "active_endpoint"
854
- : "reconnecting_endpoint",
855
- routed_at: Date.now(),
856
- }, { queueOnTransientFailure: typeof payload.seq !== "number" });
857
- if (!posted && typeof payload.seq === "number")
858
- return false;
1576
+ const isCorrelationContinuation = typeof correlation_id === "string" &&
1577
+ decision.entry.correlation_ring.has(correlation_id);
1578
+ if (isBackgroundExchangeTarget(decision.entry) &&
1579
+ !isCorrelationContinuation) {
1580
+ return addUndispatchedFromPayload({
1581
+ reason: "unaddressed",
1582
+ originalTargetEndpointId: decision.entry.endpoint_id,
1583
+ originalCorrelationId: correlation_id,
1584
+ });
859
1585
  }
860
1586
  recordInboundCorrelation(decision.entry.endpoint_id, meta);
861
1587
  if (decision.entry.state === "active") {
@@ -874,24 +1600,47 @@ export async function startBrokerDaemon(opts) {
874
1600
  }
875
1601
  else {
876
1602
  const buf = reconnectingBuffers.forEndpoint(decision.entry.endpoint_id);
877
- buf.append({
878
- id: payload.messageId ?? randomUUID(),
879
- arrived_at: new Date().toISOString(),
880
- envelope: {
881
- from: payload.from,
882
- content: payload.content,
883
- contentType: payload.contentType,
884
- metadata: meta,
885
- ...(payload.messageId !== undefined && {
886
- sourceMessageId: payload.messageId,
887
- }),
888
- },
889
- });
1603
+ try {
1604
+ buf.append({
1605
+ id: payload.messageId ?? randomUUID(),
1606
+ arrived_at: new Date().toISOString(),
1607
+ envelope: {
1608
+ from: payload.from,
1609
+ content: payload.content,
1610
+ contentType: payload.contentType,
1611
+ metadata: meta,
1612
+ ...(payload.messageId !== undefined && {
1613
+ sourceMessageId: payload.messageId,
1614
+ }),
1615
+ },
1616
+ });
1617
+ }
1618
+ catch (err) {
1619
+ logger.warn("buffer_for_reconnecting_endpoint_failed", {
1620
+ endpoint_id: decision.entry.endpoint_id,
1621
+ err: err instanceof Error ? err.message : String(err),
1622
+ });
1623
+ return false;
1624
+ }
890
1625
  logger.info("buffered_for_reconnecting_endpoint", {
891
1626
  endpoint_id: decision.entry.endpoint_id,
892
1627
  buffer_size: buf.size(),
893
1628
  });
894
1629
  }
1630
+ if (payload.messageId !== undefined) {
1631
+ const posted = await directPostRuntimeInboundRouted({
1632
+ type: "runtime_inbound_routed",
1633
+ version: 1,
1634
+ source_message_id: payload.messageId,
1635
+ routed_to_endpoint_id: decision.entry.endpoint_id,
1636
+ route_kind: decision.entry.state === "active"
1637
+ ? "active_endpoint"
1638
+ : "reconnecting_endpoint",
1639
+ routed_at: Date.now(),
1640
+ }, { queueOnTransientFailure: false });
1641
+ if (!posted)
1642
+ return false;
1643
+ }
895
1644
  return markDeliverySeqAccepted(payload.seq);
896
1645
  }
897
1646
  let reason = "unaddressed";
@@ -911,55 +1660,11 @@ export async function startBrokerDaemon(opts) {
911
1660
  });
912
1661
  }
913
1662
  }
914
- const undispatched = {
915
- id: payload.messageId ?? randomUUID(),
916
- ...(payload.messageId !== undefined && {
917
- source_message_id: payload.messageId,
918
- }),
919
- arrived_at: new Date().toISOString(),
920
- sender_address: payload.from,
921
- content: payload.content,
922
- content_type: payload.contentType,
923
- metadata: meta,
1663
+ return addUndispatchedFromPayload({
924
1664
  reason,
925
- original_target_endpoint_id: originalTarget,
926
- original_correlation_id: correlation_id,
927
- };
928
- const added = undispatchedInbox.tryAdd(undispatched);
929
- if (!added) {
930
- logger.warn("undispatched_inbox_at_capacity", {
931
- from: payload.from,
932
- size: undispatchedInbox.size(),
933
- capacity: undispatchedInbox.capacity(),
934
- });
935
- return false;
936
- }
937
- logger.info("undispatched_added", { id: undispatched.id, reason });
938
- const wireReason = undispatched.reason === "unaddressed"
939
- ? "no_target_endpoint"
940
- : undispatched.reason;
941
- const posted = await directPostUndispatched({
942
- type: "undispatched_changed",
943
- version: 1,
944
- action: "add",
945
- undispatched_id: undispatched.id,
946
- ...(undispatched.source_message_id !== undefined && {
947
- source_message_id: undispatched.source_message_id,
948
- }),
949
- sender_address: undispatched.sender_address,
950
- content_preview: truncateContentPreview(undispatched.content, CONTENT_PREVIEW_MAX_CODEPOINTS),
951
- reason: wireReason,
952
- ...(undispatched.original_target_endpoint_id !== undefined && {
953
- target_endpoint_id_hint: undispatched.original_target_endpoint_id,
954
- }),
955
- ...(typeof correlation_id === "string" && {
956
- in_reply_to: correlation_id,
957
- }),
958
- arrived_at: Date.parse(undispatched.arrived_at),
959
- }, { queueOnTransientFailure: typeof payload.seq !== "number" });
960
- if (!posted)
961
- return false;
962
- return markDeliverySeqAccepted(payload.seq);
1665
+ originalTargetEndpointId: originalTarget,
1666
+ originalCorrelationId: correlation_id,
1667
+ });
963
1668
  };
964
1669
  connector.on("message_received", ({ payload }) => {
965
1670
  let task;
@@ -1081,6 +1786,7 @@ export async function startBrokerDaemon(opts) {
1081
1786
  });
1082
1787
  }
1083
1788
  await runtimeInboundRoutedEmitter.shutdown();
1789
+ await flushRuntimeAssignmentsOnShutdown();
1084
1790
  const endpoints = registry.list();
1085
1791
  await Promise.all(endpoints.map(async (entry) => {
1086
1792
  try {