@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.
- package/dist/broker/broker-daemon.d.ts +1 -0
- package/dist/broker/broker-daemon.d.ts.map +1 -1
- package/dist/broker/broker-daemon.js +821 -115
- package/dist/broker/control-event-dispatcher.d.ts.map +1 -1
- package/dist/broker/control-event-dispatcher.js +34 -16
- package/dist/broker/control-event-types.d.ts +16 -2
- package/dist/broker/control-event-types.d.ts.map +1 -1
- package/dist/broker/endpoint-registry.d.ts +9 -0
- package/dist/broker/endpoint-registry.d.ts.map +1 -1
- package/dist/broker/endpoint-registry.js +4 -0
- package/dist/broker/entry.d.ts.map +1 -1
- package/dist/broker/entry.js +4 -1
- package/dist/broker/ipc-server.d.ts +44 -0
- package/dist/broker/ipc-server.d.ts.map +1 -1
- package/dist/broker/ipc-server.js +12 -0
- package/dist/broker/reconnecting-buffer.d.ts +2 -0
- package/dist/broker/reconnecting-buffer.d.ts.map +1 -1
- package/dist/broker/reconnecting-buffer.js +3 -0
- package/dist/broker/runtime-endpoint-port.d.ts +2 -0
- package/dist/broker/runtime-endpoint-port.d.ts.map +1 -1
- package/dist/broker/runtime-processing-state-event-types.d.ts +30 -0
- package/dist/broker/runtime-processing-state-event-types.d.ts.map +1 -0
- package/dist/broker/runtime-processing-state-event-types.js +1 -0
- package/dist/broker/version-handshake.d.ts +1 -0
- package/dist/broker/version-handshake.d.ts.map +1 -1
- package/dist/broker/version-handshake.js +1 -0
- package/dist/broker-client/broker-client.d.ts +53 -1
- package/dist/broker-client/broker-client.d.ts.map +1 -1
- package/dist/broker-client/broker-client.js +72 -0
- package/dist/broker-client/lazy-spawn.d.ts.map +1 -1
- package/dist/connector-client.d.ts +8 -0
- package/dist/connector-client.d.ts.map +1 -1
- package/dist/connector-client.js +17 -3
- package/dist/runtime-endpoint-client.d.ts +23 -2
- package/dist/runtime-endpoint-client.d.ts.map +1 -1
- package/dist/runtime-endpoint-client.js +36 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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
|
-
|
|
827
|
+
delivery_intent: "external_handoff",
|
|
434
828
|
},
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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:
|
|
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
|
|
566
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
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 {
|