@camstack/system 1.2.218 → 1.2.219

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 (93) 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-CsONUlhC.mjs → custom-action-registry-B6gOtUWA.mjs} +1 -1
  60. package/dist/{custom-action-registry-BDwM5qAK.js → custom-action-registry-DKWhaWL1.js} +1 -1
  61. package/dist/{dist-BOlMCOeT.js → dist-B17W9ngu.js} +2 -2
  62. package/dist/{dist-DigmGsQS.mjs → dist-Cyz5_i7z.mjs} +2 -2
  63. package/dist/index.d.ts +6 -1
  64. package/dist/index.js +31 -36
  65. package/dist/index.mjs +23 -38
  66. package/dist/kernel/heap-watch.d.ts +24 -1
  67. package/dist/kernel/moleculer/mesh-queue-report.d.ts +51 -0
  68. package/dist/kernel/outbound-queue-watch.d.ts +94 -0
  69. package/dist/kernel/socket-plane-report.d.ts +75 -4
  70. package/dist/kernel/transport/cap-routing-hints.d.ts +21 -0
  71. package/dist/kernel/transport/child-cap-protocol.d.ts +28 -2
  72. package/dist/kernel/transport/frame-codec.d.ts +49 -0
  73. package/dist/kernel/transport/local-child-client.d.ts +7 -0
  74. package/dist/kernel/transport/local-child-registry.d.ts +36 -1
  75. package/dist/kernel/transport/local-transport.d.ts +45 -2
  76. package/dist/kernel/transport/socket-channel.d.ts +19 -1
  77. package/dist/{manifest-system-deps-D--wyMD9.mjs → manifest-system-deps-C7dihwWT.mjs} +1210 -682
  78. package/dist/{manifest-system-deps-Bo92lg5u.js → manifest-system-deps-g_nK0xsF.js} +1252 -664
  79. package/dist/process/proc-stat.d.ts +75 -0
  80. package/dist/process/resource-monitor.d.ts +9 -1
  81. package/dist/{resource-monitor-CdnzxBLP.js → resource-monitor-LVE1BoLs.js} +42 -23
  82. package/dist/{resource-monitor-BWmQ5i-o.mjs → resource-monitor-Vp0bPV3U.mjs} +42 -22
  83. package/dist/{retired-settings-keys-vK_U-CG2.mjs → retired-settings-keys-BCYUrdsA.mjs} +1 -1
  84. package/dist/{retired-settings-keys-xEAlYnv7.js → retired-settings-keys-CjvB7Etx.js} +1 -1
  85. package/dist/wal-checkpoint-policy-CAcg63o-.mjs +92 -0
  86. package/dist/wal-checkpoint-policy-CCJZngds.js +127 -0
  87. package/dist/wal-checkpoint-worker.js +138 -0
  88. package/dist/wal-checkpoint-worker.mjs +137 -0
  89. package/package.json +1 -1
  90. package/dist/{event-loop-stall-monitor-DM9OAzy8.js → event-loop-stall-monitor-CAsHdMwP.js} +1 -1
  91. package/dist/{event-loop-stall-monitor-B3CFd9tM.mjs → event-loop-stall-monitor-GPrVvx14.mjs} +1 -1
  92. package/dist/{tls-DOTmtLCW.mjs → tls-2xbgg78V.mjs} +2 -2
  93. package/dist/{tls-BxQlomxd.js → tls-BFs4PzoW.js} +2 -2
@@ -6,12 +6,12 @@ let _camstack_types_node = require("@camstack/types/node");
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
- let node_child_process = require("node:child_process");
10
- let node_util = require("node:util");
11
9
  let node_fs = require("node:fs");
12
10
  node_fs = require_chunk.__toESM(node_fs);
11
+ let node_child_process = require("node:child_process");
13
12
  let node_os = require("node:os");
14
13
  node_os = require_chunk.__toESM(node_os);
14
+ let node_util = require("node:util");
15
15
  let node_http = require("node:http");
16
16
  let node_v8 = require("node:v8");
17
17
  node_v8 = require_chunk.__toESM(node_v8);
@@ -311,12 +311,15 @@ function createSocketPlaneReader(sources) {
311
311
  peers.push({
312
312
  peerId: child.childId,
313
313
  tx: traffic.tx,
314
- rx: traffic.rx
314
+ rx: traffic.rx,
315
+ queuedBytes: registry.getChildQueuedBytes(child.childId)
315
316
  });
316
317
  }
318
+ const forward = registry.readForwardCensus?.();
317
319
  return {
318
320
  peers,
319
- peersConnected: listed.length
321
+ peersConnected: listed.length,
322
+ ...forward === void 0 ? {} : { forward }
320
323
  };
