@camstack/system 1.2.313 → 1.2.314

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 (106) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.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/camera-grid/index.js +1 -1
  14. package/dist/builtins/camera-grid/index.mjs +1 -1
  15. package/dist/builtins/composer/claim-gate.d.ts +32 -0
  16. package/dist/builtins/composer/composed-device-set.d.ts +38 -0
  17. package/dist/builtins/composer/composed-device.d.ts +4 -3
  18. package/dist/builtins/composer/composer-grafts.d.ts +54 -0
  19. package/dist/builtins/composer/composer-plan.d.ts +89 -0
  20. package/dist/builtins/composer/composer-verdict.d.ts +19 -0
  21. package/dist/builtins/composer/composer.addon.js +2500 -422
  22. package/dist/builtins/composer/composer.addon.mjs +2493 -415
  23. package/dist/builtins/composer/composer.d.ts +66 -51
  24. package/dist/builtins/composer/composition-runtime.d.ts +84 -9
  25. package/dist/builtins/composer/confirmed-seed.d.ts +32 -0
  26. package/dist/builtins/composer/existing-target.d.ts +87 -0
  27. package/dist/builtins/composer/field-record.d.ts +41 -0
  28. package/dist/builtins/composer/graft-host.d.ts +47 -0
  29. package/dist/builtins/composer/held-claims.d.ts +12 -0
  30. package/dist/builtins/composer/owned-fields-target.d.ts +55 -0
  31. package/dist/builtins/composer/slice-assembler.d.ts +10 -0
  32. package/dist/builtins/composer/source-readings.d.ts +122 -0
  33. package/dist/builtins/composer/source-tracker.d.ts +35 -42
  34. package/dist/builtins/console-logging/index.js +1 -1
  35. package/dist/builtins/console-logging/index.mjs +1 -1
  36. package/dist/builtins/core-blocks/composition-api.d.ts +7 -4
  37. package/dist/builtins/core-blocks/composition-peers.d.ts +9 -0
  38. package/dist/builtins/core-blocks/composition-sources.d.ts +14 -3
  39. package/dist/builtins/core-blocks/core-block-store.d.ts +8 -1
  40. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +4 -3
  41. package/dist/builtins/core-blocks/core-blocks.addon.js +326 -50
  42. package/dist/builtins/core-blocks/core-blocks.addon.mjs +326 -50
  43. package/dist/builtins/device-manager/claimed-status-overlay.d.ts +12 -0
  44. package/dist/builtins/device-manager/device-manager.addon.d.ts +16 -0
  45. package/dist/builtins/device-manager/device-manager.addon.js +1825 -317
  46. package/dist/builtins/device-manager/device-manager.addon.mjs +1825 -317
  47. package/dist/builtins/device-manager/device-provider-context.d.ts +24 -2
  48. package/dist/builtins/device-manager/device-row-store.d.ts +2 -0
  49. package/dist/builtins/device-manager/device-state-claim-views.d.ts +77 -0
  50. package/dist/builtins/device-manager/device-state-claims.d.ts +43 -0
  51. package/dist/builtins/device-manager/device-state-mirror.d.ts +145 -61
  52. package/dist/builtins/device-manager/field-claims-index.d.ts +69 -0
  53. package/dist/builtins/device-manager/field-ownership.d.ts +53 -0
  54. package/dist/builtins/device-manager/migrate-device.d.ts +27 -0
  55. package/dist/builtins/device-manager/migrate-guard.d.ts +7 -0
  56. package/dist/builtins/device-manager/migrate-hardware-state.d.ts +55 -0
  57. package/dist/builtins/device-manager/migration-refused.d.ts +23 -0
  58. package/dist/builtins/device-manager/mirror-row-writer.d.ts +138 -0
  59. package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +4 -0
  60. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -3
  61. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -3
  62. package/dist/builtins/hub-forwarder/index.js +1 -1
  63. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  64. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  65. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  66. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  67. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  68. package/dist/builtins/local-network/local-network.addon.js +1 -1
  69. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  70. package/dist/builtins/loki-logging/index.js +1 -1
  71. package/dist/builtins/loki-logging/index.mjs +1 -1
  72. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  73. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  74. package/dist/builtins/platform-probe/index.js +1 -1
  75. package/dist/builtins/platform-probe/index.mjs +1 -1
  76. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  77. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  78. package/dist/builtins/snapshot/index.js +1 -1
  79. package/dist/builtins/snapshot/index.mjs +1 -1
  80. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  81. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  82. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  83. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  84. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  85. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  86. package/dist/builtins/system-config/system-config.addon.js +1 -1
  87. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  88. package/dist/builtins/winston-logging/index.js +1 -1
  89. package/dist/builtins/winston-logging/index.mjs +1 -1
  90. package/dist/{child-cap-dispatch-CHTiHbsv.mjs → child-cap-dispatch-AAltfBUH.mjs} +1484 -1417
  91. package/dist/{child-cap-dispatch-Dyn_7ezH.js → child-cap-dispatch-DzcUd_ct.js} +1503 -1424
  92. package/dist/{dist-CkRwnhbo.js → dist-BtNkk3Qz.js} +4577 -3824
  93. package/dist/{dist-Cj7pJmXq.mjs → dist-cH4t1ZEm.mjs} +4541 -3824
  94. package/dist/index.js +137 -8
  95. package/dist/index.mjs +133 -8
  96. package/dist/kernel/capability-registry.d.ts +43 -1
  97. package/dist/kernel/index.d.ts +3 -1
  98. package/dist/kernel/moleculer/device-cap-proxy.d.ts +0 -10
  99. package/dist/kernel/status-overlay.d.ts +18 -0
  100. package/dist/kernel/transport/claimed-get-status.d.ts +15 -0
  101. package/dist/kernel/transport/index.d.ts +2 -0
  102. package/dist/kernel/transport/local-child-registry.d.ts +23 -0
  103. package/dist/kernel/transport/parent-unowned-call.d.ts +25 -0
  104. package/dist/{retired-settings-keys-CGSuXhP8.js → retired-settings-keys-B7imb-qH.js} +1 -1
  105. package/dist/{retired-settings-keys-C1duPikd.mjs → retired-settings-keys-DQYHfZ0S.mjs} +1 -1
  106. package/package.json +1 -1
@@ -4631,31 +4631,6 @@ function buildNativeCapProxy(broker, addonId, capName, deviceId) {
4631
4631
  };
4632
4632
  } });
4633
4633
  }
4634
- /**
4635
- * Hub-side proxy for a device-scoped native cap whose owning runner is a
4636
- * HUB-LOCAL forked child reachable over UDS. Mirrors {@link buildNativeCapProxy}
4637
- * but dispatches via `LocalChildRegistry.callCapOnChild` (UDS) instead of
4638
- * `broker.call` — the child no longer hosts a Moleculer service after the UDS
4639
- * cutover, so a broker call fails "is not found". The `deviceId` is merged into
4640
- * the method args (provider convention) and carried as the routing field.
4641
- */
4642
- function buildUdsNativeCapProxy(registry, childId, capName, deviceId) {
4643
- return new Proxy({}, { get(_target, property) {
4644
- if (typeof property !== "string") return void 0;
4645
- return (input) => {
4646
- const mergedInput = typeof input === "object" && input !== null ? {
4647
- ...input,
4648
- deviceId
4649
- } : { deviceId };
4650
- return registry.callCapOnChild(childId, {
4651
- capName,
4652
- method: property,
4653
- args: mergedInput,
4654
- deviceId
4655
- });
4656
- };
4657
- } });
4658
- }
4659
4634
  function buildDeviceOpsProvider(device) {
4660
4635
  const assertMatch = (incoming) => {
4661
4636
  if (incoming !== device.id) throw new Error(`device-ops: deviceId mismatch — expected '${device.id}', got '${incoming}'`);
@@ -4841,6 +4816,61 @@ function createBrokerDeviceManagerApi(opts) {
4841
4816
  generation: nativeCapReadinessGeneration,
4842
4817
  sourceNodeId: nodeId
4843
4818
  });
4819
+ },
4820
+ unregisterNativeCap(capName) {
4821
+ const worker = opts.capabilityRegistry === void 0 && (opts.broker !== void 0 || opts.udsMode === true);
4822
+ if (!opts.capabilityRegistry && !worker) throw new Error(`DeviceContext.unregisterNativeCap('${capName}') called on '${stableId}' but none of capabilityRegistry / broker / udsMode was provided.`);
4823
+ const holder = opts.capabilityRegistry ? opts.capabilityRegistry.getNativeAddonId(capName, id) : workerNativeCapAddonIds.get(capName)?.get(id) ?? null;
4824
+ const staleRetract = worker && holder === null;
4825
+ if (holder !== addonId && !staleRetract) {
4826
+ opts.logger.debug("unregisterNativeCap: the cap is not registered by this addon — no-op", {
4827
+ tags: {
4828
+ deviceId: id,
4829
+ addonId
4830
+ },
4831
+ meta: { capName }
4832
+ });
4833
+ return;
4834
+ }
4835
+ if (staleRetract) opts.logger.info("unregisterNativeCap: retracting a binding this process never registered (a previous incarnation of the addon)", {
4836
+ tags: {
4837
+ deviceId: id,
4838
+ addonId
4839
+ },
4840
+ meta: { capName }
4841
+ });
4842
+ else if (opts.capabilityRegistry) opts.capabilityRegistry.unregisterNativeProvider(capName, id);
4843
+ else {
4844
+ workerNativeCaps.get(capName)?.delete(id);
4845
+ workerNativeCapAddonIds.get(capName)?.delete(id);
4846
+ onWorkerNativeCapsChanged?.();
4847
+ }
4848
+ eventBus.emit({
4849
+ id: randomUUID(),
4850
+ timestamp: /* @__PURE__ */ new Date(),
4851
+ source: {
4852
+ type: "device",
4853
+ id
4854
+ },
4855
+ category: EventCategory.DeviceBindingsChanged,
4856
+ data: {
4857
+ deviceId: id,
4858
+ capName,
4859
+ reason: "native-unregistered",
4860
+ addonId,
4861
+ nodeId
4862
+ }
4863
+ });
4864
+ emitReadiness(eventBus, {
4865
+ capName,
4866
+ scope: {
4867
+ type: "device",
4868
+ deviceId: id
4869
+ },
4870
+ state: "down",
4871
+ generation: nativeCapReadinessGeneration,
4872
+ sourceNodeId: nodeId
4873
+ });
4844
4874
  }
4845
4875
  };
4846
4876
  };
@@ -7981,1536 +8011,1566 @@ function liftRoutingHints(args) {
7981
8011
  ...typeof deviceId === "number" && Number.isFinite(deviceId) ? { deviceId } : {}
7982
8012
  };
7983
8013
  }
