@masons/runtime-broker 0.2.2 → 0.2.6

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 (41) hide show
  1. package/dist/broker/background-assignment-ledger.d.ts +27 -0
  2. package/dist/broker/background-assignment-ledger.d.ts.map +1 -0
  3. package/dist/broker/background-assignment-ledger.js +125 -0
  4. package/dist/broker/broker-daemon.d.ts +2 -1
  5. package/dist/broker/broker-daemon.d.ts.map +1 -1
  6. package/dist/broker/broker-daemon.js +429 -46
  7. package/dist/broker/control-event-types.d.ts +9 -4
  8. package/dist/broker/control-event-types.d.ts.map +1 -1
  9. package/dist/broker/endpoint-registry.d.ts +2 -0
  10. package/dist/broker/endpoint-registry.d.ts.map +1 -1
  11. package/dist/broker/endpoint-registry.js +1 -0
  12. package/dist/broker/entry.d.ts.map +1 -1
  13. package/dist/broker/entry.js +4 -1
  14. package/dist/broker/ipc-server.d.ts +13 -0
  15. package/dist/broker/ipc-server.d.ts.map +1 -1
  16. package/dist/broker/ipc-server.js +6 -0
  17. package/dist/broker/runtime-endpoint-port.d.ts +3 -2
  18. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -1
  19. package/dist/broker/runtime-inbound-routed-emitter.d.ts +4 -2
  20. package/dist/broker/runtime-inbound-routed-emitter.d.ts.map +1 -1
  21. package/dist/broker/runtime-inbound-routed-emitter.js +18 -6
  22. package/dist/broker/runtime-inbound-routed-event-types.d.ts +8 -0
  23. package/dist/broker/runtime-inbound-routed-event-types.d.ts.map +1 -1
  24. package/dist/broker/runtime-processing-state-event-types.d.ts +9 -4
  25. package/dist/broker/runtime-processing-state-event-types.d.ts.map +1 -1
  26. package/dist/broker/version-handshake.d.ts +1 -0
  27. package/dist/broker/version-handshake.d.ts.map +1 -1
  28. package/dist/broker/version-handshake.js +1 -0
  29. package/dist/broker-client/broker-client.d.ts +25 -2
  30. package/dist/broker-client/broker-client.d.ts.map +1 -1
  31. package/dist/broker-client/broker-client.js +45 -0
  32. package/dist/broker-client/lazy-spawn.d.ts.map +1 -1
  33. package/dist/connector-client.d.ts +8 -0
  34. package/dist/connector-client.d.ts.map +1 -1
  35. package/dist/connector-client.js +17 -3
  36. package/dist/runtime-endpoint-client.d.ts +22 -1
  37. package/dist/runtime-endpoint-client.d.ts.map +1 -1
  38. package/dist/runtime-endpoint-client.js +47 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +13 -12
@@ -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";
@@ -23,13 +24,27 @@ import { createUndispatchedChangedEmitter, postUndispatchedChangedViaPort, } fro
23
24
  import { createUndispatchedInbox, } from "./undispatched-inbox.js";
24
25
  const REMOTE_SPAWN_CAPABILITY = "remote_spawn_v1";
25
26
  const DEFAULT_RUNTIME_ASSIGNMENT_ACCEPTANCE_TIMEOUT_MS = 30_000;
27
+ const RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS = 24 * 60 * 60 * 1000;
26
28
  const RUNTIME_PROCESSING_RETRY_INITIAL_MS = 1_000;
27
29
  const RUNTIME_PROCESSING_RETRY_MAX_MS = 30_000;
28
30
  function isBackgroundExchangeMode(value) {
29
31
  return value === "resident_endpoint" || value === "adapter_managed_turn";
30
32
  }
31
- function normalizeBackgroundExchangeMode(value) {
32
- return isBackgroundExchangeMode(value) ? value : "resident_endpoint";
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
+ };
33
48
  }
