@camstack/system 1.2.218 → 1.2.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/addon-utils.js +2 -2
  4. package/dist/addon-utils.mjs +2 -2
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +2 -2
  28. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/gpu-probe.d.ts +18 -0
  32. package/dist/builtins/native-metrics/native-metrics-provider.d.ts +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +26 -3
  34. package/dist/builtins/native-metrics/native-metrics.addon.js +1320 -1093
  35. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1320 -1096
  36. package/dist/builtins/native-metrics/proc-process-table.d.ts +25 -0
  37. package/dist/builtins/platform-probe/index.js +3 -3
  38. package/dist/builtins/platform-probe/index.mjs +3 -3
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  40. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  41. package/dist/builtins/snapshot/index.js +1 -1
  42. package/dist/builtins/snapshot/index.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  44. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/sqlite-pragmas.d.ts +23 -0
  46. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +1 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  50. package/dist/builtins/sqlite-storage/wal-checkpoint-policy.d.ts +115 -0
  51. package/dist/builtins/sqlite-storage/wal-checkpoint-worker.d.ts +1 -0
  52. package/dist/builtins/sqlite-storage/wal-checkpointer.d.ts +70 -0
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/system-config/system-config.addon.js +1 -1
  56. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  57. package/dist/builtins/winston-logging/index.js +1 -1
  58. package/dist/builtins/winston-logging/index.mjs +1 -1
  59. package/dist/{custom-action-registry-BDwM5qAK.js → custom-action-registry-B42MC_Pv.js} +1 -1
  60. package/dist/{custom-action-registry-CsONUlhC.mjs → custom-action-registry-CpR58DXT.mjs} +1 -1
  61. package/dist/{dist-DigmGsQS.mjs → dist-Ca_jaxXs.mjs} +232 -4
  62. package/dist/{dist-BOlMCOeT.js → dist-CllKpcTd.js} +232 -4
  63. package/dist/index.d.ts +6 -1
  64. package/dist/index.js +99 -65
  65. package/dist/index.mjs +84 -67
  66. package/dist/kernel/gc-census.d.ts +144 -0
  67. package/dist/kernel/heap-watch.d.ts +32 -1
  68. package/dist/kernel/moleculer/mesh-queue-report.d.ts +51 -0
  69. package/dist/kernel/outbound-queue-watch.d.ts +94 -0
  70. package/dist/kernel/socket-plane-report.d.ts +112 -4
  71. package/dist/kernel/transport/cap-action-census.d.ts +218 -0
  72. package/dist/kernel/transport/cap-child-index.d.ts +49 -0
  73. package/dist/kernel/transport/cap-routing-hints.d.ts +21 -0
  74. package/dist/kernel/transport/child-cap-protocol.d.ts +28 -2
  75. package/dist/kernel/transport/frame-codec.d.ts +49 -0
  76. package/dist/kernel/transport/index.d.ts +2 -0
  77. package/dist/kernel/transport/local-child-client.d.ts +7 -0
  78. package/dist/kernel/transport/local-child-registry.d.ts +99 -5
  79. package/dist/kernel/transport/local-transport.d.ts +54 -2
  80. package/dist/kernel/transport/socket-channel.d.ts +40 -1
  81. package/dist/{manifest-system-deps-Bo92lg5u.js → manifest-system-deps-DfpwaXkh.js} +1725 -614
  82. package/dist/{manifest-system-deps-D--wyMD9.mjs → manifest-system-deps-iadzxSBr.mjs} +1850 -841
  83. package/dist/process/proc-stat.d.ts +75 -0
  84. package/dist/process/resource-monitor.d.ts +9 -1
  85. package/dist/{resource-monitor-CdnzxBLP.js → resource-monitor-LVE1BoLs.js} +42 -23
  86. package/dist/{resource-monitor-BWmQ5i-o.mjs → resource-monitor-Vp0bPV3U.mjs} +42 -22
  87. package/dist/{retired-settings-keys-xEAlYnv7.js → retired-settings-keys-7iEsSzmD.js} +1 -1
  88. package/dist/{retired-settings-keys-vK_U-CG2.mjs → retired-settings-keys-DQnfr__p.mjs} +1 -1
  89. package/dist/wal-checkpoint-policy-CAcg63o-.mjs +92 -0
  90. package/dist/wal-checkpoint-policy-CCJZngds.js +127 -0
  91. package/dist/wal-checkpoint-worker.js +138 -0
  92. package/dist/wal-checkpoint-worker.mjs +137 -0
  93. package/package.json +1 -1
  94. package/dist/{event-loop-stall-monitor-DM9OAzy8.js → event-loop-stall-monitor-CAsHdMwP.js} +1 -1
  95. package/dist/{event-loop-stall-monitor-B3CFd9tM.mjs → event-loop-stall-monitor-GPrVvx14.mjs} +1 -1
  96. package/dist/{tls-DOTmtLCW.mjs → tls-2xbgg78V.mjs} +2 -2
  97. package/dist/{tls-BxQlomxd.js → tls-BFs4PzoW.js} +2 -2
