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