34
49
  function derivePublicDisplayLabel(body) {
35
50
  const parts = [
@@ -76,6 +91,8 @@ export async function startBrokerDaemon(opts) {
76
91
  const controlEventDispatcher = createControlEventDispatcher();
77
92
  const runtimeAssignments = new Map();
78
93
  const runtimeAssignmentByEndpointCorrelation = new Map();
94
+ const runtimeAssignmentReplyRecords = new Map();
95
+ const runtimeAssignmentReplyInFlight = new Map();
79
96
  const spawnCorrelation = createSpawnCorrelationManager({
80
97
  timeoutMs: opts.spawnCorrelationTimeoutMs,
81
98
  });
@@ -135,6 +152,21 @@ export async function startBrokerDaemon(opts) {
135
152
  maxRetries: opts.runtimeInboundRoutedMaxRetries,
136
153
  });
137
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
+ };
138
170
  const endpointCorrelationKey = (endpointId, correlationId) => `${endpointId}\u0000${correlationId}`;
139
171
  const isBackgroundExchangeTarget = (entry) => Boolean(entry.runtime_capabilities?.includes("background_exchange_v1") &&
140
172
  isBackgroundExchangeMode(entry.background_exchange_mode));
@@ -158,6 +190,11 @@ export async function startBrokerDaemon(opts) {
158
190
  runtimeAssignmentByEndpointCorrelation.delete(correlationKey);
159
191
  }
160
192
  }
193
+ for (const replyKey of Array.from(runtimeAssignmentReplyInFlight.keys())) {
194
+ if (replyKey.startsWith(`${key}\u0000`)) {
195
+ runtimeAssignmentReplyInFlight.delete(replyKey);
196
+ }
197
+ }
161
198
  };
162
199
  const scheduleAcceptanceTimeout = (key) => {
163
200
  if (runtimeAssignmentAcceptanceTimeoutMs <= 0)
@@ -247,10 +284,45 @@ export async function startBrokerDaemon(opts) {
247
284
  detail: outcome.detail,
248
285
  });
249
286
  if (!outcome.terminal && opts.queueOnTransientFailure !== false) {
250
- runtimeInboundRoutedEmitter.enqueue(event);
287
+ const queued = runtimeInboundRoutedEmitter.enqueue(event, {
288
+ onSuccess: opts.onQueuedSuccess,
289
+ });
290
+ return opts.allowQueuedAsSuccess === true ? queued : false;
251
291
  }
252
292
  return false;
253
293
  };
294
+ const directClaimRuntimeInboundRoute = async (event) => {
295
+ const claim = opts.runtimeInboundRouteClaimPost ?? apiPort.claimRuntimeInboundRoute;
296
+ if (!claim) {
297
+ logger.warn("runtime_inbound_route_claim_unavailable", {
298
+ source_message_id: event.source_message_id,
299
+ routed_to_endpoint_id: event.routed_to_endpoint_id,
300
+ });
301
+ return "retry";
302
+ }
303
+ let outcome;
304
+ try {
305
+ outcome = await claim(event);
306
+ }
307
+ catch (err) {
308
+ outcome = {
309
+ ok: false,
310
+ terminal: false,
311
+ detail: err instanceof Error ? err.message : String(err),
312
+ };
313
+ }
314
+ if (outcome.ok) {
315
+ return outcome.action;
316
+ }
317
+ logger.warn("runtime_inbound_route_claim_failed", {
318
+ source_message_id: event.source_message_id,
319
+ routed_to_endpoint_id: event.routed_to_endpoint_id,
320
+ terminal: outcome.terminal,
321
+ status: outcome.status,
322
+ detail: outcome.detail,
323
+ });
324
+ return "retry";
325
+ };
254
326
  const markDeliverySeqAccepted = (seq) => {
255
327
  if (typeof seq !== "number")
256
328
  return true;
@@ -327,6 +399,27 @@ export async function startBrokerDaemon(opts) {
327
399
  }
328
400
  return Promise.allSettled(tasks).then(() => undefined);
329
401
  };
402
+ const restorePendingAssignmentBeforeAcceptance = (key, assignment, reason) => {
403
+ if (assignment.acceptanceTimer) {
404
+ clearTimeout(assignment.acceptanceTimer);
405
+ assignment.acceptanceTimer = undefined;
406
+ }
407
+ const readded = undispatchedInbox.tryAdd(assignment.message);
408
+ if (!readded) {
409
+ logger.error("runtime_assignment_reinsert_failed_at_capacity", {
410
+ undispatched_id: assignment.context.undispatchedId,
411
+ source_message_id: assignment.context.sourceMessageId,
412
+ target_endpoint_id: assignment.context.targetRef.projection_endpoint_id,
413
+ inbox_size: undispatchedInbox.size(),
414
+ inbox_capacity: undispatchedInbox.capacity(),
415
+ reason,
416
+ });
417
+ scheduleAcceptanceTimeout(key);
418
+ return false;
419
+ }
420
+ forgetRuntimeAssignment(key);
421
+ return true;
422
+ };
330
423
  const emitReplyEmittedWithRetry = (assignmentKey, event, attempt = 0) => {
331
424
  const assignment = runtimeAssignments.get(assignmentKey);
332
425
  if (!assignment)
@@ -351,8 +444,10 @@ export async function startBrokerDaemon(opts) {
351
444
  terminal: outcome.terminal,
352
445
  detail: outcome.detail,
353
446
  });