7984
- function readFanoutMode() {
7985
- const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
7986
- if (raw === "shadow" || raw === "broadcast") return raw;
7987
- return "filter";
8014
+ //#endregion
8015
+ //#region src/kernel/moleculer/resilient-cap-call.ts
8016
+ /** Moleculer error `type` values meaning "the service is not (yet) routable". */
8017
+ var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
8018
+ /** Default ceiling for the discovery wait — fail-fast past this. */
8019
+ var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
8020
+ function isDiscoveryError(err) {
8021
+ if (typeof err !== "object" || err === null) return false;
8022
+ const type = err.type;
8023
+ return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
7988
8024
  }
7989
8025
  /**
7990
- * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
7991
- * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
7992
- * prefix to distinguish routing failures (safe to retry via broker) from real
7993
- * provider errors (must NOT retry to avoid double-executing side effects).
7994
- */
7995
- var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
7996
- /**
7997
- * Parent-side authority for local addon-runners reachable over a
7998
- * `LocalTransportServer` (UDS). Children register their cap manifest on
7999
- * connect; the registry routes `(capName, deviceId?)` cap calls to the
8000
- * owning child over the channel and drops a child's caps on disconnect.
8026
+ * Call a Moleculer action; on a service-discovery error, wait for the
8027
+ * named service to be discovered and retry the call exactly once.
8001
8028
  */
8002
- var LocalChildRegistry = class {
8003
- children = /* @__PURE__ */ new Map();
8004
- registeredHandler = () => {};
8005
- goneHandler = () => {};
8006
- /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
8007
- nextIncarnation = 1;
8008
- eventHandler = null;
8009
- logHandler = null;
8010
- readinessHandler = null;
8011
- server;
8012
- onUnownedCall;
8013
- logger;
8014
- getActiveSingletonAddonId;
8015
- resolveChildIdForAddon;
8016
- /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
8017
- ownNodeId;
8018
- /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
8019
- isAggregatedCollectionMethod;
8020
- isAddonPinnedCall;
8021
- /** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
8022
- forwardedRaw = 0;
8023
- forwardedDecoded = 0;
8024
- /**
8025
- * The per-ACTION byte census (D456). `ForwardCensus` above is the same
8026
- * question asked of the whole plane; this one asks it per `<cap>.<method>`,
8027
- * beside the bytes that method actually moved. Both are kept: `socketFwd=`
8028
- * says whether the negotiation is working at all, and a `raw=0` on ONE
8029
- * method with `raw>0` on its neighbours is a different fault.
8030
- */
8031
- actionCensus = createCapActionCensus();
8032
- /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
8033
- capTimeoutMs;
8034
- /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
8035
- capUsageObserver;
8036
- /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
8037
- egressRoutedCaps = /* @__PURE__ */ new Set();
8038
- /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
8039
- fanoutMode = readFanoutMode();
8040
- /** Per-child event fan-out counters (sent / suppressed). */
8041
- childEventStats = /* @__PURE__ */ new Map();
8042
- /** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
8043
- categoryStats = /* @__PURE__ */ new Map();
8044
- /** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
8045
- categoryOverflow = 0;
8046
- /** One INFO line the first time the census cap trips, never per event. */
8047
- categoryOverflowLogged = false;
8048
- /** Last pattern-set string logged per child, to dedup the INFO line. */
8049
- loggedPatternSet = /* @__PURE__ */ new Map();
8050
- /**
8051
- * Derived `(cap, device?) → child` index over {@link children}, or `null`
8052
- * when it must be rebuilt. See {@link capIndex} for why it is a cache and
8053
- * never a registry (D3, D457).
8054
- */
8055
- capChildIndex = null;
8056
- /**
8057
- * Accepts either a plain positional `server` argument (backward-compatible)
8058
- * or a full `LocalChildRegistryOptions` object.
8059
- *
8060
- * Positional overloads (existing call sites are unchanged):
8061
- * new LocalChildRegistry(server)
8062
- * new LocalChildRegistry(server, onUnownedCall)
8063
- *
8064
- * Options object (new call sites that pass a logger):
8065
- * new LocalChildRegistry({ server, onUnownedCall, logger })
8066
- */
8067
- constructor(serverOrOptions, onUnownedCallArg) {
8068
- if (serverOrOptions && !("listen" in serverOrOptions)) {
8069
- const opts = serverOrOptions;
8070
- this.server = opts.server;
8071
- this.onUnownedCall = opts.onUnownedCall;
8072
- this.logger = opts.logger;
8073
- this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
8074
- this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
8075
- this.ownNodeId = opts.ownNodeId;
8076
- this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
8077
- this.isAddonPinnedCall = opts.isAddonPinnedCall;
8078
- this.capTimeoutMs = opts.capTimeoutMs;
8079
- this.capUsageObserver = opts.capUsageObserver;
8080
- } else {
8081
- this.server = serverOrOptions;
8082
- this.onUnownedCall = onUnownedCallArg;
8083
- }
8084
- }
8085
- async start() {
8086
- this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
8087
- this.server.onConnection((channel) => this.onConnection(channel));
8088
- await this.server.listen();
8089
- }
8090
- /**
8091
- * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
8092
- * child has no recorded events yet. Exposed for the starvation-verification
8093
- * surface (shadow burn-in + operator debug).
8094
- */
8095
- getChildEventStats(childId) {
8096
- const s = this.childEventStats.get(childId);
8097
- return s === void 0 ? null : {
8098
- sent: s.sent,
8099
- suppressed: s.suppressed
8100
- };
8101
- }
8102
- /**
8103
- * D467: every category this node has broadcast, with its emit count and how
8104
- * many child subscriptions matched. Cumulative for the life of the process —
8105
- * the heartbeat differences it, the same discipline as the per-child
8106
- * counters. Largest `broadcasts` first.
8107
- */
8108
- categoryFanout() {
8109
- const out = [];
8110
- for (const [category, s] of this.categoryStats) out.push({
8111
- category,
8112
- broadcasts: s.broadcasts,
8113
- wanted: s.wanted
8114
- });
8115
- return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
8116
- }
8117
- /** Broadcasts the census could not attribute because it was full (cumulative). */
8118
- categoryCensusOverflow() {
8119
- return this.categoryOverflow;
8120
- }
8121
- /**
8122
- * This child's socket traffic — bytes and messages, both directions, split by
8123
- * frame kind — or `null` when the child is gone or its channel keeps no
8124
- * counters (an in-process or test transport).
8125
- *
8126
- * The event counters above cover ONE kind of frame and, once surfaced on
8127
- * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
8128
- * is the same question asked of every frame the channel moves, and it is the
8129
- * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
8130
- * pair for the whole process and has no per-socket breakdown.
8131
- */
8132
- getChildSocketTraffic(childId) {
8133
- const entry = this.children.get(childId);
8134
- if (entry === void 0) return null;
8135
- return entry.channel.readTraffic?.() ?? null;
8029
+ async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
8030
+ try {
8031
+ return await broker.call(action, params, opts);
8032
+ } catch (err) {
8033
+ if (!isDiscoveryError(err)) throw err;
8034
+ await broker.waitForServices([serviceName], discoveryTimeoutMs);
8035
+ return await broker.call(action, params, opts);
8136
8036
  }
8037
+ }
8038
+ //#endregion
8039
+ //#region src/kernel/transport/cap-route.ts
8040
+ function buildMessage(capName, method, detail) {
8041
+ const call = method !== void 0 ? `${capName}.${method}` : capName;
8042
+ const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
8043
+ const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
8044
+ const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
8045
+ return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
8046
+ }
8047
+ var CapRouteError = class extends Error {
8048
+ reason;
8049
+ nodeId;
8050
+ rejected;
8137
8051
  /**
8138
- * Bytes this process has written to a child that the child has not yet
8139
- * taken, live, or `null` when the channel keeps no such reading. Read on
8140
- * every heartbeat probe (D446) — a child that stops draining is the shape
8141
- * of an outbound-queue episode, and the number lives on the channel.
8052
+ * @param cause Optional original error that triggered this routing failure.
8053
+ * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
8054
+ * Dispatchers wrapping transport errors MUST pass the original.
8142
8055
  */
8143
- getChildQueuedBytes(childId) {
8144
- const entry = this.children.get(childId);
8145
- if (entry === void 0) return null;
8146
- return entry.channel.queuedBytes?.() ?? null;
8056
+ constructor(capName, method, detail, cause) {
8057
+ super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
8058
+ this.name = "CapRouteError";
8059
+ this.reason = detail.reason;
8060
+ this.nodeId = detail.nodeId;
8061
+ this.rejected = detail.rejected;
8147
8062
  }
8148
- /** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
8149
- readForwardCensus() {
8150
- return {
8151
- raw: this.forwardedRaw,
8152
- decoded: this.forwardedDecoded
8153
- };
8154
- }
8155
- /**
8156
- * Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
8157
- * and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
8158
- * must be one); the heartbeat subtracts the previous reading.
8159
- *
8160
- * This is the instrument D454 closed by asking for. `socketTopBytes=` names
8161
- * the peer that moves the bytes and `txkB=` says they are responses; neither
8162
- * can say WHICH method originates them, and after raw-forward the bytes that
8163
- * cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
8164
- * calls cross unread. What costs is what hub-main LOOKS at, and that is the
8165
- * `decoded` half of an entry, next to the bytes it carried.
8166
- */
8167
- readActionCensus() {
8168
- return this.actionCensus.read();
8169
- }
8170
- /**
8171
- * The regime the counters above were produced under, read once from
8172
- * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
8173
- *
8174
- * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
8175
- * WOULD have been dropped" under `shadow`. Those are different claims, and
8176
- * until this getter existed the mode was invisible at runtime — the counters
8177
- * were readable and the regime that produced them was not.
8178
- */
8179
- get eventFanoutMode() {
8180
- return this.fanoutMode;
8181
- }
8182
- /**
8183
- * Connected children that never declared a subscription pattern set.
8184
- *
8185
- * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
8186
- * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
8187
- * `aggregateEventInterest` returns `null` — accept everything — if ANY
8188
- * connected child is undeclared, which disables the node's inbound cross-node
8189
- * gate as well. **One legacy runner silently turns off the filtering for the
8190
- * whole node**, and nothing reported it, so a zero here is as load-bearing as
8191
- * a non-zero one.
8192
- */
8193
- undeclaredChildCount() {
8194
- let count = 0;
8195
- for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
8196
- return count;
8197
- }
8198
- /**
8199
- * Does `entry` want to receive an event in `category`? Honours the fan-out
8200
- * mode: `broadcast` → always; undeclared patterns (`null`) → always
8201
- * (fail-open); else the child's declared set must match via `matchesPattern`
8202
- * — the SAME function the child's own `deliverShared` map-key gate uses,
8203
- * giving provable delivery parity. `shadow` is handled by the caller (it
8204
- * counts the would-suppress but still sends).
8205
- *
8206
- * NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
8207
- * `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
8208
- * that explicitly subscribes to a ring-denied category must still RECEIVE it.
8209
- */
8210
- childWantsEvent(entry, category) {
8211
- if (this.fanoutMode === "broadcast") return true;
8212
- return this.childSubscribesTo(entry, category);
8213
- }
8214
- /**
8215
- * The SUBSCRIPTION answer alone, with no fan-out-mode override.
8216
- *
8217
- * {@link childWantsEvent} answers "will this child be sent the event", which
8218
- * under `broadcast` is `true` for everything and therefore cannot say whether
8219
- * anyone actually asked. The D467 census needs the second question, so the
8220
- * two are separate functions and `broadcast` mode overrides only the first.
8221
- */
8222
- childSubscribesTo(entry, category) {
8223
- if (entry.eventPatterns === null) return true;
8224
- return entry.eventPatterns.some((p) => matchesPattern(p, category));
8225
- }
8226
- /**
8227
- * D2: this node's aggregate cross-node event interest — the UNION of every
8228
- * connected child's declared category patterns. Consumed by the parent's
8229
- * `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
8230
- * accepts cross-node (Moleculer) categories at least one local child wants.
8231
- *
8232
- * Returns `null` (fail-open — accept everything) when:
8233
- * - NO child is currently connected (boot window before the first forked
8234
- * child completes its UDS handshake, or every child has disconnected): a
8235
- * node MUST NOT drop 100% of inbound cross-node events during that window,
8236
- * and a child may connect imminently. This is distinct from the
8237
- * declared-empty case below.
8238
- * - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
8239
- * runner): a node cannot safely narrow its inbound set while a child's
8240
- * real interest is unknown. Mirrors the per-child fail-open in
8241
- * {@link childWantsEvent}, aggregated conservatively.
8242
- *
8243
- * Returns an EMPTY array only when ≥1 child is connected and EVERY connected
8244
- * child declared an explicit empty set — a genuine "wants nothing". Reads the
8245
- * live `children` map, so a child registering / disconnecting / sending
8246
- * `event-sub` is reflected on the next call with no extra bookkeeping.
8247
- */
8248
- aggregateEventInterest() {
8249
- if (this.children.size === 0) return null;
8250
- const union = /* @__PURE__ */ new Set();
8251
- for (const entry of this.children.values()) {
8252
- if (entry.eventPatterns === null) return null;
8253
- for (const p of entry.eventPatterns) union.add(p);
8063
+ };
8064
+ /**
8065
+ * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
8066
+ *
8067
+ * Precedence (explicit nodeId path):
8068
+ * 1. hub-in-process — hub node + hubInProcessProvides
8069
+ * 2. hub-local-uds — hub node + hubLocalChildProvides
8070
+ * 3. node-offline → CapRouteError{reason:'node-offline'}
8071
+ * 4. agent-child-forward — node is an agent AND nodeKnowsCap
8072
+ * 5. remote-moleculer — any other online non-agent node
8073
+ *
8074
+ * Singleton path (no nodeId):
8075
+ * 1. hub-in-process (hub provides in-process)
8076
+ * 2. hub-local-uds (a hub-local UDS child provides it)
8077
+ * 3. remote-moleculer (any online, non-hub node that knows it)
8078
+ * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
8079
+ *
8080
+ * PURE: no side effects, no async, no broker/registry imports.
8081
+ */
8082
+ function classifyCapRoute(capName, opts, snapshot) {
8083
+ const rejected = [];
8084
+ if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
8085
+ return classifySingleton(capName, opts.deviceId, snapshot, rejected);
8086
+ }
8087
+ function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
8088
+ if (nodeId === snap.hubNodeId) {
8089
+ const residentProvides = snap.hubResidentProvides;
8090
+ const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
8091
+ const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
8092
+ if (providesOnHub(capName)) {
8093
+ const ref = getRef?.(capName) ?? null;
8094
+ if (ref !== null) return {
8095
+ kind: "hub-in-process",
8096
+ capName,
8097
+ ref
8098
+ };
8099
+ rejected.push({
8100
+ kind: "hub-in-process",
8101
+ why: "provider ref not available in snapshot"
8102
+ });
8103
+ throw new CapRouteError(capName, void 0, {
8104
+ reason: "no-provider",
8105
+ nodeId,
8106
+ rejected
8107
+ });
8254
8108
  }
8255
- return [...union];
8256
- }
8257
- statsFor(childId) {
8258
- let s = this.childEventStats.get(childId);
8259
- if (s === void 0) {
8260
- s = {
8261
- sent: 0,
8262
- suppressed: 0
8109
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
8110
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
8111
+ if (childId !== null) return {
8112
+ kind: "hub-local-uds",
8113
+ capName,
8114
+ childId
8263
8115
  };
8264
- this.childEventStats.set(childId, s);
8116
+ rejected.push({
8117
+ kind: "hub-local-uds",
8118
+ why: "child id not resolvable from snapshot"
8119
+ });
8120
+ throw new CapRouteError(capName, void 0, {
8121
+ reason: "no-provider",
8122
+ nodeId,
8123
+ rejected
8124
+ });
8265
8125
  }
8266
- return s;
8126
+ rejected.push({
8127
+ kind: "hub-in-process",
8128
+ why: "hub does not provide this cap in-process"
8129
+ });
8130
+ rejected.push({
8131
+ kind: "hub-local-uds",
8132
+ why: "no hub-local child provides this cap"
8133
+ });
8134
+ throw new CapRouteError(capName, void 0, {
8135
+ reason: "no-provider",
8136
+ nodeId,
8137
+ rejected
8138
+ });
8267
8139
  }
8268
- /** Deduped INFO line whenever a child's declared pattern set changes. */
8269
- logPatternSet(childId, patterns) {
8270
- const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
8271
- if (this.loggedPatternSet.get(childId) === key) return;
8272
- this.loggedPatternSet.set(childId, key);
8273
- this.logger?.info("child event patterns", {
8274
- childId,
8275
- patterns: patterns ?? null
8140
+ if (!snap.nodeOnline(nodeId)) {
8141
+ rejected.push({
8142
+ kind: "remote-moleculer",
8143
+ why: `node ${nodeId} is offline`
8144
+ });
8145
+ throw new CapRouteError(capName, void 0, {
8146
+ reason: "node-offline",
8147
+ nodeId,
8148
+ rejected
8276
8149
  });
8277
8150
  }
8278
- /**
8279
- * Child id that can service a call to `capName` (optionally addressing
8280
- * `deviceId`), or null.
8281
- *
8282
- * `deviceId` is a routing HINT, not a hard filter. A singleton cap
8283
- * (`pipeline-runner`, `stream-broker`, …) addresses devices through its
8284
- * METHOD ARGUMENTS — `attachCamera({ deviceId })` is one provider serving
8285
- * many cameras — so its descriptor carries no `deviceId`. Resolving on
8286
- * `deviceId` alone would never match it and would force the call onto the
8287
- * broker fallback (which then hangs in service discovery). Hence:
8288
- * 1. exact device-scoped owner (native per-device caps where each child
8289
- * owns a disjoint device subset) is preferred, then
8290
- * 2. a singleton owner (deviceId-less descriptor) is the fallback.
8291
- * A cap is globally singleton XOR device-scoped, so the two tiers never
8292
- * compete for the same capName.
8293
- */
8294
- resolveChildId(capName, deviceId) {
8295
- const index = this.capIndex();
8296
- if (deviceId !== void 0) {
8297
- const deviceOwner = index.deviceOwner(capName, deviceId);
8298
- if (deviceOwner !== null) return deviceOwner;
8299
- }
8300
- const candidates = index.singletonCandidates(capName);
8301
- if (candidates.length === 0) return null;
8302
- if (candidates.length === 1) return candidates[0];
8303
- const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
8304
- if (preferredAddonId !== null) {
8305
- const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
8306
- if (candidates.includes(preferredChildId)) return preferredChildId;
8307
- }
8308
- return candidates[0];
8151
+ if (snap.nodeIsAgent(nodeId)) {
8152
+ if (snap.nodeKnowsCap(nodeId, capName)) return {
8153
+ kind: "agent-child-forward",
8154
+ capName,
8155
+ agentNodeId: nodeId,
8156
+ childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
8157
+ };
8158
+ rejected.push({
8159
+ kind: "agent-child-forward",
8160
+ why: `agent ${nodeId} does not know cap ${capName}`
8161
+ });
8162
+ throw new CapRouteError(capName, void 0, {
8163
+ reason: "no-provider",
8164
+ nodeId,
8165
+ rejected
8166
+ });
8309
8167
  }
8310
- /**
8311
- * Publish one cap-usage observation, if a sink is wired.
8312
- *
8313
- * Cost discipline — this is on the hot path (D181: hub-main's event loop is
8314
- * the cluster's only queue): no sink means one undefined check; with a sink
8315
- * it is one object literal and one `Date.now()`, and the sink itself is O(1).
8316
- * Wrapped so a broken observer can never fail the call it observes — the
8317
- * registry swallows too, and BOTH matter: this catch also covers a sink that
8318
- * is not the registry.
8319
- */
8320
- recordCapUsage(callerChildId, providerChildId, capName, methodName) {
8321
- const sink = this.capUsageObserver;
8322
- if (sink === void 0 || callerChildId === null) return;
8323
- try {
8324
- sink({
8325
- callerAddonId: callerChildId,
8326
- providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
8327
- capName,
8328
- methodName,
8329
- atMs: Date.now()
8330
- });
8331
- } catch {}
8168
+ return {
8169
+ kind: "remote-moleculer",
8170
+ capName,
8171
+ nodeId
8172
+ };
8173
+ }
8174
+ function classifySingleton(capName, deviceId, snap, rejected) {
8175
+ if (snap.hubInProcessProvides(capName)) {
8176
+ const ref = snap.getInProcessProviderRef?.(capName) ?? null;
8177
+ if (ref !== null) return {
8178
+ kind: "hub-in-process",
8179
+ capName,
8180
+ ref
8181
+ };
8182
+ rejected.push({
8183
+ kind: "hub-in-process",
8184
+ why: "provider ref not available in snapshot"
8185
+ });
8186
+ } else rejected.push({
8187
+ kind: "hub-in-process",
8188
+ why: "hub does not provide this cap in-process"
8189
+ });
8190
+ if (snap.hubLocalChildProvides(capName, deviceId)) {
8191
+ const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
8192
+ if (childId !== null) return {
8193
+ kind: "hub-local-uds",
8194
+ capName,
8195
+ childId
8196
+ };
8197
+ rejected.push({
8198
+ kind: "hub-local-uds",
8199
+ why: "child id not resolvable from snapshot"
8200
+ });
8201
+ } else rejected.push({
8202
+ kind: "hub-local-uds",
8203
+ why: "no hub-local child provides this cap"
8204
+ });
8205
+ const knownNodes = snap.listKnownNodeIds?.() ?? [];
8206
+ for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
8207
+ kind: "remote-moleculer",
8208
+ capName,
8209
+ nodeId
8210
+ };
8211
+ rejected.push({
8212
+ kind: "remote-moleculer",
8213
+ why: "no online remote node knows this cap"
8214
+ });
8215
+ throw new CapRouteError(capName, void 0, {
8216
+ reason: "no-provider",
8217
+ rejected
8218
+ });
8219
+ }
8220
+ //#endregion
8221
+ //#region src/kernel/transport/cap-route-resolver.ts
8222
+ /** The Moleculer service name that Task 6's agent registers. */
8223
+ var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
8224
+ /** The Moleculer action (service.action) for agent cap forwarding. */
8225
+ var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
8226
+ /** Default timeout for remote Moleculer cap calls (ms). */
8227
+ var REMOTE_CALL_TIMEOUT_MS = 6e4;
8228
+ function extractDeviceId(args) {
8229
+ if (args === null || typeof args !== "object") return void 0;
8230
+ const raw = Reflect.get(args, "deviceId");
8231
+ return typeof raw === "number" ? raw : void 0;
8232
+ }
8233
+ /**
8234
+ * Extract an inline `nodeId` string from a cap call's args — the per-call node
8235
+ * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
8236
+ * device-scoped caps carry `deviceId`, and the way the generated cap-router on
8237
+ * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
8238
+ *
8239
+ * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
8240
+ * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
8241
+ * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
8242
+ * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
8243
+ * methods that don't declare `nodeId` simply ignore the extra field (they
8244
+ * destructure the fields they need); methods that DO declare it (e.g.
8245
+ * `getEngineProvisioning({nodeId})`) still receive it unchanged.
8246
+ */
8247
+ function extractNodeId(args) {
8248
+ if (args === null || typeof args !== "object") return void 0;
8249
+ const raw = Reflect.get(args, "nodeId");
8250
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
8251
+ }
8252
+ /**
8253
+ * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
8254
+ * destructures what it needs and ignores the rest" convention
8255
+ * {@link extractNodeId} already relies on for the inline `nodeId` pin.
8256
+ * `undefined` (the overwhelmingly common case: no caller hint, or the census
8257
+ * consuming it is disarmed) returns `args` unchanged — no allocation. A
8258
+ * non-object `args` (or `null`/array) is returned unchanged too: there is
8259
+ * nowhere to attach an extra key without changing the payload's shape, and a
8260
+ * provider taking a bare value never reads `callerAddonId` off it anyway.
8261
+ */
8262
+ function withCallerAddonId(args, callerAddonId) {
8263
+ if (callerAddonId === void 0) return args;
8264
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
8265
+ return {
8266
+ ...args,
8267
+ callerAddonId
8268
+ };
8269
+ }
8270
+ var CapRouteResolver = class {
8271
+ hubNodeId;
8272
+ broker;
8273
+ hubLocalRegistry;
8274
+ nodeAuthority;
8275
+ inProcessProviders;
8276
+ hubResidentProviders;
8277
+ capTimeoutMs;
8278
+ snapshot;
8279
+ constructor(deps) {
8280
+ this.hubNodeId = deps.hubNodeId;
8281
+ this.broker = deps.broker;
8282
+ this.hubLocalRegistry = deps.hubLocalRegistry;
8283
+ this.nodeAuthority = deps.nodeAuthority;
8284
+ this.inProcessProviders = deps.inProcessProviders;
8285
+ this.hubResidentProviders = deps.hubResidentProviders;
8286
+ this.capTimeoutMs = deps.capTimeoutMs;
8287
+ this.snapshot = this.buildSnapshot();
8288
+ }
8289
+ /** Per-call RPC timeout: the cap method's declared override, else the default. */
8290
+ callTimeout(capName, method) {
8291
+ return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
8292
+ }
8293
+ resolveCapRoute(capName, opts) {
8294
+ return classifyCapRoute(capName, opts, this.snapshot);
8332
8295
  }
8333
8296
  /**
8334
- * The cap index over the CURRENT children, rebuilt on first use after any
8335
- * mutation of {@link children}.
8336
- *
8337
- * This replaced a linear walk of every child's cap manifest per
8338
- * `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
8339
- * across 39 children (D456). Why it did not exist before is the whole
8340
- * design: what it indexes moves — a runner is replaced on every update, a
8341
- * crash respawns one, a child re-registers with a replacement manifest after
8342
- * init — and an index that names a dead child routes work into a socket
8343
- * nobody is reading, which is strictly worse than a slow scan.
8297
+ * Resolve the hub-local-uds route for a cap owned by a forked hub-local
8298
+ * child, IGNORING any in-hub provider registered for the same cap name.
8344
8299
  *
8345
- * So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
8346
- * function of `children.values()`, the only two writers of that map
8347
- * ({@link invalidateCapIndex}'s call sites) drop the index in the same
8348
- * statement, and nothing else can construct one. There is no add-on-register
8349
- * / remove-on-close path that could disagree with the map — i.e. no shadow
8350
- * registry (D3): the map remains the single authority and this holds no fact
8351
- * it does not.
8300
+ * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
8301
+ * provider (e.g. a wrapper) is registered for the cap, the route always
8302
+ * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
8303
+ * reached. That is correct for the generic dispatch path (the wrapper is the
8304
+ * active provider), but WRONG for the native-cap fallback
8305
+ * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
8306
+ * the forked vendor child so a wrapper can delegate to it. A wrapper cap
8307
+ * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
8308
+ * itself, the native is never invoked, and the wrapper silently falls
8309
+ * through to its secondary strategy.
8352
8310
  *
8353
- * The operator's active-singleton preference is deliberately NOT indexed —
8354
- * it changes without any child connecting or leaving, so `resolveChildId`
8355
- * reads it live and the index only supplies the candidate list.
8311
+ * This method consults ONLY the hub-local-child authority
8312
+ * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
8313
+ * it returns the native child route regardless of any in-process shadow.
8314
+ * Returns null when no hub-local child owns `(capName, deviceId)` — the
8315
+ * caller then falls through to its remote-resolution branch.
8356
8316
  */
8357
- capIndex() {
8358
- if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
8359
- return this.capChildIndex;
8360
- }
8361
- /** Drop the derived index. MUST follow every write to {@link children}. */
8362
- invalidateCapIndex() {
8363
- this.capChildIndex = null;
8317
+ resolveHubLocalUdsRoute(capName, deviceId) {
8318
+ if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
8319
+ const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
8320
+ if (childId === null) return null;
8321
+ return {
8322
+ kind: "hub-local-uds",
8323
+ capName,
8324
+ childId
8325
+ };
8364
8326
  }
8365
8327
  /**
8366
- * Does the named child currently provide `(capName, deviceId?)`?
8367
- *
8368
- * Used by the hub proxy seam, which knows the EXACT addon (→ runner →
8369
- * childId) a provider belongs to. Unlike `resolveChildId` (which picks the
8370
- * first child owning `capName`), this targets one child — required for
8371
- * COLLECTION caps (`addon-widgets-source`, …) where many children register
8372
- * the same capName: routing by capName alone collapses every provider to
8373
- * the first child. Same deviceId-as-hint semantics as `resolveChildId`:
8374
- * a device-scoped descriptor matching `deviceId` OR a deviceId-less
8375
- * (singleton/collection) descriptor counts.
8328
+ * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
8329
+ * the addonId of the provider the CALLER already resolved (e.g.
8330
+ * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
8331
+ * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
8332
+ * route already encodes the provider in its action name
8333
+ * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
8334
+ * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
8335
+ * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
8336
+ * `dispatch`.
8337
+ * @param callerAddonId Optional out-of-band CALLER-identity hint — the
8338
+ * addon that ORIGINATED the call, distinct from `addonId` above (which
8339
+ * names the resolved PROVIDER, not the caller). Set by
8340
+ * `onUnownedCall` from `CapCallInput.callerAddonId`
8341
+ * (`LocalChildRegistry` stamped it off the registered `childId`). Only
8342
+ * the `hub-in-process` branch consumes it — merged into `args` so a
8343
+ * diagnostic tool deep in that provider (e.g. device-manager's
8344
+ * fleet-read census) can read it without every provider method changing
8345
+ * signature, the same "extra field the provider destructures or
8346
+ * ignores" convention `nodeId` already uses on this path.
8376
8347
  */
8348
+ async dispatch(route, method, args, addonId, callerAddonId) {
8349
+ try {
8350
+ return await this.dispatchInner(route, method, args, addonId, callerAddonId);
8351
+ } catch (err) {
8352
+ if (err instanceof CapRouteError) throw err;
8353
+ if (route.kind === "hub-in-process" || err instanceof PeerAnsweredError) throw err;
8354
+ const nodeId = this.routeNodeId(route);
8355
+ const cause = err instanceof Error ? err : new Error(String(err));
8356
+ throw new CapRouteError(route.capName, method, {
8357
+ reason: "transport-failed",
8358
+ nodeId,
8359
+ rejected: [{
8360
+ kind: route.kind,
8361
+ why: cause.message
8362
+ }]
8363
+ }, cause);
8364
+ }
8365
+ }
8377
8366
  /**
8378
- * Is `childId` currently connected (has it completed its UDS handshake)?
8379
- * Coarser than {@link childProvides}: it answers "is the child reachable
8380
- * over UDS at all", regardless of which caps it has announced yet. Used by
8381
- * the route-mount fallback to decide between the handler-stripped
8382
- * `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
8383
- * cap proxy's `getRoutes()` (child not yet UDS-registered).
8367
+ * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
8368
+ * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
8384
8369
  */
8385
- isChildKnown(childId) {
8386
- return this.children.has(childId);
8387
- }
8388
- childProvides(childId, capName, deviceId) {
8389
- const entry = this.children.get(childId);
8390
- if (!entry) return false;
8391
- if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
8392
- return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
8393
- }
8394
- /**
8395
- * Per-request options for a cap call: the cap method's declared `timeoutMs`
8396
- * when one exists, else `undefined` so the channel applies its default.
8397
- * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
8398
- */
8399
- capRequestOpts(input) {
8400
- const declared = this.capTimeoutMs?.(input.capName, input.method);
8401
- return typeof declared === "number" ? { timeoutMs: declared } : void 0;
8402
- }
8403
- /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
8404
- async callCapOnChild(childId, input) {
8405
- const entry = this.children.get(childId);
8406
- if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
8407
- return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
8408
- }
8409
- /** Forward a cap method call to the owning child over UDS; rejects if none. */
8410
- async callCap(input) {
8411
- const childId = this.resolveChildId(input.capName, input.deviceId);
8412
- const entry = childId === null ? void 0 : this.children.get(childId);
8413
- if (!entry) {
8414
- const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
8415
- throw new Error(`no local child provides cap "${input.capName}"${where}`);
8370
+ async dispatchInner(route, method, args, addonId, callerAddonId) {
8371
+ switch (route.kind) {
8372
+ case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
8373
+ case "hub-local-uds": {
8374
+ const registry = this.hubLocalRegistry;
8375
+ if (registry === null) throw new CapRouteError(route.capName, method, {
8376
+ reason: "no-provider",
8377
+ rejected: [{
8378
+ kind: "hub-local-uds",
8379
+ why: "UDS registry not available"
8380
+ }]
8381
+ });
8382
+ const deviceId = extractDeviceId(args);
8383
+ const input = {
8384
+ capName: route.capName,
8385
+ method,
8386
+ args,
8387
+ ...deviceId !== void 0 ? { deviceId } : {}
8388
+ };
8389
+ return registry.callCapOnChild(route.childId, input);
8390
+ }
8391
+ case "remote-moleculer": {
8392
+ const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
8393
+ if (addonId === null) throw new CapRouteError(route.capName, method, {
8394
+ reason: "no-provider",
8395
+ nodeId: route.nodeId,
8396
+ rejected: [{
8397
+ kind: "remote-moleculer",
8398
+ why: `no addonId known for ${route.nodeId}/${route.capName}`
8399
+ }]
8400
+ });
8401
+ const deviceId = extractDeviceId(args);
8402
+ const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
8403
+ const action = capActionName(addonId, route.capName, method, isNative);
8404
+ return callWithServiceDiscovery(this.broker, addonId, action, args, {
8405
+ nodeID: route.nodeId,
8406
+ timeout: this.callTimeout(route.capName, method)
8407
+ });
8408
+ }
8409
+ case "agent-child-forward": {
8410
+ const deviceId = extractDeviceId(args);
8411
+ const params = {
8412
+ capName: route.capName,
8413
+ method,
8414
+ args,
8415
+ ...route.childId !== void 0 ? { childId: route.childId } : {},
8416
+ ...deviceId !== void 0 ? { deviceId } : {},
8417
+ ...addonId !== void 0 ? { addonId } : {}
8418
+ };
8419
+ return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
8420
+ nodeID: route.agentNodeId,
8421
+ timeout: this.callTimeout(route.capName, method)
8422
+ });
8423
+ }
8416
8424
  }
8417
- return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
8418
- }
8419
- /**
8420
- * Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
8421
- * by id over UDS; rejects if that child is absent.
8422
- *
8423
- * The childId for a hub-local single-addon runner equals the addonId
8424
- * (`resolveRunnerId` returns the addonId when no `execution.group` is
8425
- * declared — no shipped addon declares one). Mirrors `callCapOnChild` for
8426
- * the cap plane; carries the two surfaces the removed per-addon Moleculer
8427
- * broker used to serve (`getRoutes` + `custom.<action>`).
8428
- */
8429
- async callAddonOnChild(childId, input) {
8430
- const entry = this.children.get(childId);
8431
- if (!entry) throw new Error(`no local child "${childId}" for addon-call (${input.target})`);
8432
- return entry.channel.request(this.toAddonCall(input));
8433
- }
8434
- /** Build the parent→child `addon-call` wire message from an addon-call input. */
8435
- toAddonCall(input) {
8436
- return {
8437
- kind: "addon-call",
8438
- addonId: input.addonId,
8439
- target: input.target,
8440
- ...input.action !== void 0 ? { action: input.action } : {},
8441
- ...input.method !== void 0 ? { method: input.method } : {},
8442
- ...input.args !== void 0 ? { args: input.args } : {},
8443
- ...input.caller !== void 0 ? { caller: input.caller } : {}
8444
- };
8445
8425
  }
8446
- /** Build the parent→child `cap-call` wire message from a routing input. */
8447
- toCapCall(input) {
8426
+ buildSnapshot() {
8427
+ const hubLocalRegistry = this.hubLocalRegistry;
8428
+ const nodeAuthority = this.nodeAuthority;
8429
+ const inProcessProviders = this.inProcessProviders;
8430
+ const hubResidentProviders = this.hubResidentProviders;
8448
8431
  return {
8449
- kind: "cap-call",
8450
- capName: input.capName,
8451
- method: input.method,
8452
- args: input.args,
8453
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
8454
- ...input.addonId !== void 0 ? { addonId: input.addonId } : {}
8432
+ hubNodeId: this.hubNodeId,
8433
+ hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
8434
+ ...hubResidentProviders !== void 0 ? {
8435
+ hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
8436
+ getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
8437
+ } : {},
8438
+ hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
8439
+ nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
8440
+ nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
8441
+ nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
8442
+ listKnownNodeIds: () => nodeAuthority.listNodeIds(),
8443
+ getInProcessProviderRef: (cap) => inProcessProviders(cap),
8444
+ getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
8445
+ getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
8455
8446
  };
8456
8447
  }
8457
- listChildren() {
8458
- return [...this.children.values()].map((e) => ({
8459
- childId: e.childId,
8460
- caps: e.caps,
8461
- incarnation: e.incarnation
8462
- }));
8463
- }
8464
- /** Register the (single) child-registered handler. Only one handler is active at a time. */
8465
- onChildRegistered(handler) {
8466
- this.registeredHandler = handler;
8448
+ /** Extract a nodeId string from a route for error reporting, or undefined. */
8449
+ routeNodeId(route) {
8450
+ switch (route.kind) {
8451
+ case "hub-in-process": return this.hubNodeId;
8452
+ case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
8453
+ case "remote-moleculer": return route.nodeId;
8454
+ case "agent-child-forward": return route.agentNodeId;
8455
+ }
8467
8456
  }
8457
+ };
8458
+ //#endregion
8459
+ //#region src/kernel/transport/claimed-get-status.ts
8460
+ var GET_STATUS_METHOD = "getStatus";
8461
+ /** The device a `getStatus` is about, or `undefined` when the call is not one. */
8462
+ function deviceOfGetStatus(call) {
8463
+ if (call.method !== "getStatus") return void 0;
8464
+ return call.deviceId ?? extractDeviceId(call.args);
8465
+ }
8466
+ async function answerClaimedGetStatus(resolve, call) {
8467
+ if (resolve === void 0) return null;
8468
+ const deviceId = deviceOfGetStatus(call);
8469
+ if (deviceId === void 0) return null;
8470
+ const answer = await resolve(call.capName, deviceId);
8471
+ if (answer.kind === "unclaimed") return null;
8472
+ return { slice: answer.kind === "claimed" ? answer.slice : null };
8473
+ }
8474
+ function readFanoutMode() {
8475
+ const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
8476
+ if (raw === "shadow" || raw === "broadcast") return raw;
8477
+ return "filter";
8478
+ }
8479
+ /**
8480
+ * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
8481
+ * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
8482
+ * prefix to distinguish routing failures (safe to retry via broker) from real
8483
+ * provider errors (must NOT retry to avoid double-executing side effects).
8484
+ */
8485
+ var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
8486
+ /**
8487
+ * Parent-side authority for local addon-runners reachable over a
8488
+ * `LocalTransportServer` (UDS). Children register their cap manifest on
8489
+ * connect; the registry routes `(capName, deviceId?)` cap calls to the
8490
+ * owning child over the channel and drops a child's caps on disconnect.
8491
+ */
8492
+ var LocalChildRegistry = class {
8493
+ children = /* @__PURE__ */ new Map();
8494
+ registeredHandler = () => {};
8495
+ goneHandler = () => {};
8496
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
8497
+ nextIncarnation = 1;
8498
+ eventHandler = null;
8499
+ logHandler = null;
8500
+ readinessHandler = null;
8501
+ server;
8502
+ onUnownedCall;
8503
+ logger;
8504
+ getActiveSingletonAddonId;
8505
+ resolveChildIdForAddon;
8506
+ /** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
8507
+ ownNodeId;
8508
+ /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
8509
+ isAggregatedCollectionMethod;
8510
+ isAddonPinnedCall;
8511
+ /** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
8512
+ forwardedRaw = 0;
8513
+ forwardedDecoded = 0;
8468
8514
  /**
8469
- * Register the (single) child-gone handler. Only one handler is active at a
8470
- * time. The handler receives the {@link RegisteredChild.incarnation} of the
8471
- * connection that closed — carry it into whatever teardown it triggers, so a
8472
- * dead generation cannot tear down its successor's work.
8515
+ * The per-ACTION byte census (D456). `ForwardCensus` above is the same
8516
+ * question asked of the whole plane; this one asks it per `<cap>.<method>`,
8517
+ * beside the bytes that method actually moved. Both are kept: `socketFwd=`
8518
+ * says whether the negotiation is working at all, and a `raw=0` on ONE
8519
+ * method with `raw>0` on its neighbours is a different fault.
8473
8520
  */
8474
- onChildGone(handler) {
8475
- this.goneHandler = handler;
8476
- }
8521
+ actionCensus = createCapActionCensus();
8522
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
8523
+ capTimeoutMs;
8524
+ /** See {@link LocalChildRegistryOptions.claimedStatus}. */
8525
+ claimedStatus;
8526
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
8527
+ capUsageObserver;
8528
+ /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
8529
+ egressRoutedCaps = /* @__PURE__ */ new Set();
8530
+ /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
8531
+ fanoutMode = readFanoutMode();
8532
+ /** Per-child event fan-out counters (sent / suppressed). */
8533
+ childEventStats = /* @__PURE__ */ new Map();
8534
+ /** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
8535
+ categoryStats = /* @__PURE__ */ new Map();
8536
+ /** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
8537
+ categoryOverflow = 0;
8538
+ /** One INFO line the first time the census cap trips, never per event. */
8539
+ categoryOverflowLogged = false;
8540
+ /** Last pattern-set string logged per child, to dedup the INFO line. */
8541
+ loggedPatternSet = /* @__PURE__ */ new Map();
8477
8542
  /**
8478
- * Register the (single) child-event handler. Invoked when a child sends an
8479
- * event via `LocalChildClient.emitEvent`. Only one handler is active at a
8480
- * time; a new handler replaces the prior one. Pass `null` to clear the
8481
- * handler entirely (used by the UDS event bridge disposer on shutdown).
8543
+ * Derived `(cap, device?) → child` index over {@link children}, or `null`
8544
+ * when it must be rebuilt. See {@link capIndex} for why it is a cache and
8545
+ * never a registry (D3, D457).
8482
8546
  */
8483
- onChildEvent(handler) {
8484
- this.eventHandler = handler;
8485
- }
8547
+ capChildIndex = null;
8486
8548
  /**
8487
- * Register the (single) child-log handler. Invoked when a child sends a log
8488
- * entry via `LocalChildClient.sendLog`. Only one handler is active.
8549
+ * Accepts either a plain positional `server` argument (backward-compatible)
8550
+ * or a full `LocalChildRegistryOptions` object.
8551
+ *
8552
+ * Positional overloads (existing call sites are unchanged):
8553
+ * new LocalChildRegistry(server)
8554
+ * new LocalChildRegistry(server, onUnownedCall)
8555
+ *
8556
+ * Options object (new call sites that pass a logger):
8557
+ * new LocalChildRegistry({ server, onUnownedCall, logger })
8489
8558
  */
8490
- onChildLog(handler) {
8491
- this.logHandler = handler;
8492
- }
8493
- /**
8494
- * Register the handler that supplies the authoritative readiness snapshot
8495
- * when a child sends a `readiness-request`. Only one handler is active.
8496
- */
8497
- onReadinessSnapshotRequest(handler) {
8498
- this.readinessHandler = handler;
8559
+ constructor(serverOrOptions, onUnownedCallArg) {
8560
+ if (serverOrOptions && !("listen" in serverOrOptions)) {
8561
+ const opts = serverOrOptions;
8562
+ this.server = opts.server;
8563
+ this.onUnownedCall = opts.onUnownedCall;
8564
+ this.logger = opts.logger;
8565
+ this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
8566
+ this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
8567
+ this.ownNodeId = opts.ownNodeId;
8568
+ this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
8569
+ this.isAddonPinnedCall = opts.isAddonPinnedCall;
8570
+ this.capTimeoutMs = opts.capTimeoutMs;
8571
+ this.claimedStatus = opts.claimedStatus;
8572
+ this.capUsageObserver = opts.capUsageObserver;
8573
+ } else {
8574
+ this.server = serverOrOptions;
8575
+ this.onUnownedCall = onUnownedCallArg;
8576
+ }
8577
+ }
8578
+ async start() {
8579
+ this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
8580
+ this.server.onConnection((channel) => this.onConnection(channel));
8581
+ await this.server.listen();
8499
8582
  }
8500
8583
  /**
8501
- * Push a parent→child event to a specific child. Fire-and-forget (uses the
8502
- * one-way `emit` path on the channel). No-op if the child is not connected.
8584
+ * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
8585
+ * child has no recorded events yet. Exposed for the starvation-verification
8586
+ * surface (shadow burn-in + operator debug).
8503
8587
  */
8504
- sendEventToChild(childId, event, sourceNodeId) {
8505
- const entry = this.children.get(childId);
8506
- if (entry === void 0) return;
8507
- const msg = {
8508
- kind: "event",
8509
- event,
8510
- sourceNodeId
8588
+ getChildEventStats(childId) {
8589
+ const s = this.childEventStats.get(childId);
8590
+ return s === void 0 ? null : {
8591
+ sent: s.sent,
8592
+ suppressed: s.suppressed
8511
8593
  };
8512
- entry.channel.emit(msg);
8513
8594
  }
8514
8595
  /**
8515
- * Push a parent→child event to every registered child, optionally skipping
8516
- * one (the originating child, to avoid echo). Fire-and-forget.
8596
+ * D467: every category this node has broadcast, with its emit count and how
8597
+ * many child subscriptions matched. Cumulative for the life of the process —
8598
+ * the heartbeat differences it, the same discipline as the per-child
8599
+ * counters. Largest `broadcasts` first.
8517
8600
  */
8518
- broadcastEventToChildren(event, sourceNodeId, exceptChildId) {
8519
- const msg = {
8520
- kind: "event",
8521
- event,
8522
- sourceNodeId
8523
- };
8524
- let subscribed = 0;
8525
- for (const entry of this.children.values()) {
8526
- if (entry.childId === exceptChildId) continue;
8527
- if (this.childSubscribesTo(entry, event.category)) subscribed += 1;
8528
- const wants = this.childWantsEvent(entry, event.category);
8529
- const stats = this.statsFor(entry.childId);
8530
- if (!wants) stats.suppressed++;
8531
- if (wants || this.fanoutMode === "shadow") {
8532
- stats.sent++;
8533
- entry.channel.emit(msg);
8534
- }
8535
- }
8536
- this.recordCategoryBroadcast(event.category, subscribed);
8601
+ categoryFanout() {
8602
+ const out = [];
8603
+ for (const [category, s] of this.categoryStats) out.push({
8604
+ category,
8605
+ broadcasts: s.broadcasts,
8606
+ wanted: s.wanted
8607
+ });
8608
+ return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
8609
+ }
8610
+ /** Broadcasts the census could not attribute because it was full (cumulative). */
8611
+ categoryCensusOverflow() {
8612
+ return this.categoryOverflow;
8537
8613
  }
8538
8614
  /**
8539
- * Record one PRODUCED event against its category. Called once per broadcast,
8540
- * outside the per-child loop, because production is what the census measures.
8615
+ * This child's socket traffic — bytes and messages, both directions, split by
8616
+ * frame kind — or `null` when the child is gone or its channel keeps no
8617
+ * counters (an in-process or test transport).
8618
+ *
8619
+ * The event counters above cover ONE kind of frame and, once surfaced on
8620
+ * 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
8621
+ * is the same question asked of every frame the channel moves, and it is the
8622
+ * only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
8623
+ * pair for the whole process and has no per-socket breakdown.
8541
8624
  */
8542
- recordCategoryBroadcast(category, subscribed) {
8543
- const existing = this.categoryStats.get(category);
8544
- if (existing !== void 0) {
8545
- existing.broadcasts += 1;
8546
- existing.wanted += subscribed;
8547
- return;
8548
- }
8549
- if (this.categoryStats.size >= 256) {
8550
- this.categoryOverflow += 1;
8551
- if (!this.categoryOverflowLogged) {
8552
- this.categoryOverflowLogged = true;
8553
- this.logger?.info("event category census full — further new categories are unattributed", {
8554
- max: 256,
8555
- firstDropped: category
8556
- });
8557
- }
8558
- return;
8559
- }
8560
- this.categoryStats.set(category, {
8561
- broadcasts: 1,
8562
- wanted: subscribed
8563
- });
8625
+ getChildSocketTraffic(childId) {
8626
+ const entry = this.children.get(childId);
8627
+ if (entry === void 0) return null;
8628
+ return entry.channel.readTraffic?.() ?? null;
8564
8629
  }
8565
8630
  /**
8566
- * E2: Send a `set-log-level` control message to a specific child.
8567
- * Returns `true` if the child is currently connected and the message was
8568
- * emitted; `false` if the child is not connected (no-op). The `false`
8569
- * return lets the caller (MoleculerService.setChildLogLevelByNodeId) fall
8570
- * back to the Moleculer `$node-mgmt.setLogLevel` action for the node.
8571
- * Mirrors the `$node-mgmt.setLogLevel` Moleculer action for UDS children.
8631
+ * Bytes this process has written to a child that the child has not yet
8632
+ * taken, live, or `null` when the channel keeps no such reading. Read on
8633
+ * every heartbeat probe (D446) — a child that stops draining is the shape
8634
+ * of an outbound-queue episode, and the number lives on the channel.
8572
8635
  */
8573
- setChildLogLevel(childId, level) {
8636
+ getChildQueuedBytes(childId) {
8574
8637
  const entry = this.children.get(childId);
8575
- if (entry === void 0) return false;
8576
- const msg = {
8577
- kind: "set-log-level",
8578
- level
8638
+ if (entry === void 0) return null;
8639
+ return entry.channel.queuedBytes?.() ?? null;
8640
+ }
8641
+ /** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
8642
+ readForwardCensus() {
8643
+ return {
8644
+ raw: this.forwardedRaw,
8645
+ decoded: this.forwardedDecoded
8579
8646
  };
8580
- entry.channel.emit(msg);
8581
- return true;
8582
8647
  }
8583
- async close() {
8584
- await this.server.close();
8648
+ /**
8649
+ * Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
8650
+ * and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
8651
+ * must be one); the heartbeat subtracts the previous reading.
8652
+ *
8653
+ * This is the instrument D454 closed by asking for. `socketTopBytes=` names
8654
+ * the peer that moves the bytes and `txkB=` says they are responses; neither
8655
+ * can say WHICH method originates them, and after raw-forward the bytes that
8656
+ * cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
8657
+ * calls cross unread. What costs is what hub-main LOOKS at, and that is the
8658
+ * `decoded` half of an entry, next to the bytes it carried.
8659
+ */
8660
+ readActionCensus() {
8661
+ return this.actionCensus.read();
8585
8662
  }
8586
- onConnection(channel) {
8587
- let childId = null;
8588
- const incarnation = this.nextIncarnation++;
8589
- channel.observeActions?.({
8590
- label: capActionLabel,
8591
- record: (label, reqBytes, resBytes) => {
8592
- const peerId = childId ?? "(pre-register)";
8593
- if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
8594
- else this.actionCensus.record(peerId, label, reqBytes, resBytes);
8595
- }
8596
- });
8597
- channel.onEvent((body) => {
8598
- const msg = body;
8599
- if (msg.kind === "event") {
8600
- if (childId !== null) this.eventHandler?.(childId, msg.event);
8601
- return;
8602
- }
8603
- if (msg.kind === "log") {
8604
- if (childId !== null) this.logHandler?.(childId, msg);
8605
- return;
8606
- }
8607
- if (msg.kind === "event-sub") {
8608
- if (childId === null) return;
8609
- const entry = this.children.get(childId);
8610
- if (entry === void 0) return;
8611
- this.children.set(childId, {
8612
- ...entry,
8613
- eventPatterns: msg.patterns
8614
- });
8615
- this.logPatternSet(childId, msg.patterns);
8616
- return;
8617
- }
8618
- });
8619
- channel.onRequest(async (body, payload) => {
8620
- const msg = body;
8621
- if (msg.kind === "register") {
8622
- if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
8623
- childId = msg.childId;
8624
- const eventPatterns = msg.eventPatterns ?? null;
8625
- const superseded = this.children.get(msg.childId);
8626
- if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
8627
- childId: msg.childId,
8628
- supersededIncarnation: superseded.incarnation,
8629
- incarnation
8630
- });
8631
- this.children.set(msg.childId, {
8632
- childId: msg.childId,
8633
- channel,
8634
- caps: msg.caps,
8635
- eventPatterns,
8636
- incarnation,
8637
- rawForward: msg.rawForward === true
8638
- });
8639
- this.invalidateCapIndex();
8640
- this.logPatternSet(msg.childId, eventPatterns);
8641
- this.registeredHandler({
8642
- childId: msg.childId,
8643
- caps: msg.caps,
8644
- incarnation,
8645
- ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
8646
- });
8647
- return {
8648
- ok: true,
8649
- rawForward: true
8650
- };
8651
- }
8652
- if (msg.kind === "cap-call-out") {
8653
- const out = msg;
8654
- const hints = out.hints ?? liftRoutingHints(out.args);
8655
- const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
8656
- const buildInput = () => ({
8657
- capName: out.capName,
8658
- method: out.method,
8659
- args: materialise(),
8660
- ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
8661
- ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
8662
- ...out.native === true ? { native: true } : {},
8663
- ...childId !== null ? { callerAddonId: childId } : {}
8664
- });
8665
- const pinnedNodeId = out.nodeId ?? hints.nodeId;
8666
- const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
8667
- const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
8668
- const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
8669
- const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
8670
- this.recordCapUsage(childId, target, out.capName, out.method);
8671
- const actionPeer = childId ?? "(pre-register)";
8672
- const actionLabel = `${out.capName}.${out.method}`;
8673
- if (target !== null) {
8674
- if (!this.egressRoutedCaps.has(out.capName)) {
8675
- this.egressRoutedCaps.add(out.capName);
8676
- this.logger?.info("routed child egress over UDS", { capName: out.capName });
8677
- }
8678
- const entry = this.children.get(target);
8679
- if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
8680
- this.forwardedRaw += 1;
8681
- this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
8682
- const forward = {
8683
- kind: "cap-call",
8684
- capName: out.capName,
8685
- method: out.method,
8686
- args: void 0,
8687
- ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
8688
- };
8689
- const declared = this.capTimeoutMs?.(out.capName, out.method);
8690
- return entry.channel.request(forward, {
8691
- ...typeof declared === "number" ? { timeoutMs: declared } : {},
8692
- payload
8693
- });
8694
- }
8695
- this.forwardedDecoded += 1;
8696
- this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
8697
- const input = buildInput();
8698
- return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
8699
- }
8700
- this.forwardedDecoded += 1;
8701
- this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
8702
- if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
8703
- throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
8704
- }
8705
- if (msg.kind === "readiness-request") return {
8706
- kind: "readiness-snapshot",
8707
- records: this.readinessHandler?.() ?? []
8708
- };
8709
- throw new Error(`unknown child request kind: ${msg.kind}`);
8710
- });
8711
- channel.onClose(() => {
8712
- if (childId === null) return;
8713
- const entry = this.children.get(childId);
8714
- if (entry === void 0) return;
8715
- if (entry.channel !== channel) {
8716
- this.logger?.info("ignoring close of a superseded local child connection", {
8717
- childId,
8718
- incarnation,
8719
- liveIncarnation: entry.incarnation
8720
- });
8721
- return;
8722
- }
8723
- this.children.delete(childId);
8724
- this.invalidateCapIndex();
8725
- this.childEventStats.delete(childId);
8726
- this.actionCensus.forget(childId);
8727
- this.loggedPatternSet.delete(childId);
8728
- this.goneHandler(childId, incarnation);
8729
- });
8730
- }
8731
- };
8732
- //#endregion
8733
- //#region src/kernel/transport/local-child-client.ts
8734
- /** Did the parent's `register` answer say it accepts payload-borne args? */
8735
- function ackAcceptsRawForward(ack) {
8736
- if (ack === null || typeof ack !== "object") return false;
8737
- return ack.rawForward === true;
8738
- }
8739
- /**
8740
- * Child side of the local UDS transport. Connects to its parent (hub or
8741
- * agent), registers its cap manifest, and serves parent→child cap calls by
8742
- * delegating to `dispatch`. The provider implementation lives in the child;
8743
- * only routing keys + call arguments cross the wire.
8744
- *
8745
- * Additional channels beyond cap-call:
8746
- * - `emitEvent` fire-and-forget event toward the parent
8747
- * - `sendLog` fire-and-forget log entry toward the parent
8748
- * - `requestReadinessSnapshot` request/response snapshot of readiness records
8749
- * - `onEvent` register a handler for parent→child events
8750
- *
8751
- * Events and logs emitted before `start()` resolves are buffered and flushed
8752
- * on connect (mirrors the `updateCaps`/`latestCaps` pattern).
8753
- */
8754
- var LocalChildClient = class {
8755
- options;
8756
- client = null;
8757
- channel = null;
8758
- /**
8759
- * The cap set `start()` will register, kept current by `updateCaps`. Native
8760
- * device caps register on device-restore, which can race AHEAD of the UDS
8761
- * connect — buffering here lets a pre-start `updateCaps` survive (start sends
8762
- * the latest set) instead of being lost or throwing.
8763
- */
8764
- latestCaps;
8765
- /**
8766
- * Per-owner (addonId) category-pattern subscription sets, and their union.
8767
- * The union is declared to the parent (in `RegisterMessage.eventPatterns`
8768
- * pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
8769
- * subscription-filter its event fan-out. Per-owner keying keeps the union
8770
- * correct if multiple event buses ever share one client (group-runner case).
8771
- */
8772
- patternsByOwner = /* @__PURE__ */ new Map();
8773
- latestEventPatterns = [];
8774
- /**
8775
- * Whether `updateEventPatterns` has ever been called. A client that never
8776
- * declared a subscription set (no event bus wired — e.g. a pure cap-call
8777
- * runner) omits `eventPatterns` from its register frame entirely, so the
8778
- * parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
8779
- * owner declares (in production every addon context's framework
8780
- * subscriptions do), the register carries the real union — even if empty.
8781
- */
8782
- hasDeclaredEventPatterns = false;
8783
8663
  /**
8784
- * The custom-action catalogs of every hosted addon, once the runner's init
8785
- * loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
8786
- * until then, and OMITTED from the register frame while null: the parent
8787
- * reads absence as "not described yet", and a pre-init register must never
8788
- * look like "this child declares no actions". Kept on the client so every
8789
- * later re-register (native-cap change, reconnect) re-carries it unchanged.
8664
+ * The regime the counters above were produced under, read once from
8665
+ * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
8666
+ *
8667
+ * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
8668
+ * WOULD have been dropped" under `shadow`. Those are different claims, and
8669
+ * until this getter existed the mode was invisible at runtime — the counters
8670
+ * were readable and the regime that produced them was not.
8790
8671
  */
8791
- latestCustomActions = null;
8792
- /** Events and logs queued while the channel is not yet open. */
8793
- pendingEmits = [];
8672
+ get eventFanoutMode() {
8673
+ return this.fanoutMode;
8674
+ }
8794
8675
  /**
8795
- * Whether the parent acknowledged raw-forward at `register`. Until it has,
8796
- * `callOut` sends args inline exactly as every child always did — a legacy
8797
- * parent would decode a payload-borne call to `args: undefined` and run the
8798
- * provider on nothing, silently. Re-read on every re-register.
8676
+ * Connected children that never declared a subscription pattern set.
8677
+ *
8678
+ * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
8679
+ * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
8680
+ * `aggregateEventInterest` returns `null` — accept everything — if ANY
8681
+ * connected child is undeclared, which disables the node's inbound cross-node
8682
+ * gate as well. **One legacy runner silently turns off the filtering for the
8683
+ * whole node**, and nothing reported it, so a zero here is as load-bearing as
8684
+ * a non-zero one.
8799
8685
  */
8800
- parentRawForward = false;
8801
- /** Handler for parent→child events. Registered via `onEvent`. */
8802
- eventHandler = null;
8686
+ undeclaredChildCount() {
8687
+ let count = 0;
8688
+ for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
8689
+ return count;
8690
+ }
8803
8691
  /**
8804
- * Handler for parent→child addon-level calls (`routes` / `custom`).
8805
- * Registered via `onAddonCall`. Resolves the loaded addon instance and
8806
- * invokes its `addon-routes.getRoutes()` or its custom-action handler.
8807
- * Replaces the per-addon Moleculer `getRoutes` / `custom.<action>` actions
8808
- * removed in F1/F2. Only one handler is active at a time.
8692
+ * Does `entry` want to receive an event in `category`? Honours the fan-out
8693
+ * mode: `broadcast` → always; undeclared patterns (`null`) → always
8694
+ * (fail-open); else the child's declared set must match via `matchesPattern`
8695
+ * — the SAME function the child's own `deliverShared` map-key gate uses,
8696
+ * giving provable delivery parity. `shadow` is handled by the caller (it
8697
+ * counts the would-suppress but still sends).
8698
+ *
8699
+ * NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
8700
+ * `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
8701
+ * that explicitly subscribes to a ring-denied category must still RECEIVE it.
8809
8702
  */
8810
- addonCallHandler = null;
8703
+ childWantsEvent(entry, category) {
8704
+ if (this.fanoutMode === "broadcast") return true;
8705
+ return this.childSubscribesTo(entry, category);
8706
+ }
8811
8707
  /**
8812
- * Handler for `set-log-level` messages pushed by the parent.
8813
- * Registered via `onSetLogLevel`. The handler applies the new level to the
8814
- * child's local Moleculer logger (mirrors `$node-mgmt.setLogLevel`).
8815
- * E2: wired in `addon-runner.ts` to forward to the broker logger.
8708
+ * The SUBSCRIPTION answer alone, with no fan-out-mode override.
8709
+ *
8710
+ * {@link childWantsEvent} answers "will this child be sent the event", which
8711
+ * under `broadcast` is `true` for everything and therefore cannot say whether
8712
+ * anyone actually asked. The D467 census needs the second question, so the
8713
+ * two are separate functions and `broadcast` mode overrides only the first.
8816
8714
  */
8817
- setLogLevelHandler = null;
8818
- /** Callbacks registered via `onConnected`. Fired on every (re)connect. */
8819
- connectedHandlers = [];
8820
- constructor(options) {
8821
- this.options = options;
8822
- this.latestCaps = options.caps;
8715
+ childSubscribesTo(entry, category) {
8716
+ if (entry.eventPatterns === null) return true;
8717
+ return entry.eventPatterns.some((p) => matchesPattern(p, category));
8823
8718
  }
8824
8719
  /**
8825
- * Declare an owner's live category-pattern subscription set. Stores it
8826
- * per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
8827
- * if the union changed — declares it to the parent:
8828
- * - pre-connect: buffered only; `start()`'s register frame carries the set.
8829
- * - post-connect: emitted as a fire-and-forget `event-sub` (full-set
8830
- * replace).
8831
- * Idempotent on an unchanged union (no redundant `event-sub` frames).
8720
+ * D2: this node's aggregate cross-node event interest — the UNION of every
8721
+ * connected child's declared category patterns. Consumed by the parent's
8722
+ * `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
8723
+ * accepts cross-node (Moleculer) categories at least one local child wants.
8724
+ *
8725
+ * Returns `null` (fail-open — accept everything) when:
8726
+ * - NO child is currently connected (boot window before the first forked
8727
+ * child completes its UDS handshake, or every child has disconnected): a
8728
+ * node MUST NOT drop 100% of inbound cross-node events during that window,
8729
+ * and a child may connect imminently. This is distinct from the
8730
+ * declared-empty case below.
8731
+ * - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
8732
+ * runner): a node cannot safely narrow its inbound set while a child's
8733
+ * real interest is unknown. Mirrors the per-child fail-open in
8734
+ * {@link childWantsEvent}, aggregated conservatively.
8735
+ *
8736
+ * Returns an EMPTY array only when ≥1 child is connected and EVERY connected
8737
+ * child declared an explicit empty set — a genuine "wants nothing". Reads the
8738
+ * live `children` map, so a child registering / disconnecting / sending
8739
+ * `event-sub` is reflected on the next call with no extra bookkeeping.
8832
8740
  */
8833
- updateEventPatterns(ownerId, patterns) {
8834
- this.hasDeclaredEventPatterns = true;
8835
- this.patternsByOwner.set(ownerId, patterns);
8836
- const union = this.computePatternUnion();
8837
- if (!this.patternsEqual(union, this.latestEventPatterns)) {
8838
- this.latestEventPatterns = union;
8839
- if (this.channel !== null) {
8840
- const msg = {
8841
- kind: "event-sub",
8842
- patterns: union
8843
- };
8844
- this.channel.emit(msg);
8845
- }
8741
+ aggregateEventInterest() {
8742
+ if (this.children.size === 0) return null;
8743
+ const union = /* @__PURE__ */ new Set();
8744
+ for (const entry of this.children.values()) {
8745
+ if (entry.eventPatterns === null) return null;
8746
+ for (const p of entry.eventPatterns) union.add(p);
8846
8747
  }
8748
+ return [...union];
8847
8749
  }
8848
- /** Sorted, de-duplicated union across all owners' pattern sets. */
8849
- computePatternUnion() {
8850
- const all = /* @__PURE__ */ new Set();
8851
- for (const patterns of this.patternsByOwner.values()) for (const p of patterns) all.add(p);
8852
- return [...all].toSorted();
8750
+ statsFor(childId) {
8751
+ let s = this.childEventStats.get(childId);
8752
+ if (s === void 0) {
8753
+ s = {
8754
+ sent: 0,
8755
+ suppressed: 0
8756
+ };
8757
+ this.childEventStats.set(childId, s);
8758
+ }
8759
+ return s;
8853
8760
  }
8854
- /** Order-insensitive equality — both inputs are already sorted unions here. */
8855
- patternsEqual(a, b) {
8856
- if (a.length !== b.length) return false;
8857
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
8858
- return true;
8761
+ /** Deduped INFO line whenever a child's declared pattern set changes. */
8762
+ logPatternSet(childId, patterns) {
8763
+ const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
8764
+ if (this.loggedPatternSet.get(childId) === key) return;
8765
+ this.loggedPatternSet.set(childId, key);
8766
+ this.logger?.info("child event patterns", {
8767
+ childId,
8768
+ patterns: patterns ?? null
8769
+ });
8859
8770
  }
8860
8771
  /**
8861
- * Register a callback that fires each time the client successfully connects
8862
- * (or reconnects) to its parent. Multiple handlers may be registered; all
8863
- * are called in registration order. Used by readiness-context in UDS mode
8864
- * to trigger a snapshot hydrate on connect/reconnect.
8772
+ * Child id that can service a call to `capName` (optionally addressing
8773
+ * `deviceId`), or null.
8774
+ *
8775
+ * `deviceId` is a routing HINT, not a hard filter. A singleton cap
8776
+ * (`pipeline-runner`, `stream-broker`, …) addresses devices through its
8777
+ * METHOD ARGUMENTS — `attachCamera({ deviceId })` is one provider serving
8778
+ * many cameras — so its descriptor carries no `deviceId`. Resolving on
8779
+ * `deviceId` alone would never match it and would force the call onto the
8780
+ * broker fallback (which then hangs in service discovery). Hence:
8781
+ * 1. exact device-scoped owner (native per-device caps where each child
8782
+ * owns a disjoint device subset) is preferred, then
8783
+ * 2. a singleton owner (deviceId-less descriptor) is the fallback.
8784
+ * A cap is globally singleton XOR device-scoped, so the two tiers never
8785
+ * compete for the same capName.
8865
8786
  */
8866
- onConnected(handler) {
8867
- this.connectedHandlers.push(handler);
8787
+ resolveChildId(capName, deviceId) {
8788
+ const index = this.capIndex();
8789
+ if (deviceId !== void 0) {
8790
+ const deviceOwner = index.deviceOwner(capName, deviceId);
8791
+ if (deviceOwner !== null) return deviceOwner;
8792
+ }
8793
+ const candidates = index.singletonCandidates(capName);
8794
+ if (candidates.length === 0) return null;
8795
+ if (candidates.length === 1) return candidates[0];
8796
+ const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
8797
+ if (preferredAddonId !== null) {
8798
+ const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
8799
+ if (candidates.includes(preferredChildId)) return preferredChildId;
8800
+ }
8801
+ return candidates[0];
8868
8802
  }
8869
8803
  /**
8870
- * Register a handler for events pushed from the parent to this child.
8871
- * Must be called before `start()` to avoid missing early events (though
8872
- * registration after start is also safe for events not yet delivered).
8873
- * Replaces any previously registered handler.
8804
+ * Publish one cap-usage observation, if a sink is wired.
8805
+ *
8806
+ * Cost discipline — this is on the hot path (D181: hub-main's event loop is
8807
+ * the cluster's only queue): no sink means one undefined check; with a sink
8808
+ * it is one object literal and one `Date.now()`, and the sink itself is O(1).
8809
+ * Wrapped so a broken observer can never fail the call it observes — the
8810
+ * registry swallows too, and BOTH matter: this catch also covers a sink that
8811
+ * is not the registry.
8874
8812
  */
8875
- onEvent(handler) {
8876
- this.eventHandler = handler;
8813
+ recordCapUsage(callerChildId, providerChildId, capName, methodName) {
8814
+ const sink = this.capUsageObserver;
8815
+ if (sink === void 0 || callerChildId === null) return;
8816
+ try {
8817
+ sink({
8818
+ callerAddonId: callerChildId,
8819
+ providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
8820
+ capName,
8821
+ methodName,
8822
+ atMs: Date.now()
8823
+ });
8824
+ } catch {}
8877
8825
  }
8878
8826
  /**
8879
- * Register the handler invoked when the parent sends an `addon-call` (the
8880
- * addon-level routes / custom-action plane). The addon-runner wires this to
8881
- * resolve the loaded addon by id and dispatch to its `getRoutes()` or its
8882
- * custom-action handler. Safe to register before or after `start()`;
8883
- * replaces any prior handler.
8827
+ * The cap index over the CURRENT children, rebuilt on first use after any
8828
+ * mutation of {@link children}.
8829
+ *
8830
+ * This replaced a linear walk of every child's cap manifest per
8831
+ * `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
8832
+ * across 39 children (D456). Why it did not exist before is the whole
8833
+ * design: what it indexes moves — a runner is replaced on every update, a
8834
+ * crash respawns one, a child re-registers with a replacement manifest after
8835
+ * init — and an index that names a dead child routes work into a socket
8836
+ * nobody is reading, which is strictly worse than a slow scan.
8837
+ *
8838
+ * So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
8839
+ * function of `children.values()`, the only two writers of that map
8840
+ * ({@link invalidateCapIndex}'s call sites) drop the index in the same
8841
+ * statement, and nothing else can construct one. There is no add-on-register
8842
+ * / remove-on-close path that could disagree with the map — i.e. no shadow
8843
+ * registry (D3): the map remains the single authority and this holds no fact
8844
+ * it does not.
8845
+ *
8846
+ * The operator's active-singleton preference is deliberately NOT indexed —
8847
+ * it changes without any child connecting or leaving, so `resolveChildId`
8848
+ * reads it live and the index only supplies the candidate list.
8884
8849
  */
8885
- onAddonCall(handler) {
8886
- this.addonCallHandler = handler;
8850
+ capIndex() {
8851
+ if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
8852
+ return this.capChildIndex;
8853
+ }
8854
+ /** Drop the derived index. MUST follow every write to {@link children}. */
8855
+ invalidateCapIndex() {
8856
+ this.capChildIndex = null;
8887
8857
  }
8888
8858
  /**
8889
- * E2: Register a handler invoked when the parent sends a `set-log-level`
8890
- * message to this child. The handler should forward the new level to the
8891
- * child's local Moleculer broker logger (mirrors `$node-mgmt.setLogLevel`).
8892
- * Safe to register before or after `start()`. Replaces any prior handler.
8859
+ * Does the named child currently provide `(capName, deviceId?)`?
8860
+ *
8861
+ * Used by the hub proxy seam, which knows the EXACT addon (→ runner →
8862
+ * childId) a provider belongs to. Unlike `resolveChildId` (which picks the
8863
+ * first child owning `capName`), this targets one child — required for
8864
+ * COLLECTION caps (`addon-widgets-source`, …) where many children register
8865
+ * the same capName: routing by capName alone collapses every provider to
8866
+ * the first child. Same deviceId-as-hint semantics as `resolveChildId`:
8867
+ * a device-scoped descriptor matching `deviceId` OR a deviceId-less
8868
+ * (singleton/collection) descriptor counts.
8893
8869
  */
8894
- onSetLogLevel(handler) {
8895
- this.setLogLevelHandler = handler;
8870
+ /**
8871
+ * Is `childId` currently connected (has it completed its UDS handshake)?
8872
+ * Coarser than {@link childProvides}: it answers "is the child reachable
8873
+ * over UDS at all", regardless of which caps it has announced yet. Used by
8874
+ * the route-mount fallback to decide between the handler-stripped
8875
+ * `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
8876
+ * cap proxy's `getRoutes()` (child not yet UDS-registered).
8877
+ */
8878
+ isChildKnown(childId) {
8879
+ return this.children.has(childId);
8880
+ }
8881
+ childProvides(childId, capName, deviceId) {
8882
+ const entry = this.children.get(childId);
8883
+ if (!entry) return false;
8884
+ if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
8885
+ return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
8896
8886
  }
8897
8887
  /**
8898
- * True once `start()` has successfully connected and registered. Used by
8899
- * callers (event-bus/logger bridges) to know the channel is live.
8888
+ * Per-request options for a cap call: the cap method's declared `timeoutMs`
8889
+ * when one exists, else `undefined` so the channel applies its default.
8890
+ * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
8900
8891
  */
8901
- get isConnected() {
8902
- return this.channel !== null;
8892
+ capRequestOpts(input) {
8893
+ const declared = this.capTimeoutMs?.(input.capName, input.method);
8894
+ return typeof declared === "number" ? { timeoutMs: declared } : void 0;
8903
8895
  }
8904
- async start() {
8905
- if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
8906
- const client = createLocalTransport().createClient(this.options.nodeId);
8907
- const channel = await client.connect();
8908
- channel.onRequest(async (body, payload) => {
8909
- const msg = body;
8910
- if (msg.kind === "cap-call") {
8911
- const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
8912
- const result = await this.options.dispatch({
8913
- capName: msg.capName,
8914
- method: msg.method,
8915
- args,
8916
- ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
8917
- ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
8918
- });
8919
- if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
8920
- return result;
8921
- }
8922
- if (msg.kind === "addon-call") {
8923
- if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
8924
- return this.addonCallHandler({
8925
- addonId: msg.addonId,
8926
- target: msg.target,
8927
- ...msg.action !== void 0 ? { action: msg.action } : {},
8928
- ...msg.method !== void 0 ? { method: msg.method } : {},
8929
- ...msg.args !== void 0 ? { args: msg.args } : {},
8930
- ...msg.caller !== void 0 ? { caller: msg.caller } : {}
8931
- });
8932
- }
8933
- throw new Error(`unknown parent request kind: ${msg.kind}`);
8934
- });
8935
- channel.onEvent((body) => {
8936
- const msg = body;
8937
- if (msg.kind === "event") {
8938
- this.eventHandler?.(msg.event);
8939
- return;
8940
- }
8941
- if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
8942
- });
8943
- const register = this.registerFrame(this.latestCaps);
8944
- try {
8945
- this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
8946
- } catch (err) {
8947
- await client.close();
8948
- throw err;
8949
- }
8950
- this.client = client;
8951
- this.channel = channel;
8952
- this.flushPending(channel);
8953
- for (const cb of this.connectedHandlers) cb();
8896
+ /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
8897
+ async callCapOnChild(childId, input) {
8898
+ const entry = this.children.get(childId);
8899
+ if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
8900
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
8954
8901
  }
8955
- /** Flush buffered pre-start emits over the now-open channel. */
8956
- flushPending(channel) {
8957
- for (const item of this.pendingEmits) channel.emit(item.msg);
8958
- this.pendingEmits.length = 0;
8902
+ /** Forward a cap method call to the owning child over UDS; rejects if none. */
8903
+ async callCap(input) {
8904
+ const childId = this.resolveChildId(input.capName, input.deviceId);
8905
+ const entry = childId === null ? void 0 : this.children.get(childId);
8906
+ if (!entry) {
8907
+ const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
8908
+ throw new Error(`no local child provides cap "${input.capName}"${where}`);
8909
+ }
8910
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
8959
8911
  }
8960
8912
  /**
8961
- * Re-send the cap manifest to the parent, atomically replacing the child's
8962
- * registered descriptor set. Call this after device-restore so newly
8963
- * registered native (device-scoped) caps become routable over UDS.
8913
+ * Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
8914
+ * by id over UDS; rejects if that child is absent.
8964
8915
  *
8965
- * Safe to call before `start()`: the new set is buffered and `start()` sends
8966
- * it (device-restore can race ahead of the UDS connect). After `start()`,
8967
- * the set is sent immediately.
8916
+ * The childId for a hub-local single-addon runner equals the addonId
8917
+ * (`resolveRunnerId` returns the addonId when no `execution.group` is
8918
+ * declared — no shipped addon declares one). Mirrors `callCapOnChild` for
8919
+ * the cap plane; carries the two surfaces the removed per-addon Moleculer
8920
+ * broker used to serve (`getRoutes` + `custom.<action>`).
8968
8921
  */
8969
- async updateCaps(caps, customActions) {
8970
- this.latestCaps = caps;
8971
- if (customActions !== void 0) this.latestCustomActions = customActions;
8972
- if (this.channel === null) return;
8973
- this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
8922
+ async callAddonOnChild(childId, input) {
8923
+ const entry = this.children.get(childId);
8924
+ if (!entry) throw new Error(`no local child "${childId}" for addon-call (${input.target})`);
8925
+ return entry.channel.request(this.toAddonCall(input));
8974
8926
  }
8975
- /**
8976
- * The register frame: caps, plus everything a re-register must atomically
8977
- * re-carry so no separate re-sync can be missed — the subscription union
8978
- * (omitted if nothing ever declared, which keeps the fail-open shape) and the
8979
- * custom-action catalogs (omitted until the init loop produced them).
8980
- */
8981
- registerFrame(caps) {
8927
+ /** Build the parent→child `addon-call` wire message from an addon-call input. */
8928
+ toAddonCall(input) {
8982
8929
  return {
8983
- kind: "register",
8984
- childId: this.options.childId,
8985
- caps,
8986
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
8987
- ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
8988
- rawForward: true
8930
+ kind: "addon-call",
8931
+ addonId: input.addonId,
8932
+ target: input.target,
8933
+ ...input.action !== void 0 ? { action: input.action } : {},
8934
+ ...input.method !== void 0 ? { method: input.method } : {},
8935
+ ...input.args !== void 0 ? { args: input.args } : {},
8936
+ ...input.caller !== void 0 ? { caller: input.caller } : {}
8989
8937
  };
8990
8938
  }
8991
- /**
8992
- * Fire-and-forget: send a system event to the parent for forwarding to the
8993
- * hub event bus. Safe to call before `start()` — events are buffered and
8994
- * flushed on connect.
8995
- */
8996
- emitEvent(event) {
8997
- const msg = {
8998
- kind: "event",
8999
- event
8939
+ /** Build the parent→child `cap-call` wire message from a routing input. */
8940
+ toCapCall(input) {
8941
+ return {
8942
+ kind: "cap-call",
8943
+ capName: input.capName,
8944
+ method: input.method,
8945
+ args: input.args,
8946
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
8947
+ ...input.addonId !== void 0 ? { addonId: input.addonId } : {}
9000
8948
  };
9001
- if (this.channel !== null) this.channel.emit(msg);
9002
- else this.pendingEmits.push({
9003
- kind: "event",
9004
- msg
9005
- });
8949
+ }
8950
+ listChildren() {
8951
+ return [...this.children.values()].map((e) => ({
8952
+ childId: e.childId,
8953
+ caps: e.caps,
8954
+ incarnation: e.incarnation
8955
+ }));
8956
+ }
8957
+ /** Register the (single) child-registered handler. Only one handler is active at a time. */
8958
+ onChildRegistered(handler) {
8959
+ this.registeredHandler = handler;
9006
8960
  }
9007
8961
  /**
9008
- * Fire-and-forget: send a structured log entry to the parent. Safe to call
9009
- * before `start()` — log entries are buffered and flushed on connect.
8962
+ * Register the (single) child-gone handler. Only one handler is active at a
8963
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
8964
+ * connection that closed — carry it into whatever teardown it triggers, so a
8965
+ * dead generation cannot tear down its successor's work.
9010
8966
  */
9011
- sendLog(entry) {
9012
- const msg = {
9013
- kind: "log",
9014
- ...entry
9015
- };
9016
- if (this.channel !== null) this.channel.emit(msg);
9017
- else this.pendingEmits.push({
9018
- kind: "log",
9019
- msg
9020
- });
8967
+ onChildGone(handler) {
8968
+ this.goneHandler = handler;
9021
8969
  }
9022
8970
  /**
9023
- * Request the current readiness snapshot from the parent. Returns the
9024
- * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
9025
- * Requires `start()` to have been called; throws with a clear message if not.
8971
+ * Register the (single) child-event handler. Invoked when a child sends an
8972
+ * event via `LocalChildClient.emitEvent`. Only one handler is active at a
8973
+ * time; a new handler replaces the prior one. Pass `null` to clear the
8974
+ * handler entirely (used by the UDS event bridge disposer on shutdown).
9026
8975
  */
9027
- async requestReadinessSnapshot() {
9028
- if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
9029
- return (await this.channel.request({ kind: "readiness-request" })).records;
8976
+ onChildEvent(handler) {
8977
+ this.eventHandler = handler;
9030
8978
  }
9031
8979
  /**
9032
- * Ask the parent to execute a cap call this child does NOT own. The parent
9033
- * routes to the owning local sibling over UDS, or — if no sibling owns it —
9034
- * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
9035
- * production). Throws if called before `start()`.
8980
+ * Register the (single) child-log handler. Invoked when a child sends a log
8981
+ * entry via `LocalChildClient.sendLog`. Only one handler is active.
9036
8982
  */
9037
- async callOut(input) {
9038
- if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
9039
- const routing = {
9040
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
9041
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
9042
- ...input.native === true ? { native: true } : {}
9043
- };
9044
- if (this.parentRawForward && input.args !== void 0) {
9045
- const hints = liftRoutingHints(input.args);
9046
- const msg = {
9047
- kind: "cap-call-out",
9048
- capName: input.capName,
9049
- method: input.method,
9050
- ...Object.keys(hints).length > 0 ? { hints } : {},
9051
- ...routing
9052
- };
9053
- const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
9054
- return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
9055
- }
8983
+ onChildLog(handler) {
8984
+ this.logHandler = handler;
8985
+ }
8986
+ /**
8987
+ * Register the handler that supplies the authoritative readiness snapshot
8988
+ * when a child sends a `readiness-request`. Only one handler is active.
8989
+ */
8990
+ onReadinessSnapshotRequest(handler) {
8991
+ this.readinessHandler = handler;
8992
+ }
8993
+ /**
8994
+ * Push a parent→child event to a specific child. Fire-and-forget (uses the
8995
+ * one-way `emit` path on the channel). No-op if the child is not connected.
8996
+ */
8997
+ sendEventToChild(childId, event, sourceNodeId) {
8998
+ const entry = this.children.get(childId);
8999
+ if (entry === void 0) return;
9056
9000
  const msg = {
9057
- kind: "cap-call-out",
9058
- capName: input.capName,
9059
- method: input.method,
9060
- args: input.args,
9061
- ...routing
9001
+ kind: "event",
9002
+ event,
9003
+ sourceNodeId
9062
9004
  };
9063
- return this.channel.request(msg);
9064
- }
9065
- /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
9066
- async close() {
9067
- await this.client?.close();
9068
- this.client = null;
9069
- this.channel = null;
9005
+ entry.channel.emit(msg);
9070
9006
  }
9071
- };
9072
- //#endregion
9073
- //#region src/kernel/transport/cap-route.ts
9074
- function buildMessage(capName, method, detail) {
9075
- const call = method !== void 0 ? `${capName}.${method}` : capName;
9076
- const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
9077
- const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
9078
- const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
9079
- return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
9080
- }
9081
- var CapRouteError = class extends Error {
9082
- reason;
9083
- nodeId;
9084
- rejected;
9085
9007
  /**
9086
- * @param cause Optional original error that triggered this routing failure.
9087
- * Stored as `Error.cause` (TC39 standard option, Node 16.9+).
9088
- * Dispatchers wrapping transport errors MUST pass the original.
9008
+ * Push a parent→child event to every registered child, optionally skipping
9009
+ * one (the originating child, to avoid echo). Fire-and-forget.
9089
9010
  */
9090
- constructor(capName, method, detail, cause) {
9091
- super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
9092
- this.name = "CapRouteError";
9093
- this.reason = detail.reason;
9094
- this.nodeId = detail.nodeId;
9095
- this.rejected = detail.rejected;
9011
+ broadcastEventToChildren(event, sourceNodeId, exceptChildId) {
9012
+ const msg = {
9013
+ kind: "event",
9014
+ event,
9015
+ sourceNodeId
9016
+ };
9017
+ let subscribed = 0;
9018
+ for (const entry of this.children.values()) {
9019
+ if (entry.childId === exceptChildId) continue;
9020
+ if (this.childSubscribesTo(entry, event.category)) subscribed += 1;
9021
+ const wants = this.childWantsEvent(entry, event.category);
9022
+ const stats = this.statsFor(entry.childId);
9023
+ if (!wants) stats.suppressed++;
9024
+ if (wants || this.fanoutMode === "shadow") {
9025
+ stats.sent++;
9026
+ entry.channel.emit(msg);
9027
+ }
9028
+ }
9029
+ this.recordCategoryBroadcast(event.category, subscribed);
9096
9030
  }
9097
- };
9098
- /**
9099
- * Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
9100
- *
9101
- * Precedence (explicit nodeId path):
9102
- * 1. hub-in-process — hub node + hubInProcessProvides
9103
- * 2. hub-local-uds — hub node + hubLocalChildProvides
9104
- * 3. node-offline → CapRouteError{reason:'node-offline'}
9105
- * 4. agent-child-forward — node is an agent AND nodeKnowsCap
9106
- * 5. remote-moleculer — any other online non-agent node
9107
- *
9108
- * Singleton path (no nodeId):
9109
- * 1. hub-in-process (hub provides in-process)
9110
- * 2. hub-local-uds (a hub-local UDS child provides it)
9111
- * 3. remote-moleculer (any online, non-hub node that knows it)
9112
- * 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
9113
- *
9114
- * PURE: no side effects, no async, no broker/registry imports.
9115
- */
9116
- function classifyCapRoute(capName, opts, snapshot) {
9117
- const rejected = [];
9118
- if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
9119
- return classifySingleton(capName, opts.deviceId, snapshot, rejected);
9120
- }
9121
- function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
9122
- if (nodeId === snap.hubNodeId) {
9123
- const residentProvides = snap.hubResidentProvides;
9124
- const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
9125
- const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
9126
- if (providesOnHub(capName)) {
9127
- const ref = getRef?.(capName) ?? null;
9128
- if (ref !== null) return {
9129
- kind: "hub-in-process",
9130
- capName,
9131
- ref
9132
- };
9133
- rejected.push({
9134
- kind: "hub-in-process",
9135
- why: "provider ref not available in snapshot"
9136
- });
9137
- throw new CapRouteError(capName, void 0, {
9138
- reason: "no-provider",
9139
- nodeId,
9140
- rejected
9141
- });
9031
+ /**
9032
+ * Record one PRODUCED event against its category. Called once per broadcast,
9033
+ * outside the per-child loop, because production is what the census measures.
9034
+ */
9035
+ recordCategoryBroadcast(category, subscribed) {
9036
+ const existing = this.categoryStats.get(category);
9037
+ if (existing !== void 0) {
9038
+ existing.broadcasts += 1;
9039
+ existing.wanted += subscribed;
9040
+ return;
9142
9041
  }
9143
- if (snap.hubLocalChildProvides(capName, deviceId)) {
9144
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
9145
- if (childId !== null) return {
9146
- kind: "hub-local-uds",
9147
- capName,
9148
- childId
9149
- };
9150
- rejected.push({
9151
- kind: "hub-local-uds",
9152
- why: "child id not resolvable from snapshot"
9153
- });
9154
- throw new CapRouteError(capName, void 0, {
9155
- reason: "no-provider",
9156
- nodeId,
9157
- rejected
9158
- });
9042
+ if (this.categoryStats.size >= 256) {
9043
+ this.categoryOverflow += 1;
9044
+ if (!this.categoryOverflowLogged) {
9045
+ this.categoryOverflowLogged = true;
9046
+ this.logger?.info("event category census full — further new categories are unattributed", {
9047
+ max: 256,
9048
+ firstDropped: category
9049
+ });
9050
+ }
9051
+ return;
9159
9052
  }
9160
- rejected.push({
9161
- kind: "hub-in-process",
9162
- why: "hub does not provide this cap in-process"
9163
- });
9164
- rejected.push({
9165
- kind: "hub-local-uds",
9166
- why: "no hub-local child provides this cap"
9167
- });
9168
- throw new CapRouteError(capName, void 0, {
9169
- reason: "no-provider",
9170
- nodeId,
9171
- rejected
9172
- });
9173
- }
9174
- if (!snap.nodeOnline(nodeId)) {
9175
- rejected.push({
9176
- kind: "remote-moleculer",
9177
- why: `node ${nodeId} is offline`
9178
- });
9179
- throw new CapRouteError(capName, void 0, {
9180
- reason: "node-offline",
9181
- nodeId,
9182
- rejected
9053
+ this.categoryStats.set(category, {
9054
+ broadcasts: 1,
9055
+ wanted: subscribed
9183
9056
  });
9184
9057
  }
9185
- if (snap.nodeIsAgent(nodeId)) {
9186
- if (snap.nodeKnowsCap(nodeId, capName)) return {
9187
- kind: "agent-child-forward",
9188
- capName,
9189
- agentNodeId: nodeId,
9190
- childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
9058
+ /**
9059
+ * E2: Send a `set-log-level` control message to a specific child.
9060
+ * Returns `true` if the child is currently connected and the message was
9061
+ * emitted; `false` if the child is not connected (no-op). The `false`
9062
+ * return lets the caller (MoleculerService.setChildLogLevelByNodeId) fall
9063
+ * back to the Moleculer `$node-mgmt.setLogLevel` action for the node.
9064
+ * Mirrors the `$node-mgmt.setLogLevel` Moleculer action for UDS children.
9065
+ */
9066
+ setChildLogLevel(childId, level) {
9067
+ const entry = this.children.get(childId);
9068
+ if (entry === void 0) return false;
9069
+ const msg = {
9070
+ kind: "set-log-level",
9071
+ level
9191
9072
  };
9192
- rejected.push({
9193
- kind: "agent-child-forward",
9194
- why: `agent ${nodeId} does not know cap ${capName}`
9073
+ entry.channel.emit(msg);
9074
+ return true;
9075
+ }
9076
+ async close() {
9077
+ await this.server.close();
9078
+ }
9079
+ onConnection(channel) {
9080
+ let childId = null;
9081
+ const incarnation = this.nextIncarnation++;
9082
+ channel.observeActions?.({
9083
+ label: capActionLabel,
9084
+ record: (label, reqBytes, resBytes) => {
9085
+ const peerId = childId ?? "(pre-register)";
9086
+ if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
9087
+ else this.actionCensus.record(peerId, label, reqBytes, resBytes);
9088
+ }
9195
9089
  });
9196
- throw new CapRouteError(capName, void 0, {
9197
- reason: "no-provider",
9198
- nodeId,
9199
- rejected
9090
+ channel.onEvent((body) => {
9091
+ const msg = body;
9092
+ if (msg.kind === "event") {
9093
+ if (childId !== null) this.eventHandler?.(childId, msg.event);
9094
+ return;
9095
+ }
9096
+ if (msg.kind === "log") {
9097
+ if (childId !== null) this.logHandler?.(childId, msg);
9098
+ return;
9099
+ }
9100
+ if (msg.kind === "event-sub") {
9101
+ if (childId === null) return;
9102
+ const entry = this.children.get(childId);
9103
+ if (entry === void 0) return;
9104
+ this.children.set(childId, {
9105
+ ...entry,
9106
+ eventPatterns: msg.patterns
9107
+ });
9108
+ this.logPatternSet(childId, msg.patterns);
9109
+ return;
9110
+ }
9200
9111
  });
9201
- }
9202
- return {
9203
- kind: "remote-moleculer",
9204
- capName,
9205
- nodeId
9206
- };
9207
- }
9208
- function classifySingleton(capName, deviceId, snap, rejected) {
9209
- if (snap.hubInProcessProvides(capName)) {
9210
- const ref = snap.getInProcessProviderRef?.(capName) ?? null;
9211
- if (ref !== null) return {
9212
- kind: "hub-in-process",
9213
- capName,
9214
- ref
9215
- };
9216
- rejected.push({
9217
- kind: "hub-in-process",
9218
- why: "provider ref not available in snapshot"
9112
+ channel.onRequest(async (body, payload) => {
9113
+ const msg = body;
9114
+ if (msg.kind === "register") {
9115
+ if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
9116
+ childId = msg.childId;
9117
+ const eventPatterns = msg.eventPatterns ?? null;
9118
+ const superseded = this.children.get(msg.childId);
9119
+ if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
9120
+ childId: msg.childId,
9121
+ supersededIncarnation: superseded.incarnation,
9122
+ incarnation
9123
+ });
9124
+ this.children.set(msg.childId, {
9125
+ childId: msg.childId,
9126
+ channel,
9127
+ caps: msg.caps,
9128
+ eventPatterns,
9129
+ incarnation,
9130
+ rawForward: msg.rawForward === true
9131
+ });
9132
+ this.invalidateCapIndex();
9133
+ this.logPatternSet(msg.childId, eventPatterns);
9134
+ this.registeredHandler({
9135
+ childId: msg.childId,
9136
+ caps: msg.caps,
9137
+ incarnation,
9138
+ ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
9139
+ });
9140
+ return {
9141
+ ok: true,
9142
+ rawForward: true
9143
+ };
9144
+ }
9145
+ if (msg.kind === "cap-call-out") {
9146
+ const out = msg;
9147
+ const hints = out.hints ?? liftRoutingHints(out.args);
9148
+ const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
9149
+ const buildInput = () => ({
9150
+ capName: out.capName,
9151
+ method: out.method,
9152
+ args: materialise(),
9153
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
9154
+ ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
9155
+ ...out.native === true ? { native: true } : {},
9156
+ ...childId !== null ? { callerAddonId: childId } : {}
9157
+ });
9158
+ if (this.claimedStatus !== void 0 && out.method === "getStatus") {
9159
+ const claimed = await answerClaimedGetStatus(this.claimedStatus, {
9160
+ capName: out.capName,
9161
+ method: out.method,
9162
+ deviceId: out.deviceId ?? (payload === void 0 ? extractDeviceId(out.args) : void 0)
9163
+ });
9164
+ if (claimed !== null) {
9165
+ this.recordCapUsage(childId, null, out.capName, out.method);
9166
+ return claimed.slice;
9167
+ }
9168
+ }
9169
+ const pinnedNodeId = out.nodeId ?? hints.nodeId;
9170
+ const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
9171
+ const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
9172
+ const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
9173
+ const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
9174
+ this.recordCapUsage(childId, target, out.capName, out.method);
9175
+ const actionPeer = childId ?? "(pre-register)";
9176
+ const actionLabel = `${out.capName}.${out.method}`;
9177
+ if (target !== null) {
9178
+ if (!this.egressRoutedCaps.has(out.capName)) {
9179
+ this.egressRoutedCaps.add(out.capName);
9180
+ this.logger?.info("routed child egress over UDS", { capName: out.capName });
9181
+ }
9182
+ const entry = this.children.get(target);
9183
+ if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
9184
+ this.forwardedRaw += 1;
9185
+ this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
9186
+ const forward = {
9187
+ kind: "cap-call",
9188
+ capName: out.capName,
9189
+ method: out.method,
9190
+ args: void 0,
9191
+ ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
9192
+ };
9193
+ const declared = this.capTimeoutMs?.(out.capName, out.method);
9194
+ return entry.channel.request(forward, {
9195
+ ...typeof declared === "number" ? { timeoutMs: declared } : {},
9196
+ payload
9197
+ });
9198
+ }
9199
+ this.forwardedDecoded += 1;
9200
+ this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
9201
+ const input = buildInput();
9202
+ return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
9203
+ }
9204
+ this.forwardedDecoded += 1;
9205
+ this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
9206
+ if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
9207
+ throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
9208
+ }
9209
+ if (msg.kind === "readiness-request") return {
9210
+ kind: "readiness-snapshot",
9211
+ records: this.readinessHandler?.() ?? []
9212
+ };
9213
+ throw new Error(`unknown child request kind: ${msg.kind}`);
9219
9214
  });
9220
- } else rejected.push({
9221
- kind: "hub-in-process",
9222
- why: "hub does not provide this cap in-process"
9223
- });
9224
- if (snap.hubLocalChildProvides(capName, deviceId)) {
9225
- const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
9226
- if (childId !== null) return {
9227
- kind: "hub-local-uds",
9228
- capName,
9229
- childId
9230
- };
9231
- rejected.push({
9232
- kind: "hub-local-uds",
9233
- why: "child id not resolvable from snapshot"
9215
+ channel.onClose(() => {
9216
+ if (childId === null) return;
9217
+ const entry = this.children.get(childId);
9218
+ if (entry === void 0) return;
9219
+ if (entry.channel !== channel) {
9220
+ this.logger?.info("ignoring close of a superseded local child connection", {
9221
+ childId,
9222
+ incarnation,
9223
+ liveIncarnation: entry.incarnation
9224
+ });
9225
+ return;
9226
+ }
9227
+ this.children.delete(childId);
9228
+ this.invalidateCapIndex();
9229
+ this.childEventStats.delete(childId);
9230
+ this.actionCensus.forget(childId);
9231
+ this.loggedPatternSet.delete(childId);
9232
+ this.goneHandler(childId, incarnation);
9234
9233
  });
9235
- } else rejected.push({
9236
- kind: "hub-local-uds",
9237
- why: "no hub-local child provides this cap"
9238
- });
9239
- const knownNodes = snap.listKnownNodeIds?.() ?? [];
9240
- for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
9241
- kind: "remote-moleculer",
9242
- capName,
9243
- nodeId
9244
- };
9245
- rejected.push({
9246
- kind: "remote-moleculer",
9247
- why: "no online remote node knows this cap"
9248
- });
9249
- throw new CapRouteError(capName, void 0, {
9250
- reason: "no-provider",
9251
- rejected
9252
- });
9253
- }
9234
+ }
9235
+ };
9254
9236
  //#endregion
9255
- //#region src/kernel/moleculer/resilient-cap-call.ts
9256
- /** Moleculer error `type` values meaning "the service is not (yet) routable". */
9257
- var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
9258
- /** Default ceiling for the discovery wait — fail-fast past this. */
9259
- var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
9260
- function isDiscoveryError(err) {
9261
- if (typeof err !== "object" || err === null) return false;
9262
- const type = err.type;
9263
- return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
9237
+ //#region src/kernel/transport/local-child-client.ts
9238
+ /** Did the parent's `register` answer say it accepts payload-borne args? */
9239
+ function ackAcceptsRawForward(ack) {
9240
+ if (ack === null || typeof ack !== "object") return false;
9241
+ return ack.rawForward === true;
9264
9242
  }
9265
9243
  /**
9266
- * Call a Moleculer action; on a service-discovery error, wait for the
9267
- * named service to be discovered and retry the call exactly once.
9268
- */
9269
- async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
9270
- try {
9271
- return await broker.call(action, params, opts);
9272
- } catch (err) {
9273
- if (!isDiscoveryError(err)) throw err;
9274
- await broker.waitForServices([serviceName], discoveryTimeoutMs);
9275
- return await broker.call(action, params, opts);
9276
- }
9277
- }
9278
- //#endregion
9279
- //#region src/kernel/transport/cap-route-resolver.ts
9280
- /** The Moleculer service name that Task 6's agent registers. */
9281
- var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
9282
- /** The Moleculer action (service.action) for agent cap forwarding. */
9283
- var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
9284
- /** Default timeout for remote Moleculer cap calls (ms). */
9285
- var REMOTE_CALL_TIMEOUT_MS = 6e4;
9286
- function extractDeviceId(args) {
9287
- if (args === null || typeof args !== "object") return void 0;
9288
- const raw = Reflect.get(args, "deviceId");
9289
- return typeof raw === "number" ? raw : void 0;
9290
- }
9291
- /**
9292
- * Extract an inline `nodeId` string from a cap call's args — the per-call node
9293
- * pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
9294
- * device-scoped caps carry `deviceId`, and the way the generated cap-router on
9295
- * the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
9244
+ * Child side of the local UDS transport. Connects to its parent (hub or
9245
+ * agent), registers its cap manifest, and serves parent→child cap calls by
9246
+ * delegating to `dispatch`. The provider implementation lives in the child;
9247
+ * only routing keys + call arguments cross the wire.
9296
9248
  *
9297
- * This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
9298
- * benchmark addon running a synthetic/decoder workload ON a chosen agent). The
9299
- * out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
9300
- * hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
9301
- * methods that don't declare `nodeId` simply ignore the extra field (they
9302
- * destructure the fields they need); methods that DO declare it (e.g.
9303
- * `getEngineProvisioning({nodeId})`) still receive it unchanged.
9304
- */
9305
- function extractNodeId(args) {
9306
- if (args === null || typeof args !== "object") return void 0;
9307
- const raw = Reflect.get(args, "nodeId");
9308
- return typeof raw === "string" && raw.length > 0 ? raw : void 0;
9309
- }
9310
- /**
9311
- * Merge `callerAddonId` into `args` as an extra field — the SAME "provider
9312
- * destructures what it needs and ignores the rest" convention
9313
- * {@link extractNodeId} already relies on for the inline `nodeId` pin.
9314
- * `undefined` (the overwhelmingly common case: no caller hint, or the census
9315
- * consuming it is disarmed) returns `args` unchanged — no allocation. A
9316
- * non-object `args` (or `null`/array) is returned unchanged too: there is
9317
- * nowhere to attach an extra key without changing the payload's shape, and a
9318
- * provider taking a bare value never reads `callerAddonId` off it anyway.
9249
+ * Additional channels beyond cap-call:
9250
+ * - `emitEvent` fire-and-forget event toward the parent
9251
+ * - `sendLog` fire-and-forget log entry toward the parent
9252
+ * - `requestReadinessSnapshot` request/response snapshot of readiness records
9253
+ * - `onEvent` register a handler for parent→child events
9254
+ *
9255
+ * Events and logs emitted before `start()` resolves are buffered and flushed
9256
+ * on connect (mirrors the `updateCaps`/`latestCaps` pattern).
9319
9257
  */
9320
- function withCallerAddonId(args, callerAddonId) {
9321
- if (callerAddonId === void 0) return args;
9322
- if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
9323
- return {
9324
- ...args,
9325
- callerAddonId
9326
- };
9327
- }
9328
- var CapRouteResolver = class {
9329
- hubNodeId;
9330
- broker;
9331
- hubLocalRegistry;
9332
- nodeAuthority;
9333
- inProcessProviders;
9334
- hubResidentProviders;
9335
- capTimeoutMs;
9336
- snapshot;
9337
- constructor(deps) {
9338
- this.hubNodeId = deps.hubNodeId;
9339
- this.broker = deps.broker;
9340
- this.hubLocalRegistry = deps.hubLocalRegistry;
9341
- this.nodeAuthority = deps.nodeAuthority;
9342
- this.inProcessProviders = deps.inProcessProviders;
9343
- this.hubResidentProviders = deps.hubResidentProviders;
9344
- this.capTimeoutMs = deps.capTimeoutMs;
9345
- this.snapshot = this.buildSnapshot();
9258
+ var LocalChildClient = class {
9259
+ options;
9260
+ client = null;
9261
+ channel = null;
9262
+ /**
9263
+ * The cap set `start()` will register, kept current by `updateCaps`. Native
9264
+ * device caps register on device-restore, which can race AHEAD of the UDS
9265
+ * connect — buffering here lets a pre-start `updateCaps` survive (start sends
9266
+ * the latest set) instead of being lost or throwing.
9267
+ */
9268
+ latestCaps;
9269
+ /**
9270
+ * Per-owner (addonId) category-pattern subscription sets, and their union.
9271
+ * The union is declared to the parent (in `RegisterMessage.eventPatterns`
9272
+ * pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
9273
+ * subscription-filter its event fan-out. Per-owner keying keeps the union
9274
+ * correct if multiple event buses ever share one client (group-runner case).
9275
+ */
9276
+ patternsByOwner = /* @__PURE__ */ new Map();
9277
+ latestEventPatterns = [];
9278
+ /**
9279
+ * Whether `updateEventPatterns` has ever been called. A client that never
9280
+ * declared a subscription set (no event bus wired — e.g. a pure cap-call
9281
+ * runner) omits `eventPatterns` from its register frame entirely, so the
9282
+ * parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
9283
+ * owner declares (in production every addon context's framework
9284
+ * subscriptions do), the register carries the real union — even if empty.
9285
+ */
9286
+ hasDeclaredEventPatterns = false;
9287
+ /**
9288
+ * The custom-action catalogs of every hosted addon, once the runner's init
9289
+ * loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
9290
+ * until then, and OMITTED from the register frame while null: the parent
9291
+ * reads absence as "not described yet", and a pre-init register must never
9292
+ * look like "this child declares no actions". Kept on the client so every
9293
+ * later re-register (native-cap change, reconnect) re-carries it unchanged.
9294
+ */
9295
+ latestCustomActions = null;
9296
+ /** Events and logs queued while the channel is not yet open. */
9297
+ pendingEmits = [];
9298
+ /**
9299
+ * Whether the parent acknowledged raw-forward at `register`. Until it has,
9300
+ * `callOut` sends args inline exactly as every child always did — a legacy
9301
+ * parent would decode a payload-borne call to `args: undefined` and run the
9302
+ * provider on nothing, silently. Re-read on every re-register.
9303
+ */
9304
+ parentRawForward = false;
9305
+ /** Handler for parent→child events. Registered via `onEvent`. */
9306
+ eventHandler = null;
9307
+ /**
9308
+ * Handler for parent→child addon-level calls (`routes` / `custom`).
9309
+ * Registered via `onAddonCall`. Resolves the loaded addon instance and
9310
+ * invokes its `addon-routes.getRoutes()` or its custom-action handler.
9311
+ * Replaces the per-addon Moleculer `getRoutes` / `custom.<action>` actions
9312
+ * removed in F1/F2. Only one handler is active at a time.
9313
+ */
9314
+ addonCallHandler = null;
9315
+ /**
9316
+ * Handler for `set-log-level` messages pushed by the parent.
9317
+ * Registered via `onSetLogLevel`. The handler applies the new level to the
9318
+ * child's local Moleculer logger (mirrors `$node-mgmt.setLogLevel`).
9319
+ * E2: wired in `addon-runner.ts` to forward to the broker logger.
9320
+ */
9321
+ setLogLevelHandler = null;
9322
+ /** Callbacks registered via `onConnected`. Fired on every (re)connect. */
9323
+ connectedHandlers = [];
9324
+ constructor(options) {
9325
+ this.options = options;
9326
+ this.latestCaps = options.caps;
9346
9327
  }
9347
- /** Per-call RPC timeout: the cap method's declared override, else the default. */
9348
- callTimeout(capName, method) {
9349
- return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
9328
+ /**
9329
+ * Declare an owner's live category-pattern subscription set. Stores it
9330
+ * per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
9331
+ * if the union changed — declares it to the parent:
9332
+ * - pre-connect: buffered only; `start()`'s register frame carries the set.
9333
+ * - post-connect: emitted as a fire-and-forget `event-sub` (full-set
9334
+ * replace).
9335
+ * Idempotent on an unchanged union (no redundant `event-sub` frames).
9336
+ */
9337
+ updateEventPatterns(ownerId, patterns) {
9338
+ this.hasDeclaredEventPatterns = true;
9339
+ this.patternsByOwner.set(ownerId, patterns);
9340
+ const union = this.computePatternUnion();
9341
+ if (!this.patternsEqual(union, this.latestEventPatterns)) {
9342
+ this.latestEventPatterns = union;
9343
+ if (this.channel !== null) {
9344
+ const msg = {
9345
+ kind: "event-sub",
9346
+ patterns: union
9347
+ };
9348
+ this.channel.emit(msg);
9349
+ }
9350
+ }
9351
+ }
9352
+ /** Sorted, de-duplicated union across all owners' pattern sets. */
9353
+ computePatternUnion() {
9354
+ const all = /* @__PURE__ */ new Set();
9355
+ for (const patterns of this.patternsByOwner.values()) for (const p of patterns) all.add(p);
9356
+ return [...all].toSorted();
9357
+ }
9358
+ /** Order-insensitive equality — both inputs are already sorted unions here. */
9359
+ patternsEqual(a, b) {
9360
+ if (a.length !== b.length) return false;
9361
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
9362
+ return true;
9363
+ }
9364
+ /**
9365
+ * Register a callback that fires each time the client successfully connects
9366
+ * (or reconnects) to its parent. Multiple handlers may be registered; all
9367
+ * are called in registration order. Used by readiness-context in UDS mode
9368
+ * to trigger a snapshot hydrate on connect/reconnect.
9369
+ */
9370
+ onConnected(handler) {
9371
+ this.connectedHandlers.push(handler);
9372
+ }
9373
+ /**
9374
+ * Register a handler for events pushed from the parent to this child.
9375
+ * Must be called before `start()` to avoid missing early events (though
9376
+ * registration after start is also safe for events not yet delivered).
9377
+ * Replaces any previously registered handler.
9378
+ */
9379
+ onEvent(handler) {
9380
+ this.eventHandler = handler;
9381
+ }
9382
+ /**
9383
+ * Register the handler invoked when the parent sends an `addon-call` (the
9384
+ * addon-level routes / custom-action plane). The addon-runner wires this to
9385
+ * resolve the loaded addon by id and dispatch to its `getRoutes()` or its
9386
+ * custom-action handler. Safe to register before or after `start()`;
9387
+ * replaces any prior handler.
9388
+ */
9389
+ onAddonCall(handler) {
9390
+ this.addonCallHandler = handler;
9391
+ }
9392
+ /**
9393
+ * E2: Register a handler invoked when the parent sends a `set-log-level`
9394
+ * message to this child. The handler should forward the new level to the
9395
+ * child's local Moleculer broker logger (mirrors `$node-mgmt.setLogLevel`).
9396
+ * Safe to register before or after `start()`. Replaces any prior handler.
9397
+ */
9398
+ onSetLogLevel(handler) {
9399
+ this.setLogLevelHandler = handler;
9400
+ }
9401
+ /**
9402
+ * True once `start()` has successfully connected and registered. Used by
9403
+ * callers (event-bus/logger bridges) to know the channel is live.
9404
+ */
9405
+ get isConnected() {
9406
+ return this.channel !== null;
9407
+ }
9408
+ async start() {
9409
+ if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
9410
+ const client = createLocalTransport().createClient(this.options.nodeId);
9411
+ const channel = await client.connect();
9412
+ channel.onRequest(async (body, payload) => {
9413
+ const msg = body;
9414
+ if (msg.kind === "cap-call") {
9415
+ const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
9416
+ const result = await this.options.dispatch({
9417
+ capName: msg.capName,
9418
+ method: msg.method,
9419
+ args,
9420
+ ...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
9421
+ ...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
9422
+ });
9423
+ if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
9424
+ return result;
9425
+ }
9426
+ if (msg.kind === "addon-call") {
9427
+ if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
9428
+ return this.addonCallHandler({
9429
+ addonId: msg.addonId,
9430
+ target: msg.target,
9431
+ ...msg.action !== void 0 ? { action: msg.action } : {},
9432
+ ...msg.method !== void 0 ? { method: msg.method } : {},
9433
+ ...msg.args !== void 0 ? { args: msg.args } : {},
9434
+ ...msg.caller !== void 0 ? { caller: msg.caller } : {}
9435
+ });
9436
+ }
9437
+ throw new Error(`unknown parent request kind: ${msg.kind}`);
9438
+ });
9439
+ channel.onEvent((body) => {
9440
+ const msg = body;
9441
+ if (msg.kind === "event") {
9442
+ this.eventHandler?.(msg.event);
9443
+ return;
9444
+ }
9445
+ if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
9446
+ });
9447
+ const register = this.registerFrame(this.latestCaps);
9448
+ try {
9449
+ this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
9450
+ } catch (err) {
9451
+ await client.close();
9452
+ throw err;
9453
+ }
9454
+ this.client = client;
9455
+ this.channel = channel;
9456
+ this.flushPending(channel);
9457
+ for (const cb of this.connectedHandlers) cb();
9350
9458
  }
9351
- resolveCapRoute(capName, opts) {
9352
- return classifyCapRoute(capName, opts, this.snapshot);
9459
+ /** Flush buffered pre-start emits over the now-open channel. */
9460
+ flushPending(channel) {
9461
+ for (const item of this.pendingEmits) channel.emit(item.msg);
9462
+ this.pendingEmits.length = 0;
9353
9463
  }
9354
9464
  /**
9355
- * Resolve the hub-local-uds route for a cap owned by a forked hub-local
9356
- * child, IGNORING any in-hub provider registered for the same cap name.
9357
- *
9358
- * `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
9359
- * provider (e.g. a wrapper) is registered for the cap, the route always
9360
- * classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
9361
- * reached. That is correct for the generic dispatch path (the wrapper is the
9362
- * active provider), but WRONG for the native-cap fallback
9363
- * (`setNativeFallback`), whose contract is to reach the NATIVE provider in
9364
- * the forked vendor child so a wrapper can delegate to it. A wrapper cap
9365
- * with a forked native (today: `snapshot`) otherwise resolves to the wrapper
9366
- * itself, the native is never invoked, and the wrapper silently falls
9367
- * through to its secondary strategy.
9465
+ * Re-send the cap manifest to the parent, atomically replacing the child's
9466
+ * registered descriptor set. Call this after device-restore so newly
9467
+ * registered native (device-scoped) caps become routable over UDS.
9368
9468
  *
9369
- * This method consults ONLY the hub-local-child authority
9370
- * (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
9371
- * it returns the native child route regardless of any in-process shadow.
9372
- * Returns null when no hub-local child owns `(capName, deviceId)` — the
9373
- * caller then falls through to its remote-resolution branch.
9469
+ * Safe to call before `start()`: the new set is buffered and `start()` sends
9470
+ * it (device-restore can race ahead of the UDS connect). After `start()`,
9471
+ * the set is sent immediately.
9374
9472
  */
9375
- resolveHubLocalUdsRoute(capName, deviceId) {
9376
- if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
9377
- const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
9378
- if (childId === null) return null;
9473
+ async updateCaps(caps, customActions) {
9474
+ this.latestCaps = caps;
9475
+ if (customActions !== void 0) this.latestCustomActions = customActions;
9476
+ if (this.channel === null) return;
9477
+ this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
9478
+ }
9479
+ /**
9480
+ * The register frame: caps, plus everything a re-register must atomically
9481
+ * re-carry so no separate re-sync can be missed — the subscription union
9482
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
9483
+ * custom-action catalogs (omitted until the init loop produced them).
9484
+ */
9485
+ registerFrame(caps) {
9379
9486
  return {
9380
- kind: "hub-local-uds",
9381
- capName,
9382
- childId
9487
+ kind: "register",
9488
+ childId: this.options.childId,
9489
+ caps,
9490
+ ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
9491
+ ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
9492
+ rawForward: true
9383
9493
  };
9384
9494
  }
9385
9495
  /**
9386
- * @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
9387
- * the addonId of the provider the CALLER already resolved (e.g.
9388
- * `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
9389
- * provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
9390
- * route already encodes the provider in its action name
9391
- * (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
9392
- * `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
9393
- * talks to `LocalChildRegistry.callCapOnChild` directly rather than through
9394
- * `dispatch`.
9395
- * @param callerAddonId Optional out-of-band CALLER-identity hint — the
9396
- * addon that ORIGINATED the call, distinct from `addonId` above (which
9397
- * names the resolved PROVIDER, not the caller). Set by
9398
- * `onUnownedCall` from `CapCallInput.callerAddonId`
9399
- * (`LocalChildRegistry` stamped it off the registered `childId`). Only
9400
- * the `hub-in-process` branch consumes it — merged into `args` so a
9401
- * diagnostic tool deep in that provider (e.g. device-manager's
9402
- * fleet-read census) can read it without every provider method changing
9403
- * signature, the same "extra field the provider destructures or
9404
- * ignores" convention `nodeId` already uses on this path.
9496
+ * Fire-and-forget: send a system event to the parent for forwarding to the
9497
+ * hub event bus. Safe to call before `start()` — events are buffered and
9498
+ * flushed on connect.
9405
9499
  */
9406
- async dispatch(route, method, args, addonId, callerAddonId) {
9407
- try {
9408
- return await this.dispatchInner(route, method, args, addonId, callerAddonId);
9409
- } catch (err) {
9410
- if (err instanceof CapRouteError) throw err;
9411
- if (route.kind === "hub-in-process" || err instanceof PeerAnsweredError) throw err;
9412
- const nodeId = this.routeNodeId(route);
9413
- const cause = err instanceof Error ? err : new Error(String(err));
9414
- throw new CapRouteError(route.capName, method, {
9415
- reason: "transport-failed",
9416
- nodeId,
9417
- rejected: [{
9418
- kind: route.kind,
9419
- why: cause.message
9420
- }]
9421
- }, cause);
9422
- }
9500
+ emitEvent(event) {
9501
+ const msg = {
9502
+ kind: "event",
9503
+ event
9504
+ };
9505
+ if (this.channel !== null) this.channel.emit(msg);
9506
+ else this.pendingEmits.push({
9507
+ kind: "event",
9508
+ msg
9509
+ });
9423
9510
  }
9424
9511
  /**
9425
- * Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
9426
- * transport errors. The outer `dispatch` wraps non-CapRouteErrors.
9512
+ * Fire-and-forget: send a structured log entry to the parent. Safe to call
9513
+ * before `start()` — log entries are buffered and flushed on connect.
9427
9514
  */
9428
- async dispatchInner(route, method, args, addonId, callerAddonId) {
9429
- switch (route.kind) {
9430
- case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
9431
- case "hub-local-uds": {
9432
- const registry = this.hubLocalRegistry;
9433
- if (registry === null) throw new CapRouteError(route.capName, method, {
9434
- reason: "no-provider",
9435
- rejected: [{
9436
- kind: "hub-local-uds",
9437
- why: "UDS registry not available"
9438
- }]
9439
- });
9440
- const deviceId = extractDeviceId(args);
9441
- const input = {
9442
- capName: route.capName,
9443
- method,
9444
- args,
9445
- ...deviceId !== void 0 ? { deviceId } : {}
9446
- };
9447
- return registry.callCapOnChild(route.childId, input);
9448
- }
9449
- case "remote-moleculer": {
9450
- const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
9451
- if (addonId === null) throw new CapRouteError(route.capName, method, {
9452
- reason: "no-provider",
9453
- nodeId: route.nodeId,
9454
- rejected: [{
9455
- kind: "remote-moleculer",
9456
- why: `no addonId known for ${route.nodeId}/${route.capName}`
9457
- }]
9458
- });
9459
- const deviceId = extractDeviceId(args);
9460
- const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
9461
- const action = capActionName(addonId, route.capName, method, isNative);
9462
- return callWithServiceDiscovery(this.broker, addonId, action, args, {
9463
- nodeID: route.nodeId,
9464
- timeout: this.callTimeout(route.capName, method)
9465
- });
9466
- }
9467
- case "agent-child-forward": {
9468
- const deviceId = extractDeviceId(args);
9469
- const params = {
9470
- capName: route.capName,
9471
- method,
9472
- args,
9473
- ...route.childId !== void 0 ? { childId: route.childId } : {},
9474
- ...deviceId !== void 0 ? { deviceId } : {},
9475
- ...addonId !== void 0 ? { addonId } : {}
9476
- };
9477
- return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
9478
- nodeID: route.agentNodeId,
9479
- timeout: this.callTimeout(route.capName, method)
9480
- });
9481
- }
9482
- }
9483
- }
9484
- buildSnapshot() {
9485
- const hubLocalRegistry = this.hubLocalRegistry;
9486
- const nodeAuthority = this.nodeAuthority;
9487
- const inProcessProviders = this.inProcessProviders;
9488
- const hubResidentProviders = this.hubResidentProviders;
9489
- return {
9490
- hubNodeId: this.hubNodeId,
9491
- hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
9492
- ...hubResidentProviders !== void 0 ? {
9493
- hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
9494
- getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
9495
- } : {},
9496
- hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
9497
- nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
9498
- nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
9499
- nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
9500
- listKnownNodeIds: () => nodeAuthority.listNodeIds(),
9501
- getInProcessProviderRef: (cap) => inProcessProviders(cap),
9502
- getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
9503
- getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
9515
+ sendLog(entry) {
9516
+ const msg = {
9517
+ kind: "log",
9518
+ ...entry
9504
9519
  };
9520
+ if (this.channel !== null) this.channel.emit(msg);
9521
+ else this.pendingEmits.push({
9522
+ kind: "log",
9523
+ msg
9524
+ });
9505
9525
  }
9506
- /** Extract a nodeId string from a route for error reporting, or undefined. */
9507
- routeNodeId(route) {
9508
- switch (route.kind) {
9509
- case "hub-in-process": return this.hubNodeId;
9510
- case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
9511
- case "remote-moleculer": return route.nodeId;
9512
- case "agent-child-forward": return route.agentNodeId;
9526
+ /**
9527
+ * Request the current readiness snapshot from the parent. Returns the
9528
+ * authoritative set of `IReadinessRegistryRecord` entries the hub holds.
9529
+ * Requires `start()` to have been called; throws with a clear message if not.
9530
+ */
9531
+ async requestReadinessSnapshot() {
9532
+ if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
9533
+ return (await this.channel.request({ kind: "readiness-request" })).records;
9534
+ }
9535
+ /**
9536
+ * Ask the parent to execute a cap call this child does NOT own. The parent
9537
+ * routes to the owning local sibling over UDS, or — if no sibling owns it —
9538
+ * to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
9539
+ * production). Throws if called before `start()`.
9540
+ */
9541
+ async callOut(input) {
9542
+ if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
9543
+ const routing = {
9544
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
9545
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
9546
+ ...input.native === true ? { native: true } : {}
9547
+ };
9548
+ if (this.parentRawForward && input.args !== void 0) {
9549
+ const hints = liftRoutingHints(input.args);
9550
+ const msg = {
9551
+ kind: "cap-call-out",
9552
+ capName: input.capName,
9553
+ method: input.method,
9554
+ ...Object.keys(hints).length > 0 ? { hints } : {},
9555
+ ...routing
9556
+ };
9557
+ const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
9558
+ return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
9513
9559
  }
9560
+ const msg = {
9561
+ kind: "cap-call-out",
9562
+ capName: input.capName,
9563
+ method: input.method,
9564
+ args: input.args,
9565
+ ...routing
9566
+ };
9567
+ return this.channel.request(msg);
9568
+ }
9569
+ /** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
9570
+ async close() {
9571
+ await this.client?.close();
9572
+ this.client = null;
9573
+ this.channel = null;
9514
9574
  }
9515
9575
  };
9516
9576
  //#endregion
@@ -9772,6 +9832,13 @@ function createParentUnownedCallHandler(deps) {
9772
9832
  });
9773
9833
  throw new Error(`no native provider for capability "${input.capName}" on device ${String(deviceId)}`);
9774
9834
  }
9835
+ const claimed = await answerClaimedGetStatus(deps.claimedStatus, {
9836
+ capName: input.capName,
9837
+ method: input.method,
9838
+ ...deviceId !== void 0 ? { deviceId } : {},
9839
+ args: input.args
9840
+ });
9841
+ if (claimed !== null) return claimed.slice;
9775
9842
  const nodeId = deps.isDataNodeIdCap?.(input.capName) === true ? void 0 : input.nodeId ?? extractNodeId(input.args);
9776
9843
  const pinnedToOtherNode = nodeId !== void 0 && nodeId !== deps.broker.nodeID;
9777
9844
  if (nodeId === void 0) {
@@ -12635,4 +12702,4 @@ function createChildCapDispatch(deps) {
12635
12702
  };
12636
12703
  }
12637
12704
  //#endregion
12638
- export { CapUsageRegistry as $, capActionLabel as $n, buildHeapSample as $t, resolveHwAccel as A, createOutboundQueueGate as An, CapabilityUnavailableError as At, udsChildLogToWorkerEntry as B, diffSocketPlane as Bn, HEAP_RECLAIM_MIN_INTERVAL_MS as Bt, getBrokerEventBus as C, formatHeapSpaces as Cn, capBareAction as Ct, subscribePassthrough as D, OUTBOUND_QUEUE_COOLDOWN_MS as Dn, serializeTypedArrays as Dt, setNodeEventInterest as E, selectReportedSpaces as En, deserializeTypedArrays as Et, ipcParentLink as F, EMPTY_SOCKET_PLANE_BASELINE as Fn, resolveNpmInvocation as Ft, CapRouteResolver as G, recordSocketFrame as Gn, HUB_MAIN_HEAP_WATCH_LABEL as Gt, createUdsLoggerWithControl as H, readSocketQueues as Hn, HEAP_RECLAIM_TRIGGER_MB as Ht, localProviderLink as I, SOCKET_PLANE_TOP_N as In, runNpm as It, classifyCapRoute as J, socketDirectionMessages as Jn, RSS_BUDGET_REANNOUNCE_MIN_MS as Jt, callWithServiceDiscovery as K, sampleSocketDirection as Kn, HUB_MAIN_RSS_BUDGET_MB as Kt, createParentUnownedCallHandler as L, SOCKET_QUEUE_TOP_N as Ln, createAddonPeerBytesFacility as Lt, brokerTransportLink as M, MESH_QUEUE_TOP_N as Mn, installManifestNativeDeps as Mt, buildLinkChain as N, createMeshQueueReader as Nn, resolveAddonClass as Nt, AddonDepsManager as O, OUTBOUND_QUEUE_SUSTAIN_MS as On, DeviceRegistry as Ot, deviceCapResolverLink as P, formatMeshQueue as Pn, buildNpmChildEnv as Pt, readMoleculerFanoutMode as Q, MAX_CAP_ACTIONS_PER_PEER as Qn, ZERO_LOOP_DELAY as Qt, createUdsEventBridge as R, createSocketPlaneMeter as Rn, hostNodeIdOf as Rt, clusterEventTopic as S, emitHeapDiagnosticReport as Sn, capActionSuffix as St, registerEventBusService as T, readHeapSpaces as Tn, parseCapAction as Tt, AGENT_CAP_FWD_ACTION as U, EMPTY_SOCKET_DIRECTION as Un, HEAP_WATCH_INTERVAL_MS as Ut, createUdsLogger as V, formatSocketPlane as Vn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Vt, AGENT_CAP_FWD_SERVICE as W, createSocketDirectionCounters as Wn, HEAP_WATCH_WARN_RATIO as Wt, LocalChildRegistry as X, CAP_ACTION_UNLABELLED as Xn, RUNNER_HEAP_WATCH_INTERVAL_MS as Xt, LocalChildClient as Y, CAP_ACTION_OVERFLOW_LABEL as Yn, RSS_BUDGET_RELEASE_RATIO as Yt, UDS_NO_ROUTE_PREFIX as Z, EMPTY_CAP_ACTION_READING as Zn, RUNNER_RSS_BUDGET_ENV as Zt, getWorkerDeviceRegistry as _, createInspectorArrayBufferCensus as _n, setWorkerNativeCapsChangeListener as _t, reapProcessGroup as a, nextRssBudgetState as an, diffEventPlane as ar, UdsLocalTransportServer as at, setHubConnected as b, DECISIVE_HEAP_SPACES as bn, NATIVE_PROVIDER_SERVICE_INFIX as bt, runnerOwnsProcessGroup as c, shouldReclaim as cn, localEndpointPath as ct, summarizeMembers as d, strandedMb as dn, buildNativeCapProxy as dt, createDeferredHeapWatchSink as en, createCapActionCensus as er, PARENT_ROUTED_PROVIDER_ADDON_ID as et, installManifestSystemDeps as f, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as fn, buildUdsNativeCapProxy as ft, createUdsAddonContext as g, createArrayBufferCensusGate as gn, mountNativeCapService as gt, createAddonContext as h, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as hn, getWorkerNativeCapSnapshot as ht, killOwnRunnerGroupAfterParentLoss as i, hubMainRssBudget as in, createEventPlaneReader as ir, UdsLocalTransportClient as it, brokerCallForCap as j, formatOutboundQueueVerdict as jn, copyBundledNativeModules as jt, createKernelHwAccel as k, OUTBOUND_QUEUE_THRESHOLD_BYTES as kn, CapabilityHandle as kt, signalAllRunnerGroups as l, startHeapWatch as ln, FrameDecoder as lt, adaptBrokerToCluster as m, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as mn, getWorkerNativeCapProvider as mt, createChildCapDispatch as n, createV8Reclaimer as nn, EVENT_PLANE_TOP_N as nr, getCapUsageRegistry as nt, registerRunnerGroup as o, parseRssBudgetMb as on, formatEventPlane as or, SocketChannel as ot, installManifestPythonDeps as p, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as pn, createBrokerDeviceManagerApi as pt, CapRouteError as q, socketDirectionBytes as qn, HUB_RSS_BUDGET_ENV as qt, defaultProcessGroupOps as r, describeRss as rn, createEventPlaneMeter as rr, createLocalTransport as rt, releaseRunnerGroup as s, reclaimIntervalMs as sn, PeerAnsweredError as st, CAP_METHOD_NOT_IMPLEMENTED as t, createLoopDelayMeter as tn, diffCapActionCensus as tr, __resetCapUsageRegistryForTests as tt, startRunnerParentDeathWatchdog as u, startRunnerHeapWatch as un, encodeFrame as ut, getOrInitReadinessRegistry as v, formatArrayBufferCensus as vn, createAddonService as vt, getMoleculerEventStats as w, heapSpaceField as wn, capServiceName as wt, EVENT_TOPIC_PREFIX as x, HEAP_SPACE_REPORT_MIN_MB as xn, capActionName as xt, getOrInitReadinessRegistryForClient as y, isArrayBufferCensusReport as yn, validateProviderRegistrations as yt, createUdsEventBus as z, createSocketPlaneReader as zn, createAddonDataPlaneFacility as zt };
12705
+ export { classifyCapRoute as $, EMPTY_CAP_ACTION_READING as $n, RUNNER_RSS_BUDGET_ENV as $t, resolveHwAccel as A, OUTBOUND_QUEUE_SUSTAIN_MS as An, DeviceRegistry as At, udsChildLogToWorkerEntry as B, createSocketPlaneMeter as Bn, hostNodeIdOf as Bt, getBrokerEventBus as C, HEAP_SPACE_REPORT_MIN_MB as Cn, capActionName as Ct, subscribePassthrough as D, readHeapSpaces as Dn, parseCapAction as Dt, setNodeEventInterest as E, heapSpaceField as En, capServiceName as Et, ipcParentLink as F, createMeshQueueReader as Fn, resolveAddonClass as Ft, UDS_NO_ROUTE_PREFIX as G, EMPTY_SOCKET_DIRECTION as Gn, HEAP_WATCH_INTERVAL_MS as Gt, createUdsLoggerWithControl as H, diffSocketPlane as Hn, HEAP_RECLAIM_MIN_INTERVAL_MS as Ht, localProviderLink as I, formatMeshQueue as In, buildNpmChildEnv as It, deviceOfGetStatus as J, sampleSocketDirection as Jn, HUB_MAIN_RSS_BUDGET_MB as Jt, GET_STATUS_METHOD as K, createSocketDirectionCounters as Kn, HEAP_WATCH_WARN_RATIO as Kt, createParentUnownedCallHandler as L, EMPTY_SOCKET_PLANE_BASELINE as Ln, resolveNpmInvocation as Lt, brokerTransportLink as M, createOutboundQueueGate as Mn, CapabilityUnavailableError as Mt, buildLinkChain as N, formatOutboundQueueVerdict as Nn, copyBundledNativeModules as Nt, AddonDepsManager as O, selectReportedSpaces as On, deserializeTypedArrays as Ot, deviceCapResolverLink as P, MESH_QUEUE_TOP_N as Pn, installManifestNativeDeps as Pt, CapRouteError as Q, CAP_ACTION_UNLABELLED as Qn, RUNNER_HEAP_WATCH_INTERVAL_MS as Qt, createUdsEventBridge as R, SOCKET_PLANE_TOP_N as Rn, runNpm as Rt, clusterEventTopic as S, DECISIVE_HEAP_SPACES as Sn, NATIVE_PROVIDER_SERVICE_INFIX as St, registerEventBusService as T, formatHeapSpaces as Tn, capBareAction as Tt, LocalChildClient as U, formatSocketPlane as Un, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Ut, createUdsLogger as V, createSocketPlaneReader as Vn, createAddonDataPlaneFacility as Vt, LocalChildRegistry as W, readSocketQueues as Wn, HEAP_RECLAIM_TRIGGER_MB as Wt, AGENT_CAP_FWD_SERVICE as X, socketDirectionMessages as Xn, RSS_BUDGET_REANNOUNCE_MIN_MS as Xt, AGENT_CAP_FWD_ACTION as Y, socketDirectionBytes as Yn, HUB_RSS_BUDGET_ENV as Yt, CapRouteResolver as Z, CAP_ACTION_OVERFLOW_LABEL as Zn, RSS_BUDGET_RELEASE_RATIO as Zt, getWorkerDeviceRegistry as _, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as _n, getWorkerNativeCapSnapshot as _t, reapProcessGroup as a, describeRss as an, createEventPlaneMeter as ar, getCapUsageRegistry as at, setHubConnected as b, formatArrayBufferCensus as bn, createAddonService as bt, runnerOwnsProcessGroup as c, parseRssBudgetMb as cn, formatEventPlane as cr, UdsLocalTransportServer as ct, summarizeMembers as d, startHeapWatch as dn, localEndpointPath as dt, ZERO_LOOP_DELAY as en, MAX_CAP_ACTIONS_PER_PEER as er, callWithServiceDiscovery as et, installManifestSystemDeps as f, startRunnerHeapWatch as fn, FrameDecoder as ft, createUdsAddonContext as g, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as gn, getWorkerNativeCapProvider as gt, createAddonContext as h, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as hn, createBrokerDeviceManagerApi as ht, killOwnRunnerGroupAfterParentLoss as i, createV8Reclaimer as in, EVENT_PLANE_TOP_N as ir, __resetCapUsageRegistryForTests as it, brokerCallForCap as j, OUTBOUND_QUEUE_THRESHOLD_BYTES as jn, CapabilityHandle as jt, createKernelHwAccel as k, OUTBOUND_QUEUE_COOLDOWN_MS as kn, serializeTypedArrays as kt, signalAllRunnerGroups as l, reclaimIntervalMs as ln, SocketChannel as lt, adaptBrokerToCluster as m, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as mn, buildNativeCapProxy as mt, createChildCapDispatch as n, createDeferredHeapWatchSink as nn, createCapActionCensus as nr, CapUsageRegistry as nt, registerRunnerGroup as o, hubMainRssBudget as on, createEventPlaneReader as or, createLocalTransport as ot, installManifestPythonDeps as p, strandedMb as pn, encodeFrame as pt, answerClaimedGetStatus as q, recordSocketFrame as qn, HUB_MAIN_HEAP_WATCH_LABEL as qt, defaultProcessGroupOps as r, createLoopDelayMeter as rn, diffCapActionCensus as rr, PARENT_ROUTED_PROVIDER_ADDON_ID as rt, releaseRunnerGroup as s, nextRssBudgetState as sn, diffEventPlane as sr, UdsLocalTransportClient as st, CAP_METHOD_NOT_IMPLEMENTED as t, buildHeapSample as tn, capActionLabel as tr, readMoleculerFanoutMode as tt, startRunnerParentDeathWatchdog as u, shouldReclaim as un, PeerAnsweredError as ut, getOrInitReadinessRegistry as v, createArrayBufferCensusGate as vn, mountNativeCapService as vt, getMoleculerEventStats as w, emitHeapDiagnosticReport as wn, capActionSuffix as wt, EVENT_TOPIC_PREFIX as x, isArrayBufferCensusReport as xn, validateProviderRegistrations as xt, getOrInitReadinessRegistryForClient as y, createInspectorArrayBufferCensus as yn, setWorkerNativeCapsChangeListener as yt, createUdsEventBus as z, SOCKET_QUEUE_TOP_N as zn, createAddonPeerBytesFacility as zt };