@@ -5,12 +5,12 @@ import { ensureBinary, ensureFfmpeg, ensurePython, installPythonPackages, instal
5
5
  import { unlink } from "node:fs/promises";
6
6
  import * as path$1 from "node:path";
7
7
  import { isAbsolute, join } from "node:path";
8
- import { execFile } from "node:child_process";
9
- import { promisify } from "node:util";
10
8
  import * as fs from "node:fs";
11
9
  import { existsSync } from "node:fs";
10
+ import { execFile } from "node:child_process";
12
11
  import * as os from "node:os";
13
12
  import { tmpdir } from "node:os";
13
+ import { promisify } from "node:util";
14
14
  import { createServer } from "node:http";
15
15
  import * as v8 from "node:v8";
16
16
  import { Session } from "node:inspector/promises";
@@ -152,6 +152,166 @@ function formatEventPlane(window) {
152
152
  return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
153
153
  }
154
154
  //#endregion
155
+ //#region src/kernel/transport/cap-action-census.ts
156
+ /** A process that has never read the census. */
157
+ var EMPTY_CAP_ACTION_READING = /* @__PURE__ */ new Map();
158
+ /**
159
+ * Bytes that crossed on a frame carrying no cap envelope.
160
+ *
161
+ * Named rather than omitted so the census's coverage is readable off its own
162
+ * output: `register`, `addon-call`, log frames and the event fan-out all land
163
+ * here. A census that reported only what it could name would look like a
164
+ * complete attribution of whatever share it happened to cover.
165
+ */
166
+ var CAP_ACTION_UNLABELLED = "(unlabelled)";
167
+ /** Bytes of actions past {@link MAX_CAP_ACTIONS_PER_PEER} on one peer. */
168
+ var CAP_ACTION_OVERFLOW_LABEL = "(overflow)";
169
+ /**
170
+ * Distinct named actions kept per peer before the overflow bucket takes over.
171
+ *
172
+ * Sixty-four against a measured worst case of 14 (`pipeline-analytics`, the
173
+ * heaviest peer on this hub, 2026-09-11) — four times the observed shape, so a
174
+ * peer has to change character before it overflows, and a bound that is never
175
+ * reached in practice still forbids the unbounded growth a wire-supplied key
176
+ * makes possible in principle.
177
+ */
178
+ var MAX_CAP_ACTIONS_PER_PEER = 64;
179
+ /**
180
+ * `<cap>.<method>` for a cap envelope, `null` for anything else.
181
+ *
182
+ * Both directions are labelled by the same function: a child's `cap-call-out`
183
+ * and the parent's `cap-call` carry `capName` + `method` in the same two keys,
184
+ * so one predicate covers the whole plane. Anything else — a `register`, an
185
+ * `addon-call`, a log or an event — is deliberately unlabelled; see
186
+ * {@link CAP_ACTION_UNLABELLED} for where its bytes go.
187
+ *
188
+ * Cost: two property reads, two `typeof`s and one concatenation of two short
189
+ * strings, called once per `req` frame (not per frame). Against the
190
+ * `encodeFrame` it sits beside — 1 948–1 975 ns on a 0.33 kB body — it is
191
+ * noise, and unlike `recordSocketFrame` it is not on the `evt` path at all.
192
+ */
193
+ function capActionLabel(body) {
194
+ if (typeof body !== "object" || body === null) return null;
195
+ const env = body;
196
+ if (env.kind !== "cap-call" && env.kind !== "cap-call-out") return null;
197
+ if (typeof env.capName !== "string" || typeof env.method !== "string") return null;
198
+ return `${env.capName}.${env.method}`;
199
+ }
200
+ function emptyCounters() {
201
+ return {
202
+ calls: 0,
203
+ reqBytes: 0,
204
+ resBytes: 0,
205
+ raw: 0,
206
+ decoded: 0
207
+ };
208
+ }
209
+ function createCapActionCensus() {
210
+ const peers = /* @__PURE__ */ new Map();
211
+ /**
212
+ * The per-peer slot for `action`, creating it under the bound.
213
+ *
214
+ * Past {@link MAX_CAP_ACTIONS_PER_PEER} a NEW label is folded into the
215
+ * overflow bucket; labels already present keep their own slot, so a peer
216
+ * that overflows does not lose the attribution it had built.
217
+ */
218
+ const slot = (peerId, action) => {
219
+ let byAction = peers.get(peerId);
220
+ if (byAction === void 0) {
221
+ byAction = /* @__PURE__ */ new Map();
222
+ peers.set(peerId, byAction);
223
+ }
224
+ const existing = byAction.get(action);
225
+ if (existing !== void 0) return existing;
226
+ if (byAction.size >= 64) {
227
+ let overflow = byAction.get(CAP_ACTION_OVERFLOW_LABEL);
228
+ if (overflow === void 0) {
229
+ overflow = emptyCounters();
230
+ byAction.set(CAP_ACTION_OVERFLOW_LABEL, overflow);
231
+ }
232
+ return overflow;
233
+ }
234
+ const fresh = emptyCounters();
235
+ byAction.set(action, fresh);
236
+ return fresh;
237
+ };
238
+ return {
239
+ record(peerId, action, reqBytes, resBytes) {
240
+ const c = slot(peerId, action);
241
+ c.calls += 1;
242
+ c.reqBytes += reqBytes;
243
+ c.resBytes += resBytes;
244
+ },
245
+ recordForward(peerId, action, kind) {
246
+ const c = slot(peerId, action);
247
+ if (kind === "raw") c.raw += 1;
248
+ else c.decoded += 1;
249
+ },
250
+ recordUnlabelled(peerId, bytes) {
251
+ const c = slot(peerId, CAP_ACTION_UNLABELLED);
252
+ c.resBytes += bytes;
253
+ },
254
+ forget(peerId) {
255
+ peers.delete(peerId);
256
+ },
257
+ read() {
258
+ const out = /* @__PURE__ */ new Map();
259
+ for (const [peerId, byAction] of peers) {
260
+ const copy = /* @__PURE__ */ new Map();
261
+ for (const [action, c] of byAction) copy.set(action, {
262
+ calls: c.calls,
263
+ reqBytes: c.reqBytes,
264
+ resBytes: c.resBytes,
265
+ raw: c.raw,
266
+ decoded: c.decoded
267
+ });
268
+ out.set(peerId, copy);
269
+ }
270
+ return out;
271
+ }
272
+ };
273
+ }
274
+ function sampleOf(reading, peerId, action) {
275
+ return reading.get(peerId)?.get(action);
276
+ }
277
+ /**
278
+ * Subtract the previous reading from the current one.
279
+ *
280
+ * A peer whose runner RESPAWNED reconnects under the same id with counters
281
+ * that went backwards. Clamped at 0, exactly as `diffSocketPlane` clamps for
282
+ * the same reason: its traffic in that window is genuinely unknown, and a
283
+ * negative number on a rate reads as a broken meter.
284
+ */
285
+ function diffCapActionCensus(before, current, topN) {
286
+ const deltas = [];
287
+ let labelledBytes = 0;
288
+ let totalBytes = 0;
289
+ for (const [peerId, byAction] of current) for (const [action, now] of byAction) {
290
+ const then = sampleOf(before, peerId, action);
291
+ const reqBytes = Math.max(0, now.reqBytes - (then?.reqBytes ?? 0));
292
+ const resBytes = Math.max(0, now.resBytes - (then?.resBytes ?? 0));
293
+ const bytes = reqBytes + resBytes;
294
+ totalBytes += bytes;
295
+ if (action === "(unlabelled)" || action === "(overflow)") continue;
296
+ labelledBytes += bytes;
297
+ if (bytes === 0) continue;
298
+ deltas.push({
299
+ peerId,
300
+ action,
301
+ calls: Math.max(0, now.calls - (then?.calls ?? 0)),
302
+ reqBytes,
303
+ resBytes,
304
+ raw: Math.max(0, now.raw - (then?.raw ?? 0)),
305
+ decoded: Math.max(0, now.decoded - (then?.decoded ?? 0))
306
+ });
307
+ }
308
+ return {
309
+ top: deltas.toSorted((a, b) => b.reqBytes + b.resBytes - (a.reqBytes + a.resBytes) || a.peerId.localeCompare(b.peerId) || a.action.localeCompare(b.action)).slice(0, topN),
310
+ labelledBytes,
311
+ totalBytes
312
+ };
313
+ }
314
+ //#endregion
155
315
  //#region src/kernel/transport/socket-traffic.ts
156
316
  function createSocketDirectionCounters() {
157
317
  return {
@@ -308,12 +468,17 @@ function createSocketPlaneReader(sources) {
308
468
  peers.push({
309
469
  peerId: child.childId,
310
470
  tx: traffic.tx,
311
- rx: traffic.rx
471
+ rx: traffic.rx,
472
+ queuedBytes: registry.getChildQueuedBytes(child.childId)
312
473
  });
313
474
  }
475
+ const forward = registry.readForwardCensus?.();
476
+ const actions = registry.readActionCensus?.();
314
477
  return {
315
478
  peers,
316
- peersConnected: listed.length
479
+ peersConnected: listed.length,
480
+ ...forward === void 0 ? {} : { forward },
481
+ ...actions === void 0 ? {} : { actions }
317
482
  };
318
483
  } catch {
319
484
  return;
@@ -331,8 +496,41 @@ function createSocketPlaneReader(sources) {
331
496
  * stops being read.
332
497
  */
333
498
  var SOCKET_PLANE_TOP_N = 5;
499
+ /** How many queue-holding peers get named. Three: a queue is an exception,
500
+ * and the line already carries five names for traffic. */
501
+ var SOCKET_QUEUE_TOP_N = 3;
502
+ /** The queue reading over the current counters — a level, so no baseline. */
503
+ function readSocketQueues(current) {
504
+ let totalBytes = 0;
505
+ let unmeasured = 0;
506
+ const holding = [];
507
+ for (const peer of current.peers) {
508
+ if (peer.queuedBytes === null) {
509
+ unmeasured += 1;
510
+ continue;
511
+ }
512
+ totalBytes += peer.queuedBytes;
513
+ if (peer.queuedBytes > 0) holding.push({
514
+ peerId: peer.peerId,
515
+ bytes: peer.queuedBytes
516
+ });
517
+ }
518
+ return {
519
+ totalBytes,
520
+ unmeasured,
521
+ top: holding.toSorted((a, b) => b.bytes - a.bytes || a.peerId.localeCompare(b.peerId)).slice(0, 3)
522
+ };
523
+ }
334
524
  /** A process that has never read the plane. */
335
525
  var EMPTY_SOCKET_PLANE_BASELINE = { peers: /* @__PURE__ */ new Map() };
526
+ /** The census over the window; a counter that went backwards reads as quiet. */
527
+ function diffForward(current, before) {
528
+ if (current === void 0) return void 0;
529
+ return {
530
+ raw: Math.max(0, current.raw - (before?.raw ?? 0)),
531
+ decoded: Math.max(0, current.decoded - (before?.decoded ?? 0))
532
+ };
533
+ }
336
534
  function diffDirection(current, before) {
337
535
  const previous = before ?? EMPTY_SOCKET_DIRECTION;
338
536
  return {
@@ -358,6 +556,14 @@ function addDirection(into, add) {
358
556
  function peerMessages(delta) {
359
557
  return socketDirectionMessages(delta.tx) + socketDirectionMessages(delta.rx);
360
558
  }
559
+ /**
560
+ * Total bytes a peer moved in the window, both directions — the OTHER
561
+ * ranking, and the one that names a peer the message ranking structurally
562
+ * cannot see (D454).
563
+ */
564
+ function peerBytes(delta) {
565
+ return socketDirectionBytes(delta.tx) + socketDirectionBytes(delta.rx);
566
+ }
361
567
  /** Subtract the previous cumulative reading from the current one. */
362
568
  function diffSocketPlane(baseline, current, topN = 5) {
363
569
  const deltas = [];
@@ -374,12 +580,18 @@ function diffSocketPlane(baseline, current, topN = 5) {
374
580
  rx = addDirection(rx, delta.rx);
375
581
  if (peerMessages(delta) > 0) deltas.push(delta);
376
582
  }
583
+ const forward = diffForward(current.forward, baseline.forward);
584
+ const actions = current.actions === void 0 ? void 0 : diffCapActionCensus(baseline.actions ?? EMPTY_CAP_ACTION_READING, current.actions, 5);
377
585
  return {
378
586
  peersConnected: current.peersConnected,
379
587
  peersMeasured: current.peers.length,
380
588
  tx,
381
589
  rx,
382
- top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN)
590
+ top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
591
+ topBytes: deltas.toSorted((a, b) => peerBytes(b) - peerBytes(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
592
+ queued: readSocketQueues(current),
593
+ ...forward === void 0 ? {} : { forward },
594
+ ...actions === void 0 ? {} : { actions }
383
595
  };
384
596
  }
385
597
  function createSocketPlaneMeter(reader, topN = 5) {
@@ -388,24 +600,62 @@ function createSocketPlaneMeter(reader, topN = 5) {
388
600
  const current = reader();
389
601
  if (current === void 0) return void 0;
390
602
  const window = diffSocketPlane(baseline, current, topN);
391
- baseline = { peers: new Map(current.peers.map((p) => [p.peerId, p])) };
603
+ baseline = {
604
+ peers: new Map(current.peers.map((p) => [p.peerId, p])),
605
+ ...current.forward === void 0 ? {} : { forward: current.forward },
606
+ ...current.actions === void 0 ? {} : { actions: current.actions }
607
+ };
392
608
  return window;
393
609
  } };
394
610
  }
395
611
  /** Bytes → whole kB. The plane moves 180–330 MB/min; raw bytes would put
396
612
  * nine-digit numbers on a line that has to stay readable. */
397
- function kb(bytes) {
613
+ function kb$1(bytes) {
398
614
  return Math.round(bytes / 1024);
399
615
  }
400
616
  /** `req:<msgs>/<kB>,res:<msgs>/<kB>,evt:<msgs>/<kB>` — every kind, always,
401
617
  * including at zero: a kind that went quiet is a reading, not a blank. */
402
618
  function formatDirection(sample) {
403
- return `req:${sample.reqMessages}/${kb(sample.reqBytes)}kB,res:${sample.resMessages}/${kb(sample.resBytes)}kB,evt:${sample.evtMessages}/${kb(sample.evtBytes)}kB`;
619
+ return `req:${sample.reqMessages}/${kb$1(sample.reqBytes)}kB,res:${sample.resMessages}/${kb$1(sample.resBytes)}kB,evt:${sample.evtMessages}/${kb$1(sample.evtBytes)}kB`;
620
+ }
621
+ /** `<req>/<res>/<evt>` in kB — the byte triple, in the same order as the
622
+ * message triple next to it, so the two read as one table. */
623
+ function formatPeerBytes(sample) {
624
+ return `${kb$1(sample.reqBytes)}/${kb$1(sample.resBytes)}/${kb$1(sample.evtBytes)}`;
404
625
  }
405
- /** `<peer>:tx=<req>/<res>/<evt>:rx=<req>/<res>/<evt>:kB=<tx>/<rx>` — the whole
406
- * peer×kind cross for one peer, which is the question in one token. */
626
+ /**
627
+ * `<peer>:tx=<req>/<res>/<evt>:rx=…:txkB=<req>/<res>/<evt>:rxkB=…` — the whole
628
+ * peer×kind cross for one peer, messages and bytes, which is the question in
629
+ * one token.
630
+ *
631
+ * The bytes were a per-direction TOTAL until D454, and the total could not
632
+ * answer the question `socket-traffic.ts` says this instrument exists to
633
+ * settle: whether a peer's bytes are RESPONSES (media, and D9/D18 re-opens) or
634
+ * fan-out (`evt`, which scales with runner count and nothing an operator
635
+ * asked for). On 2026-09-11 hub-main wrote 118 MB in one minute to
636
+ * `hub/pipeline-analytics` and the line could only say "118 MB" — separating
637
+ * `res` from `evt` took an hour of arithmetic against the plane-wide totals.
638
+ */
407
639
  function formatPeer(delta) {
408
- return `${delta.peerId}:tx=${delta.tx.reqMessages}/${delta.tx.resMessages}/${delta.tx.evtMessages}:rx=${delta.rx.reqMessages}/${delta.rx.resMessages}/${delta.rx.evtMessages}:kB=${kb(socketDirectionBytes(delta.tx))}/${kb(socketDirectionBytes(delta.rx))}`;
640
+ return `${delta.peerId}:tx=${delta.tx.reqMessages}/${delta.tx.resMessages}/${delta.tx.evtMessages}:rx=${delta.rx.reqMessages}/${delta.rx.resMessages}/${delta.rx.evtMessages}:txkB=${formatPeerBytes(delta.tx)}:rxkB=${formatPeerBytes(delta.rx)}`;
641
+ }
642
+ /**
643
+ * One named action: `<peer>!<cap>.<method>:<kB>kB/<calls>:r<raw>/d<decoded>`.
644
+ *
645
+ * `!` separates the peer from the action because `.` already separates cap
646
+ * from method and a single delimiter for both would make the token ambiguous
647
+ * to split.
648
+ *
649
+ * The raw/decoded pair is the half that did not exist anywhere before D456.
650
+ * `socketFwd=` answers it for the whole plane — 30 349/5 051 on this hub — and
651
+ * a plane-wide ratio cannot tell a method that is entirely forwarded from one
652
+ * that is entirely decoded, which is the difference between bytes hub-main
653
+ * copies and bytes hub-main reads. The census pays for the two counters it
654
+ * takes; the conclusion they buy is which of the line's megabytes are actually
655
+ * expensive.
656
+ */
657
+ function formatAction(entry) {
658
+ return `${entry.peerId}!${entry.action}:${kb$1(entry.reqBytes + entry.resBytes)}kB/${entry.calls}:r${entry.raw}/d${entry.decoded}`;
409
659
  }
410
660
  /**
411
661
  * The suffix appended to a `[mem]` line — leading space included, empty when
@@ -418,14 +668,234 @@ function formatPeer(delta) {
418
668
  * is the only way to see an unmeasured channel, and the per-kind splits because
419
669
  * "the plane went quiet for a minute" is a reading.
420
670
  *
421
- * `socketTop` is the one conditional field — a ranked list of nothing is noise —
422
- * and it is a single comma-separated value so the field COUNT does not change
423
- * from line to line.
671
+ * `socketTop` is a conditional field — a ranked list of nothing is noise — and
672
+ * it is a single comma-separated value so the field COUNT does not change from
673
+ * line to line.
674
+ *
675
+ * `socketTopBytes` is the other one, and its PRESENCE is the reading: it is
676
+ * printed only when ranking by bytes names a peer that ranking by messages
677
+ * does not. A line without it says the two rankings agree; a line with it says
678
+ * a peer is moving few, heavy frames and names it (D454).
424
679
  */
425
680
  function formatSocketPlane(window) {
426
681
  if (window === void 0) return "";
427
682
  const top = window.top.map(formatPeer).join(",");
428
- return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`);
683
+ const named = new Set(window.top.map((p) => p.peerId));
684
+ const topBytes = window.topBytes.filter((p) => !named.has(p.peerId)).map(formatPeer).join(",");
685
+ const queuedTop = window.queued.top.map((p) => `${p.peerId}:${kb$1(p.bytes)}`).join(",");
686
+ return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`) + (topBytes.length === 0 ? "" : ` socketTopBytes=${topBytes}`) + ` socketQueued=${kb$1(window.queued.totalBytes)}kB` + (window.queued.unmeasured === 0 ? "" : ` socketQueuedUnmeasured=${window.queued.unmeasured}`) + (queuedTop.length === 0 ? "" : ` socketQueuedTop=${queuedTop}`) + (window.forward === void 0 ? "" : ` socketFwd=${window.forward.raw}/${window.forward.decoded}`) + (window.actions === void 0 ? "" : ` socketActions=${kb$1(window.actions.labelledBytes)}/${kb$1(window.actions.totalBytes)}kB` + (window.actions.top.length === 0 ? "" : ` socketTopActions=${window.actions.top.map(formatAction).join(",")}`));
687
+ }
688
+ //#endregion
689
+ //#region src/kernel/moleculer/mesh-queue-report.ts
690
+ function isRecord$1(value) {
691
+ return typeof value === "object" && value !== null;
692
+ }
693
+ /** The shape `TcpWriter` keeps its sockets in. Anything else is not a mesh. */
694
+ function writerSocketsOf(broker) {
695
+ if (!isRecord$1(broker)) return void 0;
696
+ const transit = broker["transit"];
697
+ if (!isRecord$1(transit)) return void 0;
698
+ const transporter = transit["tx"];
699
+ if (!isRecord$1(transporter)) return void 0;
700
+ const writer = transporter["writer"];
701
+ if (!isRecord$1(writer)) return void 0;
702
+ const sockets = writer["sockets"];
703
+ return sockets instanceof Map ? sockets : void 0;
704
+ }
705
+ function readSocket(nodeId, socket) {
706
+ if (typeof nodeId !== "string" || !isRecord$1(socket)) return void 0;
707
+ const queuedBytes = socket["writableLength"];
708
+ const bytesWritten = socket["bytesWritten"];
709
+ if (typeof queuedBytes !== "number" || typeof bytesWritten !== "number") return void 0;
710
+ return {
711
+ nodeId,
712
+ queuedBytes,
713
+ bytesWritten
714
+ };
715
+ }
716
+ /**
717
+ * Build a reader over a lazily-resolved broker. Wrapped: a broker mid-teardown
718
+ * may throw, and a diagnostic that can crash the process it watches is worse
719
+ * than none.
720
+ */
721
+ function createMeshQueueReader(getBroker) {
722
+ return () => {
723
+ try {
724
+ const sockets = writerSocketsOf(getBroker());
725
+ if (sockets === void 0) return void 0;
726
+ const peers = [];
727
+ for (const [nodeId, socket] of sockets) {
728
+ const peer = readSocket(nodeId, socket);
729
+ if (peer !== void 0) peers.push(peer);
730
+ }
731
+ return peers;
732
+ } catch {
733
+ return;
734
+ }
735
+ };
736
+ }
737
+ /** At most this many nodes named. A cluster has a handful. */
738
+ var MESH_QUEUE_TOP_N = 3;
739
+ function kb(bytes) {
740
+ return Math.round(bytes / 1024);
741
+ }
742
+ /**
743
+ * The suffix appended to a `[mem]` line — leading space included, empty when
744
+ * this process has no mesh writer. `meshQueued` is printed always, at zero
745
+ * included: a quiet mesh is a reading. `meshQueuedTop` names only nodes with
746
+ * a queue, largest first — a ranked list of zeros is noise.
747
+ */
748
+ function formatMeshQueue(peers) {
749
+ if (peers === void 0) return "";
750
+ const total = peers.reduce((sum, p) => sum + p.queuedBytes, 0);
751
+ const top = peers.filter((p) => p.queuedBytes > 0).toSorted((a, b) => b.queuedBytes - a.queuedBytes || a.nodeId.localeCompare(b.nodeId)).slice(0, 3).map((p) => `${p.nodeId}:${kb(p.queuedBytes)}`).join(",");
752
+ return ` meshQueued=${kb(total)}kB` + (top.length === 0 ? "" : ` meshQueuedTop=${top}`);
753
+ }
754
+ //#endregion
755
+ //#region src/kernel/outbound-queue-watch.ts
756
+ /**
757
+ * outbound-queue-watch — an outbound socket queue that HOLDS is named, and its
758
+ * release is timestamped (D446).
759
+ *
760
+ * ## The blind spot this closes
761
+ *
762
+ * On 2026-09-10 hub-main's `arrayBuffers` climbed twice at a constant rate
763
+ * (20 MB/min to 1 264 MB; 63 MB/min for 77 min to 4 968 MB), LIVE across a
764
+ * forced compaction, with ~4 % of JS heap alongside, and each time released in
765
+ * one instant with no line from any owner. The investigation excluded "every
766
+ * socket write queue" by summing `writableLength` over
767
+ * `process._getActiveHandles()` — 84 MB, all of it the phone's WebSocket.
768
+ *
769
+ * `_getActiveHandles()` lists only handles that keep the loop alive. An
770
+ * UNREF'D socket is not one of them, and Moleculer's `TcpWriter.connect`
771
+ * unrefs every socket it opens to a peer node (`transporters/tcp/tcp-writer.js`,
772
+ * `socket.unref()`). So the one outbound plane that writes to another MACHINE
773
+ * — where a reader that stops draining is the ordinary failure — was never in
774
+ * the sum. The UDS children were in it, but only as a total, at one instant,
775
+ * by hand.
776
+ *
777
+ * ## What this is
778
+ *
779
+ * A pure gate over per-peer queue depths the heartbeat reads on every probe:
780
+ * the UDS children's `socket.writableLength` (`SocketChannel.queuedBytes`) and
781
+ * the mesh writer sockets' (`mesh-queue-report.ts`). It says three things and
782
+ * nothing else:
783
+ *
784
+ * - a peer whose queue has been at or above {@link OUTBOUND_QUEUE_THRESHOLD_BYTES}
785
+ * for {@link OUTBOUND_QUEUE_SUSTAIN_MS} is NAMED, with its current and peak
786
+ * depth and how long it has held — at most once per
787
+ * {@link OUTBOUND_QUEUE_COOLDOWN_MS};
788
+ * - a named peer whose queue then falls under half the threshold, or that
789
+ * vanishes from the readings (its socket died — which is how a queue is
790
+ * dropped in one instant), is reported as RELEASED with its peak and the
791
+ * hold, so the release the episodes never logged has a line and a peer;
792
+ * - a burst that drains inside the sustain window says nothing. Backpressure
793
+ * measures LIVENESS, not identity (D444): a peer momentarily behind is
794
+ * coming back.
795
+ *
796
+ * ## What it is NOT
797
+ *
798
+ * Not a bound. It terminates nothing: a UDS child whose channel is destroyed
799
+ * loses every in-flight call, and a mesh socket Moleculer reconnects on its
800
+ * own terms. The WS guard (`ws-slow-consumer-guard.ts`) bounds the one plane
801
+ * whose client is expendable. Here the line is the deliverable; what to do
802
+ * about a peer that holds is decided with the peer named, not before.
803
+ */
804
+ /** 64 MiB. Below this a queue is a burst on a busy peer (the orchestrator's
805
+ * channel moves ~1.5 MB/s of audio responses); above it, and held, it is
806
+ * retention with a name. */
807
+ var OUTBOUND_QUEUE_THRESHOLD_BYTES = 64 * 1024 * 1024;
808
+ /** Matches the ArrayBuffer census sustain: a minute above the bound. */
809
+ var OUTBOUND_QUEUE_SUSTAIN_MS = 6e4;
810
+ /** A held queue is re-named every ten minutes, not every probe. */
811
+ var OUTBOUND_QUEUE_COOLDOWN_MS = 10 * 6e4;
812
+ /** A release is a fall under this fraction of the threshold. */
813
+ var RELEASE_RATIO = .5;
814
+ /**
815
+ * The pure decision — threshold, sustain, cooldown, release, gone — testable
816
+ * without a socket.
817
+ */
818
+ function createOutboundQueueGate(options = {}) {
819
+ const thresholdBytes = options.thresholdBytes ?? 67108864;
820
+ const sustainMs = options.sustainMs ?? 6e4;
821
+ const cooldownMs = options.cooldownMs ?? 6e5;
822
+ const tracked = /* @__PURE__ */ new Map();
823
+ const judgeOne = (peerId, bytes, at) => {
824
+ const state = tracked.get(peerId);
825
+ if (bytes >= thresholdBytes) {
826
+ const next = state === void 0 ? {
827
+ aboveSince: at,
828
+ peakBytes: bytes,
829
+ lastNamedAt: null
830
+ } : {
831
+ ...state,
832
+ peakBytes: Math.max(state.peakBytes, bytes)
833
+ };
834
+ const heldForMs = at - next.aboveSince;
835
+ const due = heldForMs >= sustainMs && (next.lastNamedAt === null || at - next.lastNamedAt >= cooldownMs);
836
+ tracked.set(peerId, due ? {
837
+ ...next,
838
+ lastNamedAt: at
839
+ } : next);
840
+ return due ? {
841
+ kind: "sustained",
842
+ peerId,
843
+ bytes,
844
+ peakBytes: next.peakBytes,
845
+ heldForMs
846
+ } : void 0;
847
+ }
848
+ if (state === void 0) return void 0;
849
+ if (bytes >= thresholdBytes * RELEASE_RATIO) return void 0;
850
+ tracked.delete(peerId);
851
+ if (state.lastNamedAt === null) return void 0;
852
+ return {
853
+ kind: "released",
854
+ peerId,
855
+ peakBytes: state.peakBytes,
856
+ toBytes: bytes,
857
+ heldForMs: at - state.aboveSince,
858
+ gone: false
859
+ };
860
+ };
861
+ return { observe(readings, at) {
862
+ const verdicts = [];
863
+ const seen = /* @__PURE__ */ new Set();
864
+ for (const reading of readings) {
865
+ seen.add(reading.peerId);
866
+ const verdict = judgeOne(reading.peerId, reading.bytes, at);
867
+ if (verdict !== void 0) verdicts.push(verdict);
868
+ }
869
+ for (const [peerId, state] of tracked) {
870
+ if (seen.has(peerId)) continue;
871
+ tracked.delete(peerId);
872
+ if (state.lastNamedAt === null) continue;
873
+ verdicts.push({
874
+ kind: "released",
875
+ peerId,
876
+ peakBytes: state.peakBytes,
877
+ toBytes: 0,
878
+ heldForMs: at - state.aboveSince,
879
+ gone: true
880
+ });
881
+ }
882
+ return verdicts;
883
+ } };
884
+ }
885
+ var BYTES_PER_MB$1 = 1048576;
886
+ function mb$2(bytes) {
887
+ return Math.round(bytes / BYTES_PER_MB$1);
888
+ }
889
+ /** `60s` under two minutes, `77m` from there — the hold is read at a glance. */
890
+ function duration(ms) {
891
+ const seconds = Math.round(ms / 1e3);
892
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)}m`;
893
+ }
894
+ /** The line for one verdict. Both carry the peer, the bytes and the hold;
895
+ * the sustained one also says why the older census could not see it. */
896
+ function formatOutboundQueueVerdict(label, verdict) {
897
+ if (verdict.kind === "sustained") return `[mem] ${label} outbound queue to ${verdict.peerId} has held ${mb$2(verdict.bytes)}MB (peak ${mb$2(verdict.peakBytes)}MB) for ${duration(verdict.heldForMs)} — the peer is not draining what this process writes to it. This is socket.writableLength on the channel itself; a sum over _getActiveHandles() does not list an unref'd socket and would miss it.`;
898
+ return `[mem] ${label} outbound queue to ${verdict.peerId} RELEASED — ${mb$2(verdict.peakBytes)}MB at peak, ${mb$2(verdict.toBytes)}MB now, held ${duration(verdict.heldForMs)}` + (verdict.gone ? " (peer gone: the socket closed and dropped its queue)" : " (drained)");
429
899
  }
430
900
  //#endregion
431
901
  //#region src/kernel/heap-spaces.ts
@@ -849,6 +1319,125 @@ function formatArrayBufferCensus(label, report, context) {
849
1319
  return `[mem] ${label} arrayBuffer census: live=${live}MB in ${report.count} ArrayBuffers (raw arrayBuffers=${context.rawMb}MB at the trigger → ${verdict}) top sizes: ${report.topSizes.map(formatExact).join(", ") || "(none)"}; buckets: ${report.buckets.map(formatBucket).join(", ") || "(none)"}; detached=${report.detached} took=${context.tookMs}ms — cost: ONE forced full GC (the same pause as a reclaim pass), walked in memory, nothing serialised, nothing written; trigger: ${context.reason}`;
850
1320
  }
851
1321
  //#endregion
1322
+ //#region src/kernel/gc-census.ts
1323
+ function countsOf(entries, kinds) {
1324
+ let majors = 0;
1325
+ let minors = 0;
1326
+ let incrementals = 0;
1327
+ let pauseMs = 0;
1328
+ for (const e of entries) {
1329
+ if (e.kind === kinds.major) majors++;
1330
+ else if (e.kind === kinds.minor) minors++;
1331
+ else if (e.kind === kinds.incremental) incrementals++;
1332
+ pauseMs += e.durationMs;
1333
+ }
1334
+ return {
1335
+ majors,
1336
+ minors,
1337
+ incrementals,
1338
+ pauseMs,
1339
+ windowMs: 0
1340
+ };
1341
+ }
1342
+ /**
1343
+ * Build a census over an injected observer.
1344
+ *
1345
+ * Returns `undefined` when the observer cannot be installed — the caller then
1346
+ * prints nothing, which is the honest reading. It must never degrade into a
1347
+ * census that reports zero collections: a process whose GC is not being
1348
+ * observed and a process that is not collecting look identical on the line, and
1349
+ * they are the two answers furthest apart.
1350
+ */
1351
+ function createGcCensus(install, kinds, startedAt) {
1352
+ let pending = [];
1353
+ const stopObserving = install((entry) => {
1354
+ pending.push(entry);
1355
+ });
1356
+ if (stopObserving === void 0) return void 0;
1357
+ let windowStart = startedAt;
1358
+ return {
1359
+ read: (at) => {
1360
+ const windowMs = at - windowStart;
1361
+ if (windowMs < 1e3) return null;
1362
+ const taken = pending;
1363
+ pending = [];
1364
+ windowStart = at;
1365
+ return {
1366
+ ...countsOf(taken, kinds),
1367
+ windowMs
1368
+ };
1369
+ },
1370
+ stop: () => {
1371
+ stopObserving();
1372
+ pending = [];
1373
+ }
1374
+ };
1375
+ }
1376
+ /**
1377
+ * The observer this process actually gets.
1378
+ *
1379
+ * Everything that can fail is inside the try: a runtime without
1380
+ * `perf_hooks`, without the `gc` entry type, or without the kind constants
1381
+ * yields `undefined` from `install`, and {@link createGcCensus} then declines
1382
+ * to exist rather than reporting zeros.
1383
+ */
1384
+ function nodeGcObserver() {
1385
+ return {
1386
+ kinds: readKinds({
1387
+ major: 4,
1388
+ minor: 1,
1389
+ incremental: 8
1390
+ }),
1391
+ install: (observe) => {
1392
+ try {
1393
+ const observer = new (process.getBuiltinModule("node:perf_hooks")).PerformanceObserver((list) => {
1394
+ for (const entry of list.getEntries()) {
1395
+ const detail = "detail" in entry ? entry.detail : void 0;
1396
+ observe({
1397
+ kind: typeof detail === "object" && detail !== null && "kind" in detail && typeof detail.kind === "number" ? detail.kind : -1,
1398
+ durationMs: entry.duration
1399
+ });
1400
+ }
1401
+ });
1402
+ observer.observe({ entryTypes: ["gc"] });
1403
+ return () => {
1404
+ observer.disconnect();
1405
+ };
1406
+ } catch {
1407
+ return;
1408
+ }
1409
+ }
1410
+ };
1411
+ }
1412
+ function readKinds(fallback) {
1413
+ try {
1414
+ const c = process.getBuiltinModule("node:perf_hooks").constants;
1415
+ return {
1416
+ major: c.NODE_PERFORMANCE_GC_MAJOR,
1417
+ minor: c.NODE_PERFORMANCE_GC_MINOR,
1418
+ incremental: c.NODE_PERFORMANCE_GC_INCREMENTAL
1419
+ };
1420
+ } catch {
1421
+ return fallback;
1422
+ }
1423
+ }
1424
+ /**
1425
+ * The two fields, APPENDED LAST on the `[mem]` line.
1426
+ *
1427
+ * D446's rule: no existing field moves, so every Loki prefix written since
1428
+ * 2026-08-29 still matches. `gc=` prints even when nothing was collected — a
1429
+ * window with no collections is a reading, and it is the reading that says a
1430
+ * process is idle rather than unobserved. `gcPauseMs=` is the MAIN-THREAD
1431
+ * pause only; the concurrent marking that cost hub-main 70 % of its CPU is on
1432
+ * the platform workers and no in-process counter sees it, which is why the
1433
+ * field is named for the thread it describes (D456, part 4).
1434
+ */
1435
+ function formatGcCensus(window) {
1436
+ if (window === null || window === void 0) return "";
1437
+ const perMin = (n) => window.windowMs <= 0 ? "?" : (n / (window.windowMs / 6e4)).toFixed(1);
1438
+ return ` gc=maj${window.majors}/min${window.minors}/inc${window.incrementals}@${Math.round(window.windowMs / 1e3)}s gcMajPerMin=${perMin(window.majors)} gcPauseMs=${Math.round(window.pauseMs)}`;
1439
+ }
1440
+ //#endregion
852
1441
  //#region src/kernel/heap-watch.ts
853
1442
  /**
854
1443
  * heap-watch — hub-main's own memory, on the record, and returned to the OS.
@@ -1413,6 +2002,27 @@ function format(label, s, loop, budgetMb, suffix) {
1413
2002
  return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms${suffix}`;
1414
2003
  }
1415
2004
  /**
2005
+ * Every outbound queue the heartbeat can reach, as the gate wants them: the
2006
+ * UDS children (only those whose channel keeps a reading — an unmeasured
2007
+ * peer is not a zero) and the mesh writer sockets, each prefixed with its
2008
+ * plane.
2009
+ */
2010
+ function outboundQueueReadings(uds, mesh) {
2011
+ const readings = [];
2012
+ for (const peer of uds?.peers ?? []) {
2013
+ if (peer.queuedBytes === null) continue;
2014
+ readings.push({
2015
+ peerId: `uds:${peer.peerId}`,
2016
+ bytes: peer.queuedBytes
2017
+ });
2018
+ }
2019
+ for (const node of mesh ?? []) readings.push({
2020
+ peerId: `mesh:${node.nodeId}`,
2021
+ bytes: node.queuedBytes
2022
+ });
2023
+ return readings;
2024
+ }
2025
+ /**
1416
2026
  * Start the heartbeat. Returns a stop function.
1417
2027
  *
1418
2028
  * `unref`ed so it can never hold the process open, and every read is wrapped:
@@ -1424,6 +2034,8 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1424
2034
  const socketPlane = probes?.socketPlane === void 0 ? void 0 : createSocketPlaneMeter(probes.socketPlane);
1425
2035
  const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
1426
2036
  const now = reclaimOptions?.now ?? (() => Date.now());
2037
+ const gcObserver = probes?.gcObserver ?? nodeGcObserver();
2038
+ const gcCensus = createGcCensus(gcObserver.install, gcObserver.kinds, now());
1427
2039
  const triggerMb = reclaimOptions?.triggerMb ?? 1024;
1428
2040
  const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
1429
2041
  const steadyStateIntervalMs = reclaimOptions?.steadyStateIntervalMs ?? 6e5;
@@ -1526,7 +2138,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1526
2138
  const due = at - lastLoggedAt >= intervalMs;
1527
2139
  if (mode === "escalated" || due) {
1528
2140
  lastLoggedAt = at;
1529
- const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()));
2141
+ const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()) + formatGcCensus(gcCensus?.read(at)));
1530
2142
  if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
1531
2143
  else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
1532
2144
  else sink.info(line);
@@ -1535,8 +2147,17 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1535
2147
  checkRssBudget(sample, at);
1536
2148
  maybeReclaim(sample);
1537
2149
  judgeArrayBuffers(sample, at);
2150
+ judgeOutboundQueues(at);
1538
2151
  } catch {}
1539
2152
  };
2153
+ const meshQueue = probes?.meshQueue;
2154
+ const queueReader = probes?.socketPlane;
2155
+ const queueGate = queueReader === void 0 && meshQueue === void 0 ? void 0 : createOutboundQueueGate(probes?.outboundQueue);
2156
+ const judgeOutboundQueues = (at) => {
2157
+ if (queueGate === void 0) return;
2158
+ const readings = outboundQueueReadings(queueReader?.(), meshQueue?.());
2159
+ for (const verdict of queueGate.observe(readings, at)) sink.warn(formatOutboundQueueVerdict(label, verdict));
2160
+ };
1540
2161
  const censusProbe = probes?.arrayBufferCensus;
1541
2162
  const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
1542
2163
  let censusInFlight = false;
@@ -1574,6 +2195,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1574
2195
  stopped = true;
1575
2196
  clearInterval(timer);
1576
2197
  loopDelay?.stop();
2198
+ gcCensus?.stop();
1577
2199
  };
1578
2200
  }
1579
2201
  /**
@@ -5333,6 +5955,74 @@ function encodeFrame(value) {
5333
5955
  Buffer.from(body.buffer, body.byteOffset, body.byteLength).copy(out, 4);
5334
5956
  return out;
5335
5957
  }
5958
+ /** msgpack `fixstr` of {@link PAYLOAD_KEY}: `0xa0 | length`, then the bytes. */
5959
+ var PAYLOAD_KEY_BYTES = Buffer.from([162, ...Buffer.from("pl")]);
5960
+ /** msgpack `bin 32` marker: one byte, then a 4-byte big-endian length. */
5961
+ var BIN32_MARKER = 198;
5962
+ /** msgpack `fixmap` range: `0x80 | count`, count in `0..15`. */
5963
+ var FIXMAP_BASE = 128;
5964
+ var FIXMAP_MAX_COUNT = 15;
5965
+ /**
5966
+ * Encode the HEAD of a frame that carries an opaque payload: the length
5967
+ * prefix, the msgpack envelope with one extra key ({@link PAYLOAD_KEY}), and
5968
+ * the `bin 32` header for the payload — everything but the payload bytes
5969
+ * themselves, which the caller writes straight after it.
5970
+ *
5971
+ * ## Why the payload is the frame's TAIL and not a field
5972
+ *
5973
+ * hub-main routes every sibling↔sibling call. Until this existed it decoded
5974
+ * the whole request (args included), built a new message, and re-encoded it
5975
+ * — walking a 13.5 KB PCM chunk or a 2 000-row settings answer twice on the
5976
+ * one thread that serves every tRPC request, and allocating an `Encoder`
5977
+ * buffer that doubles from 2 KiB up to the body's size on every write
5978
+ * (D441: ~27 % of hub-main's busy time in msgpack decode, ~6 % in encode,
5979
+ * and the churn that fed a 29.5 % GC share).
5980
+ *
5981
+ * With the payload appended as the LAST key of the map, the receiver's
5982
+ * ordinary msgpack decode yields it as a zero-copy VIEW of the frame
5983
+ * (`@msgpack/msgpack` decodes `bin` as `subarray`), the router reads only the
5984
+ * envelope, and forwarding is: encode a ~60-byte head, write it, write the
5985
+ * view. The wire stays plain msgpack — a peer that never heard of payloads
5986
+ * decodes `{ …frame, pl: <bytes> }` and can be told, at register, not to be
5987
+ * sent one.
5988
+ *
5989
+ * ## The one hand-written msgpack in this repository
5990
+ *
5991
+ * `@msgpack/msgpack` has no "encode a map, then append a key" API, so the
5992
+ * envelope is encoded normally and its `fixmap` count byte is bumped by one
5993
+ * before the key and the `bin 32` header are appended. That patch is only
5994
+ * valid for a `fixmap` (≤ 15 keys); a `Frame` has at most four, and anything
5995
+ * else is refused rather than silently corrupted.
5996
+ */
5997
+ function encodeFrameHead(frame, payload) {
5998
+ const envelope = encode(frame, ENCODE_OPTIONS);
5999
+ const first = envelope[0];
6000
+ if (envelope.byteLength === 0 || first === void 0 || first < FIXMAP_BASE || first > 142) throw new Error(`encodeFrameHead: envelope must be a msgpack fixmap with at most ${FIXMAP_MAX_COUNT - 1} keys (got leading byte 0x${(first ?? 0).toString(16)})`);
6001
+ const bodyLength = envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5 + payload.byteLength;
6002
+ const head = Buffer.allocUnsafe(HEADER_BYTES + envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5);
6003
+ head.writeUInt32BE(bodyLength, 0);
6004
+ Buffer.from(envelope.buffer, envelope.byteOffset, envelope.byteLength).copy(head, HEADER_BYTES);
6005
+ head[HEADER_BYTES] = first + 1;
6006
+ let at = HEADER_BYTES + envelope.byteLength;
6007
+ PAYLOAD_KEY_BYTES.copy(head, at);
6008
+ at += PAYLOAD_KEY_BYTES.byteLength;
6009
+ head[at] = BIN32_MARKER;
6010
+ head.writeUInt32BE(payload.byteLength, at + 1);
6011
+ return head;
6012
+ }
6013
+ /**
6014
+ * Serialise a value into the bytes a payload carries. The SAME options as the
6015
+ * envelope codec, so `undefined` properties are omitted rather than turned
6016
+ * into `null` — a `z.void()` cap output must read back as `undefined` on the
6017
+ * far side exactly as it did when it travelled inline.
6018
+ */
6019
+ function packPayload(value) {
6020
+ return encode(value, ENCODE_OPTIONS);
6021
+ }
6022
+ /** Inverse of {@link packPayload}. A `bin` inside decodes as a view of `bytes`. */
6023
+ function unpackPayload(bytes) {
6024
+ return decode(bytes);
6025
+ }
5336
6026
  /** Bytes in the big-endian length prefix that opens every frame. */
5337
6027
  var HEADER_BYTES = 4;
5338
6028
  /**
@@ -5491,6 +6181,25 @@ function localEndpointPath(nodeId) {
5491
6181
  if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
5492
6182
  return join(tmpdir(), `camstack-${safe}.sock`);
5493
6183
  }
6184
+ //#endregion
6185
+ //#region src/kernel/transport/local-transport.ts
6186
+ /**
6187
+ * An answer that carries an opaque payload beside its body.
6188
+ *
6189
+ * A CLASS rather than a shape so the channel can tell it from a provider's
6190
+ * ordinary `{ body, payload }` return value by identity, never by duck-typing:
6191
+ * a cap method is free to return an object with those two keys and it must
6192
+ * travel inline like any other. `request()` resolves with one of these only
6193
+ * when the peer's `res` frame carried a payload.
6194
+ */
6195
+ var PayloadReply = class {
6196
+ body;
6197
+ payload;
6198
+ constructor(body, payload) {
6199
+ this.body = body;
6200
+ this.payload = payload;
6201
+ }
6202
+ };
5494
6203
  var SocketChannel = class {
5495
6204
  socket;
5496
6205
  nextId = 1;
@@ -5508,6 +6217,8 @@ var SocketChannel = class {
5508
6217
  txCounters = createSocketDirectionCounters();
5509
6218
  /** Frames read from the peer, cumulative, split by kind. */
5510
6219
  rxCounters = createSocketDirectionCounters();
6220
+ /** See {@link CapActionChannelObserver}. Absent = no per-action census. */
6221
+ actionObserver = null;
5511
6222
  constructor(socket) {
5512
6223
  this.socket = socket;
5513
6224
  socket.on("data", (chunk) => this.onData(chunk));
@@ -5538,15 +6249,25 @@ var SocketChannel = class {
5538
6249
  }, timeoutMs);
5539
6250
  timer.unref?.();
5540
6251
  }
6252
+ const label = this.labelSafely(body);
5541
6253
  this.pending.set(id, {
5542
6254
  resolve,
5543
6255
  reject,
5544
- timer
6256
+ timer,
6257
+ action: null
5545
6258
  });
5546
- this.send({
6259
+ const reqBytes = this.send({
5547
6260
  k: "req",
5548
6261
  id,
5549
6262
  body
6263
+ }, opts?.payload);
6264
+ const entry = this.pending.get(id);
6265
+ if (entry !== void 0 && label !== void 0) this.pending.set(id, {
6266
+ ...entry,
6267
+ action: {
6268
+ label,
6269
+ reqBytes
6270
+ }
5550
6271
  });
5551
6272
  });
5552
6273
  }
@@ -5557,6 +6278,36 @@ var SocketChannel = class {
5557
6278
  body
5558
6279
  });
5559
6280
  }
6281
+ /**
6282
+ * Wire the per-action census. Only one observer is active at a time; an
6283
+ * unwired channel records nothing at all (never zeros).
6284
+ */
6285
+ observeActions(observer) {
6286
+ this.actionObserver = observer;
6287
+ }
6288
+ /**
6289
+ * The label for one envelope, or `undefined` when there is nothing to
6290
+ * record. Wrapped: a broken labeller must never fail the call it watches —
6291
+ * the same discipline `LocalChildRegistry.recordCapUsage` applies, and for
6292
+ * the same reason.
6293
+ */
6294
+ labelSafely(body) {
6295
+ const obs = this.actionObserver;
6296
+ if (obs === null) return void 0;
6297
+ try {
6298
+ return obs.label(body);
6299
+ } catch {
6300
+ return null;
6301
+ }
6302
+ }
6303
+ /** Settle one request/response pair on the census. Never throws. */
6304
+ recordAction(label, reqBytes, resBytes) {
6305
+ const obs = this.actionObserver;
6306
+ if (obs === null) return;
6307
+ try {
6308
+ obs.record(label, reqBytes, resBytes);
6309
+ } catch {}
6310
+ }
5560
6311
  /** Replace the request handler. Only one handler is active at a time. */
5561
6312
  onRequest(handler) {
5562
6313
  this.requestHandler = handler;
@@ -5587,11 +6338,44 @@ var SocketChannel = class {
5587
6338
  rx: sampleSocketDirection(this.rxCounters)
5588
6339
  };
5589
6340
  }
5590
- send(frame) {
5591
- if (this.closed) return;
5592
- const encoded = encodeFrame(frame);
5593
- recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
5594
- this.socket.write(encoded);
6341
+ /**
6342
+ * Bytes handed to the socket that the kernel has not taken yet — the
6343
+ * `Writable` buffer plus the write in flight. Read LIVE, never cached: this
6344
+ * is the number that says whether the peer is draining what we write, and
6345
+ * it is read on every heartbeat probe (D446). An unref'd socket keeps it
6346
+ * exactly like a ref'd one, which is why it is read here and not summed
6347
+ * over `_getActiveHandles()`.
6348
+ */
6349
+ queuedBytes() {
6350
+ return this.socket.writableLength;
6351
+ }
6352
+ /**
6353
+ * Write one frame. With a payload the head and the tail are two `write`
6354
+ * calls under `cork()` — the kernel sees one `writev`, and the payload bytes
6355
+ * are never copied into the head. A forwarded payload is a view into the
6356
+ * frame it arrived on, and that frame's buffer is never mutated, so handing
6357
+ * the view to the socket is safe.
6358
+ *
6359
+ * Returns the on-the-wire bytes it charged — the same number the per-kind
6360
+ * counter got, so the per-action census can never disagree with the per-peer
6361
+ * one. `0` when the channel is closed and nothing was written.
6362
+ */
6363
+ send(frame, payload) {
6364
+ if (this.closed) return 0;
6365
+ if (payload === void 0) {
6366
+ const encoded = encodeFrame(frame);
6367
+ recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
6368
+ this.socket.write(encoded);
6369
+ return encoded.byteLength;
6370
+ }
6371
+ const head = encodeFrameHead(frame, payload);
6372
+ const bytes = head.byteLength + payload.byteLength;
6373
+ recordSocketFrame(this.txCounters, frame.k, bytes);
6374
+ this.socket.cork();
6375
+ this.socket.write(head);
6376
+ this.socket.write(payload);
6377
+ this.socket.uncork();
6378
+ return bytes;
5595
6379
  }
5596
6380
  onData(chunk) {
5597
6381
  const frames = this.decoder.push(chunk);
@@ -5610,22 +6394,31 @@ var SocketChannel = class {
5610
6394
  async handleFrame(frame, wireBytes) {
5611
6395
  recordSocketFrame(this.rxCounters, frame.k, wireBytes);
5612
6396
  if (frame.k === "req") {
6397
+ const label = this.labelSafely(frame.body);
6398
+ let resBytes = 0;
5613
6399
  try {
5614
- const result = await this.requestHandler(frame.body);
5615
- this.send({
6400
+ const result = await this.requestHandler(frame.body, frame.pl);
6401
+ if (result instanceof PayloadReply) resBytes = this.send({
6402
+ k: "res",
6403
+ id: frame.id,
6404
+ ok: true,
6405
+ body: result.body
6406
+ }, result.payload);
6407
+ else resBytes = this.send({
5616
6408
  k: "res",
5617
6409
  id: frame.id,
5618
6410
  ok: true,
5619
6411
  body: result
5620
6412
  });
5621
6413
  } catch (err) {
5622
- this.send({
6414
+ resBytes = this.send({
5623
6415
  k: "res",
5624
6416
  id: frame.id,
5625
6417
  ok: false,
5626
6418
  err: err instanceof Error ? err.message : String(err)
5627
6419
  });
5628
6420
  }
6421
+ if (label !== void 0) this.recordAction(label, wireBytes, resBytes);
5629
6422
  return;
5630
6423
  }
5631
6424
  if (frame.k === "res") {
@@ -5633,8 +6426,10 @@ var SocketChannel = class {
5633
6426
  if (!p) return;
5634
6427
  this.pending.delete(frame.id);
5635
6428
  if (p.timer !== null) clearTimeout(p.timer);
5636
- if (frame.ok) p.resolve(frame.body);
5637
- else p.reject(new Error(frame.err));
6429
+ if (p.action !== null) this.recordAction(p.action.label, p.action.reqBytes, wireBytes);
6430
+ if (!frame.ok) p.reject(new Error(frame.err));
6431
+ else if (frame.pl !== void 0) p.resolve(new PayloadReply(frame.body, frame.pl));
6432
+ else p.resolve(frame.body);
5638
6433
  return;
5639
6434
  }
5640
6435
  this.eventHandler(frame.body);
@@ -6185,760 +6980,457 @@ function deliverShared(state, event) {
6185
6980
  }
6186
6981
  }
6187
6982
  //#endregion
6188
- //#region src/kernel/moleculer/resilient-cap-call.ts
6189
- /** Moleculer error `type` values meaning "the service is not (yet) routable". */
6190
- var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
6191
- /** Default ceiling for the discovery wait — fail-fast past this. */
6192
- var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
6193
- function isDiscoveryError(err) {
6194
- if (typeof err !== "object" || err === null) return false;
6195
- const type = err.type;
6196
- return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
6197
- }
6983
+ //#region src/kernel/transport/cap-child-index.ts
6984
+ /** Separator that cannot occur in a kebab cap name, so the key is unambiguous. */
6985
+ var DEVICE_KEY_SEPARATOR = "\0";
6986
+ var NO_CANDIDATES = Object.freeze([]);
6987
+ var MapCapChildIndex = class {
6988
+ deviceOwners;
6989
+ singletons;
6990
+ constructor(deviceOwners, singletons) {
6991
+ this.deviceOwners = deviceOwners;
6992
+ this.singletons = singletons;
6993
+ }
6994
+ deviceOwner(capName, deviceId) {
6995
+ return this.deviceOwners.get(`${capName}${DEVICE_KEY_SEPARATOR}${deviceId}`) ?? null;
6996
+ }
6997
+ singletonCandidates(capName) {
6998
+ return this.singletons.get(capName) ?? NO_CANDIDATES;
6999
+ }
7000
+ };
6198
7001
  /**
6199
- * Call a Moleculer action; on a service-discovery error, wait for the
6200
- * named service to be discovered and retry the call exactly once.
6201
- */
6202
- async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
6203
- try {
6204
- return await broker.call(action, params, opts);
6205
- } catch (err) {
6206
- if (!isDiscoveryError(err)) throw err;
6207
- await broker.waitForServices([serviceName], discoveryTimeoutMs);
6208
- return await broker.call(action, params, opts);
7002
+ * Build the `(cap, device?) → child` index from the connected children.
7003
+ *
7004
+ * A PURE function of what it is handed — it captures nothing, subscribes to
7005
+ * nothing and cannot be updated in place. That is the whole safety argument
7006
+ * (D3, D457): the only way to change what it says is to build a new one from
7007
+ * the `children` map, so an index can never name a child that map has already
7008
+ * dropped. An incremental index — add on register, remove on close — would be
7009
+ * a second registry that can disagree with the first, and a stale one routes
7010
+ * to a dead runner, which is strictly worse than a slow scan.
7011
+ *
7012
+ * Cost: O(children × descriptors), paid once per mutation of the children map
7013
+ * (a register, a re-register, a disconnect) rather than once per cap call.
7014
+ *
7015
+ * The two tiers stay separate, exactly as the scan had them: a device lookup
7016
+ * is never answered from a deviceId-less descriptor. `resolveChildId` owns
7017
+ * the fallback from tier 1 to tier 2, and folding it in here would make it
7018
+ * unreachable.
7019
+ */
7020
+ function buildCapChildIndex(entries) {
7021
+ const deviceOwners = /* @__PURE__ */ new Map();
7022
+ const singletons = /* @__PURE__ */ new Map();
7023
+ for (const entry of entries) {
7024
+ const namedForThisChild = /* @__PURE__ */ new Set();
7025
+ for (const cap of entry.caps) {
7026
+ if (cap.deviceId === void 0) {
7027
+ if (namedForThisChild.has(cap.capName)) continue;
7028
+ namedForThisChild.add(cap.capName);
7029
+ const candidates = singletons.get(cap.capName);
7030
+ if (candidates === void 0) singletons.set(cap.capName, [entry.childId]);
7031
+ else candidates.push(entry.childId);
7032
+ continue;
7033
+ }
7034
+ const key = `${cap.capName}${DEVICE_KEY_SEPARATOR}${cap.deviceId}`;
7035
+ if (!deviceOwners.has(key)) deviceOwners.set(key, entry.childId);
7036
+ }
6209
7037
  }
7038
+ return new MapCapChildIndex(deviceOwners, singletons);
6210
7039
  }
6211
7040
  //#endregion
6212
- //#region src/kernel/transport/cap-route.ts
6213
- function buildMessage(capName, method, detail) {
6214
- const call = method !== void 0 ? `${capName}.${method}` : capName;
6215
- const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
6216
- const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
6217
- const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
6218
- return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
7041
+ //#region src/kernel/transport/cap-routing-hints.ts
7042
+ /** Lift {@link CapRoutingHints} from a call's args. Never throws. */
7043
+ function liftRoutingHints(args) {
7044
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return {};
7045
+ const nodeId = Reflect.get(args, "nodeId");
7046
+ const addonId = Reflect.get(args, "addonId");
7047
+ return {
7048
+ ...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
7049
+ ...typeof addonId === "string" && addonId.length > 0 ? { addonId } : {}
7050
+ };
7051
+ }
7052
+ function readFanoutMode() {
7053
+ const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
7054
+ if (raw === "shadow" || raw === "broadcast") return raw;
7055
+ return "filter";
6219
7056
  }
6220
- var CapRouteError = class extends Error {
6221
- reason;
6222
- nodeId;
6223
- rejected;
6224
- /**
6225
- * @param cause Optional original error that triggered this routing failure.
6226
- * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
6227
- * Dispatchers wrapping transport errors MUST pass the original.
6228
- */
6229
- constructor(capName, method, detail, cause) {
6230
- super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
6231
- this.name = "CapRouteError";
6232
- this.reason = detail.reason;
6233
- this.nodeId = detail.nodeId;
6234
- this.rejected = detail.rejected;
6235
- }
6236
- };
6237
7057
  /**
6238
- * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
6239
- *
6240
- * Precedence (explicit nodeId path):
6241
- * 1. hub-in-process — hub node + hubInProcessProvides
6242
- * 2. hub-local-uds — hub node + hubLocalChildProvides
6243
- * 3. node-offline → CapRouteError{reason:'node-offline'}
6244
- * 4. agent-child-forward — node is an agent AND nodeKnowsCap
6245
- * 5. remote-moleculer — any other online non-agent node
6246
- *
6247
- * Singleton path (no nodeId):
6248
- * 1. hub-in-process (hub provides in-process)
6249
- * 2. hub-local-uds (a hub-local UDS child provides it)
6250
- * 3. remote-moleculer (any online, non-hub node that knows it)
6251
- * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
6252
- *
6253
- * PURE: no side effects, no async, no broker/registry imports.
7058
+ * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
7059
+ * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
7060
+ * prefix to distinguish routing failures (safe to retry via broker) from real
7061
+ * provider errors (must NOT retry to avoid double-executing side effects).
6254
7062
  */
6255
- function classifyCapRoute(capName, opts, snapshot) {
6256
- const rejected = [];
6257
- if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
6258
- return classifySingleton(capName, opts.deviceId, snapshot, rejected);
6259
- }
6260
- function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
6261
- if (nodeId === snap.hubNodeId) {
6262
- const residentProvides = snap.hubResidentProvides;
6263
- const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
6264
- const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
6265
- if (providesOnHub(capName)) {
6266
- const ref = getRef?.(capName) ?? null;
6267
- if (ref !== null) return {
6268
- kind: "hub-in-process",
6269
- capName,
6270
- ref
6271
- };
6272
- rejected.push({
6273
- kind: "hub-in-process",
6274
- why: "provider ref not available in snapshot"
6275
- });
6276
- throw new CapRouteError(capName, void 0, {
6277
- reason: "no-provider",
6278
- nodeId,
6279
- rejected
6280
- });
6281
- }
6282
- if (snap.hubLocalChildProvides(capName, deviceId)) {
6283
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
6284
- if (childId !== null) return {
6285
- kind: "hub-local-uds",
6286
- capName,
6287
- childId
6288
- };
6289
- rejected.push({
6290
- kind: "hub-local-uds",
6291
- why: "child id not resolvable from snapshot"
6292
- });
6293
- throw new CapRouteError(capName, void 0, {
6294
- reason: "no-provider",
6295
- nodeId,
6296
- rejected
6297
- });
7063
+ var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
7064
+ /**
7065
+ * Parent-side authority for local addon-runners reachable over a
7066
+ * `LocalTransportServer` (UDS). Children register their cap manifest on
7067
+ * connect; the registry routes `(capName, deviceId?)` cap calls to the
7068
+ * owning child over the channel and drops a child's caps on disconnect.
7069
+ */
7070
+ var LocalChildRegistry = class {
7071
+ children = /* @__PURE__ */ new Map();
7072
+ registeredHandler = () => {};
7073
+ goneHandler = () => {};
7074
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
7075
+ nextIncarnation = 1;
7076
+ eventHandler = null;
7077
+ logHandler = null;
7078
+ readinessHandler = null;
7079
+ server;
7080
+ onUnownedCall;
7081
+ logger;
7082
+ getActiveSingletonAddonId;
7083
+ resolveChildIdForAddon;
7084
+ /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
7085
+ ownNodeId;
7086
+ /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
7087
+ isAggregatedCollectionMethod;
7088
+ isAddonPinnedCall;
7089
+ /** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
7090
+ forwardedRaw = 0;
7091
+ forwardedDecoded = 0;
7092
+ /**
7093
+ * The per-ACTION byte census (D456). `ForwardCensus` above is the same
7094
+ * question asked of the whole plane; this one asks it per `<cap>.<method>`,
7095
+ * beside the bytes that method actually moved. Both are kept: `socketFwd=`
7096
+ * says whether the negotiation is working at all, and a `raw=0` on ONE
7097
+ * method with `raw>0` on its neighbours is a different fault.
7098
+ */
7099
+ actionCensus = createCapActionCensus();
7100
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
7101
+ capTimeoutMs;
7102
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
7103
+ capUsageObserver;
7104
+ /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
7105
+ egressRoutedCaps = /* @__PURE__ */ new Set();
7106
+ /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
7107
+ fanoutMode = readFanoutMode();
7108
+ /** Per-child event fan-out counters (sent / suppressed). */
7109
+ childEventStats = /* @__PURE__ */ new Map();
7110
+ /** Last pattern-set string logged per child, to dedup the INFO line. */
7111
+ loggedPatternSet = /* @__PURE__ */ new Map();
7112
+ /**
7113
+ * Derived `(cap, device?) → child` index over {@link children}, or `null`
7114
+ * when it must be rebuilt. See {@link capIndex} for why it is a cache and
7115
+ * never a registry (D3, D457).
7116
+ */
7117
+ capChildIndex = null;
7118
+ /**
7119
+ * Accepts either a plain positional `server` argument (backward-compatible)
7120
+ * or a full `LocalChildRegistryOptions` object.
7121
+ *
7122
+ * Positional overloads (existing call sites are unchanged):
7123
+ * new LocalChildRegistry(server)
7124
+ * new LocalChildRegistry(server, onUnownedCall)
7125
+ *
7126
+ * Options object (new call sites that pass a logger):
7127
+ * new LocalChildRegistry({ server, onUnownedCall, logger })
7128
+ */
7129
+ constructor(serverOrOptions, onUnownedCallArg) {
7130
+ if (serverOrOptions && !("listen" in serverOrOptions)) {
7131
+ const opts = serverOrOptions;
7132
+ this.server = opts.server;
7133
+ this.onUnownedCall = opts.onUnownedCall;
7134
+ this.logger = opts.logger;
7135
+ this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
7136
+ this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
7137
+ this.ownNodeId = opts.ownNodeId;
7138
+ this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
7139
+ this.isAddonPinnedCall = opts.isAddonPinnedCall;
7140
+ this.capTimeoutMs = opts.capTimeoutMs;
7141
+ this.capUsageObserver = opts.capUsageObserver;
7142
+ } else {
7143
+ this.server = serverOrOptions;
7144
+ this.onUnownedCall = onUnownedCallArg;
6298
7145
  }
6299
- rejected.push({
6300
- kind: "hub-in-process",
6301
- why: "hub does not provide this cap in-process"
6302
- });
6303
- rejected.push({
6304
- kind: "hub-local-uds",
6305
- why: "no hub-local child provides this cap"
6306
- });
6307
- throw new CapRouteError(capName, void 0, {
6308
- reason: "no-provider",
6309
- nodeId,
6310
- rejected
6311
- });
6312
7146
  }
6313
- if (!snap.nodeOnline(nodeId)) {
6314
- rejected.push({
6315
- kind: "remote-moleculer",
6316
- why: `node ${nodeId} is offline`
6317
- });
6318
- throw new CapRouteError(capName, void 0, {
6319
- reason: "node-offline",
6320
- nodeId,
6321
- rejected
6322
- });
7147
+ async start() {
7148
+ this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
7149
+ this.server.onConnection((channel) => this.onConnection(channel));
7150
+ await this.server.listen();
6323
7151
  }
6324
- if (snap.nodeIsAgent(nodeId)) {
6325
- if (snap.nodeKnowsCap(nodeId, capName)) return {
6326
- kind: "agent-child-forward",
6327
- capName,
6328
- agentNodeId: nodeId,
6329
- childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
7152
+ /**
7153
+ * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
7154
+ * child has no recorded events yet. Exposed for the starvation-verification
7155
+ * surface (shadow burn-in + operator debug).
7156
+ */
7157
+ getChildEventStats(childId) {
7158
+ const s = this.childEventStats.get(childId);
7159
+ return s === void 0 ? null : {
7160
+ sent: s.sent,
7161
+ suppressed: s.suppressed
6330
7162
  };
6331
- rejected.push({
6332
- kind: "agent-child-forward",
6333
- why: `agent ${nodeId} does not know cap ${capName}`
6334
- });
6335
- throw new CapRouteError(capName, void 0, {
6336
- reason: "no-provider",
6337
- nodeId,
6338
- rejected
6339
- });
6340
7163
  }
6341
- return {
6342
- kind: "remote-moleculer",
6343
- capName,
6344
- nodeId
6345
- };
6346
- }
6347
- function classifySingleton(capName, deviceId, snap, rejected) {
6348
- if (snap.hubInProcessProvides(capName)) {
6349
- const ref = snap.getInProcessProviderRef?.(capName) ?? null;
6350
- if (ref !== null) return {
6351
- kind: "hub-in-process",
6352
- capName,
6353
- ref
6354
- };
6355
- rejected.push({
6356
- kind: "hub-in-process",
6357
- why: "provider ref not available in snapshot"
6358
- });
6359
- } else rejected.push({
6360
- kind: "hub-in-process",
6361
- why: "hub does not provide this cap in-process"
6362
- });
6363
- if (snap.hubLocalChildProvides(capName, deviceId)) {
6364
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
6365
- if (childId !== null) return {
6366
- kind: "hub-local-uds",
6367
- capName,
6368
- childId
7164
+ /**
7165
+ * This child's socket traffic — bytes and messages, both directions, split by
7166
+ * frame kind — or `null` when the child is gone or its channel keeps no
7167
+ * counters (an in-process or test transport).
7168
+ *
7169
+ * The event counters above cover ONE kind of frame and, once surfaced on
7170
+ * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
7171
+ * is the same question asked of every frame the channel moves, and it is the
7172
+ * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
7173
+ * pair for the whole process and has no per-socket breakdown.
7174
+ */
7175
+ getChildSocketTraffic(childId) {
7176
+ const entry = this.children.get(childId);
7177
+ if (entry === void 0) return null;
7178
+ return entry.channel.readTraffic?.() ?? null;
7179
+ }
7180
+ /**
7181
+ * Bytes this process has written to a child that the child has not yet
7182
+ * taken, live, or `null` when the channel keeps no such reading. Read on
7183
+ * every heartbeat probe (D446) — a child that stops draining is the shape
7184
+ * of an outbound-queue episode, and the number lives on the channel.
7185
+ */
7186
+ getChildQueuedBytes(childId) {
7187
+ const entry = this.children.get(childId);
7188
+ if (entry === void 0) return null;
7189
+ return entry.channel.queuedBytes?.() ?? null;
7190
+ }
7191
+ /** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
7192
+ readForwardCensus() {
7193
+ return {
7194
+ raw: this.forwardedRaw,
7195
+ decoded: this.forwardedDecoded
6369
7196
  };
6370
- rejected.push({
6371
- kind: "hub-local-uds",
6372
- why: "child id not resolvable from snapshot"
6373
- });
6374
- } else rejected.push({
6375
- kind: "hub-local-uds",
6376
- why: "no hub-local child provides this cap"
6377
- });
6378
- const knownNodes = snap.listKnownNodeIds?.() ?? [];
6379
- for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
6380
- kind: "remote-moleculer",
6381
- capName,
6382
- nodeId
6383
- };
6384
- rejected.push({
6385
- kind: "remote-moleculer",
6386
- why: "no online remote node knows this cap"
6387
- });
6388
- throw new CapRouteError(capName, void 0, {
6389
- reason: "no-provider",
6390
- rejected
6391
- });
6392
- }
6393
- //#endregion
6394
- //#region src/kernel/transport/cap-route-resolver.ts
6395
- /** The Moleculer service name that Task 6's agent registers. */
6396
- var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
6397
- /** The Moleculer action (service.action) for agent cap forwarding. */
6398
- var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
6399
- /** Default timeout for remote Moleculer cap calls (ms). */
6400
- var REMOTE_CALL_TIMEOUT_MS = 6e4;
6401
- function extractDeviceId(args) {
6402
- if (args === null || typeof args !== "object") return void 0;
6403
- const raw = Reflect.get(args, "deviceId");
6404
- return typeof raw === "number" ? raw : void 0;
6405
- }
6406
- /**
6407
- * Extract an inline `nodeId` string from a cap call's args — the per-call node
6408
- * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
6409
- * device-scoped caps carry `deviceId`, and the way the generated cap-router on
6410
- * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
6411
- *
6412
- * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
6413
- * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
6414
- * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
6415
- * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
6416
- * methods that don't declare `nodeId` simply ignore the extra field (they
6417
- * destructure the fields they need); methods that DO declare it (e.g.
6418
- * `getEngineProvisioning({nodeId})`) still receive it unchanged.
6419
- */
6420
- function extractNodeId(args) {
6421
- if (args === null || typeof args !== "object") return void 0;
6422
- const raw = Reflect.get(args, "nodeId");
6423
- return typeof raw === "string" && raw.length > 0 ? raw : void 0;
6424
- }
6425
- /**
6426
- * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
6427
- * destructures what it needs and ignores the rest" convention
6428
- * {@link extractNodeId} already relies on for the inline `nodeId` pin.
6429
- * `undefined` (the overwhelmingly common case: no caller hint, or the census
6430
- * consuming it is disarmed) returns `args` unchanged — no allocation. A
6431
- * non-object `args` (or `null`/array) is returned unchanged too: there is
6432
- * nowhere to attach an extra key without changing the payload's shape, and a
6433
- * provider taking a bare value never reads `callerAddonId` off it anyway.
6434
- */
6435
- function withCallerAddonId(args, callerAddonId) {
6436
- if (callerAddonId === void 0) return args;
6437
- if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
6438
- return {
6439
- ...args,
6440
- callerAddonId
6441
- };
6442
- }
6443
- var CapRouteResolver = class {
6444
- hubNodeId;
6445
- broker;
6446
- hubLocalRegistry;
6447
- nodeAuthority;
6448
- inProcessProviders;
6449
- hubResidentProviders;
6450
- capTimeoutMs;
6451
- snapshot;
6452
- constructor(deps) {
6453
- this.hubNodeId = deps.hubNodeId;
6454
- this.broker = deps.broker;
6455
- this.hubLocalRegistry = deps.hubLocalRegistry;
6456
- this.nodeAuthority = deps.nodeAuthority;
6457
- this.inProcessProviders = deps.inProcessProviders;
6458
- this.hubResidentProviders = deps.hubResidentProviders;
6459
- this.capTimeoutMs = deps.capTimeoutMs;
6460
- this.snapshot = this.buildSnapshot();
6461
7197
  }
6462
- /** Per-call RPC timeout: the cap method's declared override, else the default. */
6463
- callTimeout(capName, method) {
6464
- return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
7198
+ /**
7199
+ * Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
7200
+ * and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
7201
+ * must be one); the heartbeat subtracts the previous reading.
7202
+ *
7203
+ * This is the instrument D454 closed by asking for. `socketTopBytes=` names
7204
+ * the peer that moves the bytes and `txkB=` says they are responses; neither
7205
+ * can say WHICH method originates them, and after raw-forward the bytes that
7206
+ * cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
7207
+ * calls cross unread. What costs is what hub-main LOOKS at, and that is the
7208
+ * `decoded` half of an entry, next to the bytes it carried.
7209
+ */
7210
+ readActionCensus() {
7211
+ return this.actionCensus.read();
6465
7212
  }
6466
- resolveCapRoute(capName, opts) {
6467
- return classifyCapRoute(capName, opts, this.snapshot);
7213
+ /**
7214
+ * The regime the counters above were produced under, read once from
7215
+ * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
7216
+ *
7217
+ * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
7218
+ * WOULD have been dropped" under `shadow`. Those are different claims, and
7219
+ * until this getter existed the mode was invisible at runtime — the counters
7220
+ * were readable and the regime that produced them was not.
7221
+ */
7222
+ get eventFanoutMode() {
7223
+ return this.fanoutMode;
6468
7224
  }
6469
7225
  /**
6470
- * Resolve the hub-local-uds route for a cap owned by a forked hub-local
6471
- * child, IGNORING any in-hub provider registered for the same cap name.
7226
+ * Connected children that never declared a subscription pattern set.
6472
7227
  *
6473
- * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
6474
- * provider (e.g. a wrapper) is registered for the cap, the route always
6475
- * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
6476
- * reached. That is correct for the generic dispatch path (the wrapper is the
6477
- * active provider), but WRONG for the native-cap fallback
6478
- * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
6479
- * the forked vendor child so a wrapper can delegate to it. A wrapper cap
6480
- * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
6481
- * itself, the native is never invoked, and the wrapper silently falls
6482
- * through to its secondary strategy.
7228
+ * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
7229
+ * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
7230
+ * `aggregateEventInterest` returns `null` — accept everything — if ANY
7231
+ * connected child is undeclared, which disables the node's inbound cross-node
7232
+ * gate as well. **One legacy runner silently turns off the filtering for the
7233
+ * whole node**, and nothing reported it, so a zero here is as load-bearing as
7234
+ * a non-zero one.
7235
+ */
7236
+ undeclaredChildCount() {
7237
+ let count = 0;
7238
+ for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
7239
+ return count;
7240
+ }
7241
+ /**
7242
+ * Does `entry` want to receive an event in `category`? Honours the fan-out
7243
+ * mode: `broadcast` → always; undeclared patterns (`null`) → always
7244
+ * (fail-open); else the child's declared set must match via `matchesPattern`
7245
+ * — the SAME function the child's own `deliverShared` map-key gate uses,
7246
+ * giving provable delivery parity. `shadow` is handled by the caller (it
7247
+ * counts the would-suppress but still sends).
6483
7248
  *
6484
- * This method consults ONLY the hub-local-child authority
6485
- * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
6486
- * it returns the native child route regardless of any in-process shadow.
6487
- * Returns null when no hub-local child owns `(capName, deviceId)` — the
6488
- * caller then falls through to its remote-resolution branch.
7249
+ * NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
7250
+ * `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
7251
+ * that explicitly subscribes to a ring-denied category must still RECEIVE it.
6489
7252
  */
6490
- resolveHubLocalUdsRoute(capName, deviceId) {
6491
- if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
6492
- const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
6493
- if (childId === null) return null;
6494
- return {
6495
- kind: "hub-local-uds",
6496
- capName,
6497
- childId
6498
- };
7253
+ childWantsEvent(entry, category) {
7254
+ if (this.fanoutMode === "broadcast") return true;
7255
+ if (entry.eventPatterns === null) return true;
7256
+ return entry.eventPatterns.some((p) => matchesPattern(p, category));
6499
7257
  }
6500
7258
  /**
6501
- * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
6502
- * the addonId of the provider the CALLER already resolved (e.g.
6503
- * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
6504
- * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
6505
- * route already encodes the provider in its action name
6506
- * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
6507
- * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
6508
- * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
6509
- * `dispatch`.
6510
- * @param callerAddonId Optional out-of-band CALLER-identity hint — the
6511
- * addon that ORIGINATED the call, distinct from `addonId` above (which
6512
- * names the resolved PROVIDER, not the caller). Set by
6513
- * `onUnownedCall` from `CapCallInput.callerAddonId`
6514
- * (`LocalChildRegistry` stamped it off the registered `childId`). Only
6515
- * the `hub-in-process` branch consumes it — merged into `args` so a
6516
- * diagnostic tool deep in that provider (e.g. device-manager's
6517
- * fleet-read census) can read it without every provider method changing
6518
- * signature, the same "extra field the provider destructures or
6519
- * ignores" convention `nodeId` already uses on this path.
7259
+ * D2: this node's aggregate cross-node event interest — the UNION of every
7260
+ * connected child's declared category patterns. Consumed by the parent's
7261
+ * `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
7262
+ * accepts cross-node (Moleculer) categories at least one local child wants.
7263
+ *
7264
+ * Returns `null` (fail-open — accept everything) when:
7265
+ * - NO child is currently connected (boot window before the first forked
7266
+ * child completes its UDS handshake, or every child has disconnected): a
7267
+ * node MUST NOT drop 100% of inbound cross-node events during that window,
7268
+ * and a child may connect imminently. This is distinct from the
7269
+ * declared-empty case below.
7270
+ * - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
7271
+ * runner): a node cannot safely narrow its inbound set while a child's
7272
+ * real interest is unknown. Mirrors the per-child fail-open in
7273
+ * {@link childWantsEvent}, aggregated conservatively.
7274
+ *
7275
+ * Returns an EMPTY array only when ≥1 child is connected and EVERY connected
7276
+ * child declared an explicit empty set — a genuine "wants nothing". Reads the
7277
+ * live `children` map, so a child registering / disconnecting / sending
7278
+ * `event-sub` is reflected on the next call with no extra bookkeeping.
6520
7279
  */
6521
- async dispatch(route, method, args, addonId, callerAddonId) {
6522
- try {
6523
- return await this.dispatchInner(route, method, args, addonId, callerAddonId);
6524
- } catch (err) {
6525
- if (err instanceof CapRouteError) throw err;
6526
- const nodeId = this.routeNodeId(route);
6527
- const cause = err instanceof Error ? err : new Error(String(err));
6528
- throw new CapRouteError(route.capName, method, {
6529
- reason: "transport-failed",
6530
- nodeId,
6531
- rejected: [{
6532
- kind: route.kind,
6533
- why: cause.message
6534
- }]
6535
- }, cause);
7280
+ aggregateEventInterest() {
7281
+ if (this.children.size === 0) return null;
7282
+ const union = /* @__PURE__ */ new Set();
7283
+ for (const entry of this.children.values()) {
7284
+ if (entry.eventPatterns === null) return null;
7285
+ for (const p of entry.eventPatterns) union.add(p);
7286
+ }
7287
+ return [...union];
7288
+ }
7289
+ statsFor(childId) {
7290
+ let s = this.childEventStats.get(childId);
7291
+ if (s === void 0) {
7292
+ s = {
7293
+ sent: 0,
7294
+ suppressed: 0
7295
+ };
7296
+ this.childEventStats.set(childId, s);
6536
7297
  }
7298
+ return s;
7299
+ }
7300
+ /** Deduped INFO line whenever a child's declared pattern set changes. */
7301
+ logPatternSet(childId, patterns) {
7302
+ const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
7303
+ if (this.loggedPatternSet.get(childId) === key) return;
7304
+ this.loggedPatternSet.set(childId, key);
7305
+ this.logger?.info("child event patterns", {
7306
+ childId,
7307
+ patterns: patterns ?? null
7308
+ });
6537
7309
  }
6538
7310
  /**
6539
- * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
6540
- * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
7311
+ * Child id that can service a call to `capName` (optionally addressing
7312
+ * `deviceId`), or null.
7313
+ *
7314
+ * `deviceId` is a routing HINT, not a hard filter. A singleton cap
7315
+ * (`pipeline-runner`, `stream-broker`, …) addresses devices through its
7316
+ * METHOD ARGUMENTS — `attachCamera({ deviceId })` is one provider serving
7317
+ * many cameras — so its descriptor carries no `deviceId`. Resolving on
7318
+ * `deviceId` alone would never match it and would force the call onto the
7319
+ * broker fallback (which then hangs in service discovery). Hence:
7320
+ * 1. exact device-scoped owner (native per-device caps where each child
7321
+ * owns a disjoint device subset) is preferred, then
7322
+ * 2. a singleton owner (deviceId-less descriptor) is the fallback.
7323
+ * A cap is globally singleton XOR device-scoped, so the two tiers never
7324
+ * compete for the same capName.
6541
7325
  */
6542
- async dispatchInner(route, method, args, addonId, callerAddonId) {
6543
- switch (route.kind) {
6544
- case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
6545
- case "hub-local-uds": {
6546
- const registry = this.hubLocalRegistry;
6547
- if (registry === null) throw new CapRouteError(route.capName, method, {
6548
- reason: "no-provider",
6549
- rejected: [{
6550
- kind: "hub-local-uds",
6551
- why: "UDS registry not available"
6552
- }]
6553
- });
6554
- const deviceId = extractDeviceId(args);
6555
- const input = {
6556
- capName: route.capName,
6557
- method,
6558
- args,
6559
- ...deviceId !== void 0 ? { deviceId } : {}
6560
- };
6561
- return registry.callCapOnChild(route.childId, input);
6562
- }
6563
- case "remote-moleculer": {
6564
- const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
6565
- if (addonId === null) throw new CapRouteError(route.capName, method, {
6566
- reason: "no-provider",
6567
- nodeId: route.nodeId,
6568
- rejected: [{
6569
- kind: "remote-moleculer",
6570
- why: `no addonId known for ${route.nodeId}/${route.capName}`
6571
- }]
6572
- });
6573
- const deviceId = extractDeviceId(args);
6574
- const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
6575
- const action = capActionName(addonId, route.capName, method, isNative);
6576
- return callWithServiceDiscovery(this.broker, addonId, action, args, {
6577
- nodeID: route.nodeId,
6578
- timeout: this.callTimeout(route.capName, method)
6579
- });
6580
- }
6581
- case "agent-child-forward": {
6582
- const deviceId = extractDeviceId(args);
6583
- const params = {
6584
- capName: route.capName,
6585
- method,
6586
- args,
6587
- ...route.childId !== void 0 ? { childId: route.childId } : {},
6588
- ...deviceId !== void 0 ? { deviceId } : {},
6589
- ...addonId !== void 0 ? { addonId } : {}
6590
- };
6591
- return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
6592
- nodeID: route.agentNodeId,
6593
- timeout: this.callTimeout(route.capName, method)
6594
- });
6595
- }
7326
+ resolveChildId(capName, deviceId) {
7327
+ const index = this.capIndex();
7328
+ if (deviceId !== void 0) {
7329
+ const deviceOwner = index.deviceOwner(capName, deviceId);
7330
+ if (deviceOwner !== null) return deviceOwner;
6596
7331
  }
6597
- }
6598
- buildSnapshot() {
6599
- const hubLocalRegistry = this.hubLocalRegistry;
6600
- const nodeAuthority = this.nodeAuthority;
6601
- const inProcessProviders = this.inProcessProviders;
6602
- const hubResidentProviders = this.hubResidentProviders;
6603
- return {
6604
- hubNodeId: this.hubNodeId,
6605
- hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
6606
- ...hubResidentProviders !== void 0 ? {
6607
- hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
6608
- getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
6609
- } : {},
6610
- hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
6611
- nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
6612
- nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
6613
- nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
6614
- listKnownNodeIds: () => nodeAuthority.listNodeIds(),
6615
- getInProcessProviderRef: (cap) => inProcessProviders(cap),
6616
- getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
6617
- getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
6618
- };
6619
- }
6620
- /** Extract a nodeId string from a route for error reporting, or undefined. */
6621
- routeNodeId(route) {
6622
- switch (route.kind) {
6623
- case "hub-in-process": return this.hubNodeId;
6624
- case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
6625
- case "remote-moleculer": return route.nodeId;
6626
- case "agent-child-forward": return route.agentNodeId;
7332
+ const candidates = index.singletonCandidates(capName);
7333
+ if (candidates.length === 0) return null;
7334
+ if (candidates.length === 1) return candidates[0];
7335
+ const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
7336
+ if (preferredAddonId !== null) {
7337
+ const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
7338
+ if (candidates.includes(preferredChildId)) return preferredChildId;
6627
7339
  }
7340
+ return candidates[0];
6628
7341
  }
6629
- };
6630
- //#endregion
6631
- //#region src/kernel/transport/local-child-registry.ts
6632
- function readFanoutMode() {
6633
- const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
6634
- if (raw === "shadow" || raw === "broadcast") return raw;
6635
- return "filter";
6636
- }
6637
- /**
6638
- * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
6639
- * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
6640
- * prefix to distinguish routing failures (safe to retry via broker) from real
6641
- * provider errors (must NOT retry to avoid double-executing side effects).
6642
- */
6643
- var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
6644
- /**
6645
- * Parent-side authority for local addon-runners reachable over a
6646
- * `LocalTransportServer` (UDS). Children register their cap manifest on
6647
- * connect; the registry routes `(capName, deviceId?)` cap calls to the
6648
- * owning child over the channel and drops a child's caps on disconnect.
6649
- */
6650
- var LocalChildRegistry = class {
6651
- children = /* @__PURE__ */ new Map();
6652
- registeredHandler = () => {};
6653
- goneHandler = () => {};
6654
- /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
6655
- nextIncarnation = 1;
6656
- eventHandler = null;
6657
- logHandler = null;
6658
- readinessHandler = null;
6659
- server;
6660
- onUnownedCall;
6661
- logger;
6662
- getActiveSingletonAddonId;
6663
- resolveChildIdForAddon;
6664
- /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
6665
- ownNodeId;
6666
- /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
6667
- isAggregatedCollectionMethod;
6668
- isAddonPinnedCall;
6669
- /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
6670
- capTimeoutMs;
6671
- /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
6672
- capUsageObserver;
6673
- /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
6674
- egressRoutedCaps = /* @__PURE__ */ new Set();
6675
- /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
6676
- fanoutMode = readFanoutMode();
6677
- /** Per-child event fan-out counters (sent / suppressed). */
6678
- childEventStats = /* @__PURE__ */ new Map();
6679
- /** Last pattern-set string logged per child, to dedup the INFO line. */
6680
- loggedPatternSet = /* @__PURE__ */ new Map();
6681
7342
  /**
6682
- * Accepts either a plain positional `server` argument (backward-compatible)
6683
- * or a full `LocalChildRegistryOptions` object.
6684
- *
6685
- * Positional overloads (existing call sites are unchanged):
6686
- * new LocalChildRegistry(server)
6687
- * new LocalChildRegistry(server, onUnownedCall)
7343
+ * Publish one cap-usage observation, if a sink is wired.
6688
7344
  *
6689
- * Options object (new call sites that pass a logger):
6690
- * new LocalChildRegistry({ server, onUnownedCall, logger })
7345
+ * Cost discipline — this is on the hot path (D181: hub-main's event loop is
7346
+ * the cluster's only queue): no sink means one undefined check; with a sink
7347
+ * it is one object literal and one `Date.now()`, and the sink itself is O(1).
7348
+ * Wrapped so a broken observer can never fail the call it observes — the
7349
+ * registry swallows too, and BOTH matter: this catch also covers a sink that
7350
+ * is not the registry.
6691
7351
  */
6692
- constructor(serverOrOptions, onUnownedCallArg) {
6693
- if (serverOrOptions && !("listen" in serverOrOptions)) {
6694
- const opts = serverOrOptions;
6695
- this.server = opts.server;
6696
- this.onUnownedCall = opts.onUnownedCall;
6697
- this.logger = opts.logger;
6698
- this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
6699
- this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
6700
- this.ownNodeId = opts.ownNodeId;
6701
- this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
6702
- this.isAddonPinnedCall = opts.isAddonPinnedCall;
6703
- this.capTimeoutMs = opts.capTimeoutMs;
6704
- this.capUsageObserver = opts.capUsageObserver;
6705
- } else {
6706
- this.server = serverOrOptions;
6707
- this.onUnownedCall = onUnownedCallArg;
6708
- }
6709
- }
6710
- async start() {
6711
- this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
6712
- this.server.onConnection((channel) => this.onConnection(channel));
6713
- await this.server.listen();
7352
+ recordCapUsage(callerChildId, providerChildId, capName, methodName) {
7353
+ const sink = this.capUsageObserver;
7354
+ if (sink === void 0 || callerChildId === null) return;
7355
+ try {
7356
+ sink({
7357
+ callerAddonId: callerChildId,
7358
+ providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
7359
+ capName,
7360
+ methodName,
7361
+ atMs: Date.now()
7362
+ });
7363
+ } catch {}
6714
7364
  }
6715
7365
  /**
6716
- * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
6717
- * child has no recorded events yet. Exposed for the starvation-verification
6718
- * surface (shadow burn-in + operator debug).
7366
+ * The cap index over the CURRENT children, rebuilt on first use after any
7367
+ * mutation of {@link children}.
7368
+ *
7369
+ * This replaced a linear walk of every child's cap manifest per
7370
+ * `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
7371
+ * across 39 children (D456). Why it did not exist before is the whole
7372
+ * design: what it indexes moves — a runner is replaced on every update, a
7373
+ * crash respawns one, a child re-registers with a replacement manifest after
7374
+ * init — and an index that names a dead child routes work into a socket
7375
+ * nobody is reading, which is strictly worse than a slow scan.
7376
+ *
7377
+ * So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
7378
+ * function of `children.values()`, the only two writers of that map
7379
+ * ({@link invalidateCapIndex}'s call sites) drop the index in the same
7380
+ * statement, and nothing else can construct one. There is no add-on-register
7381
+ * / remove-on-close path that could disagree with the map — i.e. no shadow
7382
+ * registry (D3): the map remains the single authority and this holds no fact
7383
+ * it does not.
7384
+ *
7385
+ * The operator's active-singleton preference is deliberately NOT indexed —
7386
+ * it changes without any child connecting or leaving, so `resolveChildId`
7387
+ * reads it live and the index only supplies the candidate list.
6719
7388
  */
6720
- getChildEventStats(childId) {
6721
- const s = this.childEventStats.get(childId);
6722
- return s === void 0 ? null : {
6723
- sent: s.sent,
6724
- suppressed: s.suppressed
6725
- };
7389
+ capIndex() {
7390
+ if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
7391
+ return this.capChildIndex;
7392
+ }
7393
+ /** Drop the derived index. MUST follow every write to {@link children}. */
7394
+ invalidateCapIndex() {
7395
+ this.capChildIndex = null;
6726
7396
  }
6727
7397
  /**
6728
- * This child's socket traffic — bytes and messages, both directions, split by
6729
- * frame kind — or `null` when the child is gone or its channel keeps no
6730
- * counters (an in-process or test transport).
7398
+ * Does the named child currently provide `(capName, deviceId?)`?
6731
7399
  *
6732
- * The event counters above cover ONE kind of frame and, once surfaced on
6733
- * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
6734
- * is the same question asked of every frame the channel moves, and it is the
6735
- * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
6736
- * pair for the whole process and has no per-socket breakdown.
7400
+ * Used by the hub proxy seam, which knows the EXACT addon (→ runner →
7401
+ * childId) a provider belongs to. Unlike `resolveChildId` (which picks the
7402
+ * first child owning `capName`), this targets one child — required for
7403
+ * COLLECTION caps (`addon-widgets-source`, …) where many children register
7404
+ * the same capName: routing by capName alone collapses every provider to
7405
+ * the first child. Same deviceId-as-hint semantics as `resolveChildId`:
7406
+ * a device-scoped descriptor matching `deviceId` OR a deviceId-less
7407
+ * (singleton/collection) descriptor counts.
6737
7408
  */
6738
- getChildSocketTraffic(childId) {
6739
- const entry = this.children.get(childId);
6740
- if (entry === void 0) return null;
6741
- return entry.channel.readTraffic?.() ?? null;
6742
- }
6743
7409
  /**
6744
- * The regime the counters above were produced under, read once from
6745
- * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
6746
- *
6747
- * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
6748
- * WOULD have been dropped" under `shadow`. Those are different claims, and
6749
- * until this getter existed the mode was invisible at runtime — the counters
6750
- * were readable and the regime that produced them was not.
7410
+ * Is `childId` currently connected (has it completed its UDS handshake)?
7411
+ * Coarser than {@link childProvides}: it answers "is the child reachable
7412
+ * over UDS at all", regardless of which caps it has announced yet. Used by
7413
+ * the route-mount fallback to decide between the handler-stripped
7414
+ * `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
7415
+ * cap proxy's `getRoutes()` (child not yet UDS-registered).
6751
7416
  */
6752
- get eventFanoutMode() {
6753
- return this.fanoutMode;
7417
+ isChildKnown(childId) {
7418
+ return this.children.has(childId);
7419
+ }
7420
+ childProvides(childId, capName, deviceId) {
7421
+ const entry = this.children.get(childId);
7422
+ if (!entry) return false;
7423
+ if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
7424
+ return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
6754
7425
  }
6755
7426
  /**
6756
- * Connected children that never declared a subscription pattern set.
6757
- *
6758
- * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
6759
- * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
6760
- * `aggregateEventInterest` returns `null` — accept everything — if ANY
6761
- * connected child is undeclared, which disables the node's inbound cross-node
6762
- * gate as well. **One legacy runner silently turns off the filtering for the
6763
- * whole node**, and nothing reported it, so a zero here is as load-bearing as
6764
- * a non-zero one.
7427
+ * Per-request options for a cap call: the cap method's declared `timeoutMs`
7428
+ * when one exists, else `undefined` so the channel applies its default.
7429
+ * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
6765
7430
  */
6766
- undeclaredChildCount() {
6767
- let count = 0;
6768
- for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
6769
- return count;
6770
- }
6771
- /**
6772
- * Does `entry` want to receive an event in `category`? Honours the fan-out
6773
- * mode: `broadcast` → always; undeclared patterns (`null`) → always
6774
- * (fail-open); else the child's declared set must match via `matchesPattern`
6775
- * — the SAME function the child's own `deliverShared` map-key gate uses,
6776
- * giving provable delivery parity. `shadow` is handled by the caller (it
6777
- * counts the would-suppress but still sends).
6778
- *
6779
- * NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
6780
- * `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
6781
- * that explicitly subscribes to a ring-denied category must still RECEIVE it.
6782
- */
6783
- childWantsEvent(entry, category) {
6784
- if (this.fanoutMode === "broadcast") return true;
6785
- if (entry.eventPatterns === null) return true;
6786
- return entry.eventPatterns.some((p) => matchesPattern(p, category));
6787
- }
6788
- /**
6789
- * D2: this node's aggregate cross-node event interest — the UNION of every
6790
- * connected child's declared category patterns. Consumed by the parent's
6791
- * `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
6792
- * accepts cross-node (Moleculer) categories at least one local child wants.
6793
- *
6794
- * Returns `null` (fail-open — accept everything) when:
6795
- * - NO child is currently connected (boot window before the first forked
6796
- * child completes its UDS handshake, or every child has disconnected): a
6797
- * node MUST NOT drop 100% of inbound cross-node events during that window,
6798
- * and a child may connect imminently. This is distinct from the
6799
- * declared-empty case below.
6800
- * - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
6801
- * runner): a node cannot safely narrow its inbound set while a child's
6802
- * real interest is unknown. Mirrors the per-child fail-open in
6803
- * {@link childWantsEvent}, aggregated conservatively.
6804
- *
6805
- * Returns an EMPTY array only when ≥1 child is connected and EVERY connected
6806
- * child declared an explicit empty set — a genuine "wants nothing". Reads the
6807
- * live `children` map, so a child registering / disconnecting / sending
6808
- * `event-sub` is reflected on the next call with no extra bookkeeping.
6809
- */
6810
- aggregateEventInterest() {
6811
- if (this.children.size === 0) return null;
6812
- const union = /* @__PURE__ */ new Set();
6813
- for (const entry of this.children.values()) {
6814
- if (entry.eventPatterns === null) return null;
6815
- for (const p of entry.eventPatterns) union.add(p);
6816
- }
6817
- return [...union];
6818
- }
6819
- statsFor(childId) {
6820
- let s = this.childEventStats.get(childId);
6821
- if (s === void 0) {
6822
- s = {
6823
- sent: 0,
6824
- suppressed: 0
6825
- };
6826
- this.childEventStats.set(childId, s);
6827
- }
6828
- return s;
6829
- }
6830
- /** Deduped INFO line whenever a child's declared pattern set changes. */
6831
- logPatternSet(childId, patterns) {
6832
- const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
6833
- if (this.loggedPatternSet.get(childId) === key) return;
6834
- this.loggedPatternSet.set(childId, key);
6835
- this.logger?.info("child event patterns", {
6836
- childId,
6837
- patterns: patterns ?? null
6838
- });
6839
- }
6840
- /**
6841
- * Child id that can service a call to `capName` (optionally addressing
6842
- * `deviceId`), or null.
6843
- *
6844
- * `deviceId` is a routing HINT, not a hard filter. A singleton cap
6845
- * (`pipeline-runner`, `stream-broker`, …) addresses devices through its
6846
- * METHOD ARGUMENTS — `attachCamera({ deviceId })` is one provider serving
6847
- * many cameras — so its descriptor carries no `deviceId`. Resolving on
6848
- * `deviceId` alone would never match it and would force the call onto the
6849
- * broker fallback (which then hangs in service discovery). Hence:
6850
- * 1. exact device-scoped owner (native per-device caps where each child
6851
- * owns a disjoint device subset) is preferred, then
6852
- * 2. a singleton owner (deviceId-less descriptor) is the fallback.
6853
- * A cap is globally singleton XOR device-scoped, so the two tiers never
6854
- * compete for the same capName.
6855
- */
6856
- resolveChildId(capName, deviceId) {
6857
- if (deviceId !== void 0) {
6858
- const deviceOwner = this.findChildId((cap) => cap.capName === capName && cap.deviceId === deviceId);
6859
- if (deviceOwner !== null) return deviceOwner;
6860
- }
6861
- const candidates = this.findAllChildIds((cap) => cap.capName === capName && cap.deviceId === void 0);
6862
- if (candidates.length === 0) return null;
6863
- if (candidates.length === 1) return candidates[0];
6864
- const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
6865
- if (preferredAddonId !== null) {
6866
- const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
6867
- if (candidates.includes(preferredChildId)) return preferredChildId;
6868
- }
6869
- return candidates[0];
6870
- }
6871
- /**
6872
- * Publish one cap-usage observation, if a sink is wired.
6873
- *
6874
- * Cost discipline — this is on the hot path (D181: hub-main's event loop is
6875
- * the cluster's only queue): no sink means one undefined check; with a sink
6876
- * it is one object literal and one `Date.now()`, and the sink itself is O(1).
6877
- * Wrapped so a broken observer can never fail the call it observes — the
6878
- * registry swallows too, and BOTH matter: this catch also covers a sink that
6879
- * is not the registry.
6880
- */
6881
- recordCapUsage(callerChildId, providerChildId, capName, methodName) {
6882
- const sink = this.capUsageObserver;
6883
- if (sink === void 0 || callerChildId === null) return;
6884
- try {
6885
- sink({
6886
- callerAddonId: callerChildId,
6887
- providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
6888
- capName,
6889
- methodName,
6890
- atMs: Date.now()
6891
- });
6892
- } catch {}
6893
- }
6894
- /** First child whose cap manifest contains a descriptor matching `predicate`. */
6895
- findChildId(predicate) {
6896
- for (const entry of this.children.values()) if (entry.caps.some(predicate)) return entry.childId;
6897
- return null;
6898
- }
6899
- /** Every child whose cap manifest contains a descriptor matching `predicate`. */
6900
- findAllChildIds(predicate) {
6901
- const out = [];
6902
- for (const entry of this.children.values()) if (entry.caps.some(predicate)) out.push(entry.childId);
6903
- return out;
6904
- }
6905
- /**
6906
- * Does the named child currently provide `(capName, deviceId?)`?
6907
- *
6908
- * Used by the hub proxy seam, which knows the EXACT addon (→ runner →
6909
- * childId) a provider belongs to. Unlike `resolveChildId` (which picks the
6910
- * first child owning `capName`), this targets one child — required for
6911
- * COLLECTION caps (`addon-widgets-source`, …) where many children register
6912
- * the same capName: routing by capName alone collapses every provider to
6913
- * the first child. Same deviceId-as-hint semantics as `resolveChildId`:
6914
- * a device-scoped descriptor matching `deviceId` OR a deviceId-less
6915
- * (singleton/collection) descriptor counts.
6916
- */
6917
- /**
6918
- * Is `childId` currently connected (has it completed its UDS handshake)?
6919
- * Coarser than {@link childProvides}: it answers "is the child reachable
6920
- * over UDS at all", regardless of which caps it has announced yet. Used by
6921
- * the route-mount fallback to decide between the handler-stripped
6922
- * `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
6923
- * cap proxy's `getRoutes()` (child not yet UDS-registered).
6924
- */
6925
- isChildKnown(childId) {
6926
- return this.children.has(childId);
6927
- }
6928
- childProvides(childId, capName, deviceId) {
6929
- const entry = this.children.get(childId);
6930
- if (!entry) return false;
6931
- if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
6932
- return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
6933
- }
6934
- /**
6935
- * Per-request options for a cap call: the cap method's declared `timeoutMs`
6936
- * when one exists, else `undefined` so the channel applies its default.
6937
- * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
6938
- */
6939
- capRequestOpts(input) {
6940
- const declared = this.capTimeoutMs?.(input.capName, input.method);
6941
- return typeof declared === "number" ? { timeoutMs: declared } : void 0;
7431
+ capRequestOpts(input) {
7432
+ const declared = this.capTimeoutMs?.(input.capName, input.method);
7433
+ return typeof declared === "number" ? { timeoutMs: declared } : void 0;
6942
7434
  }
6943
7435
  /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
6944
7436
  async callCapOnChild(childId, input) {
@@ -7096,6 +7588,14 @@ var LocalChildRegistry = class {
7096
7588
  onConnection(channel) {
7097
7589
  let childId = null;
7098
7590
  const incarnation = this.nextIncarnation++;
7591
+ channel.observeActions?.({
7592
+ label: capActionLabel,
7593
+ record: (label, reqBytes, resBytes) => {
7594
+ const peerId = childId ?? "(pre-register)";
7595
+ if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
7596
+ else this.actionCensus.record(peerId, label, reqBytes, resBytes);
7597
+ }
7598
+ });
7099
7599
  channel.onEvent((body) => {
7100
7600
  const msg = body;
7101
7601
  if (msg.kind === "event") {
@@ -7118,7 +7618,7 @@ var LocalChildRegistry = class {
7118
7618
  return;
7119
7619
  }
7120
7620
  });
7121
- channel.onRequest(async (body) => {
7621
+ channel.onRequest(async (body, payload) => {
7122
7622
  const msg = body;
7123
7623
  if (msg.kind === "register") {
7124
7624
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
@@ -7135,8 +7635,10 @@ var LocalChildRegistry = class {
7135
7635
  channel,
7136
7636
  caps: msg.caps,
7137
7637
  eventPatterns,
7138
- incarnation
7638
+ incarnation,
7639
+ rawForward: msg.rawForward === true
7139
7640
  });
7641
+ this.invalidateCapIndex();
7140
7642
  this.logPatternSet(msg.childId, eventPatterns);
7141
7643
  this.registeredHandler({
7142
7644
  childId: msg.childId,
@@ -7144,33 +7646,62 @@ var LocalChildRegistry = class {
7144
7646
  incarnation,
7145
7647
  ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
7146
7648
  });
7147
- return { ok: true };
7649
+ return {
7650
+ ok: true,
7651
+ rawForward: true
7652
+ };
7148
7653
  }
7149
7654
  if (msg.kind === "cap-call-out") {
7150
7655
  const out = msg;
7151
- const input = {
7656
+ const hints = out.hints ?? liftRoutingHints(out.args);
7657
+ const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
7658
+ const buildInput = () => ({
7152
7659
  capName: out.capName,
7153
7660
  method: out.method,
7154
- args: out.args,
7661
+ args: materialise(),
7155
7662
  ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
7156
7663
  ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
7157
7664
  ...out.native === true ? { native: true } : {},
7158
7665
  ...childId !== null ? { callerAddonId: childId } : {}
7159
- };
7160
- const pinnedNodeId = out.nodeId ?? extractNodeId(out.args);
7666
+ });
7667
+ const pinnedNodeId = out.nodeId ?? hints.nodeId;
7161
7668
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
7162
7669
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
7163
- const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
7670
+ const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
7164
7671
  const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
7165
7672
  this.recordCapUsage(childId, target, out.capName, out.method);
7673
+ const actionPeer = childId ?? "(pre-register)";
7674
+ const actionLabel = `${out.capName}.${out.method}`;
7166
7675
  if (target !== null) {
7167
7676
  if (!this.egressRoutedCaps.has(out.capName)) {
7168
7677
  this.egressRoutedCaps.add(out.capName);
7169
7678
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
7170
7679
  }
7171
- return this.callCap(input);
7680
+ const entry = this.children.get(target);
7681
+ if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
7682
+ this.forwardedRaw += 1;
7683
+ this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
7684
+ const forward = {
7685
+ kind: "cap-call",
7686
+ capName: out.capName,
7687
+ method: out.method,
7688
+ args: void 0,
7689
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
7690
+ };
7691
+ const declared = this.capTimeoutMs?.(out.capName, out.method);
7692
+ return entry.channel.request(forward, {
7693
+ ...typeof declared === "number" ? { timeoutMs: declared } : {},
7694
+ payload
7695
+ });
7696
+ }
7697
+ this.forwardedDecoded += 1;
7698
+ this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
7699
+ const input = buildInput();
7700
+ return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
7172
7701
  }
7173
- if (this.onUnownedCall !== void 0) return this.onUnownedCall(input);
7702
+ this.forwardedDecoded += 1;
7703
+ this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
7704
+ if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
7174
7705
  throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
7175
7706
  }
7176
7707
  if (msg.kind === "readiness-request") return {
@@ -7192,7 +7723,9 @@ var LocalChildRegistry = class {
7192
7723
  return;
7193
7724
  }
7194
7725
  this.children.delete(childId);
7726
+ this.invalidateCapIndex();
7195
7727
  this.childEventStats.delete(childId);
7728
+ this.actionCensus.forget(childId);
7196
7729
  this.loggedPatternSet.delete(childId);
7197
7730
  this.goneHandler(childId, incarnation);
7198
7731
  });
@@ -7200,6 +7733,11 @@ var LocalChildRegistry = class {
7200
7733
  };
7201
7734
  //#endregion
7202
7735
  //#region src/kernel/transport/local-child-client.ts
7736
+ /** Did the parent's `register` answer say it accepts payload-borne args? */
7737
+ function ackAcceptsRawForward(ack) {
7738
+ if (ack === null || typeof ack !== "object") return false;
7739
+ return ack.rawForward === true;
7740
+ }
7203
7741
  /**
7204
7742
  * Child side of the local UDS transport. Connects to its parent (hub or
7205
7743
  * agent), registers its cap manifest, and serves parent→child cap calls by
@@ -7255,6 +7793,13 @@ var LocalChildClient = class {
7255
7793
  latestCustomActions = null;
7256
7794
  /** Events and logs queued while the channel is not yet open. */
7257
7795
  pendingEmits = [];
7796
+ /**
7797
+ * Whether the parent acknowledged raw-forward at `register`. Until it has,
7798
+ * `callOut` sends args inline exactly as every child always did — a legacy
7799
+ * parent would decode a payload-borne call to `args: undefined` and run the
7800
+ * provider on nothing, silently. Re-read on every re-register.
7801
+ */
7802
+ parentRawForward = false;
7258
7803
  /** Handler for parent→child events. Registered via `onEvent`. */
7259
7804
  eventHandler = null;
7260
7805
  /**
@@ -7362,15 +7907,20 @@ var LocalChildClient = class {
7362
7907
  if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
7363
7908
  const client = createLocalTransport().createClient(this.options.nodeId);
7364
7909
  const channel = await client.connect();
7365
- channel.onRequest(async (body) => {
7910
+ channel.onRequest(async (body, payload) => {
7366
7911
  const msg = body;
7367
- if (msg.kind === "cap-call") return this.options.dispatch({
7368
- capName: msg.capName,
7369
- method: msg.method,
7370
- args: msg.args,
7371
- ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
7372
- ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
7373
- });
7912
+ if (msg.kind === "cap-call") {
7913
+ const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
7914
+ const result = await this.options.dispatch({
7915
+ capName: msg.capName,
7916
+ method: msg.method,
7917
+ args,
7918
+ ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
7919
+ ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
7920
+ });
7921
+ if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
7922
+ return result;
7923
+ }
7374
7924
  if (msg.kind === "addon-call") {
7375
7925
  if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
7376
7926
  return this.addonCallHandler({
@@ -7394,7 +7944,7 @@ var LocalChildClient = class {
7394
7944
  });
7395
7945
  const register = this.registerFrame(this.latestCaps);
7396
7946
  try {
7397
- await channel.request(register);
7947
+ this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
7398
7948
  } catch (err) {
7399
7949
  await client.close();
7400
7950
  throw err;
@@ -7404,105 +7954,564 @@ var LocalChildClient = class {
7404
7954
  this.flushPending(channel);
7405
7955
  for (const cb of this.connectedHandlers) cb();
7406
7956
  }
7407
- /** Flush buffered pre-start emits over the now-open channel. */
7408
- flushPending(channel) {
7409
- for (const item of this.pendingEmits) channel.emit(item.msg);
7410
- this.pendingEmits.length = 0;
7957
+ /** Flush buffered pre-start emits over the now-open channel. */
7958
+ flushPending(channel) {
7959
+ for (const item of this.pendingEmits) channel.emit(item.msg);
7960
+ this.pendingEmits.length = 0;
7961
+ }
7962
+ /**
7963
+ * Re-send the cap manifest to the parent, atomically replacing the child's
7964
+ * registered descriptor set. Call this after device-restore so newly
7965
+ * registered native (device-scoped) caps become routable over UDS.
7966
+ *
7967
+ * Safe to call before `start()`: the new set is buffered and `start()` sends
7968
+ * it (device-restore can race ahead of the UDS connect). After `start()`,
7969
+ * the set is sent immediately.
7970
+ */
7971
+ async updateCaps(caps, customActions) {
7972
+ this.latestCaps = caps;
7973
+ if (customActions !== void 0) this.latestCustomActions = customActions;
7974
+ if (this.channel === null) return;
7975
+ this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
7976
+ }
7977
+ /**
7978
+ * The register frame: caps, plus everything a re-register must atomically
7979
+ * re-carry so no separate re-sync can be missed — the subscription union
7980
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7981
+ * custom-action catalogs (omitted until the init loop produced them).
7982
+ */
7983
+ registerFrame(caps) {
7984
+ return {
7985
+ kind: "register",
7986
+ childId: this.options.childId,
7987
+ caps,
7988
+ ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7989
+ ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
7990
+ rawForward: true
7991
+ };
7992
+ }
7993
+ /**
7994
+ * Fire-and-forget: send a system event to the parent for forwarding to the
7995
+ * hub event bus. Safe to call before `start()` — events are buffered and
7996
+ * flushed on connect.
7997
+ */
7998
+ emitEvent(event) {
7999
+ const msg = {
8000
+ kind: "event",
8001
+ event
8002
+ };
8003
+ if (this.channel !== null) this.channel.emit(msg);
8004
+ else this.pendingEmits.push({
8005
+ kind: "event",
8006
+ msg
8007
+ });
8008
+ }
8009
+ /**
8010
+ * Fire-and-forget: send a structured log entry to the parent. Safe to call
8011
+ * before `start()` — log entries are buffered and flushed on connect.
8012
+ */
8013
+ sendLog(entry) {
8014
+ const msg = {
8015
+ kind: "log",
8016
+ ...entry
8017
+ };
8018
+ if (this.channel !== null) this.channel.emit(msg);
8019
+ else this.pendingEmits.push({
8020
+ kind: "log",
8021
+ msg
8022
+ });
8023
+ }
8024
+ /**
8025
+ * Request the current readiness snapshot from the parent. Returns the
8026
+ * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
8027
+ * Requires `start()` to have been called; throws with a clear message if not.
8028
+ */
8029
+ async requestReadinessSnapshot() {
8030
+ if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
8031
+ return (await this.channel.request({ kind: "readiness-request" })).records;
8032
+ }
8033
+ /**
8034
+ * Ask the parent to execute a cap call this child does NOT own. The parent
8035
+ * routes to the owning local sibling over UDS, or — if no sibling owns it —
8036
+ * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
8037
+ * production). Throws if called before `start()`.
8038
+ */
8039
+ async callOut(input) {
8040
+ if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
8041
+ const routing = {
8042
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
8043
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
8044
+ ...input.native === true ? { native: true } : {}
8045
+ };
8046
+ if (this.parentRawForward && input.args !== void 0) {
8047
+ const hints = liftRoutingHints(input.args);
8048
+ const msg = {
8049
+ kind: "cap-call-out",
8050
+ capName: input.capName,
8051
+ method: input.method,
8052
+ ...Object.keys(hints).length > 0 ? { hints } : {},
8053
+ ...routing
8054
+ };
8055
+ const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
8056
+ return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
8057
+ }
8058
+ const msg = {
8059
+ kind: "cap-call-out",
8060
+ capName: input.capName,
8061
+ method: input.method,
8062
+ args: input.args,
8063
+ ...routing
8064
+ };
8065
+ return this.channel.request(msg);
8066
+ }
8067
+ /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
8068
+ async close() {
8069
+ await this.client?.close();
8070
+ this.client = null;
8071
+ this.channel = null;
8072
+ }
8073
+ };
8074
+ //#endregion
8075
+ //#region src/kernel/transport/cap-route.ts
8076
+ function buildMessage(capName, method, detail) {
8077
+ const call = method !== void 0 ? `${capName}.${method}` : capName;
8078
+ const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
8079
+ const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
8080
+ const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
8081
+ return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
8082
+ }
8083
+ var CapRouteError = class extends Error {
8084
+ reason;
8085
+ nodeId;
8086
+ rejected;
8087
+ /**
8088
+ * @param cause Optional original error that triggered this routing failure.
8089
+ * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
8090
+ * Dispatchers wrapping transport errors MUST pass the original.
8091
+ */
8092
+ constructor(capName, method, detail, cause) {
8093
+ super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
8094
+ this.name = "CapRouteError";
8095
+ this.reason = detail.reason;
8096
+ this.nodeId = detail.nodeId;
8097
+ this.rejected = detail.rejected;
8098
+ }
8099
+ };
8100
+ /**
8101
+ * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
8102
+ *
8103
+ * Precedence (explicit nodeId path):
8104
+ * 1. hub-in-process — hub node + hubInProcessProvides
8105
+ * 2. hub-local-uds — hub node + hubLocalChildProvides
8106
+ * 3. node-offline → CapRouteError{reason:'node-offline'}
8107
+ * 4. agent-child-forward — node is an agent AND nodeKnowsCap
8108
+ * 5. remote-moleculer — any other online non-agent node
8109
+ *
8110
+ * Singleton path (no nodeId):
8111
+ * 1. hub-in-process (hub provides in-process)
8112
+ * 2. hub-local-uds (a hub-local UDS child provides it)
8113
+ * 3. remote-moleculer (any online, non-hub node that knows it)
8114
+ * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
8115
+ *
8116
+ * PURE: no side effects, no async, no broker/registry imports.
8117
+ */
8118
+ function classifyCapRoute(capName, opts, snapshot) {
8119
+ const rejected = [];
8120
+ if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
8121
+ return classifySingleton(capName, opts.deviceId, snapshot, rejected);
8122
+ }
8123
+ function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
8124
+ if (nodeId === snap.hubNodeId) {
8125
+ const residentProvides = snap.hubResidentProvides;
8126
+ const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
8127
+ const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
8128
+ if (providesOnHub(capName)) {
8129
+ const ref = getRef?.(capName) ?? null;
8130
+ if (ref !== null) return {
8131
+ kind: "hub-in-process",
8132
+ capName,
8133
+ ref
8134
+ };
8135
+ rejected.push({
8136
+ kind: "hub-in-process",
8137
+ why: "provider ref not available in snapshot"
8138
+ });
8139
+ throw new CapRouteError(capName, void 0, {
8140
+ reason: "no-provider",
8141
+ nodeId,
8142
+ rejected
8143
+ });
8144
+ }
8145
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
8146
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
8147
+ if (childId !== null) return {
8148
+ kind: "hub-local-uds",
8149
+ capName,
8150
+ childId
8151
+ };
8152
+ rejected.push({
8153
+ kind: "hub-local-uds",
8154
+ why: "child id not resolvable from snapshot"
8155
+ });
8156
+ throw new CapRouteError(capName, void 0, {
8157
+ reason: "no-provider",
8158
+ nodeId,
8159
+ rejected
8160
+ });
8161
+ }
8162
+ rejected.push({
8163
+ kind: "hub-in-process",
8164
+ why: "hub does not provide this cap in-process"
8165
+ });
8166
+ rejected.push({
8167
+ kind: "hub-local-uds",
8168
+ why: "no hub-local child provides this cap"
8169
+ });
8170
+ throw new CapRouteError(capName, void 0, {
8171
+ reason: "no-provider",
8172
+ nodeId,
8173
+ rejected
8174
+ });
8175
+ }
8176
+ if (!snap.nodeOnline(nodeId)) {
8177
+ rejected.push({
8178
+ kind: "remote-moleculer",
8179
+ why: `node ${nodeId} is offline`
8180
+ });
8181
+ throw new CapRouteError(capName, void 0, {
8182
+ reason: "node-offline",
8183
+ nodeId,
8184
+ rejected
8185
+ });
8186
+ }
8187
+ if (snap.nodeIsAgent(nodeId)) {
8188
+ if (snap.nodeKnowsCap(nodeId, capName)) return {
8189
+ kind: "agent-child-forward",
8190
+ capName,
8191
+ agentNodeId: nodeId,
8192
+ childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
8193
+ };
8194
+ rejected.push({
8195
+ kind: "agent-child-forward",
8196
+ why: `agent ${nodeId} does not know cap ${capName}`
8197
+ });
8198
+ throw new CapRouteError(capName, void 0, {
8199
+ reason: "no-provider",
8200
+ nodeId,
8201
+ rejected
8202
+ });
8203
+ }
8204
+ return {
8205
+ kind: "remote-moleculer",
8206
+ capName,
8207
+ nodeId
8208
+ };
8209
+ }
8210
+ function classifySingleton(capName, deviceId, snap, rejected) {
8211
+ if (snap.hubInProcessProvides(capName)) {
8212
+ const ref = snap.getInProcessProviderRef?.(capName) ?? null;
8213
+ if (ref !== null) return {
8214
+ kind: "hub-in-process",
8215
+ capName,
8216
+ ref
8217
+ };
8218
+ rejected.push({
8219
+ kind: "hub-in-process",
8220
+ why: "provider ref not available in snapshot"
8221
+ });
8222
+ } else rejected.push({
8223
+ kind: "hub-in-process",
8224
+ why: "hub does not provide this cap in-process"
8225
+ });
8226
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
8227
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
8228
+ if (childId !== null) return {
8229
+ kind: "hub-local-uds",
8230
+ capName,
8231
+ childId
8232
+ };
8233
+ rejected.push({
8234
+ kind: "hub-local-uds",
8235
+ why: "child id not resolvable from snapshot"
8236
+ });
8237
+ } else rejected.push({
8238
+ kind: "hub-local-uds",
8239
+ why: "no hub-local child provides this cap"
8240
+ });
8241
+ const knownNodes = snap.listKnownNodeIds?.() ?? [];
8242
+ for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
8243
+ kind: "remote-moleculer",
8244
+ capName,
8245
+ nodeId
8246
+ };
8247
+ rejected.push({
8248
+ kind: "remote-moleculer",
8249
+ why: "no online remote node knows this cap"
8250
+ });
8251
+ throw new CapRouteError(capName, void 0, {
8252
+ reason: "no-provider",
8253
+ rejected
8254
+ });
8255
+ }
8256
+ //#endregion
8257
+ //#region src/kernel/moleculer/resilient-cap-call.ts
8258
+ /** Moleculer error `type` values meaning "the service is not (yet) routable". */
8259
+ var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
8260
+ /** Default ceiling for the discovery wait — fail-fast past this. */
8261
+ var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
8262
+ function isDiscoveryError(err) {
8263
+ if (typeof err !== "object" || err === null) return false;
8264
+ const type = err.type;
8265
+ return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
8266
+ }
8267
+ /**
8268
+ * Call a Moleculer action; on a service-discovery error, wait for the
8269
+ * named service to be discovered and retry the call exactly once.
8270
+ */
8271
+ async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
8272
+ try {
8273
+ return await broker.call(action, params, opts);
8274
+ } catch (err) {
8275
+ if (!isDiscoveryError(err)) throw err;
8276
+ await broker.waitForServices([serviceName], discoveryTimeoutMs);
8277
+ return await broker.call(action, params, opts);
8278
+ }
8279
+ }
8280
+ //#endregion
8281
+ //#region src/kernel/transport/cap-route-resolver.ts
8282
+ /** The Moleculer service name that Task 6's agent registers. */
8283
+ var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
8284
+ /** The Moleculer action (service.action) for agent cap forwarding. */
8285
+ var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
8286
+ /** Default timeout for remote Moleculer cap calls (ms). */
8287
+ var REMOTE_CALL_TIMEOUT_MS = 6e4;
8288
+ function extractDeviceId(args) {
8289
+ if (args === null || typeof args !== "object") return void 0;
8290
+ const raw = Reflect.get(args, "deviceId");
8291
+ return typeof raw === "number" ? raw : void 0;
8292
+ }
8293
+ /**
8294
+ * Extract an inline `nodeId` string from a cap call's args — the per-call node
8295
+ * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
8296
+ * device-scoped caps carry `deviceId`, and the way the generated cap-router on
8297
+ * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
8298
+ *
8299
+ * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
8300
+ * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
8301
+ * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
8302
+ * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
8303
+ * methods that don't declare `nodeId` simply ignore the extra field (they
8304
+ * destructure the fields they need); methods that DO declare it (e.g.
8305
+ * `getEngineProvisioning({nodeId})`) still receive it unchanged.
8306
+ */
8307
+ function extractNodeId(args) {
8308
+ if (args === null || typeof args !== "object") return void 0;
8309
+ const raw = Reflect.get(args, "nodeId");
8310
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
8311
+ }
8312
+ /**
8313
+ * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
8314
+ * destructures what it needs and ignores the rest" convention
8315
+ * {@link extractNodeId} already relies on for the inline `nodeId` pin.
8316
+ * `undefined` (the overwhelmingly common case: no caller hint, or the census
8317
+ * consuming it is disarmed) returns `args` unchanged — no allocation. A
8318
+ * non-object `args` (or `null`/array) is returned unchanged too: there is
8319
+ * nowhere to attach an extra key without changing the payload's shape, and a
8320
+ * provider taking a bare value never reads `callerAddonId` off it anyway.
8321
+ */
8322
+ function withCallerAddonId(args, callerAddonId) {
8323
+ if (callerAddonId === void 0) return args;
8324
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
8325
+ return {
8326
+ ...args,
8327
+ callerAddonId
8328
+ };
8329
+ }
8330
+ var CapRouteResolver = class {
8331
+ hubNodeId;
8332
+ broker;
8333
+ hubLocalRegistry;
8334
+ nodeAuthority;
8335
+ inProcessProviders;
8336
+ hubResidentProviders;
8337
+ capTimeoutMs;
8338
+ snapshot;
8339
+ constructor(deps) {
8340
+ this.hubNodeId = deps.hubNodeId;
8341
+ this.broker = deps.broker;
8342
+ this.hubLocalRegistry = deps.hubLocalRegistry;
8343
+ this.nodeAuthority = deps.nodeAuthority;
8344
+ this.inProcessProviders = deps.inProcessProviders;
8345
+ this.hubResidentProviders = deps.hubResidentProviders;
8346
+ this.capTimeoutMs = deps.capTimeoutMs;
8347
+ this.snapshot = this.buildSnapshot();
8348
+ }
8349
+ /** Per-call RPC timeout: the cap method's declared override, else the default. */
8350
+ callTimeout(capName, method) {
8351
+ return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
7411
8352
  }
7412
- /**
7413
- * Re-send the cap manifest to the parent, atomically replacing the child's
7414
- * registered descriptor set. Call this after device-restore so newly
7415
- * registered native (device-scoped) caps become routable over UDS.
7416
- *
7417
- * Safe to call before `start()`: the new set is buffered and `start()` sends
7418
- * it (device-restore can race ahead of the UDS connect). After `start()`,
7419
- * the set is sent immediately.
7420
- */
7421
- async updateCaps(caps, customActions) {
7422
- this.latestCaps = caps;
7423
- if (customActions !== void 0) this.latestCustomActions = customActions;
7424
- if (this.channel === null) return;
7425
- await this.channel.request(this.registerFrame(caps));
8353
+ resolveCapRoute(capName, opts) {
8354
+ return classifyCapRoute(capName, opts, this.snapshot);
7426
8355
  }
7427
8356
  /**
7428
- * The register frame: caps, plus everything a re-register must atomically
7429
- * re-carry so no separate re-sync can be missed — the subscription union
7430
- * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7431
- * custom-action catalogs (omitted until the init loop produced them).
8357
+ * Resolve the hub-local-uds route for a cap owned by a forked hub-local
8358
+ * child, IGNORING any in-hub provider registered for the same cap name.
8359
+ *
8360
+ * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
8361
+ * provider (e.g. a wrapper) is registered for the cap, the route always
8362
+ * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
8363
+ * reached. That is correct for the generic dispatch path (the wrapper is the
8364
+ * active provider), but WRONG for the native-cap fallback
8365
+ * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
8366
+ * the forked vendor child so a wrapper can delegate to it. A wrapper cap
8367
+ * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
8368
+ * itself, the native is never invoked, and the wrapper silently falls
8369
+ * through to its secondary strategy.
8370
+ *
8371
+ * This method consults ONLY the hub-local-child authority
8372
+ * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
8373
+ * it returns the native child route regardless of any in-process shadow.
8374
+ * Returns null when no hub-local child owns `(capName, deviceId)` — the
8375
+ * caller then falls through to its remote-resolution branch.
7432
8376
  */
7433
- registerFrame(caps) {
8377
+ resolveHubLocalUdsRoute(capName, deviceId) {
8378
+ if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
8379
+ const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
8380
+ if (childId === null) return null;
7434
8381
  return {
7435
- kind: "register",
7436
- childId: this.options.childId,
7437
- caps,
7438
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7439
- ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
7440
- };
7441
- }
7442
- /**
7443
- * Fire-and-forget: send a system event to the parent for forwarding to the
7444
- * hub event bus. Safe to call before `start()` — events are buffered and
7445
- * flushed on connect.
7446
- */
7447
- emitEvent(event) {
7448
- const msg = {
7449
- kind: "event",
7450
- event
8382
+ kind: "hub-local-uds",
8383
+ capName,
8384
+ childId
7451
8385
  };
7452
- if (this.channel !== null) this.channel.emit(msg);
7453
- else this.pendingEmits.push({
7454
- kind: "event",
7455
- msg
7456
- });
7457
8386
  }
7458
8387
  /**
7459
- * Fire-and-forget: send a structured log entry to the parent. Safe to call
7460
- * before `start()` — log entries are buffered and flushed on connect.
8388
+ * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
8389
+ * the addonId of the provider the CALLER already resolved (e.g.
8390
+ * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
8391
+ * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
8392
+ * route already encodes the provider in its action name
8393
+ * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
8394
+ * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
8395
+ * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
8396
+ * `dispatch`.
8397
+ * @param callerAddonId Optional out-of-band CALLER-identity hint — the
8398
+ * addon that ORIGINATED the call, distinct from `addonId` above (which
8399
+ * names the resolved PROVIDER, not the caller). Set by
8400
+ * `onUnownedCall` from `CapCallInput.callerAddonId`
8401
+ * (`LocalChildRegistry` stamped it off the registered `childId`). Only
8402
+ * the `hub-in-process` branch consumes it — merged into `args` so a
8403
+ * diagnostic tool deep in that provider (e.g. device-manager's
8404
+ * fleet-read census) can read it without every provider method changing
8405
+ * signature, the same "extra field the provider destructures or
8406
+ * ignores" convention `nodeId` already uses on this path.
7461
8407
  */
7462
- sendLog(entry) {
7463
- const msg = {
7464
- kind: "log",
7465
- ...entry
7466
- };
7467
- if (this.channel !== null) this.channel.emit(msg);
7468
- else this.pendingEmits.push({
7469
- kind: "log",
7470
- msg
7471
- });
8408
+ async dispatch(route, method, args, addonId, callerAddonId) {
8409
+ try {
8410
+ return await this.dispatchInner(route, method, args, addonId, callerAddonId);
8411
+ } catch (err) {
8412
+ if (err instanceof CapRouteError) throw err;
8413
+ const nodeId = this.routeNodeId(route);
8414
+ const cause = err instanceof Error ? err : new Error(String(err));
8415
+ throw new CapRouteError(route.capName, method, {
8416
+ reason: "transport-failed",
8417
+ nodeId,
8418
+ rejected: [{
8419
+ kind: route.kind,
8420
+ why: cause.message
8421
+ }]
8422
+ }, cause);
8423
+ }
7472
8424
  }
7473
8425
  /**
7474
- * Request the current readiness snapshot from the parent. Returns the
7475
- * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
7476
- * Requires `start()` to have been called; throws with a clear message if not.
8426
+ * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
8427
+ * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
7477
8428
  */
7478
- async requestReadinessSnapshot() {
7479
- if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
7480
- return (await this.channel.request({ kind: "readiness-request" })).records;
8429
+ async dispatchInner(route, method, args, addonId, callerAddonId) {
8430
+ switch (route.kind) {
8431
+ case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
8432
+ case "hub-local-uds": {
8433
+ const registry = this.hubLocalRegistry;
8434
+ if (registry === null) throw new CapRouteError(route.capName, method, {
8435
+ reason: "no-provider",
8436
+ rejected: [{
8437
+ kind: "hub-local-uds",
8438
+ why: "UDS registry not available"
8439
+ }]
8440
+ });
8441
+ const deviceId = extractDeviceId(args);
8442
+ const input = {
8443
+ capName: route.capName,
8444
+ method,
8445
+ args,
8446
+ ...deviceId !== void 0 ? { deviceId } : {}
8447
+ };
8448
+ return registry.callCapOnChild(route.childId, input);
8449
+ }
8450
+ case "remote-moleculer": {
8451
+ const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
8452
+ if (addonId === null) throw new CapRouteError(route.capName, method, {
8453
+ reason: "no-provider",
8454
+ nodeId: route.nodeId,
8455
+ rejected: [{
8456
+ kind: "remote-moleculer",
8457
+ why: `no addonId known for ${route.nodeId}/${route.capName}`
8458
+ }]
8459
+ });
8460
+ const deviceId = extractDeviceId(args);
8461
+ const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
8462
+ const action = capActionName(addonId, route.capName, method, isNative);
8463
+ return callWithServiceDiscovery(this.broker, addonId, action, args, {
8464
+ nodeID: route.nodeId,
8465
+ timeout: this.callTimeout(route.capName, method)
8466
+ });
8467
+ }
8468
+ case "agent-child-forward": {
8469
+ const deviceId = extractDeviceId(args);
8470
+ const params = {
8471
+ capName: route.capName,
8472
+ method,
8473
+ args,
8474
+ ...route.childId !== void 0 ? { childId: route.childId } : {},
8475
+ ...deviceId !== void 0 ? { deviceId } : {},
8476
+ ...addonId !== void 0 ? { addonId } : {}
8477
+ };
8478
+ return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
8479
+ nodeID: route.agentNodeId,
8480
+ timeout: this.callTimeout(route.capName, method)
8481
+ });
8482
+ }
8483
+ }
7481
8484
  }
7482
- /**
7483
- * Ask the parent to execute a cap call this child does NOT own. The parent
7484
- * routes to the owning local sibling over UDS, or — if no sibling owns it —
7485
- * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
7486
- * production). Throws if called before `start()`.
7487
- */
7488
- async callOut(input) {
7489
- if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
7490
- const msg = {
7491
- kind: "cap-call-out",
7492
- capName: input.capName,
7493
- method: input.method,
7494
- args: input.args,
7495
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
7496
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
7497
- ...input.native === true ? { native: true } : {}
8485
+ buildSnapshot() {
8486
+ const hubLocalRegistry = this.hubLocalRegistry;
8487
+ const nodeAuthority = this.nodeAuthority;
8488
+ const inProcessProviders = this.inProcessProviders;
8489
+ const hubResidentProviders = this.hubResidentProviders;
8490
+ return {
8491
+ hubNodeId: this.hubNodeId,
8492
+ hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
8493
+ ...hubResidentProviders !== void 0 ? {
8494
+ hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
8495
+ getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
8496
+ } : {},
8497
+ hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
8498
+ nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
8499
+ nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
8500
+ nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
8501
+ listKnownNodeIds: () => nodeAuthority.listNodeIds(),
8502
+ getInProcessProviderRef: (cap) => inProcessProviders(cap),
8503
+ getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
8504
+ getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
7498
8505
  };
7499
- return this.channel.request(msg);
7500
8506
  }
7501
- /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
7502
- async close() {
7503
- await this.client?.close();
7504
- this.client = null;
7505
- this.channel = null;
8507
+ /** Extract a nodeId string from a route for error reporting, or undefined. */
8508
+ routeNodeId(route) {
8509
+ switch (route.kind) {
8510
+ case "hub-in-process": return this.hubNodeId;
8511
+ case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
8512
+ case "remote-moleculer": return route.nodeId;
8513
+ case "agent-child-forward": return route.agentNodeId;
8514
+ }
7506
8515
  }
7507
8516
  };
7508
8517
  //#endregion
@@ -10174,4 +11183,4 @@ async function defaultRun(command, args) {
10174
11183
  await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
10175
11184
  }
10176
11185
  //#endregion
10177
- export { buildUdsNativeCapProxy as $, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as $t, createUdsLoggerWithControl as A, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, createV8Reclaimer as Bt, ipcParentLink as C, EVENT_PLANE_TOP_N as Cn, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, formatEventPlane as Dn, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, diffEventPlane as En, HEAP_RECLAIM_TRIGGER_MB as Et, AGENT_CAP_FWD_SERVICE as F, RUNNER_RSS_BUDGET_ENV as Ft, createLocalTransport as G, reclaimIntervalMs as Gt, PARENT_ROUTED_PROVIDER_ADDON_ID as H, hubMainRssBudget as Ht, CapRouteResolver as I, ZERO_LOOP_DELAY as It, SocketChannel as J, startRunnerHeapWatch as Jt, UdsLocalTransportClient as K, shouldReclaim as Kt, CapRouteError as L, buildHeapSample as Lt, LocalChildRegistry as M, RSS_BUDGET_REANNOUNCE_MIN_MS as Mt, UDS_NO_ROUTE_PREFIX as N, RSS_BUDGET_RELEASE_RATIO as Nt, udsChildLogToWorkerEntry as O, HEAP_WATCH_WARN_RATIO as Ot, AGENT_CAP_FWD_ACTION as P, RUNNER_HEAP_WATCH_INTERVAL_MS as Pt, buildNativeCapProxy as Q, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as Qt, classifyCapRoute as R, createDeferredHeapWatchSink as Rt, buildLinkChain as S, socketDirectionMessages as Sn, runNpm as St, createParentUnownedCallHandler as T, createEventPlaneReader as Tn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Tt, __resetCapUsageRegistryForTests as U, nextRssBudgetState as Ut, CapUsageRegistry as V, describeRss as Vt, getCapUsageRegistry as W, parseRssBudgetMb as Wt, FrameDecoder as X, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as Xt, localEndpointPath as Y, strandedMb as Yt, encodeFrame as Z, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as Zt, AddonDepsManager as _, EMPTY_SOCKET_DIRECTION as _n, CapabilityUnavailableError as _t, createUdsAddonContext as a, HEAP_SPACE_REPORT_MIN_MB as an, createAddonService as at, brokerCallForCap as b, sampleSocketDirection as bn, resolveAddonClass as bt, getOrInitReadinessRegistryForClient as c, heapSpaceField as cn, capActionName as ct, clusterEventTopic as d, EMPTY_SOCKET_PLANE_BASELINE as dn, capServiceName as dt, createArrayBufferCensusGate as en, createBrokerDeviceManagerApi as et, getBrokerEventBus as f, SOCKET_PLANE_TOP_N as fn, parseCapAction as ft, subscribePassthrough as g, formatSocketPlane as gn, CapabilityHandle as gt, setNodeEventInterest as h, diffSocketPlane as hn, DeviceRegistry as ht, createAddonContext as i, DECISIVE_HEAP_SPACES as in, setWorkerNativeCapsChangeListener as it, LocalChildClient as j, HUB_RSS_BUDGET_ENV as jt, createUdsLogger as k, HUB_MAIN_HEAP_WATCH_LABEL as kt, setHubConnected as l, readHeapSpaces as ln, capActionSuffix as lt, registerEventBusService as m, createSocketPlaneReader as mn, serializeTypedArrays as mt, installManifestPythonDeps as n, formatArrayBufferCensus as nn, getWorkerNativeCapSnapshot as nt, getWorkerDeviceRegistry as o, emitHeapDiagnosticReport as on, validateProviderRegistrations as ot, getMoleculerEventStats as p, createSocketPlaneMeter as pn, deserializeTypedArrays as pt, UdsLocalTransportServer as q, startHeapWatch as qt, adaptBrokerToCluster as r, isArrayBufferCensusReport as rn, mountNativeCapService as rt, getOrInitReadinessRegistry as s, formatHeapSpaces as sn, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestSystemDeps as t, createInspectorArrayBufferCensus as tn, getWorkerNativeCapProvider as tt, EVENT_TOPIC_PREFIX as u, selectReportedSpaces as un, capBareAction as ut, createKernelHwAccel as v, createSocketDirectionCounters as vn, copyBundledNativeModules as vt, localProviderLink as w, createEventPlaneMeter as wn, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, brokerTransportLink as x, socketDirectionBytes as xn, resolveNpmInvocation as xt, resolveHwAccel as y, recordSocketFrame as yn, installManifestNativeDeps as yt, callWithServiceDiscovery as z, createLoopDelayMeter as zt };
11186
+ export { buildUdsNativeCapProxy as $, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as $t, createUdsLoggerWithControl as A, sampleSocketDirection as An, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, EVENT_PLANE_TOP_N as Bn, createV8Reclaimer as Bt, ipcParentLink as C, createSocketPlaneReader as Cn, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, EMPTY_SOCKET_DIRECTION as Dn, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, readSocketQueues as En, HEAP_RECLAIM_TRIGGER_MB as Et, CapRouteError as F, EMPTY_CAP_ACTION_READING as Fn, RUNNER_RSS_BUDGET_ENV as Ft, createLocalTransport as G, reclaimIntervalMs as Gt, PARENT_ROUTED_PROVIDER_ADDON_ID as H, createEventPlaneReader as Hn, hubMainRssBudget as Ht, classifyCapRoute as I, MAX_CAP_ACTIONS_PER_PEER as In, ZERO_LOOP_DELAY as It, SocketChannel as J, startRunnerHeapWatch as Jt, UdsLocalTransportClient as K, shouldReclaim as Kt, LocalChildClient as L, capActionLabel as Ln, buildHeapSample as Lt, AGENT_CAP_FWD_SERVICE as M, socketDirectionMessages as Mn, RSS_BUDGET_REANNOUNCE_MIN_MS as Mt, CapRouteResolver as N, CAP_ACTION_OVERFLOW_LABEL as Nn, RSS_BUDGET_RELEASE_RATIO as Nt, udsChildLogToWorkerEntry as O, createSocketDirectionCounters as On, HEAP_WATCH_WARN_RATIO as Ot, callWithServiceDiscovery as P, CAP_ACTION_UNLABELLED as Pn, RUNNER_HEAP_WATCH_INTERVAL_MS as Pt, buildNativeCapProxy as Q, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as Qt, LocalChildRegistry as R, createCapActionCensus as Rn, createDeferredHeapWatchSink as Rt, buildLinkChain as S, createSocketPlaneMeter as Sn, runNpm as St, createParentUnownedCallHandler as T, formatSocketPlane as Tn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Tt, __resetCapUsageRegistryForTests as U, diffEventPlane as Un, nextRssBudgetState as Ut, CapUsageRegistry as V, createEventPlaneMeter as Vn, describeRss as Vt, getCapUsageRegistry as W, formatEventPlane as Wn, parseRssBudgetMb as Wt, FrameDecoder as X, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as Xt, localEndpointPath as Y, strandedMb as Yt, encodeFrame as Z, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as Zt, AddonDepsManager as _, createMeshQueueReader as _n, CapabilityUnavailableError as _t, createUdsAddonContext as a, HEAP_SPACE_REPORT_MIN_MB as an, createAddonService as at, brokerCallForCap as b, SOCKET_PLANE_TOP_N as bn, resolveAddonClass as bt, getOrInitReadinessRegistryForClient as c, heapSpaceField as cn, capActionName as ct, clusterEventTopic as d, OUTBOUND_QUEUE_COOLDOWN_MS as dn, capServiceName as dt, createArrayBufferCensusGate as en, createBrokerDeviceManagerApi as et, getBrokerEventBus as f, OUTBOUND_QUEUE_SUSTAIN_MS as fn, parseCapAction as ft, subscribePassthrough as g, MESH_QUEUE_TOP_N as gn, CapabilityHandle as gt, setNodeEventInterest as h, formatOutboundQueueVerdict as hn, DeviceRegistry as ht, createAddonContext as i, DECISIVE_HEAP_SPACES as in, setWorkerNativeCapsChangeListener as it, AGENT_CAP_FWD_ACTION as j, socketDirectionBytes as jn, HUB_RSS_BUDGET_ENV as jt, createUdsLogger as k, recordSocketFrame as kn, HUB_MAIN_HEAP_WATCH_LABEL as kt, setHubConnected as l, readHeapSpaces as ln, capActionSuffix as lt, registerEventBusService as m, createOutboundQueueGate as mn, serializeTypedArrays as mt, installManifestPythonDeps as n, formatArrayBufferCensus as nn, getWorkerNativeCapSnapshot as nt, getWorkerDeviceRegistry as o, emitHeapDiagnosticReport as on, validateProviderRegistrations as ot, getMoleculerEventStats as p, OUTBOUND_QUEUE_THRESHOLD_BYTES as pn, deserializeTypedArrays as pt, UdsLocalTransportServer as q, startHeapWatch as qt, adaptBrokerToCluster as r, isArrayBufferCensusReport as rn, mountNativeCapService as rt, getOrInitReadinessRegistry as s, formatHeapSpaces as sn, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestSystemDeps as t, createInspectorArrayBufferCensus as tn, getWorkerNativeCapProvider as tt, EVENT_TOPIC_PREFIX as u, selectReportedSpaces as un, capBareAction as ut, createKernelHwAccel as v, formatMeshQueue as vn, copyBundledNativeModules as vt, localProviderLink as w, diffSocketPlane as wn, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, brokerTransportLink as x, SOCKET_QUEUE_TOP_N as xn, resolveNpmInvocation as xt, resolveHwAccel as y, EMPTY_SOCKET_PLANE_BASELINE as yn, installManifestNativeDeps as yt, UDS_NO_ROUTE_PREFIX as z, diffCapActionCensus as zn, createLoopDelayMeter as zt };