354
- if (outcome.terminal)
447
+ if (outcome.terminal) {
448
+ forgetRuntimeAssignment(assignmentKey);
355
449
  return;
450
+ }
356
451
  const delay = Math.min(RUNTIME_PROCESSING_RETRY_INITIAL_MS * 2 ** attempt, RUNTIME_PROCESSING_RETRY_MAX_MS);
357
452
  const timer = setTimeout(() => {
358
453
  emitReplyEmittedWithRetry(assignmentKey, event, attempt + 1);
@@ -380,6 +475,9 @@ export async function startBrokerDaemon(opts) {
380
475
  terminal: outcome.terminal,
381
476
  detail: outcome.detail,
382
477
  });
478
+ if (outcome.terminal) {
479
+ forgetRuntimeAssignment(key);
480
+ }
383
481
  }
384
482
  }
385
483
  for (const endpointId of endpointIds) {
@@ -606,6 +704,12 @@ export async function startBrokerDaemon(opts) {
606
704
  }
607
705
  };
608
706
  controlEventDispatcher.on("dispatch_undispatched", async (event) => {
707
+ if (event.broker_projection_token === undefined) {
708
+ return {
709
+ ok: false,
710
+ detail: "dispatch broker_projection_token missing",
711
+ };
712
+ }
609
713
  const targetEndpointId = event.target_ref?.projection_endpoint_id ?? event.target_endpoint_id;
610
714
  if (!targetEndpointId) {
611
715
  return {
@@ -668,13 +772,31 @@ export async function startBrokerDaemon(opts) {
668
772
  detail: `target endpoint not found: ${targetEndpointId}`,
669
773
  };
670
774
  }
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) {
775
+ let targetRef = event.target_ref;
776
+ if (!targetRef) {
777
+ if (target.background_exchange_mode === "resident_endpoint") {
778
+ targetRef = {
779
+ kind: "projection_endpoint",
780
+ projection_endpoint_id: targetEndpointId,
781
+ background_exchange_mode: "resident_endpoint",
782
+ };
783
+ }
784
+ else if (target.background_exchange_mode === "adapter_managed_turn" &&
785
+ target.runtime_session_id) {
786
+ targetRef = {
787
+ kind: "projection_endpoint",
788
+ projection_endpoint_id: targetEndpointId,
789
+ runtime_session_id: target.runtime_session_id,
790
+ background_exchange_mode: "adapter_managed_turn",
791
+ };
792
+ }
793
+ }
794
+ if (!targetRef ||
795
+ !target.runtime_capabilities?.includes("background_exchange_v1") ||
796
+ target.background_exchange_mode !==
797
+ targetRef.background_exchange_mode ||
798
+ (targetRef.background_exchange_mode === "adapter_managed_turn" &&
799
+ target.runtime_session_id !== targetRef.runtime_session_id)) {
678
800
  undispatchedInbox.tryAdd(taken);
679
801
  return {
680
802
  ok: false,
@@ -693,6 +815,9 @@ export async function startBrokerDaemon(opts) {
693
815
  state: "assigned_to_runtime_target",
694
816
  occurred_at: assignedAt,
695
817
  undispatched_id: event.undispatched_id,
818
+ ...(event.broker_projection_token !== undefined && {
819
+ broker_projection_token: event.broker_projection_token,
820
+ }),
696
821
  };
697
822
  const processingResult = await emitRuntimeProcessingState(processingEvent);
698
823
  if (!processingResult.ok) {
@@ -724,12 +849,20 @@ export async function startBrokerDaemon(opts) {
724
849
  const storedAssignment = existingAssignment ?? {
725
850
  context: runtimeAssignment,
726
851
  message: taken,
852
+ ...(event.broker_projection_token !== undefined && {
853
+ brokerProjectionToken: event.broker_projection_token,
854
+ }),
727
855
  accepted: false,
728
856
  assignedAt,
857
+ lastStateSequence: processingEvent.state_sequence,
729
858
  };
730
859
  storedAssignment.context = runtimeAssignment;
731
860
  storedAssignment.message = taken;
861
+ if (event.broker_projection_token !== undefined) {
862
+ storedAssignment.brokerProjectionToken = event.broker_projection_token;
863
+ }
732
864
  storedAssignment.assignedAt = assignedAt;
865
+ storedAssignment.lastStateSequence = Math.max(storedAssignment.lastStateSequence, processingEvent.state_sequence);
733
866
  runtimeAssignments.set(assignmentKey, storedAssignment);
734
867
  scheduleAcceptanceTimeout(assignmentKey);
735
868
  rememberRuntimeAssignmentCorrelation(target.endpoint_id, taken.metadata.correlation_id, assignmentKey);
@@ -883,6 +1016,7 @@ export async function startBrokerDaemon(opts) {
883
1016
  const runtimeCapabilities = runtimeCapabilitiesSet.size > 0
884
1017
  ? Array.from(runtimeCapabilitiesSet)
885
1018
  : undefined;
1019
+ const runtimeSessionId = body.runtime_session_id?.trim() || undefined;
886
1020
  const apiResp = await apiPort.register({
887
1021
  runtime_kind: body.kind,
888
1022
  endpoint_nonce: randomUUID(),
@@ -895,6 +1029,7 @@ export async function startBrokerDaemon(opts) {
895
1029
  task_hint: body.task_hint,
896
1030
  runtime_capabilities: runtimeCapabilities,
897
1031
  execution_surface: body.execution_surface,
1032
+ runtime_session_id: runtimeSessionId,
898
1033
  background_exchange_mode: body.background_exchange_mode,
899
1034
  });
900
1035
  registry.register({
@@ -904,6 +1039,7 @@ export async function startBrokerDaemon(opts) {
904
1039
  ipc_ws: boundWs,
905
1040
  runtime_capabilities: runtimeCapabilities,
906
1041
  execution_surface: body.execution_surface,
1042
+ runtime_session_id: runtimeSessionId,
907
1043
  background_exchange_mode: body.background_exchange_mode,
908
1044
  display_metadata: {
909
1045
  session_name: body.session_name,
@@ -1014,35 +1150,186 @@ export async function startBrokerDaemon(opts) {
1014
1150
  }
1015
1151
  catch (err) {
1016
1152
  const msg = err instanceof Error ? err.message : String(err);
1017
- if (msg === "WebSocket not connected" || msg === "SEND_ACK timeout") {
1153
+ if (err instanceof ConnectorClientUnavailableError) {
1018
1154
  throw new BrokerHttpError(503, "connector_unavailable", msg, {
1019
1155
  retryable: true,
1020
1156
  });
1021
1157
  }
1158
+ if (err instanceof ConnectorSendAckTimeoutError) {
1159
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
1160
+ }
1022
1161
  throw err;
1023
1162
  }
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, {
1163
+ return { messageId: ack.messageId, status: ack.status };
1164
+ },
1165
+ async sendRuntimeAssignmentReply(body) {
1166
+ const entry = registry.get(body.endpoint_id);
1167
+ if (!entry) {
1168
+ throw new BrokerHttpError(404, "unknown_endpoint", `unknown endpoint_id: ${body.endpoint_id}`);
1169
+ }
1170
+ if (entry.plugin_pid !== body.plugin_pid) {
1171
+ throw new BrokerHttpError(403, "endpoint_pid_mismatch", "plugin_pid does not own endpoint_id");
1172
+ }
1173
+ if (entry.state !== "active") {
1174
+ throw new BrokerHttpError(409, "endpoint_not_active", `endpoint ${body.endpoint_id} is ${entry.state}; assignment reply is only allowed from Active`);
1175
+ }
1176
+ if (!body.reply_request_id) {
1177
+ throw new BrokerHttpError(400, "reply_request_id_required", "reply_request_id is required for assignment replies");
1178
+ }
1179
+ const assignmentKey = runtimeAssignmentKey(body.source_message_id, body.assignment_id);
1180
+ const replyKey = runtimeAssignmentReplyKey(body.source_message_id, body.assignment_id, body.reply_request_id);
1181
+ pruneRuntimeAssignmentReplyRecords();
1182
+ const requestFingerprint = runtimeAssignmentReplyFingerprint(body.content, body.contentType);
1183
+ const assignment = runtimeAssignments.get(assignmentKey);
1184
+ const existingRecord = runtimeAssignmentReplyRecords.get(replyKey);
1185
+ const inFlight = runtimeAssignmentReplyInFlight.get(replyKey);
1186
+ if (assignment) {
1187
+ if (assignment.context.targetRef.projection_endpoint_id !==
1188
+ body.endpoint_id) {
1189
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1190
+ }
1191
+ if (!assignment.accepted) {
1192
+ throw new BrokerHttpError(409, "runtime_assignment_not_accepted", "assignment replies require adapter_accepted first");
1193
+ }
1194
+ }
1195
+ else if (existingRecord || inFlight) {
1196
+ const owner = existingRecord ?? inFlight;
1197
+ if (owner?.endpointId !== body.endpoint_id ||
1198
+ owner.pluginPid !== body.plugin_pid) {
1199
+ throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1200
+ }
1201
+ }
1202
+ else {
1203
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1204
+ }
1205
+ if (existingRecord &&
1206
+ existingRecord.requestFingerprint !== requestFingerprint) {
1207
+ throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
1208
+ }
1209
+ if (existingRecord?.outcomeUnknown) {
1210
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", existingRecord.outcomeUnknown.message, { retryable: false });
1211
+ }
1212
+ if (existingRecord?.result)
1213
+ return existingRecord.result;
1214
+ if (inFlight) {
1215
+ if (inFlight.requestFingerprint !== requestFingerprint) {
1216
+ throw new BrokerHttpError(409, "runtime_assignment_reply_idempotency_conflict", "reply_request_id was already used with different reply content");
1217
+ }
1218
+ return inFlight.task;
1219
+ }
1220
+ if (!assignment) {
1221
+ throw new BrokerHttpError(409, "runtime_assignment_unknown", "source assignment is not known to this broker");
1222
+ }
1223
+ if (assignment.brokerProjectionToken === undefined) {
1224
+ throw new BrokerHttpError(409, "runtime_assignment_projection_authority_missing", "runtime assignment reply requires broker projection authority");
1225
+ }
1226
+ const task = (async () => {
1227
+ let record = runtimeAssignmentReplyRecords.get(replyKey);
1228
+ if (!record) {
1229
+ const replyRequestedSequence = assignment.lastStateSequence + 1;
1230
+ const replyRequestedEvent = {
1031
1231
  type: "runtime_processing_state",
1032
1232
  version: 1,
1033
1233
  source_message_id: assignment.context.sourceMessageId,
1034
1234
  assignment_id: assignment.context.assignmentId,
1035
- state_event_id: `${assignment.context.assignmentId}:reply:${ack.messageId}`,
1036
- state_sequence: Number.MAX_SAFE_INTEGER,
1235
+ state_event_id: `${assignment.context.assignmentId}:reply_requested:${body.reply_request_id}`,
1236
+ state_sequence: replyRequestedSequence,
1037
1237
  target_ref: assignment.context.targetRef,
1038
- state: "reply_emitted",
1238
+ state: "reply_requested",
1039
1239
  occurred_at: Date.now(),
1040
1240
  undispatched_id: assignment.context.undispatchedId,
1041
- outbound_message_id: ack.messageId,
1042
- });
1241
+ reply_request_id: body.reply_request_id,
1242
+ ...(assignment.brokerProjectionToken !== undefined && {
1243
+ broker_projection_token: assignment.brokerProjectionToken,
1244
+ }),
1245
+ };
1246
+ const replyRequested = await emitRuntimeProcessingState(replyRequestedEvent);
1247
+ if (!replyRequested.ok) {
1248
+ throw new BrokerHttpError(replyRequested.terminal ? 409 : 503, "runtime_reply_request_rejected", replyRequested.detail ?? "runtime reply request was not accepted");
1249
+ }
1250
+ record = {
1251
+ replyRequestedEvent,
1252
+ endpointId: body.endpoint_id,
1253
+ pluginPid: body.plugin_pid,
1254
+ requestFingerprint,
1255
+ wireMessageId: randomUUID(),
1256
+ expiresAt: Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS,
1257
+ };
1258
+ runtimeAssignmentReplyRecords.set(replyKey, record);
1259
+ assignment.lastStateSequence = replyRequestedSequence;
1043
1260
  }
1261
+ const correlationId = typeof assignment.context.replyCorrelationId === "string" &&
1262
+ assignment.context.replyCorrelationId.length > 0
1263
+ ? assignment.context.replyCorrelationId
1264
+ : randomUUID();
1265
+ entry.correlation_ring.add(correlationId);
1266
+ const remoteMessageId = assignment.message.metadata.message_id;
1267
+ const metadata = {
1268
+ message_id: record.wireMessageId,
1269
+ ...(typeof remoteMessageId === "string" &&
1270
+ remoteMessageId.length > 0 && { in_reply_to: remoteMessageId }),
1271
+ correlation_id: correlationId,
1272
+ source_endpoint_id: body.endpoint_id,
1273
+ require_live: false,
1274
+ };
1275
+ let ack;
1276
+ try {
1277
+ ack = await connector.send(assignment.message.sender_address, body.content, body.contentType ?? "text", metadata);
1278
+ }
1279
+ catch (err) {
1280
+ const msg = err instanceof Error ? err.message : String(err);
1281
+ if (err instanceof ConnectorClientUnavailableError) {
1282
+ throw new BrokerHttpError(503, "connector_unavailable", msg, {
1283
+ retryable: true,
1284
+ });
1285
+ }
1286
+ if (err instanceof ConnectorSendAckTimeoutError) {
1287
+ record.outcomeUnknown = { message: msg };
1288
+ record.expiresAt =
1289
+ Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
1290
+ throw new BrokerHttpError(503, "connector_send_outcome_unknown", msg, { retryable: false });
1291
+ }
1292
+ throw err;
1293
+ }
1294
+ const result = {
1295
+ messageId: record.wireMessageId,
1296
+ status: ack.status,
1297
+ };
1298
+ record.result = result;
1299
+ record.expiresAt = Date.now() + RUNTIME_ASSIGNMENT_REPLY_RECORD_TTL_MS;
1300
+ const replyEmittedSequence = assignment.lastStateSequence + 1;
1301
+ emitReplyEmittedWithRetry(assignmentKey, {
1302
+ type: "runtime_processing_state",
1303
+ version: 1,
1304
+ source_message_id: assignment.context.sourceMessageId,
1305
+ assignment_id: assignment.context.assignmentId,
1306
+ state_event_id: `${assignment.context.assignmentId}:reply_emitted:${body.reply_request_id}`,
1307
+ state_sequence: replyEmittedSequence,
1308
+ target_ref: assignment.context.targetRef,
1309
+ state: "reply_emitted",
1310
+ occurred_at: Date.now(),
1311
+ undispatched_id: assignment.context.undispatchedId,
1312
+ reply_request_id: body.reply_request_id,
1313
+ outbound_message_id: record.wireMessageId,
1314
+ ...(assignment.brokerProjectionToken !== undefined && {
1315
+ broker_projection_token: assignment.brokerProjectionToken,
1316
+ }),
1317
+ });
1318
+ assignment.lastStateSequence = replyEmittedSequence;
1319
+ return result;
1320
+ })();
1321
+ runtimeAssignmentReplyInFlight.set(replyKey, {
1322
+ endpointId: body.endpoint_id,
1323
+ pluginPid: body.plugin_pid,
1324
+ requestFingerprint,
1325
+ task,
1326
+ });
1327
+ try {
1328
+ return await task;
1329
+ }
1330
+ finally {
1331
+ runtimeAssignmentReplyInFlight.delete(replyKey);
1044
1332
  }
1045
- return { messageId: ack.messageId, status: ack.status };
1046
1333
  },
1047
1334
  async reportProcessingState(body) {
1048
1335
  const entry = registry.get(body.endpoint_id);
@@ -1060,8 +1347,10 @@ export async function startBrokerDaemon(opts) {
1060
1347
  if (assignment.context.targetRef.projection_endpoint_id !== body.endpoint_id) {
1061
1348
  throw new BrokerHttpError(403, "runtime_assignment_target_mismatch", "endpoint_id does not match the assigned target");
1062
1349
  }
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");
1350
+ if (body.state === "assigned_to_runtime_target" ||
1351
+ body.state === "reply_requested" ||
1352
+ body.state === "reply_emitted") {
1353
+ throw new BrokerHttpError(403, `${body.state}_broker_owned`, `${body.state} is emitted by the broker assignment lifecycle path`);
1065
1354
  }
1066
1355
  const event = {
1067
1356
  type: "runtime_processing_state",
@@ -1090,6 +1379,7 @@ export async function startBrokerDaemon(opts) {
1090
1379
  if (!outcome.ok) {
1091
1380
  throw new BrokerHttpError(outcome.terminal ? 409 : 503, "runtime_processing_state_rejected", outcome.detail ?? "runtime processing state was not accepted");
1092
1381
  }
1382
+ assignment.lastStateSequence = Math.max(assignment.lastStateSequence, body.state_sequence);
1093
1383
  if (body.state === "adapter_accepted") {
1094
1384
  assignment.accepted = true;
1095
1385
  if (assignment.acceptanceTimer) {
@@ -1097,6 +1387,21 @@ export async function startBrokerDaemon(opts) {
1097
1387
  assignment.acceptanceTimer = undefined;
1098
1388
  }
1099
1389
  }
1390
+ if (!assignment.accepted &&
1391
+ (body.state === "processing_failed" ||
1392
+ body.state === "waiting_for_runtime_capability" ||
1393
+ body.state === "target_lost_before_acceptance")) {
1394
+ if (body.state === "processing_failed" && body.retryable !== true) {
1395
+ if (assignment.acceptanceTimer) {
1396
+ clearTimeout(assignment.acceptanceTimer);
1397
+ assignment.acceptanceTimer = undefined;
1398
+ }
1399
+ forgetRuntimeAssignment(key);
1400
+ return;
1401
+ }
1402
+ restorePendingAssignmentBeforeAcceptance(key, assignment, body.state);
1403
+ return;
1404
+ }
1100
1405
  if (body.state === "processing_failed" || body.state === "expired") {
1101
1406
  forgetRuntimeAssignment(key);
1102
1407
  }
@@ -1175,12 +1480,9 @@ export async function startBrokerDaemon(opts) {
1175
1480
  emitted_at: Date.now(),
1176
1481
  undispatched_id,
1177
1482
  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
- },
1483
+ target_ref: buildLocalDispatchTargetRef(target_endpoint_id, target),
1183
1484
  delivery_intent: "external_handoff",
1485
+ broker_projection_token: randomUUID(),
1184
1486
  });
1185
1487
  if (!result.ok) {
1186
1488
  throw new BrokerHttpError(400, "dispatch_failed", result.detail ?? "unknown");
@@ -1259,7 +1561,36 @@ export async function startBrokerDaemon(opts) {
1259
1561
  });
1260
1562
  const inboundProcessing = new Set();
1261
1563
  const blockedInboundSeqs = new Set();
1564
+ const asyncAcceptedInboundSeqs = new Set();
1262
1565
  let unsequencedInboundBlocked = false;
1566
+ let deferredDeliveryPendingUpTo;
1567
+ const deliveredInboundSourceRoutes = new Map();
1568
+ const deliveredInboundSourceRouteOrder = [];
1569
+ const DELIVERED_INBOUND_SOURCE_ROUTE_CAP = 4096;
1570
+ const maybeAckDeferredDeliveryPending = () => {
1571
+ if (deferredDeliveryPendingUpTo === undefined ||
1572
+ unsequencedInboundBlocked ||
1573
+ blockedInboundSeqs.size > 0) {
1574
+ return;
1575
+ }
1576
+ const upTo = deferredDeliveryPendingUpTo;
1577
+ deferredDeliveryPendingUpTo = undefined;
1578
+ connector.ackDelivery(upTo);
1579
+ };
1580
+ const rememberDeliveredInboundSourceRoute = (sourceMessageId, event) => {
1581
+ if (deliveredInboundSourceRoutes.has(sourceMessageId)) {
1582
+ deliveredInboundSourceRoutes.set(sourceMessageId, event);
1583
+ return;
1584
+ }
1585
+ deliveredInboundSourceRoutes.set(sourceMessageId, event);
1586
+ deliveredInboundSourceRouteOrder.push(sourceMessageId);
1587
+ while (deliveredInboundSourceRouteOrder.length >
1588
+ DELIVERED_INBOUND_SOURCE_ROUTE_CAP) {
1589
+ const evicted = deliveredInboundSourceRouteOrder.shift();
1590
+ if (evicted)
1591
+ deliveredInboundSourceRoutes.delete(evicted);
1592
+ }
1593
+ };
1263
1594
  const handleInboundMessage = async (payload) => {
1264
1595
  const meta = payload.metadata ?? {};
1265
1596
  const target_endpoint_id = typeof meta.target_endpoint_id === "string"
@@ -1332,6 +1663,58 @@ export async function startBrokerDaemon(opts) {
1332
1663
  originalCorrelationId: correlation_id,
1333
1664
  });
1334
1665
  }
1666
+ const routeKind = decision.entry.state === "active"
1667
+ ? "active_endpoint"
1668
+ : "reconnecting_endpoint";
1669
+ const deliverySeq = payload.seq;
1670
+ const onQueuedFinalizationSuccess = typeof deliverySeq === "number"
1671
+ ? () => {
1672
+ const wasAlreadyBlocked = blockedInboundSeqs.delete(deliverySeq);
1673
+ if (!wasAlreadyBlocked) {
1674
+ asyncAcceptedInboundSeqs.add(deliverySeq);
1675
+ }
1676
+ markDeliverySeqAccepted(deliverySeq);
1677
+ maybeAckDeferredDeliveryPending();
1678
+ }
1679
+ : undefined;
1680
+ if (payload.messageId !== undefined) {
1681
+ const delivered = deliveredInboundSourceRoutes.get(payload.messageId);
1682
+ if (delivered) {
1683
+ const posted = await directPostRuntimeInboundRouted(delivered, {
1684
+ allowQueuedAsSuccess: payload.seq === undefined,
1685
+ onQueuedSuccess: onQueuedFinalizationSuccess,
1686
+ });
1687
+ if (!posted)
1688
+ return false;
1689
+ const accepted = markDeliverySeqAccepted(payload.seq);
1690
+ if (accepted)
1691
+ maybeAckDeferredDeliveryPending();
1692
+ return accepted;
1693
+ }
1694
+ const claim = await directClaimRuntimeInboundRoute({
1695
+ type: "runtime_inbound_route_claim",
1696
+ version: 1,
1697
+ source_message_id: payload.messageId,
1698
+ routed_to_endpoint_id: decision.entry.endpoint_id,
1699
+ route_kind: routeKind,
1700
+ claimed_at: Date.now(),
1701
+ });
1702
+ if (claim === "retry")
1703
+ return false;
1704
+ if (claim === "skip")
1705
+ return markDeliverySeqAccepted(payload.seq);
1706
+ }
1707
+ let routedEvent;
1708
+ if (payload.messageId !== undefined) {
1709
+ routedEvent = {
1710
+ type: "runtime_inbound_routed",
1711
+ version: 1,
1712
+ source_message_id: payload.messageId,
1713
+ routed_to_endpoint_id: decision.entry.endpoint_id,
1714
+ route_kind: routeKind,
1715
+ routed_at: Date.now(),
1716
+ };
1717
+ }
1335
1718
  recordInboundCorrelation(decision.entry.endpoint_id, meta);
1336
1719
  if (decision.entry.state === "active") {
1337
1720
  const pushed = pushToPlugin(decision.entry.ipc_ws, {
@@ -1376,18 +1759,13 @@ export async function startBrokerDaemon(opts) {
1376
1759
  buffer_size: buf.size(),
1377
1760
  });
1378
1761
  }
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")
1762
+ if (routedEvent !== undefined && payload.messageId !== undefined) {
1763
+ rememberDeliveredInboundSourceRoute(payload.messageId, routedEvent);
1764
+ const posted = await directPostRuntimeInboundRouted(routedEvent, {
1765
+ allowQueuedAsSuccess: payload.seq === undefined,
1766
+ onQueuedSuccess: onQueuedFinalizationSuccess,
1767
+ });
1768
+ if (!posted)
1391
1769
  return false;
1392
1770
  }
1393
1771
  return markDeliverySeqAccepted(payload.seq);
@@ -1420,10 +1798,13 @@ export async function startBrokerDaemon(opts) {
1420
1798
  task = handleInboundMessage(payload)
1421
1799
  .then((accepted) => {
1422
1800
  if (typeof payload.seq === "number") {
1423
- if (accepted)
1801
+ if (accepted || asyncAcceptedInboundSeqs.has(payload.seq)) {
1424
1802
  blockedInboundSeqs.delete(payload.seq);
1425
- else
1803
+ asyncAcceptedInboundSeqs.delete(payload.seq);
1804
+ }
1805
+ else {
1426
1806
  blockedInboundSeqs.add(payload.seq);
1807
+ }
1427
1808
  return;
1428
1809
  }
1429
1810
  if (accepted)
@@ -1454,6 +1835,7 @@ export async function startBrokerDaemon(opts) {
1454
1835
  await Promise.allSettled([...inboundProcessing]);
1455
1836
  }
1456
1837
  if (unsequencedInboundBlocked || blockedInboundSeqs.size > 0) {
1838
+ deferredDeliveryPendingUpTo = payload.upTo;
1457
1839
  logger.warn("delivery_ack_deferred_until_replay", {
1458
1840
  upTo: payload.upTo,
1459
1841
  count: payload.count,
@@ -1462,6 +1844,7 @@ export async function startBrokerDaemon(opts) {
1462
1844
  });
1463
1845
  return;
1464
1846
  }
1847
+ deferredDeliveryPendingUpTo = undefined;
1465
1848
  connector.ackDelivery(payload.upTo);
1466
1849
  });
1467
1850
  writeDiscoveryFile(paths.discoveryFile, {