321
324
  } catch {
322
325
  return;
@@ -334,8 +337,41 @@ function createSocketPlaneReader(sources) {
334
337
  * stops being read.
335
338
  */
336
339
  var SOCKET_PLANE_TOP_N = 5;
340
+ /** How many queue-holding peers get named. Three: a queue is an exception,
341
+ * and the line already carries five names for traffic. */
342
+ var SOCKET_QUEUE_TOP_N = 3;
343
+ /** The queue reading over the current counters — a level, so no baseline. */
344
+ function readSocketQueues(current) {
345
+ let totalBytes = 0;
346
+ let unmeasured = 0;
347
+ const holding = [];
348
+ for (const peer of current.peers) {
349
+ if (peer.queuedBytes === null) {
350
+ unmeasured += 1;
351
+ continue;
352
+ }
353
+ totalBytes += peer.queuedBytes;
354
+ if (peer.queuedBytes > 0) holding.push({
355
+ peerId: peer.peerId,
356
+ bytes: peer.queuedBytes
357
+ });
358
+ }
359
+ return {
360
+ totalBytes,
361
+ unmeasured,
362
+ top: holding.toSorted((a, b) => b.bytes - a.bytes || a.peerId.localeCompare(b.peerId)).slice(0, 3)
363
+ };
364
+ }
337
365
  /** A process that has never read the plane. */
338
366
  var EMPTY_SOCKET_PLANE_BASELINE = { peers: /* @__PURE__ */ new Map() };
367
+ /** The census over the window; a counter that went backwards reads as quiet. */
368
+ function diffForward(current, before) {
369
+ if (current === void 0) return void 0;
370
+ return {
371
+ raw: Math.max(0, current.raw - (before?.raw ?? 0)),
372
+ decoded: Math.max(0, current.decoded - (before?.decoded ?? 0))
373
+ };
374
+ }
339
375
  function diffDirection(current, before) {
340
376
  const previous = before ?? EMPTY_SOCKET_DIRECTION;
341
377
  return {
@@ -361,6 +397,14 @@ function addDirection(into, add) {
361
397
  function peerMessages(delta) {
362
398
  return socketDirectionMessages(delta.tx) + socketDirectionMessages(delta.rx);
363
399
  }
400
+ /**
401
+ * Total bytes a peer moved in the window, both directions — the OTHER
402
+ * ranking, and the one that names a peer the message ranking structurally
403
+ * cannot see (D454).
404
+ */
405
+ function peerBytes(delta) {
406
+ return socketDirectionBytes(delta.tx) + socketDirectionBytes(delta.rx);
407
+ }
364
408
  /** Subtract the previous cumulative reading from the current one. */
365
409
  function diffSocketPlane(baseline, current, topN = 5) {
366
410
  const deltas = [];
@@ -377,12 +421,16 @@ function diffSocketPlane(baseline, current, topN = 5) {
377
421
  rx = addDirection(rx, delta.rx);
378
422
  if (peerMessages(delta) > 0) deltas.push(delta);
379
423
  }
424
+ const forward = diffForward(current.forward, baseline.forward);
380
425
  return {
381
426
  peersConnected: current.peersConnected,
382
427
  peersMeasured: current.peers.length,
383
428
  tx,
384
429
  rx,
385
- top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN)
430
+ top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
431
+ topBytes: deltas.toSorted((a, b) => peerBytes(b) - peerBytes(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
432
+ queued: readSocketQueues(current),
433
+ ...forward === void 0 ? {} : { forward }
386
434
  };
387
435
  }
388
436
  function createSocketPlaneMeter(reader, topN = 5) {
@@ -391,24 +439,43 @@ function createSocketPlaneMeter(reader, topN = 5) {
391
439
  const current = reader();
392
440
  if (current === void 0) return void 0;
393
441
  const window = diffSocketPlane(baseline, current, topN);
394
- baseline = { peers: new Map(current.peers.map((p) => [p.peerId, p])) };
442
+ baseline = {
443
+ peers: new Map(current.peers.map((p) => [p.peerId, p])),
444
+ ...current.forward === void 0 ? {} : { forward: current.forward }
445
+ };
395
446
  return window;
396
447
  } };
397
448
  }
398
449
  /** Bytes → whole kB. The plane moves 180–330 MB/min; raw bytes would put
399
450
  * nine-digit numbers on a line that has to stay readable. */
400
- function kb(bytes) {
451
+ function kb$1(bytes) {
401
452
  return Math.round(bytes / 1024);
402
453
  }
403
454
  /** `req:<msgs>/<kB>,res:<msgs>/<kB>,evt:<msgs>/<kB>` — every kind, always,
404
455
  * including at zero: a kind that went quiet is a reading, not a blank. */
405
456
  function formatDirection(sample) {
406
- return `req:${sample.reqMessages}/${kb(sample.reqBytes)}kB,res:${sample.resMessages}/${kb(sample.resBytes)}kB,evt:${sample.evtMessages}/${kb(sample.evtBytes)}kB`;
457
+ 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`;
407
458
  }
408
- /** `<peer>:tx=<req>/<res>/<evt>:rx=<req>/<res>/<evt>:kB=<tx>/<rx>` — the whole
409
- * peer×kind cross for one peer, which is the question in one token. */
459
+ /** `<req>/<res>/<evt>` in kB — the byte triple, in the same order as the
460
+ * message triple next to it, so the two read as one table. */
461
+ function formatPeerBytes(sample) {
462
+ return `${kb$1(sample.reqBytes)}/${kb$1(sample.resBytes)}/${kb$1(sample.evtBytes)}`;
463
+ }
464
+ /**
465
+ * `<peer>:tx=<req>/<res>/<evt>:rx=…:txkB=<req>/<res>/<evt>:rxkB=…` — the whole
466
+ * peer×kind cross for one peer, messages and bytes, which is the question in
467
+ * one token.
468
+ *
469
+ * The bytes were a per-direction TOTAL until D454, and the total could not
470
+ * answer the question `socket-traffic.ts` says this instrument exists to
471
+ * settle: whether a peer's bytes are RESPONSES (media, and D9/D18 re-opens) or
472
+ * fan-out (`evt`, which scales with runner count and nothing an operator
473
+ * asked for). On 2026-09-11 hub-main wrote 118 MB in one minute to
474
+ * `hub/pipeline-analytics` and the line could only say "118 MB" — separating
475
+ * `res` from `evt` took an hour of arithmetic against the plane-wide totals.
476
+ */
410
477
  function formatPeer(delta) {
411
- 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))}`;
478
+ 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)}`;
412
479
  }
413
480
  /**
414
481
  * The suffix appended to a `[mem]` line — leading space included, empty when
@@ -421,14 +488,234 @@ function formatPeer(delta) {
421
488
  * is the only way to see an unmeasured channel, and the per-kind splits because
422
489
  * "the plane went quiet for a minute" is a reading.
423
490
  *
424
- * `socketTop` is the one conditional field — a ranked list of nothing is noise —
425
- * and it is a single comma-separated value so the field COUNT does not change
426
- * from line to line.
491
+ * `socketTop` is a conditional field — a ranked list of nothing is noise — and
492
+ * it is a single comma-separated value so the field COUNT does not change from
493
+ * line to line.
494
+ *
495
+ * `socketTopBytes` is the other one, and its PRESENCE is the reading: it is
496
+ * printed only when ranking by bytes names a peer that ranking by messages
497
+ * does not. A line without it says the two rankings agree; a line with it says
498
+ * a peer is moving few, heavy frames and names it (D454).
427
499
  */
428
500
  function formatSocketPlane(window) {
429
501
  if (window === void 0) return "";
430
502
  const top = window.top.map(formatPeer).join(",");
431
- return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`);
503
+ const named = new Set(window.top.map((p) => p.peerId));
504
+ const topBytes = window.topBytes.filter((p) => !named.has(p.peerId)).map(formatPeer).join(",");
505
+ const queuedTop = window.queued.top.map((p) => `${p.peerId}:${kb$1(p.bytes)}`).join(",");
506
+ 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}`);
507
+ }
508
+ //#endregion
509
+ //#region src/kernel/moleculer/mesh-queue-report.ts
510
+ function isRecord$1(value) {
511
+ return typeof value === "object" && value !== null;
512
+ }
513
+ /** The shape `TcpWriter` keeps its sockets in. Anything else is not a mesh. */
514
+ function writerSocketsOf(broker) {
515
+ if (!isRecord$1(broker)) return void 0;
516
+ const transit = broker["transit"];
517
+ if (!isRecord$1(transit)) return void 0;
518
+ const transporter = transit["tx"];
519
+ if (!isRecord$1(transporter)) return void 0;
520
+ const writer = transporter["writer"];
521
+ if (!isRecord$1(writer)) return void 0;
522
+ const sockets = writer["sockets"];
523
+ return sockets instanceof Map ? sockets : void 0;
524
+ }
525
+ function readSocket(nodeId, socket) {
526
+ if (typeof nodeId !== "string" || !isRecord$1(socket)) return void 0;
527
+ const queuedBytes = socket["writableLength"];
528
+ const bytesWritten = socket["bytesWritten"];
529
+ if (typeof queuedBytes !== "number" || typeof bytesWritten !== "number") return void 0;
530
+ return {
531
+ nodeId,
532
+ queuedBytes,
533
+ bytesWritten
534
+ };
535
+ }
536
+ /**
537
+ * Build a reader over a lazily-resolved broker. Wrapped: a broker mid-teardown
538
+ * may throw, and a diagnostic that can crash the process it watches is worse
539
+ * than none.
540
+ */
541
+ function createMeshQueueReader(getBroker) {
542
+ return () => {
543
+ try {
544
+ const sockets = writerSocketsOf(getBroker());
545
+ if (sockets === void 0) return void 0;
546
+ const peers = [];
547
+ for (const [nodeId, socket] of sockets) {
548
+ const peer = readSocket(nodeId, socket);
549
+ if (peer !== void 0) peers.push(peer);
550
+ }
551
+ return peers;
552
+ } catch {
553
+ return;
554
+ }
555
+ };
556
+ }
557
+ /** At most this many nodes named. A cluster has a handful. */
558
+ var MESH_QUEUE_TOP_N = 3;
559
+ function kb(bytes) {
560
+ return Math.round(bytes / 1024);
561
+ }
562
+ /**
563
+ * The suffix appended to a `[mem]` line — leading space included, empty when
564
+ * this process has no mesh writer. `meshQueued` is printed always, at zero
565
+ * included: a quiet mesh is a reading. `meshQueuedTop` names only nodes with
566
+ * a queue, largest first — a ranked list of zeros is noise.
567
+ */
568
+ function formatMeshQueue(peers) {
569
+ if (peers === void 0) return "";
570
+ const total = peers.reduce((sum, p) => sum + p.queuedBytes, 0);
571
+ 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(",");
572
+ return ` meshQueued=${kb(total)}kB` + (top.length === 0 ? "" : ` meshQueuedTop=${top}`);
573
+ }
574
+ //#endregion
575
+ //#region src/kernel/outbound-queue-watch.ts
576
+ /**
577
+ * outbound-queue-watch — an outbound socket queue that HOLDS is named, and its
578
+ * release is timestamped (D446).
579
+ *
580
+ * ## The blind spot this closes
581
+ *
582
+ * On 2026-09-10 hub-main's `arrayBuffers` climbed twice at a constant rate
583
+ * (20 MB/min to 1 264 MB; 63 MB/min for 77 min to 4 968 MB), LIVE across a
584
+ * forced compaction, with ~4 % of JS heap alongside, and each time released in
585
+ * one instant with no line from any owner. The investigation excluded "every
586
+ * socket write queue" by summing `writableLength` over
587
+ * `process._getActiveHandles()` — 84 MB, all of it the phone's WebSocket.
588
+ *
589
+ * `_getActiveHandles()` lists only handles that keep the loop alive. An
590
+ * UNREF'D socket is not one of them, and Moleculer's `TcpWriter.connect`
591
+ * unrefs every socket it opens to a peer node (`transporters/tcp/tcp-writer.js`,
592
+ * `socket.unref()`). So the one outbound plane that writes to another MACHINE
593
+ * — where a reader that stops draining is the ordinary failure — was never in
594
+ * the sum. The UDS children were in it, but only as a total, at one instant,
595
+ * by hand.
596
+ *
597
+ * ## What this is
598
+ *
599
+ * A pure gate over per-peer queue depths the heartbeat reads on every probe:
600
+ * the UDS children's `socket.writableLength` (`SocketChannel.queuedBytes`) and
601
+ * the mesh writer sockets' (`mesh-queue-report.ts`). It says three things and
602
+ * nothing else:
603
+ *
604
+ * - a peer whose queue has been at or above {@link OUTBOUND_QUEUE_THRESHOLD_BYTES}
605
+ * for {@link OUTBOUND_QUEUE_SUSTAIN_MS} is NAMED, with its current and peak
606
+ * depth and how long it has held — at most once per
607
+ * {@link OUTBOUND_QUEUE_COOLDOWN_MS};
608
+ * - a named peer whose queue then falls under half the threshold, or that
609
+ * vanishes from the readings (its socket died — which is how a queue is
610
+ * dropped in one instant), is reported as RELEASED with its peak and the
611
+ * hold, so the release the episodes never logged has a line and a peer;
612
+ * - a burst that drains inside the sustain window says nothing. Backpressure
613
+ * measures LIVENESS, not identity (D444): a peer momentarily behind is
614
+ * coming back.
615
+ *
616
+ * ## What it is NOT
617
+ *
618
+ * Not a bound. It terminates nothing: a UDS child whose channel is destroyed
619
+ * loses every in-flight call, and a mesh socket Moleculer reconnects on its
620
+ * own terms. The WS guard (`ws-slow-consumer-guard.ts`) bounds the one plane
621
+ * whose client is expendable. Here the line is the deliverable; what to do
622
+ * about a peer that holds is decided with the peer named, not before.
623
+ */
624
+ /** 64 MiB. Below this a queue is a burst on a busy peer (the orchestrator's
625
+ * channel moves ~1.5 MB/s of audio responses); above it, and held, it is
626
+ * retention with a name. */
627
+ var OUTBOUND_QUEUE_THRESHOLD_BYTES = 64 * 1024 * 1024;
628
+ /** Matches the ArrayBuffer census sustain: a minute above the bound. */
629
+ var OUTBOUND_QUEUE_SUSTAIN_MS = 6e4;
630
+ /** A held queue is re-named every ten minutes, not every probe. */
631
+ var OUTBOUND_QUEUE_COOLDOWN_MS = 10 * 6e4;
632
+ /** A release is a fall under this fraction of the threshold. */
633
+ var RELEASE_RATIO = .5;
634
+ /**
635
+ * The pure decision — threshold, sustain, cooldown, release, gone — testable
636
+ * without a socket.
637
+ */
638
+ function createOutboundQueueGate(options = {}) {
639
+ const thresholdBytes = options.thresholdBytes ?? 67108864;
640
+ const sustainMs = options.sustainMs ?? 6e4;
641
+ const cooldownMs = options.cooldownMs ?? 6e5;
642
+ const tracked = /* @__PURE__ */ new Map();
643
+ const judgeOne = (peerId, bytes, at) => {
644
+ const state = tracked.get(peerId);
645
+ if (bytes >= thresholdBytes) {
646
+ const next = state === void 0 ? {
647
+ aboveSince: at,
648
+ peakBytes: bytes,
649
+ lastNamedAt: null
650
+ } : {
651
+ ...state,
652
+ peakBytes: Math.max(state.peakBytes, bytes)
653
+ };
654
+ const heldForMs = at - next.aboveSince;
655
+ const due = heldForMs >= sustainMs && (next.lastNamedAt === null || at - next.lastNamedAt >= cooldownMs);
656
+ tracked.set(peerId, due ? {
657
+ ...next,
658
+ lastNamedAt: at
659
+ } : next);
660
+ return due ? {
661
+ kind: "sustained",
662
+ peerId,
663
+ bytes,
664
+ peakBytes: next.peakBytes,
665
+ heldForMs
666
+ } : void 0;
667
+ }
668
+ if (state === void 0) return void 0;
669
+ if (bytes >= thresholdBytes * RELEASE_RATIO) return void 0;
670
+ tracked.delete(peerId);
671
+ if (state.lastNamedAt === null) return void 0;
672
+ return {
673
+ kind: "released",
674
+ peerId,
675
+ peakBytes: state.peakBytes,
676
+ toBytes: bytes,
677
+ heldForMs: at - state.aboveSince,
678
+ gone: false
679
+ };
680
+ };
681
+ return { observe(readings, at) {
682
+ const verdicts = [];
683
+ const seen = /* @__PURE__ */ new Set();
684
+ for (const reading of readings) {
685
+ seen.add(reading.peerId);
686
+ const verdict = judgeOne(reading.peerId, reading.bytes, at);
687
+ if (verdict !== void 0) verdicts.push(verdict);
688
+ }
689
+ for (const [peerId, state] of tracked) {
690
+ if (seen.has(peerId)) continue;
691
+ tracked.delete(peerId);
692
+ if (state.lastNamedAt === null) continue;
693
+ verdicts.push({
694
+ kind: "released",
695
+ peerId,
696
+ peakBytes: state.peakBytes,
697
+ toBytes: 0,
698
+ heldForMs: at - state.aboveSince,
699
+ gone: true
700
+ });
701
+ }
702
+ return verdicts;
703
+ } };
704
+ }
705
+ var BYTES_PER_MB$1 = 1048576;
706
+ function mb$2(bytes) {
707
+ return Math.round(bytes / BYTES_PER_MB$1);
708
+ }
709
+ /** `60s` under two minutes, `77m` from there — the hold is read at a glance. */
710
+ function duration(ms) {
711
+ const seconds = Math.round(ms / 1e3);
712
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)}m`;
713
+ }
714
+ /** The line for one verdict. Both carry the peer, the bytes and the hold;
715
+ * the sustained one also says why the older census could not see it. */
716
+ function formatOutboundQueueVerdict(label, verdict) {
717
+ 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.`;
718
+ 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)");
432
719
  }
433
720
  //#endregion
434
721
  //#region src/kernel/heap-spaces.ts
@@ -1416,6 +1703,27 @@ function format(label, s, loop, budgetMb, suffix) {
1416
1703
  return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms${suffix}`;
1417
1704
  }
1418
1705
  /**
1706
+ * Every outbound queue the heartbeat can reach, as the gate wants them: the
1707
+ * UDS children (only those whose channel keeps a reading — an unmeasured
1708
+ * peer is not a zero) and the mesh writer sockets, each prefixed with its
1709
+ * plane.
1710
+ */
1711
+ function outboundQueueReadings(uds, mesh) {
1712
+ const readings = [];
1713
+ for (const peer of uds?.peers ?? []) {
1714
+ if (peer.queuedBytes === null) continue;
1715
+ readings.push({
1716
+ peerId: `uds:${peer.peerId}`,
1717
+ bytes: peer.queuedBytes
1718
+ });
1719
+ }
1720
+ for (const node of mesh ?? []) readings.push({
1721
+ peerId: `mesh:${node.nodeId}`,
1722
+ bytes: node.queuedBytes
1723
+ });
1724
+ return readings;
1725
+ }
1726
+ /**
1419
1727
  * Start the heartbeat. Returns a stop function.
1420
1728
  *
1421
1729
  * `unref`ed so it can never hold the process open, and every read is wrapped:
@@ -1529,7 +1837,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1529
1837
  const due = at - lastLoggedAt >= intervalMs;
1530
1838
  if (mode === "escalated" || due) {
1531
1839
  lastLoggedAt = at;
1532
- const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()));
1840
+ const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()));
1533
1841
  if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
1534
1842
  else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
1535
1843
  else sink.info(line);
@@ -1538,8 +1846,17 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1538
1846
  checkRssBudget(sample, at);
1539
1847
  maybeReclaim(sample);
1540
1848
  judgeArrayBuffers(sample, at);
1849
+ judgeOutboundQueues(at);
1541
1850
  } catch {}
1542
1851
  };
1852
+ const meshQueue = probes?.meshQueue;
1853
+ const queueReader = probes?.socketPlane;
1854
+ const queueGate = queueReader === void 0 && meshQueue === void 0 ? void 0 : createOutboundQueueGate(probes?.outboundQueue);
1855
+ const judgeOutboundQueues = (at) => {
1856
+ if (queueGate === void 0) return;
1857
+ const readings = outboundQueueReadings(queueReader?.(), meshQueue?.());
1858
+ for (const verdict of queueGate.observe(readings, at)) sink.warn(formatOutboundQueueVerdict(label, verdict));
1859
+ };
1543
1860
  const censusProbe = probes?.arrayBufferCensus;
1544
1861
  const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
1545
1862
  let censusInFlight = false;
@@ -5336,6 +5653,74 @@ function encodeFrame(value) {
5336
5653
  Buffer.from(body.buffer, body.byteOffset, body.byteLength).copy(out, 4);
5337
5654
  return out;
5338
5655
  }
5656
+ /** msgpack `fixstr` of {@link PAYLOAD_KEY}: `0xa0 | length`, then the bytes. */
5657
+ var PAYLOAD_KEY_BYTES = Buffer.from([162, ...Buffer.from("pl")]);
5658
+ /** msgpack `bin 32` marker: one byte, then a 4-byte big-endian length. */
5659
+ var BIN32_MARKER = 198;
5660
+ /** msgpack `fixmap` range: `0x80 | count`, count in `0..15`. */
5661
+ var FIXMAP_BASE = 128;
5662
+ var FIXMAP_MAX_COUNT = 15;
5663
+ /**
5664
+ * Encode the HEAD of a frame that carries an opaque payload: the length
5665
+ * prefix, the msgpack envelope with one extra key ({@link PAYLOAD_KEY}), and
5666
+ * the `bin 32` header for the payload — everything but the payload bytes
5667
+ * themselves, which the caller writes straight after it.
5668
+ *
5669
+ * ## Why the payload is the frame's TAIL and not a field
5670
+ *
5671
+ * hub-main routes every sibling↔sibling call. Until this existed it decoded
5672
+ * the whole request (args included), built a new message, and re-encoded it
5673
+ * — walking a 13.5 KB PCM chunk or a 2 000-row settings answer twice on the
5674
+ * one thread that serves every tRPC request, and allocating an `Encoder`
5675
+ * buffer that doubles from 2 KiB up to the body's size on every write
5676
+ * (D441: ~27 % of hub-main's busy time in msgpack decode, ~6 % in encode,
5677
+ * and the churn that fed a 29.5 % GC share).
5678
+ *
5679
+ * With the payload appended as the LAST key of the map, the receiver's
5680
+ * ordinary msgpack decode yields it as a zero-copy VIEW of the frame
5681
+ * (`@msgpack/msgpack` decodes `bin` as `subarray`), the router reads only the
5682
+ * envelope, and forwarding is: encode a ~60-byte head, write it, write the
5683
+ * view. The wire stays plain msgpack — a peer that never heard of payloads
5684
+ * decodes `{ …frame, pl: <bytes> }` and can be told, at register, not to be
5685
+ * sent one.
5686
+ *
5687
+ * ## The one hand-written msgpack in this repository
5688
+ *
5689
+ * `@msgpack/msgpack` has no "encode a map, then append a key" API, so the
5690
+ * envelope is encoded normally and its `fixmap` count byte is bumped by one
5691
+ * before the key and the `bin 32` header are appended. That patch is only
5692
+ * valid for a `fixmap` (≤ 15 keys); a `Frame` has at most four, and anything
5693
+ * else is refused rather than silently corrupted.
5694
+ */
5695
+ function encodeFrameHead(frame, payload) {
5696
+ const envelope = encode(frame, ENCODE_OPTIONS);
5697
+ const first = envelope[0];
5698
+ 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)})`);
5699
+ const bodyLength = envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5 + payload.byteLength;
5700
+ const head = Buffer.allocUnsafe(HEADER_BYTES + envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5);
5701
+ head.writeUInt32BE(bodyLength, 0);
5702
+ Buffer.from(envelope.buffer, envelope.byteOffset, envelope.byteLength).copy(head, HEADER_BYTES);
5703
+ head[HEADER_BYTES] = first + 1;
5704
+ let at = HEADER_BYTES + envelope.byteLength;
5705
+ PAYLOAD_KEY_BYTES.copy(head, at);
5706
+ at += PAYLOAD_KEY_BYTES.byteLength;
5707
+ head[at] = BIN32_MARKER;
5708
+ head.writeUInt32BE(payload.byteLength, at + 1);
5709
+ return head;
5710
+ }
5711
+ /**
5712
+ * Serialise a value into the bytes a payload carries. The SAME options as the
5713
+ * envelope codec, so `undefined` properties are omitted rather than turned
5714
+ * into `null` — a `z.void()` cap output must read back as `undefined` on the
5715
+ * far side exactly as it did when it travelled inline.
5716
+ */
5717
+ function packPayload(value) {
5718
+ return encode(value, ENCODE_OPTIONS);
5719
+ }
5720
+ /** Inverse of {@link packPayload}. A `bin` inside decodes as a view of `bytes`. */
5721
+ function unpackPayload(bytes) {
5722
+ return decode(bytes);
5723
+ }
5339
5724
  /** Bytes in the big-endian length prefix that opens every frame. */
5340
5725
  var HEADER_BYTES = 4;
5341
5726
  /**
@@ -5494,6 +5879,25 @@ function localEndpointPath(nodeId) {
5494
5879
  if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
5495
5880
  return (0, node_path.join)((0, node_os.tmpdir)(), `camstack-${safe}.sock`);
5496
5881
  }
5882
+ //#endregion
5883
+ //#region src/kernel/transport/local-transport.ts
5884
+ /**
5885
+ * An answer that carries an opaque payload beside its body.
5886
+ *
5887
+ * A CLASS rather than a shape so the channel can tell it from a provider's
5888
+ * ordinary `{ body, payload }` return value by identity, never by duck-typing:
5889
+ * a cap method is free to return an object with those two keys and it must
5890
+ * travel inline like any other. `request()` resolves with one of these only
5891
+ * when the peer's `res` frame carried a payload.
5892
+ */
5893
+ var PayloadReply = class {
5894
+ body;
5895
+ payload;
5896
+ constructor(body, payload) {
5897
+ this.body = body;
5898
+ this.payload = payload;
5899
+ }
5900
+ };
5497
5901
  var SocketChannel = class {
5498
5902
  socket;
5499
5903
  nextId = 1;
@@ -5550,7 +5954,7 @@ var SocketChannel = class {
5550
5954
  k: "req",
5551
5955
  id,
5552
5956
  body
5553
- });
5957
+ }, opts?.payload);
5554
5958
  });
5555
5959
  }
5556
5960
  emit(body) {
@@ -5590,11 +5994,38 @@ var SocketChannel = class {
5590
5994
  rx: sampleSocketDirection(this.rxCounters)
5591
5995
  };
5592
5996
  }
5593
- send(frame) {
5997
+ /**
5998
+ * Bytes handed to the socket that the kernel has not taken yet — the
5999
+ * `Writable` buffer plus the write in flight. Read LIVE, never cached: this
6000
+ * is the number that says whether the peer is draining what we write, and
6001
+ * it is read on every heartbeat probe (D446). An unref'd socket keeps it
6002
+ * exactly like a ref'd one, which is why it is read here and not summed
6003
+ * over `_getActiveHandles()`.
6004
+ */
6005
+ queuedBytes() {
6006
+ return this.socket.writableLength;
6007
+ }
6008
+ /**
6009
+ * Write one frame. With a payload the head and the tail are two `write`
6010
+ * calls under `cork()` — the kernel sees one `writev`, and the payload bytes
6011
+ * are never copied into the head. A forwarded payload is a view into the
6012
+ * frame it arrived on, and that frame's buffer is never mutated, so handing
6013
+ * the view to the socket is safe.
6014
+ */
6015
+ send(frame, payload) {
5594
6016
  if (this.closed) return;
5595
- const encoded = encodeFrame(frame);
5596
- recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
5597
- this.socket.write(encoded);
6017
+ if (payload === void 0) {
6018
+ const encoded = encodeFrame(frame);
6019
+ recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
6020
+ this.socket.write(encoded);
6021
+ return;
6022
+ }
6023
+ const head = encodeFrameHead(frame, payload);
6024
+ recordSocketFrame(this.txCounters, frame.k, head.byteLength + payload.byteLength);
6025
+ this.socket.cork();
6026
+ this.socket.write(head);
6027
+ this.socket.write(payload);
6028
+ this.socket.uncork();
5598
6029
  }
5599
6030
  onData(chunk) {
5600
6031
  const frames = this.decoder.push(chunk);
@@ -5614,8 +6045,14 @@ var SocketChannel = class {
5614
6045
  recordSocketFrame(this.rxCounters, frame.k, wireBytes);
5615
6046
  if (frame.k === "req") {
5616
6047
  try {
5617
- const result = await this.requestHandler(frame.body);
5618
- this.send({
6048
+ const result = await this.requestHandler(frame.body, frame.pl);
6049
+ if (result instanceof PayloadReply) this.send({
6050
+ k: "res",
6051
+ id: frame.id,
6052
+ ok: true,
6053
+ body: result.body
6054
+ }, result.payload);
6055
+ else this.send({
5619
6056
  k: "res",
5620
6057
  id: frame.id,
5621
6058
  ok: true,
@@ -5636,8 +6073,9 @@ var SocketChannel = class {
5636
6073
  if (!p) return;
5637
6074
  this.pending.delete(frame.id);
5638
6075
  if (p.timer !== null) clearTimeout(p.timer);
5639
- if (frame.ok) p.resolve(frame.body);
5640
- else p.reject(new Error(frame.err));
6076
+ if (!frame.ok) p.reject(new Error(frame.err));
6077
+ else if (frame.pl !== void 0) p.resolve(new PayloadReply(frame.body, frame.pl));
6078
+ else p.resolve(frame.body);
5641
6079
  return;
5642
6080
  }
5643
6081
  this.eventHandler(frame.body);
@@ -6188,572 +6626,162 @@ function deliverShared(state, event) {
6188
6626
  }
6189
6627
  }
6190
6628
  //#endregion
6191
- //#region src/kernel/moleculer/resilient-cap-call.ts
6192
- /** Moleculer error `type` values meaning "the service is not (yet) routable". */
6193
- var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
6194
- /** Default ceiling for the discovery wait — fail-fast past this. */
6195
- var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
6196
- function isDiscoveryError(err) {
6197
- if (typeof err !== "object" || err === null) return false;
6198
- const type = err.type;
6199
- return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
6629
+ //#region src/kernel/transport/cap-routing-hints.ts
6630
+ /** Lift {@link CapRoutingHints} from a call's args. Never throws. */
6631
+ function liftRoutingHints(args) {
6632
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return {};
6633
+ const nodeId = Reflect.get(args, "nodeId");
6634
+ const addonId = Reflect.get(args, "addonId");
6635
+ return {
6636
+ ...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
6637
+ ...typeof addonId === "string" && addonId.length > 0 ? { addonId } : {}
6638
+ };
6639
+ }
6640
+ //#endregion
6641
+ //#region src/kernel/transport/local-child-registry.ts
6642
+ function readFanoutMode() {
6643
+ const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
6644
+ if (raw === "shadow" || raw === "broadcast") return raw;
6645
+ return "filter";
6200
6646
  }
6201
6647
  /**
6202
- * Call a Moleculer action; on a service-discovery error, wait for the
6203
- * named service to be discovered and retry the call exactly once.
6648
+ * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
6649
+ * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
6650
+ * prefix to distinguish routing failures (safe to retry via broker) from real
6651
+ * provider errors (must NOT retry to avoid double-executing side effects).
6204
6652
  */
6205
- async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
6206
- try {
6207
- return await broker.call(action, params, opts);
6208
- } catch (err) {
6209
- if (!isDiscoveryError(err)) throw err;
6210
- await broker.waitForServices([serviceName], discoveryTimeoutMs);
6211
- return await broker.call(action, params, opts);
6653
+ var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
6654
+ /**
6655
+ * Parent-side authority for local addon-runners reachable over a
6656
+ * `LocalTransportServer` (UDS). Children register their cap manifest on
6657
+ * connect; the registry routes `(capName, deviceId?)` cap calls to the
6658
+ * owning child over the channel and drops a child's caps on disconnect.
6659
+ */
6660
+ var LocalChildRegistry = class {
6661
+ children = /* @__PURE__ */ new Map();
6662
+ registeredHandler = () => {};
6663
+ goneHandler = () => {};
6664
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
6665
+ nextIncarnation = 1;
6666
+ eventHandler = null;
6667
+ logHandler = null;
6668
+ readinessHandler = null;
6669
+ server;
6670
+ onUnownedCall;
6671
+ logger;
6672
+ getActiveSingletonAddonId;
6673
+ resolveChildIdForAddon;
6674
+ /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
6675
+ ownNodeId;
6676
+ /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
6677
+ isAggregatedCollectionMethod;
6678
+ isAddonPinnedCall;
6679
+ /** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
6680
+ forwardedRaw = 0;
6681
+ forwardedDecoded = 0;
6682
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
6683
+ capTimeoutMs;
6684
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
6685
+ capUsageObserver;
6686
+ /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
6687
+ egressRoutedCaps = /* @__PURE__ */ new Set();
6688
+ /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
6689
+ fanoutMode = readFanoutMode();
6690
+ /** Per-child event fan-out counters (sent / suppressed). */
6691
+ childEventStats = /* @__PURE__ */ new Map();
6692
+ /** Last pattern-set string logged per child, to dedup the INFO line. */
6693
+ loggedPatternSet = /* @__PURE__ */ new Map();
6694
+ /**
6695
+ * Accepts either a plain positional `server` argument (backward-compatible)
6696
+ * or a full `LocalChildRegistryOptions` object.
6697
+ *
6698
+ * Positional overloads (existing call sites are unchanged):
6699
+ * new LocalChildRegistry(server)
6700
+ * new LocalChildRegistry(server, onUnownedCall)
6701
+ *
6702
+ * Options object (new call sites that pass a logger):
6703
+ * new LocalChildRegistry({ server, onUnownedCall, logger })
6704
+ */
6705
+ constructor(serverOrOptions, onUnownedCallArg) {
6706
+ if (serverOrOptions && !("listen" in serverOrOptions)) {
6707
+ const opts = serverOrOptions;
6708
+ this.server = opts.server;
6709
+ this.onUnownedCall = opts.onUnownedCall;
6710
+ this.logger = opts.logger;
6711
+ this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
6712
+ this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
6713
+ this.ownNodeId = opts.ownNodeId;
6714
+ this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
6715
+ this.isAddonPinnedCall = opts.isAddonPinnedCall;
6716
+ this.capTimeoutMs = opts.capTimeoutMs;
6717
+ this.capUsageObserver = opts.capUsageObserver;
6718
+ } else {
6719
+ this.server = serverOrOptions;
6720
+ this.onUnownedCall = onUnownedCallArg;
6721
+ }
6722
+ }
6723
+ async start() {
6724
+ this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
6725
+ this.server.onConnection((channel) => this.onConnection(channel));
6726
+ await this.server.listen();
6212
6727
  }
6213
- }
6214
- //#endregion
6215
- //#region src/kernel/transport/cap-route.ts
6216
- function buildMessage(capName, method, detail) {
6217
- const call = method !== void 0 ? `${capName}.${method}` : capName;
6218
- const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
6219
- const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
6220
- const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
6221
- return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
6222
- }
6223
- var CapRouteError = class extends Error {
6224
- reason;
6225
- nodeId;
6226
- rejected;
6227
6728
  /**
6228
- * @param cause Optional original error that triggered this routing failure.
6229
- * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
6230
- * Dispatchers wrapping transport errors MUST pass the original.
6729
+ * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
6730
+ * child has no recorded events yet. Exposed for the starvation-verification
6731
+ * surface (shadow burn-in + operator debug).
6231
6732
  */
6232
- constructor(capName, method, detail, cause) {
6233
- super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
6234
- this.name = "CapRouteError";
6235
- this.reason = detail.reason;
6236
- this.nodeId = detail.nodeId;
6237
- this.rejected = detail.rejected;
6733
+ getChildEventStats(childId) {
6734
+ const s = this.childEventStats.get(childId);
6735
+ return s === void 0 ? null : {
6736
+ sent: s.sent,
6737
+ suppressed: s.suppressed
6738
+ };
6238
6739
  }
6239
- };
6240
- /**
6241
- * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
6242
- *
6243
- * Precedence (explicit nodeId path):
6244
- * 1. hub-in-process — hub node + hubInProcessProvides
6245
- * 2. hub-local-uds — hub node + hubLocalChildProvides
6246
- * 3. node-offline → CapRouteError{reason:'node-offline'}
6247
- * 4. agent-child-forward — node is an agent AND nodeKnowsCap
6248
- * 5. remote-moleculer — any other online non-agent node
6249
- *
6250
- * Singleton path (no nodeId):
6251
- * 1. hub-in-process (hub provides in-process)
6252
- * 2. hub-local-uds (a hub-local UDS child provides it)
6253
- * 3. remote-moleculer (any online, non-hub node that knows it)
6254
- * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
6255
- *
6256
- * PURE: no side effects, no async, no broker/registry imports.
6257
- */
6258
- function classifyCapRoute(capName, opts, snapshot) {
6259
- const rejected = [];
6260
- if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
6261
- return classifySingleton(capName, opts.deviceId, snapshot, rejected);
6262
- }
6263
- function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
6264
- if (nodeId === snap.hubNodeId) {
6265
- const residentProvides = snap.hubResidentProvides;
6266
- const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
6267
- const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
6268
- if (providesOnHub(capName)) {
6269
- const ref = getRef?.(capName) ?? null;
6270
- if (ref !== null) return {
6271
- kind: "hub-in-process",
6272
- capName,
6273
- ref
6274
- };
6275
- rejected.push({
6276
- kind: "hub-in-process",
6277
- why: "provider ref not available in snapshot"
6278
- });
6279
- throw new CapRouteError(capName, void 0, {
6280
- reason: "no-provider",
6281
- nodeId,
6282
- rejected
6283
- });
6284
- }
6285
- if (snap.hubLocalChildProvides(capName, deviceId)) {
6286
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
6287
- if (childId !== null) return {
6288
- kind: "hub-local-uds",
6289
- capName,
6290
- childId
6291
- };
6292
- rejected.push({
6293
- kind: "hub-local-uds",
6294
- why: "child id not resolvable from snapshot"
6295
- });
6296
- throw new CapRouteError(capName, void 0, {
6297
- reason: "no-provider",
6298
- nodeId,
6299
- rejected
6300
- });
6301
- }
6302
- rejected.push({
6303
- kind: "hub-in-process",
6304
- why: "hub does not provide this cap in-process"
6305
- });
6306
- rejected.push({
6307
- kind: "hub-local-uds",
6308
- why: "no hub-local child provides this cap"
6309
- });
6310
- throw new CapRouteError(capName, void 0, {
6311
- reason: "no-provider",
6312
- nodeId,
6313
- rejected
6314
- });
6315
- }
6316
- if (!snap.nodeOnline(nodeId)) {
6317
- rejected.push({
6318
- kind: "remote-moleculer",
6319
- why: `node ${nodeId} is offline`
6320
- });
6321
- throw new CapRouteError(capName, void 0, {
6322
- reason: "node-offline",
6323
- nodeId,
6324
- rejected
6325
- });
6740
+ /**
6741
+ * This child's socket traffic — bytes and messages, both directions, split by
6742
+ * frame kind — or `null` when the child is gone or its channel keeps no
6743
+ * counters (an in-process or test transport).
6744
+ *
6745
+ * The event counters above cover ONE kind of frame and, once surfaced on
6746
+ * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
6747
+ * is the same question asked of every frame the channel moves, and it is the
6748
+ * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
6749
+ * pair for the whole process and has no per-socket breakdown.
6750
+ */
6751
+ getChildSocketTraffic(childId) {
6752
+ const entry = this.children.get(childId);
6753
+ if (entry === void 0) return null;
6754
+ return entry.channel.readTraffic?.() ?? null;
6326
6755
  }
6327
- if (snap.nodeIsAgent(nodeId)) {
6328
- if (snap.nodeKnowsCap(nodeId, capName)) return {
6329
- kind: "agent-child-forward",
6330
- capName,
6331
- agentNodeId: nodeId,
6332
- childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
6333
- };
6334
- rejected.push({
6335
- kind: "agent-child-forward",
6336
- why: `agent ${nodeId} does not know cap ${capName}`
6337
- });
6338
- throw new CapRouteError(capName, void 0, {
6339
- reason: "no-provider",
6340
- nodeId,
6341
- rejected
6342
- });
6756
+ /**
6757
+ * Bytes this process has written to a child that the child has not yet
6758
+ * taken, live, or `null` when the channel keeps no such reading. Read on
6759
+ * every heartbeat probe (D446) — a child that stops draining is the shape
6760
+ * of an outbound-queue episode, and the number lives on the channel.
6761
+ */
6762
+ getChildQueuedBytes(childId) {
6763
+ const entry = this.children.get(childId);
6764
+ if (entry === void 0) return null;
6765
+ return entry.channel.queuedBytes?.() ?? null;
6343
6766
  }
6344
- return {
6345
- kind: "remote-moleculer",
6346
- capName,
6347
- nodeId
6348
- };
6349
- }
6350
- function classifySingleton(capName, deviceId, snap, rejected) {
6351
- if (snap.hubInProcessProvides(capName)) {
6352
- const ref = snap.getInProcessProviderRef?.(capName) ?? null;
6353
- if (ref !== null) return {
6354
- kind: "hub-in-process",
6355
- capName,
6356
- ref
6357
- };
6358
- rejected.push({
6359
- kind: "hub-in-process",
6360
- why: "provider ref not available in snapshot"
6361
- });
6362
- } else rejected.push({
6363
- kind: "hub-in-process",
6364
- why: "hub does not provide this cap in-process"
6365
- });
6366
- if (snap.hubLocalChildProvides(capName, deviceId)) {
6367
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
6368
- if (childId !== null) return {
6369
- kind: "hub-local-uds",
6370
- capName,
6371
- childId
6767
+ /** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
6768
+ readForwardCensus() {
6769
+ return {
6770
+ raw: this.forwardedRaw,
6771
+ decoded: this.forwardedDecoded
6372
6772
  };
6373
- rejected.push({
6374
- kind: "hub-local-uds",
6375
- why: "child id not resolvable from snapshot"
6376
- });
6377
- } else rejected.push({
6378
- kind: "hub-local-uds",
6379
- why: "no hub-local child provides this cap"
6380
- });
6381
- const knownNodes = snap.listKnownNodeIds?.() ?? [];
6382
- for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
6383
- kind: "remote-moleculer",
6384
- capName,
6385
- nodeId
6386
- };
6387
- rejected.push({
6388
- kind: "remote-moleculer",
6389
- why: "no online remote node knows this cap"
6390
- });
6391
- throw new CapRouteError(capName, void 0, {
6392
- reason: "no-provider",
6393
- rejected
6394
- });
6395
- }
6396
- //#endregion
6397
- //#region src/kernel/transport/cap-route-resolver.ts
6398
- /** The Moleculer service name that Task 6's agent registers. */
6399
- var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
6400
- /** The Moleculer action (service.action) for agent cap forwarding. */
6401
- var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
6402
- /** Default timeout for remote Moleculer cap calls (ms). */
6403
- var REMOTE_CALL_TIMEOUT_MS = 6e4;
6404
- function extractDeviceId(args) {
6405
- if (args === null || typeof args !== "object") return void 0;
6406
- const raw = Reflect.get(args, "deviceId");
6407
- return typeof raw === "number" ? raw : void 0;
6408
- }
6409
- /**
6410
- * Extract an inline `nodeId` string from a cap call's args — the per-call node
6411
- * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
6412
- * device-scoped caps carry `deviceId`, and the way the generated cap-router on
6413
- * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
6414
- *
6415
- * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
6416
- * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
6417
- * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
6418
- * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
6419
- * methods that don't declare `nodeId` simply ignore the extra field (they
6420
- * destructure the fields they need); methods that DO declare it (e.g.
6421
- * `getEngineProvisioning({nodeId})`) still receive it unchanged.
6422
- */
6423
- function extractNodeId(args) {
6424
- if (args === null || typeof args !== "object") return void 0;
6425
- const raw = Reflect.get(args, "nodeId");
6426
- return typeof raw === "string" && raw.length > 0 ? raw : void 0;
6427
- }
6428
- /**
6429
- * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
6430
- * destructures what it needs and ignores the rest" convention
6431
- * {@link extractNodeId} already relies on for the inline `nodeId` pin.
6432
- * `undefined` (the overwhelmingly common case: no caller hint, or the census
6433
- * consuming it is disarmed) returns `args` unchanged — no allocation. A
6434
- * non-object `args` (or `null`/array) is returned unchanged too: there is
6435
- * nowhere to attach an extra key without changing the payload's shape, and a
6436
- * provider taking a bare value never reads `callerAddonId` off it anyway.
6437
- */
6438
- function withCallerAddonId(args, callerAddonId) {
6439
- if (callerAddonId === void 0) return args;
6440
- if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
6441
- return {
6442
- ...args,
6443
- callerAddonId
6444
- };
6445
- }
6446
- var CapRouteResolver = class {
6447
- hubNodeId;
6448
- broker;
6449
- hubLocalRegistry;
6450
- nodeAuthority;
6451
- inProcessProviders;
6452
- hubResidentProviders;
6453
- capTimeoutMs;
6454
- snapshot;
6455
- constructor(deps) {
6456
- this.hubNodeId = deps.hubNodeId;
6457
- this.broker = deps.broker;
6458
- this.hubLocalRegistry = deps.hubLocalRegistry;
6459
- this.nodeAuthority = deps.nodeAuthority;
6460
- this.inProcessProviders = deps.inProcessProviders;
6461
- this.hubResidentProviders = deps.hubResidentProviders;
6462
- this.capTimeoutMs = deps.capTimeoutMs;
6463
- this.snapshot = this.buildSnapshot();
6464
- }
6465
- /** Per-call RPC timeout: the cap method's declared override, else the default. */
6466
- callTimeout(capName, method) {
6467
- return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
6468
- }
6469
- resolveCapRoute(capName, opts) {
6470
- return classifyCapRoute(capName, opts, this.snapshot);
6471
6773
  }
6472
6774
  /**
6473
- * Resolve the hub-local-uds route for a cap owned by a forked hub-local
6474
- * child, IGNORING any in-hub provider registered for the same cap name.
6475
- *
6476
- * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
6477
- * provider (e.g. a wrapper) is registered for the cap, the route always
6478
- * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
6479
- * reached. That is correct for the generic dispatch path (the wrapper is the
6480
- * active provider), but WRONG for the native-cap fallback
6481
- * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
6482
- * the forked vendor child so a wrapper can delegate to it. A wrapper cap
6483
- * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
6484
- * itself, the native is never invoked, and the wrapper silently falls
6485
- * through to its secondary strategy.
6775
+ * The regime the counters above were produced under, read once from
6776
+ * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
6486
6777
  *
6487
- * This method consults ONLY the hub-local-child authority
6488
- * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
6489
- * it returns the native child route regardless of any in-process shadow.
6490
- * Returns null when no hub-local child owns `(capName, deviceId)` — the
6491
- * caller then falls through to its remote-resolution branch.
6778
+ * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
6779
+ * WOULD have been dropped" under `shadow`. Those are different claims, and
6780
+ * until this getter existed the mode was invisible at runtime — the counters
6781
+ * were readable and the regime that produced them was not.
6492
6782
  */
6493
- resolveHubLocalUdsRoute(capName, deviceId) {
6494
- if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
6495
- const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
6496
- if (childId === null) return null;
6497
- return {
6498
- kind: "hub-local-uds",
6499
- capName,
6500
- childId
6501
- };
6502
- }
6503
- /**
6504
- * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
6505
- * the addonId of the provider the CALLER already resolved (e.g.
6506
- * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
6507
- * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
6508
- * route already encodes the provider in its action name
6509
- * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
6510
- * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
6511
- * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
6512
- * `dispatch`.
6513
- * @param callerAddonId Optional out-of-band CALLER-identity hint — the
6514
- * addon that ORIGINATED the call, distinct from `addonId` above (which
6515
- * names the resolved PROVIDER, not the caller). Set by
6516
- * `onUnownedCall` from `CapCallInput.callerAddonId`
6517
- * (`LocalChildRegistry` stamped it off the registered `childId`). Only
6518
- * the `hub-in-process` branch consumes it — merged into `args` so a
6519
- * diagnostic tool deep in that provider (e.g. device-manager's
6520
- * fleet-read census) can read it without every provider method changing
6521
- * signature, the same "extra field the provider destructures or
6522
- * ignores" convention `nodeId` already uses on this path.
6523
- */
6524
- async dispatch(route, method, args, addonId, callerAddonId) {
6525
- try {
6526
- return await this.dispatchInner(route, method, args, addonId, callerAddonId);
6527
- } catch (err) {
6528
- if (err instanceof CapRouteError) throw err;
6529
- const nodeId = this.routeNodeId(route);
6530
- const cause = err instanceof Error ? err : new Error(String(err));
6531
- throw new CapRouteError(route.capName, method, {
6532
- reason: "transport-failed",
6533
- nodeId,
6534
- rejected: [{
6535
- kind: route.kind,
6536
- why: cause.message
6537
- }]
6538
- }, cause);
6539
- }
6540
- }
6541
- /**
6542
- * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
6543
- * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
6544
- */
6545
- async dispatchInner(route, method, args, addonId, callerAddonId) {
6546
- switch (route.kind) {
6547
- case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
6548
- case "hub-local-uds": {
6549
- const registry = this.hubLocalRegistry;
6550
- if (registry === null) throw new CapRouteError(route.capName, method, {
6551
- reason: "no-provider",
6552
- rejected: [{
6553
- kind: "hub-local-uds",
6554
- why: "UDS registry not available"
6555
- }]
6556
- });
6557
- const deviceId = extractDeviceId(args);
6558
- const input = {
6559
- capName: route.capName,
6560
- method,
6561
- args,
6562
- ...deviceId !== void 0 ? { deviceId } : {}
6563
- };
6564
- return registry.callCapOnChild(route.childId, input);
6565
- }
6566
- case "remote-moleculer": {
6567
- const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
6568
- if (addonId === null) throw new CapRouteError(route.capName, method, {
6569
- reason: "no-provider",
6570
- nodeId: route.nodeId,
6571
- rejected: [{
6572
- kind: "remote-moleculer",
6573
- why: `no addonId known for ${route.nodeId}/${route.capName}`
6574
- }]
6575
- });
6576
- const deviceId = extractDeviceId(args);
6577
- const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
6578
- const action = capActionName(addonId, route.capName, method, isNative);
6579
- return callWithServiceDiscovery(this.broker, addonId, action, args, {
6580
- nodeID: route.nodeId,
6581
- timeout: this.callTimeout(route.capName, method)
6582
- });
6583
- }
6584
- case "agent-child-forward": {
6585
- const deviceId = extractDeviceId(args);
6586
- const params = {
6587
- capName: route.capName,
6588
- method,
6589
- args,
6590
- ...route.childId !== void 0 ? { childId: route.childId } : {},
6591
- ...deviceId !== void 0 ? { deviceId } : {},
6592
- ...addonId !== void 0 ? { addonId } : {}
6593
- };
6594
- return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
6595
- nodeID: route.agentNodeId,
6596
- timeout: this.callTimeout(route.capName, method)
6597
- });
6598
- }
6599
- }
6600
- }
6601
- buildSnapshot() {
6602
- const hubLocalRegistry = this.hubLocalRegistry;
6603
- const nodeAuthority = this.nodeAuthority;
6604
- const inProcessProviders = this.inProcessProviders;
6605
- const hubResidentProviders = this.hubResidentProviders;
6606
- return {
6607
- hubNodeId: this.hubNodeId,
6608
- hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
6609
- ...hubResidentProviders !== void 0 ? {
6610
- hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
6611
- getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
6612
- } : {},
6613
- hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
6614
- nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
6615
- nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
6616
- nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
6617
- listKnownNodeIds: () => nodeAuthority.listNodeIds(),
6618
- getInProcessProviderRef: (cap) => inProcessProviders(cap),
6619
- getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
6620
- getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
6621
- };
6622
- }
6623
- /** Extract a nodeId string from a route for error reporting, or undefined. */
6624
- routeNodeId(route) {
6625
- switch (route.kind) {
6626
- case "hub-in-process": return this.hubNodeId;
6627
- case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
6628
- case "remote-moleculer": return route.nodeId;
6629
- case "agent-child-forward": return route.agentNodeId;
6630
- }
6631
- }
6632
- };
6633
- //#endregion
6634
- //#region src/kernel/transport/local-child-registry.ts
6635
- function readFanoutMode() {
6636
- const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
6637
- if (raw === "shadow" || raw === "broadcast") return raw;
6638
- return "filter";
6639
- }
6640
- /**
6641
- * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
6642
- * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
6643
- * prefix to distinguish routing failures (safe to retry via broker) from real
6644
- * provider errors (must NOT retry to avoid double-executing side effects).
6645
- */
6646
- var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
6647
- /**
6648
- * Parent-side authority for local addon-runners reachable over a
6649
- * `LocalTransportServer` (UDS). Children register their cap manifest on
6650
- * connect; the registry routes `(capName, deviceId?)` cap calls to the
6651
- * owning child over the channel and drops a child's caps on disconnect.
6652
- */
6653
- var LocalChildRegistry = class {
6654
- children = /* @__PURE__ */ new Map();
6655
- registeredHandler = () => {};
6656
- goneHandler = () => {};
6657
- /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
6658
- nextIncarnation = 1;
6659
- eventHandler = null;
6660
- logHandler = null;
6661
- readinessHandler = null;
6662
- server;
6663
- onUnownedCall;
6664
- logger;
6665
- getActiveSingletonAddonId;
6666
- resolveChildIdForAddon;
6667
- /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
6668
- ownNodeId;
6669
- /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
6670
- isAggregatedCollectionMethod;
6671
- isAddonPinnedCall;
6672
- /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
6673
- capTimeoutMs;
6674
- /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
6675
- capUsageObserver;
6676
- /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
6677
- egressRoutedCaps = /* @__PURE__ */ new Set();
6678
- /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
6679
- fanoutMode = readFanoutMode();
6680
- /** Per-child event fan-out counters (sent / suppressed). */
6681
- childEventStats = /* @__PURE__ */ new Map();
6682
- /** Last pattern-set string logged per child, to dedup the INFO line. */
6683
- loggedPatternSet = /* @__PURE__ */ new Map();
6684
- /**
6685
- * Accepts either a plain positional `server` argument (backward-compatible)
6686
- * or a full `LocalChildRegistryOptions` object.
6687
- *
6688
- * Positional overloads (existing call sites are unchanged):
6689
- * new LocalChildRegistry(server)
6690
- * new LocalChildRegistry(server, onUnownedCall)
6691
- *
6692
- * Options object (new call sites that pass a logger):
6693
- * new LocalChildRegistry({ server, onUnownedCall, logger })
6694
- */
6695
- constructor(serverOrOptions, onUnownedCallArg) {
6696
- if (serverOrOptions && !("listen" in serverOrOptions)) {
6697
- const opts = serverOrOptions;
6698
- this.server = opts.server;
6699
- this.onUnownedCall = opts.onUnownedCall;
6700
- this.logger = opts.logger;
6701
- this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
6702
- this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
6703
- this.ownNodeId = opts.ownNodeId;
6704
- this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
6705
- this.isAddonPinnedCall = opts.isAddonPinnedCall;
6706
- this.capTimeoutMs = opts.capTimeoutMs;
6707
- this.capUsageObserver = opts.capUsageObserver;
6708
- } else {
6709
- this.server = serverOrOptions;
6710
- this.onUnownedCall = onUnownedCallArg;
6711
- }
6712
- }
6713
- async start() {
6714
- this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
6715
- this.server.onConnection((channel) => this.onConnection(channel));
6716
- await this.server.listen();
6717
- }
6718
- /**
6719
- * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
6720
- * child has no recorded events yet. Exposed for the starvation-verification
6721
- * surface (shadow burn-in + operator debug).
6722
- */
6723
- getChildEventStats(childId) {
6724
- const s = this.childEventStats.get(childId);
6725
- return s === void 0 ? null : {
6726
- sent: s.sent,
6727
- suppressed: s.suppressed
6728
- };
6729
- }
6730
- /**
6731
- * This child's socket traffic — bytes and messages, both directions, split by
6732
- * frame kind — or `null` when the child is gone or its channel keeps no
6733
- * counters (an in-process or test transport).
6734
- *
6735
- * The event counters above cover ONE kind of frame and, once surfaced on
6736
- * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
6737
- * is the same question asked of every frame the channel moves, and it is the
6738
- * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
6739
- * pair for the whole process and has no per-socket breakdown.
6740
- */
6741
- getChildSocketTraffic(childId) {
6742
- const entry = this.children.get(childId);
6743
- if (entry === void 0) return null;
6744
- return entry.channel.readTraffic?.() ?? null;
6745
- }
6746
- /**
6747
- * The regime the counters above were produced under, read once from
6748
- * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
6749
- *
6750
- * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
6751
- * WOULD have been dropped" under `shadow`. Those are different claims, and
6752
- * until this getter existed the mode was invisible at runtime — the counters
6753
- * were readable and the regime that produced them was not.
6754
- */
6755
- get eventFanoutMode() {
6756
- return this.fanoutMode;
6783
+ get eventFanoutMode() {
6784
+ return this.fanoutMode;
6757
6785
  }
6758
6786
  /**
6759
6787
  * Connected children that never declared a subscription pattern set.
@@ -7121,7 +7149,7 @@ var LocalChildRegistry = class {
7121
7149
  return;
7122
7150
  }
7123
7151
  });
7124
- channel.onRequest(async (body) => {
7152
+ channel.onRequest(async (body, payload) => {
7125
7153
  const msg = body;
7126
7154
  if (msg.kind === "register") {
7127
7155
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
@@ -7138,7 +7166,8 @@ var LocalChildRegistry = class {
7138
7166
  channel,
7139
7167
  caps: msg.caps,
7140
7168
  eventPatterns,
7141
- incarnation
7169
+ incarnation,
7170
+ rawForward: msg.rawForward === true
7142
7171
  });
7143
7172
  this.logPatternSet(msg.childId, eventPatterns);
7144
7173
  this.registeredHandler({
@@ -7147,23 +7176,28 @@ var LocalChildRegistry = class {
7147
7176
  incarnation,
7148
7177
  ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
7149
7178
  });
7150
- return { ok: true };
7151
- }
7179
+ return {
7180
+ ok: true,
7181
+ rawForward: true
7182
+ };
7183
+ }
7152
7184
  if (msg.kind === "cap-call-out") {
7153
7185
  const out = msg;
7154
- const input = {
7186
+ const hints = out.hints ?? liftRoutingHints(out.args);
7187
+ const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
7188
+ const buildInput = () => ({
7155
7189
  capName: out.capName,
7156
7190
  method: out.method,
7157
- args: out.args,
7191
+ args: materialise(),
7158
7192
  ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
7159
7193
  ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
7160
7194
  ...out.native === true ? { native: true } : {},
7161
7195
  ...childId !== null ? { callerAddonId: childId } : {}
7162
- };
7163
- const pinnedNodeId = out.nodeId ?? extractNodeId(out.args);
7196
+ });
7197
+ const pinnedNodeId = out.nodeId ?? hints.nodeId;
7164
7198
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
7165
7199
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
7166
- const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
7200
+ const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
7167
7201
  const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
7168
7202
  this.recordCapUsage(childId, target, out.capName, out.method);
7169
7203
  if (target !== null) {
@@ -7171,9 +7205,27 @@ var LocalChildRegistry = class {
7171
7205
  this.egressRoutedCaps.add(out.capName);
7172
7206
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
7173
7207
  }
7174
- return this.callCap(input);
7208
+ const entry = this.children.get(target);
7209
+ if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
7210
+ this.forwardedRaw += 1;
7211
+ const forward = {
7212
+ kind: "cap-call",
7213
+ capName: out.capName,
7214
+ method: out.method,
7215
+ args: void 0,
7216
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
7217
+ };
7218
+ const declared = this.capTimeoutMs?.(out.capName, out.method);
7219
+ return entry.channel.request(forward, {
7220
+ ...typeof declared === "number" ? { timeoutMs: declared } : {},
7221
+ payload
7222
+ });
7223
+ }
7224
+ this.forwardedDecoded += 1;
7225
+ return this.callCap(buildInput());
7175
7226
  }
7176
- if (this.onUnownedCall !== void 0) return this.onUnownedCall(input);
7227
+ this.forwardedDecoded += 1;
7228
+ if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
7177
7229
  throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
7178
7230
  }
7179
7231
  if (msg.kind === "readiness-request") return {
@@ -7203,6 +7255,11 @@ var LocalChildRegistry = class {
7203
7255
  };
7204
7256
  //#endregion
7205
7257
  //#region src/kernel/transport/local-child-client.ts
7258
+ /** Did the parent's `register` answer say it accepts payload-borne args? */
7259
+ function ackAcceptsRawForward(ack) {
7260
+ if (ack === null || typeof ack !== "object") return false;
7261
+ return ack.rawForward === true;
7262
+ }
7206
7263
  /**
7207
7264
  * Child side of the local UDS transport. Connects to its parent (hub or
7208
7265
  * agent), registers its cap manifest, and serves parent→child cap calls by
@@ -7258,6 +7315,13 @@ var LocalChildClient = class {
7258
7315
  latestCustomActions = null;
7259
7316
  /** Events and logs queued while the channel is not yet open. */
7260
7317
  pendingEmits = [];
7318
+ /**
7319
+ * Whether the parent acknowledged raw-forward at `register`. Until it has,
7320
+ * `callOut` sends args inline exactly as every child always did — a legacy
7321
+ * parent would decode a payload-borne call to `args: undefined` and run the
7322
+ * provider on nothing, silently. Re-read on every re-register.
7323
+ */
7324
+ parentRawForward = false;
7261
7325
  /** Handler for parent→child events. Registered via `onEvent`. */
7262
7326
  eventHandler = null;
7263
7327
  /**
@@ -7365,15 +7429,20 @@ var LocalChildClient = class {
7365
7429
  if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
7366
7430
  const client = createLocalTransport().createClient(this.options.nodeId);
7367
7431
  const channel = await client.connect();
7368
- channel.onRequest(async (body) => {
7432
+ channel.onRequest(async (body, payload) => {
7369
7433
  const msg = body;
7370
- if (msg.kind === "cap-call") return this.options.dispatch({
7371
- capName: msg.capName,
7372
- method: msg.method,
7373
- args: msg.args,
7374
- ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
7375
- ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
7376
- });
7434
+ if (msg.kind === "cap-call") {
7435
+ const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
7436
+ const result = await this.options.dispatch({
7437
+ capName: msg.capName,
7438
+ method: msg.method,
7439
+ args,
7440
+ ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
7441
+ ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
7442
+ });
7443
+ if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
7444
+ return result;
7445
+ }
7377
7446
  if (msg.kind === "addon-call") {
7378
7447
  if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
7379
7448
  return this.addonCallHandler({
@@ -7397,7 +7466,7 @@ var LocalChildClient = class {
7397
7466
  });
7398
7467
  const register = this.registerFrame(this.latestCaps);
7399
7468
  try {
7400
- await channel.request(register);
7469
+ this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
7401
7470
  } catch (err) {
7402
7471
  await client.close();
7403
7472
  throw err;
@@ -7425,87 +7494,546 @@ var LocalChildClient = class {
7425
7494
  this.latestCaps = caps;
7426
7495
  if (customActions !== void 0) this.latestCustomActions = customActions;
7427
7496
  if (this.channel === null) return;
7428
- await this.channel.request(this.registerFrame(caps));
7497
+ this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
7498
+ }
7499
+ /**
7500
+ * The register frame: caps, plus everything a re-register must atomically
7501
+ * re-carry so no separate re-sync can be missed — the subscription union
7502
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7503
+ * custom-action catalogs (omitted until the init loop produced them).
7504
+ */
7505
+ registerFrame(caps) {
7506
+ return {
7507
+ kind: "register",
7508
+ childId: this.options.childId,
7509
+ caps,
7510
+ ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7511
+ ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
7512
+ rawForward: true
7513
+ };
7514
+ }
7515
+ /**
7516
+ * Fire-and-forget: send a system event to the parent for forwarding to the
7517
+ * hub event bus. Safe to call before `start()` — events are buffered and
7518
+ * flushed on connect.
7519
+ */
7520
+ emitEvent(event) {
7521
+ const msg = {
7522
+ kind: "event",
7523
+ event
7524
+ };
7525
+ if (this.channel !== null) this.channel.emit(msg);
7526
+ else this.pendingEmits.push({
7527
+ kind: "event",
7528
+ msg
7529
+ });
7530
+ }
7531
+ /**
7532
+ * Fire-and-forget: send a structured log entry to the parent. Safe to call
7533
+ * before `start()` — log entries are buffered and flushed on connect.
7534
+ */
7535
+ sendLog(entry) {
7536
+ const msg = {
7537
+ kind: "log",
7538
+ ...entry
7539
+ };
7540
+ if (this.channel !== null) this.channel.emit(msg);
7541
+ else this.pendingEmits.push({
7542
+ kind: "log",
7543
+ msg
7544
+ });
7545
+ }
7546
+ /**
7547
+ * Request the current readiness snapshot from the parent. Returns the
7548
+ * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
7549
+ * Requires `start()` to have been called; throws with a clear message if not.
7550
+ */
7551
+ async requestReadinessSnapshot() {
7552
+ if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
7553
+ return (await this.channel.request({ kind: "readiness-request" })).records;
7554
+ }
7555
+ /**
7556
+ * Ask the parent to execute a cap call this child does NOT own. The parent
7557
+ * routes to the owning local sibling over UDS, or — if no sibling owns it —
7558
+ * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
7559
+ * production). Throws if called before `start()`.
7560
+ */
7561
+ async callOut(input) {
7562
+ if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
7563
+ const routing = {
7564
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
7565
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
7566
+ ...input.native === true ? { native: true } : {}
7567
+ };
7568
+ if (this.parentRawForward && input.args !== void 0) {
7569
+ const hints = liftRoutingHints(input.args);
7570
+ const msg = {
7571
+ kind: "cap-call-out",
7572
+ capName: input.capName,
7573
+ method: input.method,
7574
+ ...Object.keys(hints).length > 0 ? { hints } : {},
7575
+ ...routing
7576
+ };
7577
+ const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
7578
+ return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
7579
+ }
7580
+ const msg = {
7581
+ kind: "cap-call-out",
7582
+ capName: input.capName,
7583
+ method: input.method,
7584
+ args: input.args,
7585
+ ...routing
7586
+ };
7587
+ return this.channel.request(msg);
7588
+ }
7589
+ /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
7590
+ async close() {
7591
+ await this.client?.close();
7592
+ this.client = null;
7593
+ this.channel = null;
7594
+ }
7595
+ };
7596
+ //#endregion
7597
+ //#region src/kernel/transport/cap-route.ts
7598
+ function buildMessage(capName, method, detail) {
7599
+ const call = method !== void 0 ? `${capName}.${method}` : capName;
7600
+ const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
7601
+ const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
7602
+ const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
7603
+ return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
7604
+ }
7605
+ var CapRouteError = class extends Error {
7606
+ reason;
7607
+ nodeId;
7608
+ rejected;
7609
+ /**
7610
+ * @param cause Optional original error that triggered this routing failure.
7611
+ * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
7612
+ * Dispatchers wrapping transport errors MUST pass the original.
7613
+ */
7614
+ constructor(capName, method, detail, cause) {
7615
+ super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
7616
+ this.name = "CapRouteError";
7617
+ this.reason = detail.reason;
7618
+ this.nodeId = detail.nodeId;
7619
+ this.rejected = detail.rejected;
7620
+ }
7621
+ };
7622
+ /**
7623
+ * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
7624
+ *
7625
+ * Precedence (explicit nodeId path):
7626
+ * 1. hub-in-process — hub node + hubInProcessProvides
7627
+ * 2. hub-local-uds — hub node + hubLocalChildProvides
7628
+ * 3. node-offline → CapRouteError{reason:'node-offline'}
7629
+ * 4. agent-child-forward — node is an agent AND nodeKnowsCap
7630
+ * 5. remote-moleculer — any other online non-agent node
7631
+ *
7632
+ * Singleton path (no nodeId):
7633
+ * 1. hub-in-process (hub provides in-process)
7634
+ * 2. hub-local-uds (a hub-local UDS child provides it)
7635
+ * 3. remote-moleculer (any online, non-hub node that knows it)
7636
+ * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
7637
+ *
7638
+ * PURE: no side effects, no async, no broker/registry imports.
7639
+ */
7640
+ function classifyCapRoute(capName, opts, snapshot) {
7641
+ const rejected = [];
7642
+ if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
7643
+ return classifySingleton(capName, opts.deviceId, snapshot, rejected);
7644
+ }
7645
+ function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
7646
+ if (nodeId === snap.hubNodeId) {
7647
+ const residentProvides = snap.hubResidentProvides;
7648
+ const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
7649
+ const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
7650
+ if (providesOnHub(capName)) {
7651
+ const ref = getRef?.(capName) ?? null;
7652
+ if (ref !== null) return {
7653
+ kind: "hub-in-process",
7654
+ capName,
7655
+ ref
7656
+ };
7657
+ rejected.push({
7658
+ kind: "hub-in-process",
7659
+ why: "provider ref not available in snapshot"
7660
+ });
7661
+ throw new CapRouteError(capName, void 0, {
7662
+ reason: "no-provider",
7663
+ nodeId,
7664
+ rejected
7665
+ });
7666
+ }
7667
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
7668
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
7669
+ if (childId !== null) return {
7670
+ kind: "hub-local-uds",
7671
+ capName,
7672
+ childId
7673
+ };
7674
+ rejected.push({
7675
+ kind: "hub-local-uds",
7676
+ why: "child id not resolvable from snapshot"
7677
+ });
7678
+ throw new CapRouteError(capName, void 0, {
7679
+ reason: "no-provider",
7680
+ nodeId,
7681
+ rejected
7682
+ });
7683
+ }
7684
+ rejected.push({
7685
+ kind: "hub-in-process",
7686
+ why: "hub does not provide this cap in-process"
7687
+ });
7688
+ rejected.push({
7689
+ kind: "hub-local-uds",
7690
+ why: "no hub-local child provides this cap"
7691
+ });
7692
+ throw new CapRouteError(capName, void 0, {
7693
+ reason: "no-provider",
7694
+ nodeId,
7695
+ rejected
7696
+ });
7697
+ }
7698
+ if (!snap.nodeOnline(nodeId)) {
7699
+ rejected.push({
7700
+ kind: "remote-moleculer",
7701
+ why: `node ${nodeId} is offline`
7702
+ });
7703
+ throw new CapRouteError(capName, void 0, {
7704
+ reason: "node-offline",
7705
+ nodeId,
7706
+ rejected
7707
+ });
7708
+ }
7709
+ if (snap.nodeIsAgent(nodeId)) {
7710
+ if (snap.nodeKnowsCap(nodeId, capName)) return {
7711
+ kind: "agent-child-forward",
7712
+ capName,
7713
+ agentNodeId: nodeId,
7714
+ childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
7715
+ };
7716
+ rejected.push({
7717
+ kind: "agent-child-forward",
7718
+ why: `agent ${nodeId} does not know cap ${capName}`
7719
+ });
7720
+ throw new CapRouteError(capName, void 0, {
7721
+ reason: "no-provider",
7722
+ nodeId,
7723
+ rejected
7724
+ });
7725
+ }
7726
+ return {
7727
+ kind: "remote-moleculer",
7728
+ capName,
7729
+ nodeId
7730
+ };
7731
+ }
7732
+ function classifySingleton(capName, deviceId, snap, rejected) {
7733
+ if (snap.hubInProcessProvides(capName)) {
7734
+ const ref = snap.getInProcessProviderRef?.(capName) ?? null;
7735
+ if (ref !== null) return {
7736
+ kind: "hub-in-process",
7737
+ capName,
7738
+ ref
7739
+ };
7740
+ rejected.push({
7741
+ kind: "hub-in-process",
7742
+ why: "provider ref not available in snapshot"
7743
+ });
7744
+ } else rejected.push({
7745
+ kind: "hub-in-process",
7746
+ why: "hub does not provide this cap in-process"
7747
+ });
7748
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
7749
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
7750
+ if (childId !== null) return {
7751
+ kind: "hub-local-uds",
7752
+ capName,
7753
+ childId
7754
+ };
7755
+ rejected.push({
7756
+ kind: "hub-local-uds",
7757
+ why: "child id not resolvable from snapshot"
7758
+ });
7759
+ } else rejected.push({
7760
+ kind: "hub-local-uds",
7761
+ why: "no hub-local child provides this cap"
7762
+ });
7763
+ const knownNodes = snap.listKnownNodeIds?.() ?? [];
7764
+ for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
7765
+ kind: "remote-moleculer",
7766
+ capName,
7767
+ nodeId
7768
+ };
7769
+ rejected.push({
7770
+ kind: "remote-moleculer",
7771
+ why: "no online remote node knows this cap"
7772
+ });
7773
+ throw new CapRouteError(capName, void 0, {
7774
+ reason: "no-provider",
7775
+ rejected
7776
+ });
7777
+ }
7778
+ //#endregion
7779
+ //#region src/kernel/moleculer/resilient-cap-call.ts
7780
+ /** Moleculer error `type` values meaning "the service is not (yet) routable". */
7781
+ var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
7782
+ /** Default ceiling for the discovery wait — fail-fast past this. */
7783
+ var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
7784
+ function isDiscoveryError(err) {
7785
+ if (typeof err !== "object" || err === null) return false;
7786
+ const type = err.type;
7787
+ return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
7788
+ }
7789
+ /**
7790
+ * Call a Moleculer action; on a service-discovery error, wait for the
7791
+ * named service to be discovered and retry the call exactly once.
7792
+ */
7793
+ async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
7794
+ try {
7795
+ return await broker.call(action, params, opts);
7796
+ } catch (err) {
7797
+ if (!isDiscoveryError(err)) throw err;
7798
+ await broker.waitForServices([serviceName], discoveryTimeoutMs);
7799
+ return await broker.call(action, params, opts);
7800
+ }
7801
+ }
7802
+ //#endregion
7803
+ //#region src/kernel/transport/cap-route-resolver.ts
7804
+ /** The Moleculer service name that Task 6's agent registers. */
7805
+ var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
7806
+ /** The Moleculer action (service.action) for agent cap forwarding. */
7807
+ var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
7808
+ /** Default timeout for remote Moleculer cap calls (ms). */
7809
+ var REMOTE_CALL_TIMEOUT_MS = 6e4;
7810
+ function extractDeviceId(args) {
7811
+ if (args === null || typeof args !== "object") return void 0;
7812
+ const raw = Reflect.get(args, "deviceId");
7813
+ return typeof raw === "number" ? raw : void 0;
7814
+ }
7815
+ /**
7816
+ * Extract an inline `nodeId` string from a cap call's args — the per-call node
7817
+ * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
7818
+ * device-scoped caps carry `deviceId`, and the way the generated cap-router on
7819
+ * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
7820
+ *
7821
+ * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
7822
+ * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
7823
+ * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
7824
+ * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
7825
+ * methods that don't declare `nodeId` simply ignore the extra field (they
7826
+ * destructure the fields they need); methods that DO declare it (e.g.
7827
+ * `getEngineProvisioning({nodeId})`) still receive it unchanged.
7828
+ */
7829
+ function extractNodeId(args) {
7830
+ if (args === null || typeof args !== "object") return void 0;
7831
+ const raw = Reflect.get(args, "nodeId");
7832
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
7833
+ }
7834
+ /**
7835
+ * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
7836
+ * destructures what it needs and ignores the rest" convention
7837
+ * {@link extractNodeId} already relies on for the inline `nodeId` pin.
7838
+ * `undefined` (the overwhelmingly common case: no caller hint, or the census
7839
+ * consuming it is disarmed) returns `args` unchanged — no allocation. A
7840
+ * non-object `args` (or `null`/array) is returned unchanged too: there is
7841
+ * nowhere to attach an extra key without changing the payload's shape, and a
7842
+ * provider taking a bare value never reads `callerAddonId` off it anyway.
7843
+ */
7844
+ function withCallerAddonId(args, callerAddonId) {
7845
+ if (callerAddonId === void 0) return args;
7846
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
7847
+ return {
7848
+ ...args,
7849
+ callerAddonId
7850
+ };
7851
+ }
7852
+ var CapRouteResolver = class {
7853
+ hubNodeId;
7854
+ broker;
7855
+ hubLocalRegistry;
7856
+ nodeAuthority;
7857
+ inProcessProviders;
7858
+ hubResidentProviders;
7859
+ capTimeoutMs;
7860
+ snapshot;
7861
+ constructor(deps) {
7862
+ this.hubNodeId = deps.hubNodeId;
7863
+ this.broker = deps.broker;
7864
+ this.hubLocalRegistry = deps.hubLocalRegistry;
7865
+ this.nodeAuthority = deps.nodeAuthority;
7866
+ this.inProcessProviders = deps.inProcessProviders;
7867
+ this.hubResidentProviders = deps.hubResidentProviders;
7868
+ this.capTimeoutMs = deps.capTimeoutMs;
7869
+ this.snapshot = this.buildSnapshot();
7870
+ }
7871
+ /** Per-call RPC timeout: the cap method's declared override, else the default. */
7872
+ callTimeout(capName, method) {
7873
+ return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
7429
7874
  }
7430
- /**
7431
- * The register frame: caps, plus everything a re-register must atomically
7432
- * re-carry so no separate re-sync can be missed — the subscription union
7433
- * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7434
- * custom-action catalogs (omitted until the init loop produced them).
7435
- */
7436
- registerFrame(caps) {
7437
- return {
7438
- kind: "register",
7439
- childId: this.options.childId,
7440
- caps,
7441
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7442
- ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
7443
- };
7875
+ resolveCapRoute(capName, opts) {
7876
+ return classifyCapRoute(capName, opts, this.snapshot);
7444
7877
  }
7445
7878
  /**
7446
- * Fire-and-forget: send a system event to the parent for forwarding to the
7447
- * hub event bus. Safe to call before `start()` — events are buffered and
7448
- * flushed on connect.
7879
+ * Resolve the hub-local-uds route for a cap owned by a forked hub-local
7880
+ * child, IGNORING any in-hub provider registered for the same cap name.
7881
+ *
7882
+ * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
7883
+ * provider (e.g. a wrapper) is registered for the cap, the route always
7884
+ * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
7885
+ * reached. That is correct for the generic dispatch path (the wrapper is the
7886
+ * active provider), but WRONG for the native-cap fallback
7887
+ * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
7888
+ * the forked vendor child so a wrapper can delegate to it. A wrapper cap
7889
+ * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
7890
+ * itself, the native is never invoked, and the wrapper silently falls
7891
+ * through to its secondary strategy.
7892
+ *
7893
+ * This method consults ONLY the hub-local-child authority
7894
+ * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
7895
+ * it returns the native child route regardless of any in-process shadow.
7896
+ * Returns null when no hub-local child owns `(capName, deviceId)` — the
7897
+ * caller then falls through to its remote-resolution branch.
7449
7898
  */
7450
- emitEvent(event) {
7451
- const msg = {
7452
- kind: "event",
7453
- event
7899
+ resolveHubLocalUdsRoute(capName, deviceId) {
7900
+ if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
7901
+ const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
7902
+ if (childId === null) return null;
7903
+ return {
7904
+ kind: "hub-local-uds",
7905
+ capName,
7906
+ childId
7454
7907
  };
7455
- if (this.channel !== null) this.channel.emit(msg);
7456
- else this.pendingEmits.push({
7457
- kind: "event",
7458
- msg
7459
- });
7460
7908
  }
7461
7909
  /**
7462
- * Fire-and-forget: send a structured log entry to the parent. Safe to call
7463
- * before `start()` — log entries are buffered and flushed on connect.
7910
+ * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
7911
+ * the addonId of the provider the CALLER already resolved (e.g.
7912
+ * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
7913
+ * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
7914
+ * route already encodes the provider in its action name
7915
+ * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
7916
+ * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
7917
+ * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
7918
+ * `dispatch`.
7919
+ * @param callerAddonId Optional out-of-band CALLER-identity hint — the
7920
+ * addon that ORIGINATED the call, distinct from `addonId` above (which
7921
+ * names the resolved PROVIDER, not the caller). Set by
7922
+ * `onUnownedCall` from `CapCallInput.callerAddonId`
7923
+ * (`LocalChildRegistry` stamped it off the registered `childId`). Only
7924
+ * the `hub-in-process` branch consumes it — merged into `args` so a
7925
+ * diagnostic tool deep in that provider (e.g. device-manager's
7926
+ * fleet-read census) can read it without every provider method changing
7927
+ * signature, the same "extra field the provider destructures or
7928
+ * ignores" convention `nodeId` already uses on this path.
7464
7929
  */
7465
- sendLog(entry) {
7466
- const msg = {
7467
- kind: "log",
7468
- ...entry
7469
- };
7470
- if (this.channel !== null) this.channel.emit(msg);
7471
- else this.pendingEmits.push({
7472
- kind: "log",
7473
- msg
7474
- });
7930
+ async dispatch(route, method, args, addonId, callerAddonId) {
7931
+ try {
7932
+ return await this.dispatchInner(route, method, args, addonId, callerAddonId);
7933
+ } catch (err) {
7934
+ if (err instanceof CapRouteError) throw err;
7935
+ const nodeId = this.routeNodeId(route);
7936
+ const cause = err instanceof Error ? err : new Error(String(err));
7937
+ throw new CapRouteError(route.capName, method, {
7938
+ reason: "transport-failed",
7939
+ nodeId,
7940
+ rejected: [{
7941
+ kind: route.kind,
7942
+ why: cause.message
7943
+ }]
7944
+ }, cause);
7945
+ }
7475
7946
  }
7476
7947
  /**
7477
- * Request the current readiness snapshot from the parent. Returns the
7478
- * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
7479
- * Requires `start()` to have been called; throws with a clear message if not.
7948
+ * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
7949
+ * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
7480
7950
  */
7481
- async requestReadinessSnapshot() {
7482
- if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
7483
- return (await this.channel.request({ kind: "readiness-request" })).records;
7951
+ async dispatchInner(route, method, args, addonId, callerAddonId) {
7952
+ switch (route.kind) {
7953
+ case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
7954
+ case "hub-local-uds": {
7955
+ const registry = this.hubLocalRegistry;
7956
+ if (registry === null) throw new CapRouteError(route.capName, method, {
7957
+ reason: "no-provider",
7958
+ rejected: [{
7959
+ kind: "hub-local-uds",
7960
+ why: "UDS registry not available"
7961
+ }]
7962
+ });
7963
+ const deviceId = extractDeviceId(args);
7964
+ const input = {
7965
+ capName: route.capName,
7966
+ method,
7967
+ args,
7968
+ ...deviceId !== void 0 ? { deviceId } : {}
7969
+ };
7970
+ return registry.callCapOnChild(route.childId, input);
7971
+ }
7972
+ case "remote-moleculer": {
7973
+ const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
7974
+ if (addonId === null) throw new CapRouteError(route.capName, method, {
7975
+ reason: "no-provider",
7976
+ nodeId: route.nodeId,
7977
+ rejected: [{
7978
+ kind: "remote-moleculer",
7979
+ why: `no addonId known for ${route.nodeId}/${route.capName}`
7980
+ }]
7981
+ });
7982
+ const deviceId = extractDeviceId(args);
7983
+ const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
7984
+ const action = capActionName(addonId, route.capName, method, isNative);
7985
+ return callWithServiceDiscovery(this.broker, addonId, action, args, {
7986
+ nodeID: route.nodeId,
7987
+ timeout: this.callTimeout(route.capName, method)
7988
+ });
7989
+ }
7990
+ case "agent-child-forward": {
7991
+ const deviceId = extractDeviceId(args);
7992
+ const params = {
7993
+ capName: route.capName,
7994
+ method,
7995
+ args,
7996
+ ...route.childId !== void 0 ? { childId: route.childId } : {},
7997
+ ...deviceId !== void 0 ? { deviceId } : {},
7998
+ ...addonId !== void 0 ? { addonId } : {}
7999
+ };
8000
+ return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
8001
+ nodeID: route.agentNodeId,
8002
+ timeout: this.callTimeout(route.capName, method)
8003
+ });
8004
+ }
8005
+ }
7484
8006
  }
7485
- /**
7486
- * Ask the parent to execute a cap call this child does NOT own. The parent
7487
- * routes to the owning local sibling over UDS, or — if no sibling owns it —
7488
- * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
7489
- * production). Throws if called before `start()`.
7490
- */
7491
- async callOut(input) {
7492
- if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
7493
- const msg = {
7494
- kind: "cap-call-out",
7495
- capName: input.capName,
7496
- method: input.method,
7497
- args: input.args,
7498
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
7499
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
7500
- ...input.native === true ? { native: true } : {}
8007
+ buildSnapshot() {
8008
+ const hubLocalRegistry = this.hubLocalRegistry;
8009
+ const nodeAuthority = this.nodeAuthority;
8010
+ const inProcessProviders = this.inProcessProviders;
8011
+ const hubResidentProviders = this.hubResidentProviders;
8012
+ return {
8013
+ hubNodeId: this.hubNodeId,
8014
+ hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
8015
+ ...hubResidentProviders !== void 0 ? {
8016
+ hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
8017
+ getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
8018
+ } : {},
8019
+ hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
8020
+ nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
8021
+ nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
8022
+ nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
8023
+ listKnownNodeIds: () => nodeAuthority.listNodeIds(),
8024
+ getInProcessProviderRef: (cap) => inProcessProviders(cap),
8025
+ getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
8026
+ getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
7501
8027
  };
7502
- return this.channel.request(msg);
7503
8028
  }
7504
- /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
7505
- async close() {
7506
- await this.client?.close();
7507
- this.client = null;
7508
- this.channel = null;
8029
+ /** Extract a nodeId string from a route for error reporting, or undefined. */
8030
+ routeNodeId(route) {
8031
+ switch (route.kind) {
8032
+ case "hub-in-process": return this.hubNodeId;
8033
+ case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
8034
+ case "remote-moleculer": return route.nodeId;
8035
+ case "agent-child-forward": return route.agentNodeId;
8036
+ }
7509
8037
  }
7510
8038
  };
7511
8039
  //#endregion
@@ -10357,12 +10885,36 @@ Object.defineProperty(exports, "LocalChildRegistry", {
10357
10885
  return LocalChildRegistry;
10358
10886
  }
10359
10887
  });
10888
+ Object.defineProperty(exports, "MESH_QUEUE_TOP_N", {
10889
+ enumerable: true,
10890
+ get: function() {
10891
+ return MESH_QUEUE_TOP_N;
10892
+ }
10893
+ });
10360
10894
  Object.defineProperty(exports, "NATIVE_PROVIDER_SERVICE_INFIX", {
10361
10895
  enumerable: true,
10362
10896
  get: function() {
10363
10897
  return NATIVE_PROVIDER_SERVICE_INFIX;
10364
10898
  }
10365
10899
  });
10900
+ Object.defineProperty(exports, "OUTBOUND_QUEUE_COOLDOWN_MS", {
10901
+ enumerable: true,
10902
+ get: function() {
10903
+ return OUTBOUND_QUEUE_COOLDOWN_MS;
10904
+ }
10905
+ });
10906
+ Object.defineProperty(exports, "OUTBOUND_QUEUE_SUSTAIN_MS", {
10907
+ enumerable: true,
10908
+ get: function() {
10909
+ return OUTBOUND_QUEUE_SUSTAIN_MS;
10910
+ }
10911
+ });
10912
+ Object.defineProperty(exports, "OUTBOUND_QUEUE_THRESHOLD_BYTES", {
10913
+ enumerable: true,
10914
+ get: function() {
10915
+ return OUTBOUND_QUEUE_THRESHOLD_BYTES;
10916
+ }
10917
+ });
10366
10918
  Object.defineProperty(exports, "PARENT_ROUTED_PROVIDER_ADDON_ID", {
10367
10919
  enumerable: true,
10368
10920
  get: function() {
@@ -10399,6 +10951,12 @@ Object.defineProperty(exports, "SOCKET_PLANE_TOP_N", {
10399
10951
  return SOCKET_PLANE_TOP_N;
10400
10952
  }
10401
10953
  });
10954
+ Object.defineProperty(exports, "SOCKET_QUEUE_TOP_N", {
10955
+ enumerable: true,
10956
+ get: function() {
10957
+ return SOCKET_QUEUE_TOP_N;
10958
+ }
10959
+ });
10402
10960
  Object.defineProperty(exports, "SocketChannel", {
10403
10961
  enumerable: true,
10404
10962
  get: function() {
@@ -10597,6 +11155,18 @@ Object.defineProperty(exports, "createLoopDelayMeter", {
10597
11155
  return createLoopDelayMeter;
10598
11156
  }
10599
11157
  });
11158
+ Object.defineProperty(exports, "createMeshQueueReader", {
11159
+ enumerable: true,
11160
+ get: function() {
11161
+ return createMeshQueueReader;
11162
+ }
11163
+ });
11164
+ Object.defineProperty(exports, "createOutboundQueueGate", {
11165
+ enumerable: true,
11166
+ get: function() {
11167
+ return createOutboundQueueGate;
11168
+ }
11169
+ });
10600
11170
  Object.defineProperty(exports, "createParentUnownedCallHandler", {
10601
11171
  enumerable: true,
10602
11172
  get: function() {
@@ -10711,6 +11281,18 @@ Object.defineProperty(exports, "formatHeapSpaces", {
10711
11281
  return formatHeapSpaces;
10712
11282
  }
10713
11283
  });
11284
+ Object.defineProperty(exports, "formatMeshQueue", {
11285
+ enumerable: true,
11286
+ get: function() {
11287
+ return formatMeshQueue;
11288
+ }
11289
+ });
11290
+ Object.defineProperty(exports, "formatOutboundQueueVerdict", {
11291
+ enumerable: true,
11292
+ get: function() {
11293
+ return formatOutboundQueueVerdict;
11294
+ }
11295
+ });
10714
11296
  Object.defineProperty(exports, "formatSocketPlane", {
10715
11297
  enumerable: true,
10716
11298
  get: function() {
@@ -10855,6 +11437,12 @@ Object.defineProperty(exports, "readMoleculerFanoutMode", {
10855
11437
  return readMoleculerFanoutMode;
10856
11438
  }
10857
11439
  });
11440
+ Object.defineProperty(exports, "readSocketQueues", {
11441
+ enumerable: true,
11442
+ get: function() {
11443
+ return readSocketQueues;
11444
+ }
11445
+ });
10858
11446
  Object.defineProperty(exports, "reclaimIntervalMs", {
10859
11447
  enumerable: true,
10860
11448
  get: function() {