@camstack/system 1.2.159 → 1.2.160

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 (70) 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/backup-orchestrator/backup-orchestrator.addon.d.ts +10 -7
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +50 -12
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +50 -12
  12. package/dist/builtins/backup-orchestrator/write-targets.d.ts +63 -0
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-bindings-store.d.ts +56 -10
  18. package/dist/builtins/device-manager/device-manager.addon.d.ts +11 -0
  19. package/dist/builtins/device-manager/device-manager.addon.js +216 -81
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +216 -81
  21. package/dist/builtins/device-manager/device-meta-actions.d.ts +9 -3
  22. package/dist/builtins/device-manager/device-meta-store.d.ts +32 -5
  23. package/dist/builtins/device-manager/device-queries.d.ts +7 -3
  24. package/dist/builtins/device-manager/write-lock.d.ts +17 -0
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  27. package/dist/builtins/hub-forwarder/index.js +1 -1
  28. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  33. package/dist/builtins/local-network/local-network.addon.js +1 -1
  34. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  35. package/dist/builtins/loki-logging/index.js +1 -1
  36. package/dist/builtins/loki-logging/index.mjs +1 -1
  37. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  39. package/dist/builtins/platform-probe/index.js +1 -1
  40. package/dist/builtins/platform-probe/index.mjs +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/snapshot/index.js +1 -1
  44. package/dist/builtins/snapshot/index.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/filter-compiler.d.ts +41 -2
  48. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +15 -7
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  51. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +3 -2
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +63 -8
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +63 -8
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +19 -0
  55. package/dist/builtins/system-config/system-config.addon.js +1 -1
  56. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  57. package/dist/builtins/winston-logging/index.js +1 -1
  58. package/dist/builtins/winston-logging/index.mjs +1 -1
  59. package/dist/{dist-Ck2jkBZk.js → dist-CcvXUhHK.js} +74 -7
  60. package/dist/{dist-DLh4_rn3.mjs → dist-DKs---Vx.mjs} +74 -7
  61. package/dist/index.js +3 -2
  62. package/dist/index.mjs +3 -3
  63. package/dist/kernel/index.d.ts +2 -2
  64. package/dist/kernel/moleculer/cap-usage-registry.d.ts +132 -7
  65. package/dist/kernel/transport/local-child-registry.d.ts +57 -0
  66. package/dist/{manifest-system-deps-tZZYG3RR.mjs → manifest-system-deps-DnoPpLGp.mjs} +271 -77
  67. package/dist/{manifest-system-deps-8boi90D9.js → manifest-system-deps-oJMkWSX-.js} +276 -76
  68. package/dist/{retired-settings-keys-Bi0ZKBjb.mjs → retired-settings-keys-C3eONTvV.mjs} +1 -1
  69. package/dist/{retired-settings-keys-Dp_CyuCW.js → retired-settings-keys-DXZ2xe7C.js} +1 -1
  70. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
2
+ import { CapUsageCallRecord } from '../moleculer/cap-usage-registry.js';
2
3
  import { AddonCallInput, CapCallInput, ChildLogMessage, RegisteredChild } from './child-cap-protocol.js';
3
4
  import { LocalTransportServer } from './local-transport.js';
4
5
  import { SocketTrafficSample } from './socket-traffic.js';
@@ -129,6 +130,49 @@ export interface LocalChildRegistryOptions {
129
130
  * channel default for every cap call.
130
131
  */
131
132
  readonly capTimeoutMs?: (capName: string, method: string) => number | undefined;
133
+ /**
134
+ * Sink for observed cap calls — the ONE place a forked addon's outbound
135
+ * `ctx.api` traffic can be recorded in the process that also holds the reader.
136
+ *
137
+ * ── Why here, and not in the caller ───────────────────────────────────────
138
+ * `getCapUsageRegistry()` is a per-process module singleton and the reader
139
+ * (`nodes.getCapUsageGraph`) runs on hub-main. The only writer used to be
140
+ * `addon-context-factory.ts`'s `localProviderLink` observer, which runs in
141
+ * the FORKED RUNNER — so every observation it made was written to an object
142
+ * hub-main can never read, and it fired only for co-located providers anyway
143
+ * (`ipcParentLink`, which carries every cross-process call, takes no
144
+ * observer). `nodes.getCapUsageGraph` therefore returned `[]` in 0.21 s
145
+ * against a hub running 64 addons and 1019 devices.
146
+ *
147
+ * ── Why here, and not in `onUnownedCall` ──────────────────────────────────
148
+ * Because most inter-addon traffic never gets there. `resolveChildId`
149
+ * short-circuits a cap owned by a sibling runner straight to `callCap`, and
150
+ * on the hub every addon IS a sibling runner. Recording in `onUnownedCall`
151
+ * would have seen `ctx.settings.*` (hub-in-process `settings-store`) and
152
+ * missed every addon→addon call — a graph with a whole plane silently
153
+ * missing, which is the defect it was meant to end.
154
+ *
155
+ * This handler is strictly upstream of BOTH branches, and it is where
156
+ * `callerAddonId` is already stamped off the UDS connection the frame
157
+ * arrived on — a caller identity the child cannot spoof.
158
+ *
159
+ * ── What it still cannot see (say it, do not hide it) ─────────────────────
160
+ * 1. **hub-main → hub-main.** An in-process builtin's `ctx.api` is
161
+ * `AddonRegistryService.getBrokerApi()` — ONE lazily-built tRPC client
162
+ * shared by every addon, so there is no per-caller identity to record.
163
+ * Giving it an observer needs a per-addon client first.
164
+ * 2. **A runner's co-located calls.** `localProviderLink` answers inside the
165
+ * runner and the frame never reaches this socket. Only the `detection` /
166
+ * `ml` execution groups have co-located providers at all (D9, D29).
167
+ * 3. **An agent's own runners.** Their parent is the agent process, whose
168
+ * `LocalChildRegistry` writes to the agent's singleton. Aggregating that
169
+ * needs a transport hop; `$hub-cap-fwd`'s envelope carries no
170
+ * `callerAddonId` today, so an agent-originated call reaching the hub is
171
+ * recorded with no caller at all — i.e. not recorded.
172
+ *
173
+ * MUST NOT throw; the call site catches anyway (see the record site).
174
+ */
175
+ readonly capUsageObserver?: (obs: CapUsageCallRecord) => void;
132
176
  }
133
177
  /**
134
178
  * Parent-side authority for local addon-runners reachable over a
@@ -157,6 +201,8 @@ export declare class LocalChildRegistry {
157
201
  private readonly isAddonPinnedCall?;
158
202
  /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
159
203
  private readonly capTimeoutMs?;
204
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
205
+ private readonly capUsageObserver?;
160
206
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
161
207
  private readonly egressRoutedCaps;
162
208
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -274,6 +320,17 @@ export declare class LocalChildRegistry {
274
320
  * compete for the same capName.
275
321
  */
276
322
  resolveChildId(capName: string, deviceId?: number): string | null;
323
+ /**
324
+ * Publish one cap-usage observation, if a sink is wired.
325
+ *
326
+ * Cost discipline — this is on the hot path (D181: hub-main's event loop is
327
+ * the cluster's only queue): no sink means one undefined check; with a sink
328
+ * it is one object literal and one `Date.now()`, and the sink itself is O(1).
329
+ * Wrapped so a broken observer can never fail the call it observes — the
330
+ * registry swallows too, and BOTH matter: this catch also covers a sink that
331
+ * is not the registry.
332
+ */
333
+ private recordCapUsage;
277
334
  /** First child whose cap manifest contains a descriptor matching `predicate`. */
278
335
  private findChildId;
279
336
  /** Every child whose cap manifest contains a descriptor matching `predicate`. */
@@ -1,4 +1,4 @@
1
- import "./dist-DLh4_rn3.mjs";
1
+ import "./dist-DKs---Vx.mjs";
2
2
  import * as crypto$1 from "node:crypto";
3
3
  import { randomBytes, randomUUID } from "node:crypto";
4
4
  import { ensureBinary, ensureFfmpeg, ensurePython, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
@@ -5546,6 +5546,246 @@ function createLocalTransport() {
5546
5546
  };
5547
5547
  }
5548
5548
  //#endregion
5549
+ //#region src/kernel/moleculer/cap-usage-registry.ts
5550
+ /**
5551
+ * In-memory observation of tRPC cap calls between addons — the store behind
5552
+ * `nodes.getCapUsageGraph`.
5553
+ *
5554
+ * Storage: `Map<callerAddonId, Map<providerAddonId, Map<capName, RollingWindow>>>`.
5555
+ * `RollingWindow` is a fixed-size ring of 1-second buckets — default 300
5556
+ * buckets = 5 minutes retention. Queries narrower than the retention window
5557
+ * filter buckets at read time.
5558
+ *
5559
+ * ── The ring ages, and that is not free decoration ─────────────────────────
5560
+ * Buckets are indexed `second % retention`, so a bucket written at second S is
5561
+ * hit again at S + retention·k forever. Until 2026-08-30 nothing ever zeroed
5562
+ * them: the `lastCallAtMs` guard hid it while a triple was fully idle, but ONE
5563
+ * fresh call re-armed the entire stale ring. A triple that made 1000 calls and
5564
+ * then one call five minutes later reported **1001 calls/min instead of 1**.
5565
+ * An over-counted edge is worse than the empty graph this instrument shipped
5566
+ * with — silence invites doubt, a confident wrong number sends the next
5567
+ * investigation at the wrong addon.
5568
+ *
5569
+ * The fix is clear-forward: on each record, zero the buckets the ring has
5570
+ * ADVANCED over since the previous write, and never read past the last written
5571
+ * second. Amortised O(1) — a busy triple clears 0–1 buckets per call, and an
5572
+ * idle one clears at most `retention` once, when it wakes.
5573
+ *
5574
+ * ── Cardinality is bounded, because this map lives on hub-main ─────────────
5575
+ * The previous docblock claimed "~22 MB worst case … under 200 KB at ~10 active
5576
+ * triples". Both numbers were written for a smaller machine and the second was
5577
+ * an assumption, not a bound: the key space is caller × provider × cap, and on
5578
+ * the live hub that is 64 addons × 64 runners × ~150 caps. Nothing capped it.
5579
+ *
5580
+ * Today a ring is one `Uint32Array(retention)` — 1.2 KB of ArrayBuffer at the
5581
+ * default — plus the three Map entries that reach it. MEASURED (`heapUsed` +
5582
+ * `arrayBuffers`, node 24, `--expose-gc`): **1.7 KB per triple**, so ≈1.0 MB at
5583
+ * the realistic hub figure (64 callers × a handful of caps each, ≈600 triples)
5584
+ * and **6.3 MB at the {@link DEFAULT_MAX_TRIPLES} ceiling**. `recordCall` costs
5585
+ * ≈46 ns (1e6 calls in 46 ms) — at the 39 717 calls/min that started this
5586
+ * investigation, ≈30 µs of hub-main per second.
5587
+ *
5588
+ * Reclamation is what keeps it at the low end: a triple whose ring has entirely
5589
+ * aged out is dead data and is dropped on the next admission sweep. The hard
5590
+ * cap only exists so that a pathological key space (a caller id derived from
5591
+ * something unbounded) cannot take hub-main's heap with it — and when it bites,
5592
+ * {@link CapUsageRegistry.getStats} says how often, because an instrument that
5593
+ * truncates silently is the defect this whole file exists to fix (D237).
5594
+ */
5595
+ /** Ring length in seconds when the caller does not pick one. */
5596
+ var DEFAULT_RETENTION_SECONDS = 300;
5597
+ /**
5598
+ * Ceiling on live `(caller, provider, cap)` triples. ≈1.5 KB each → ≈6 MB.
5599
+ * Roughly 6× the realistic hub figure, so it is a backstop against an
5600
+ * unbounded key space, not a working limit.
5601
+ */
5602
+ var DEFAULT_MAX_TRIPLES = 4e3;
5603
+ /**
5604
+ * Provider placeholder for a call the PARENT routed rather than short-
5605
+ * circuiting to a sibling runner: hub-in-process (`settings-store`,
5606
+ * `device-manager`, the `$core-caps` bridge), or a remote node. At the layer
5607
+ * that records — the parent's `cap-call-out` handler — the sibling id IS the
5608
+ * provider and everything else has not been resolved yet.
5609
+ *
5610
+ * It is a NAMED unknown, not a dropped observation, for the reason D237 was
5611
+ * written: the caller is always known, and "who is hammering `settings-store`"
5612
+ * is the question this graph exists to answer. Sibling to
5613
+ * `UNRESOLVED_PROVIDER_ADDON_ID` in `trpc-links.ts`, which names the same kind
5614
+ * of unknown one layer up.
5615
+ */
5616
+ var PARENT_ROUTED_PROVIDER_ADDON_ID = "(unresolved: parent-routed)";
5617
+ var CapUsageRegistry = class {
5618
+ retentionSeconds;
5619
+ maxTriples;
5620
+ map = /* @__PURE__ */ new Map();
5621
+ triples = 0;
5622
+ droppedTriples = 0;
5623
+ constructor(opts) {
5624
+ this.retentionSeconds = Math.max(1, opts?.retentionSeconds ?? DEFAULT_RETENTION_SECONDS);
5625
+ this.maxTriples = Math.max(1, opts?.maxTriples ?? DEFAULT_MAX_TRIPLES);
5626
+ }
5627
+ /**
5628
+ * Record one observed cap call.
5629
+ *
5630
+ * On the hot path: this runs once per cross-process cap call on hub-main's
5631
+ * event loop, which is the cluster's only queue (D181). Three Map lookups, an
5632
+ * integer increment, and — only when the wall clock has moved on — a bounded
5633
+ * clear. No allocation once a triple exists, and it NEVER throws into the
5634
+ * call it observes.
5635
+ */
5636
+ recordCall(rec) {
5637
+ try {
5638
+ if (rec.callerAddonId === "" || rec.providerAddonId === "" || rec.capName === "") return;
5639
+ if (!Number.isFinite(rec.atMs) || rec.atMs < 0) return;
5640
+ const window = this.windowFor(rec);
5641
+ if (window === null) return;
5642
+ const sec = Math.floor(rec.atMs / 1e3);
5643
+ if (window.lastCallAtMs !== 0) {
5644
+ const lastSec = Math.floor(window.lastCallAtMs / 1e3);
5645
+ const advanced = Math.min(sec - lastSec, this.retentionSeconds);
5646
+ for (let k = 1; k <= advanced; k++) window.buckets[(lastSec + k) % this.retentionSeconds] = 0;
5647
+ }
5648
+ const idx = sec % this.retentionSeconds;
5649
+ window.buckets[idx] = (window.buckets[idx] ?? 0) + 1;
5650
+ if (rec.atMs > window.lastCallAtMs) window.lastCallAtMs = rec.atMs;
5651
+ } catch {}
5652
+ }
5653
+ getGraph(opts) {
5654
+ const windowSeconds = Math.max(1, Math.min(opts.windowSeconds, this.retentionSeconds));
5655
+ const minMs = opts.nowMs - windowSeconds * 1e3;
5656
+ const nowSec = Math.floor(opts.nowMs / 1e3);
5657
+ const out = [];
5658
+ for (const [caller, byProvider] of this.map) for (const [provider, byCap] of byProvider) for (const [capName, window] of byCap) {
5659
+ if (window.lastCallAtMs < minMs) continue;
5660
+ const count = this.sumWindow(window, nowSec, windowSeconds);
5661
+ if (count === 0) continue;
5662
+ const callsPerMin = count / windowSeconds * 60;
5663
+ out.push({
5664
+ callerAddonId: caller,
5665
+ providerAddonId: provider,
5666
+ capName,
5667
+ callsPerMin,
5668
+ lastCallAtMs: window.lastCallAtMs
5669
+ });
5670
+ }
5671
+ return out;
5672
+ }
5673
+ /** See {@link CapUsageStats}. Cheap — three counters, no scan. */
5674
+ getStats() {
5675
+ return {
5676
+ triples: this.triples,
5677
+ maxTriples: this.maxTriples,
5678
+ droppedTriples: this.droppedTriples
5679
+ };
5680
+ }
5681
+ /** Test / diagnostic helper — drops all recorded calls. */
5682
+ clear() {
5683
+ this.map.clear();
5684
+ this.triples = 0;
5685
+ this.droppedTriples = 0;
5686
+ }
5687
+ /**
5688
+ * Sum the buckets covering `(nowSec - windowSeconds, nowSec]`.
5689
+ *
5690
+ * The scan starts at `lastSec`, never at `nowSec`. `recordCall` clears the
5691
+ * buckets the ring has advanced OVER, which by definition stops at the last
5692
+ * write: the buckets for the seconds since then still hold whatever the
5693
+ * previous pass left in them, and reading `nowSec` down would count it. A
5694
+ * triple that went quiet for a couple of minutes and then made one call would
5695
+ * report the burst it made five minutes earlier.
5696
+ *
5697
+ * No lower bound is needed beyond the window itself: every bucket at or below
5698
+ * `lastSec - retention` was cleared by the advance that reached `lastSec`, so
5699
+ * it reads zero anyway. The loop is bounded by `windowSeconds ≤ retention`.
5700
+ */
5701
+ sumWindow(window, nowSec, windowSeconds) {
5702
+ const lastSec = Math.floor(window.lastCallAtMs / 1e3);
5703
+ const from = Math.min(nowSec, lastSec);
5704
+ const floorSec = nowSec - windowSeconds;
5705
+ let count = 0;
5706
+ for (let s = from; s > floorSec; s--) count += window.buckets[s % this.retentionSeconds] ?? 0;
5707
+ return count;
5708
+ }
5709
+ /**
5710
+ * The ring for this triple, creating it if the ceiling allows. `null` means
5711
+ * the observation is dropped — counted, never silent (see
5712
+ * {@link CapUsageStats.droppedTriples}).
5713
+ */
5714
+ windowFor(rec) {
5715
+ let byProvider = this.map.get(rec.callerAddonId);
5716
+ if (!byProvider) {
5717
+ byProvider = /* @__PURE__ */ new Map();
5718
+ this.map.set(rec.callerAddonId, byProvider);
5719
+ }
5720
+ let byCap = byProvider.get(rec.providerAddonId);
5721
+ if (!byCap) {
5722
+ byCap = /* @__PURE__ */ new Map();
5723
+ byProvider.set(rec.providerAddonId, byCap);
5724
+ }
5725
+ const existing = byCap.get(rec.capName);
5726
+ if (existing) return existing;
5727
+ if (this.triples >= this.maxTriples) {
5728
+ this.reclaimExpired(rec.atMs);
5729
+ if (this.triples >= this.maxTriples) {
5730
+ this.droppedTriples++;
5731
+ return null;
5732
+ }
5733
+ byProvider = this.map.get(rec.callerAddonId);
5734
+ if (!byProvider) {
5735
+ byProvider = /* @__PURE__ */ new Map();
5736
+ this.map.set(rec.callerAddonId, byProvider);
5737
+ }
5738
+ byCap = byProvider.get(rec.providerAddonId);
5739
+ if (!byCap) {
5740
+ byCap = /* @__PURE__ */ new Map();
5741
+ byProvider.set(rec.providerAddonId, byCap);
5742
+ }
5743
+ }
5744
+ const created = {
5745
+ buckets: new Uint32Array(this.retentionSeconds),
5746
+ lastCallAtMs: 0
5747
+ };
5748
+ byCap.set(rec.capName, created);
5749
+ this.triples++;
5750
+ return created;
5751
+ }
5752
+ /**
5753
+ * Drop every triple whose whole ring has aged out — its last call is older
5754
+ * than the retention window, so it can contribute to no query. O(triples),
5755
+ * and only reachable when the ceiling is full, so it is amortised away by the
5756
+ * admissions it enables.
5757
+ */
5758
+ reclaimExpired(nowMs) {
5759
+ const cutoff = nowMs - this.retentionSeconds * 1e3;
5760
+ for (const [caller, byProvider] of this.map) {
5761
+ for (const [provider, byCap] of byProvider) {
5762
+ for (const [capName, window] of byCap) {
5763
+ if (window.lastCallAtMs >= cutoff) continue;
5764
+ byCap.delete(capName);
5765
+ this.triples--;
5766
+ }
5767
+ if (byCap.size === 0) byProvider.delete(provider);
5768
+ }
5769
+ if (byProvider.size === 0) this.map.delete(caller);
5770
+ }
5771
+ }
5772
+ };
5773
+ var singleton = null;
5774
+ /**
5775
+ * The registry for THIS process. Deliberately per-process: the reader
5776
+ * (`nodes.getCapUsageGraph` → `cap-providers.ts`) runs on hub-main, so only
5777
+ * writes made on hub-main are ever visible. See the blind-spot list on
5778
+ * `LocalChildRegistryOptions.capUsageObserver` for what that excludes.
5779
+ */
5780
+ function getCapUsageRegistry() {
5781
+ if (!singleton) singleton = new CapUsageRegistry();
5782
+ return singleton;
5783
+ }
5784
+ /** Test helper — clears the singleton (kept module-private elsewhere). */
5785
+ function __resetCapUsageRegistryForTests() {
5786
+ singleton = null;
5787
+ }
5788
+ //#endregion
5549
5789
  //#region src/kernel/moleculer/event-bus-core.ts
5550
5790
  /**
5551
5791
  * @param retainRecent - `true` ONLY for the hub main broker bus (it serves
@@ -6237,6 +6477,8 @@ var LocalChildRegistry = class {
6237
6477
  isAddonPinnedCall;
6238
6478
  /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
6239
6479
  capTimeoutMs;
6480
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
6481
+ capUsageObserver;
6240
6482
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
6241
6483
  egressRoutedCaps = /* @__PURE__ */ new Set();
6242
6484
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -6268,6 +6510,7 @@ var LocalChildRegistry = class {
6268
6510
  this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
6269
6511
  this.isAddonPinnedCall = opts.isAddonPinnedCall;
6270
6512
  this.capTimeoutMs = opts.capTimeoutMs;
6513
+ this.capUsageObserver = opts.capUsageObserver;
6271
6514
  } else {
6272
6515
  this.server = serverOrOptions;
6273
6516
  this.onUnownedCall = onUnownedCallArg;
@@ -6434,6 +6677,29 @@ var LocalChildRegistry = class {
6434
6677
  }
6435
6678
  return candidates[0];
6436
6679
  }
6680
+ /**
6681
+ * Publish one cap-usage observation, if a sink is wired.
6682
+ *
6683
+ * Cost discipline — this is on the hot path (D181: hub-main's event loop is
6684
+ * the cluster's only queue): no sink means one undefined check; with a sink
6685
+ * it is one object literal and one `Date.now()`, and the sink itself is O(1).
6686
+ * Wrapped so a broken observer can never fail the call it observes — the
6687
+ * registry swallows too, and BOTH matter: this catch also covers a sink that
6688
+ * is not the registry.
6689
+ */
6690
+ recordCapUsage(callerChildId, providerChildId, capName, methodName) {
6691
+ const sink = this.capUsageObserver;
6692
+ if (sink === void 0 || callerChildId === null) return;
6693
+ try {
6694
+ sink({
6695
+ callerAddonId: callerChildId,
6696
+ providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
6697
+ capName,
6698
+ methodName,
6699
+ atMs: Date.now()
6700
+ });
6701
+ } catch {}
6702
+ }
6437
6703
  /** First child whose cap manifest contains a descriptor matching `predicate`. */
6438
6704
  findChildId(predicate) {
6439
6705
  for (const entry of this.children.values()) if (entry.caps.some(predicate)) return entry.childId;
@@ -6703,7 +6969,9 @@ var LocalChildRegistry = class {
6703
6969
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
6704
6970
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
6705
6971
  const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
6706
- if ((!(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
6972
+ const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
6973
+ this.recordCapUsage(childId, target, out.capName, out.method);
6974
+ if (target !== null) {
6707
6975
  if (!this.egressRoutedCaps.has(out.capName)) {
6708
6976
  this.egressRoutedCaps.add(out.capName);
6709
6977
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
@@ -7855,80 +8123,6 @@ function buildLinkChain(target, localResolver, hookup) {
7855
8123
  return links;
7856
8124
  }
7857
8125
  //#endregion
7858
- //#region src/kernel/moleculer/cap-usage-registry.ts
7859
- var CapUsageRegistry = class {
7860
- retentionSeconds;
7861
- map = /* @__PURE__ */ new Map();
7862
- constructor(opts) {
7863
- this.retentionSeconds = Math.max(1, opts?.retentionSeconds ?? 300);
7864
- }
7865
- recordCall(rec) {
7866
- try {
7867
- if (rec.callerAddonId === "" || rec.providerAddonId === "" || rec.capName === "") return;
7868
- if (!Number.isFinite(rec.atMs) || rec.atMs < 0) return;
7869
- let byProvider = this.map.get(rec.callerAddonId);
7870
- if (!byProvider) {
7871
- byProvider = /* @__PURE__ */ new Map();
7872
- this.map.set(rec.callerAddonId, byProvider);
7873
- }
7874
- let byCap = byProvider.get(rec.providerAddonId);
7875
- if (!byCap) {
7876
- byCap = /* @__PURE__ */ new Map();
7877
- byProvider.set(rec.providerAddonId, byCap);
7878
- }
7879
- let window = byCap.get(rec.capName);
7880
- if (!window) {
7881
- window = {
7882
- buckets: Array.from({ length: this.retentionSeconds }, () => 0),
7883
- lastCallAtMs: 0
7884
- };
7885
- byCap.set(rec.capName, window);
7886
- }
7887
- const bucketIdx = Math.floor(rec.atMs / 1e3) % this.retentionSeconds;
7888
- window.buckets[bucketIdx] = (window.buckets[bucketIdx] ?? 0) + 1;
7889
- if (rec.atMs > window.lastCallAtMs) window.lastCallAtMs = rec.atMs;
7890
- } catch {}
7891
- }
7892
- getGraph(opts) {
7893
- const windowSeconds = Math.max(1, Math.min(opts.windowSeconds, this.retentionSeconds));
7894
- const minMs = opts.nowMs - windowSeconds * 1e3;
7895
- const out = [];
7896
- for (const [caller, byProvider] of this.map) for (const [provider, byCap] of byProvider) for (const [capName, window] of byCap) {
7897
- if (window.lastCallAtMs < minMs) continue;
7898
- const nowBucket = Math.floor(opts.nowMs / 1e3);
7899
- let count = 0;
7900
- for (let i = 0; i < windowSeconds; i++) {
7901
- const b = (nowBucket - i + this.retentionSeconds * 1e6) % this.retentionSeconds;
7902
- count += window.buckets[b] ?? 0;
7903
- }
7904
- if (count === 0) continue;
7905
- const callsPerMin = count / windowSeconds * 60;
7906
- out.push({
7907
- callerAddonId: caller,
7908
- providerAddonId: provider,
7909
- capName,
7910
- callsPerMin,
7911
- lastCallAtMs: window.lastCallAtMs
7912
- });
7913
- }
7914
- return out;
7915
- }
7916
- /** Test / diagnostic helper — drops all recorded calls. */
7917
- clear() {
7918
- this.map.clear();
7919
- }
7920
- };
7921
- var singleton = null;
7922
- /** Hub-side singleton. The first caller seeds it; subsequent calls return the same. */
7923
- function getCapUsageRegistry() {
7924
- if (!singleton) singleton = new CapUsageRegistry();
7925
- return singleton;
7926
- }
7927
- /** Test helper — clears the singleton (kept module-private elsewhere). */
7928
- function __resetCapUsageRegistryForTests() {
7929
- singleton = null;
7930
- }
7931
- //#endregion
7932
8126
  //#region src/kernel/hwaccel/hwaccel-resolver.ts
7933
8127
  /**
7934
8128
  * Platform-probe for hardware video decode acceleration.
@@ -9689,4 +9883,4 @@ async function defaultRun(command, args) {
9689
9883
  await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
9690
9884
  }
9691
9885
  //#endregion
9692
- export { encodeFrame as $, HEAP_SPACE_REPORT_MIN_MB as $t, createHubCapForwardService as A, HEAP_WATCH_WARN_RATIO as At, AGENT_CAP_FWD_ACTION as B, createDeferredHeapWatchSink as Bt, brokerCallForCap as C, formatEventPlane as Cn, resolveNpmInvocation as Ct, localProviderLink as D, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Dt, ipcParentLink as E, HEAP_RECLAIM_MIN_INTERVAL_MS as Et, createUdsLogger as F, RSS_BUDGET_RELEASE_RATIO as Ft, callWithServiceDiscovery as G, nextRssBudgetState as Gt, CapRouteResolver as H, createV8Reclaimer as Ht, createUdsLoggerWithControl as I, RUNNER_HEAP_WATCH_INTERVAL_MS as It, UdsLocalTransportClient as J, shouldReclaim as Jt, readMoleculerFanoutMode as K, parseRssBudgetMb as Kt, LocalChildClient as L, RUNNER_RSS_BUDGET_ENV as Lt, createUdsEventBridge as M, HUB_MAIN_RSS_BUDGET_MB as Mt, createUdsEventBus as N, HUB_RSS_BUDGET_ENV as Nt, HUB_CAP_FWD_ACTION as O, HEAP_RECLAIM_TRIGGER_MB as Ot, udsChildLogToWorkerEntry as P, RSS_BUDGET_REANNOUNCE_MIN_MS as Pt, FrameDecoder as Q, DECISIVE_HEAP_SPACES as Qt, LocalChildRegistry as R, ZERO_LOOP_DELAY as Rt, getCapUsageRegistry as S, diffEventPlane as Sn, resolveAddonClass as St, buildLinkChain as T, createAddonDataPlaneFacility as Tt, CapRouteError as U, describeRss as Ut, AGENT_CAP_FWD_SERVICE as V, createLoopDelayMeter as Vt, classifyCapRoute as W, hubMainRssBudget as Wt, SocketChannel as X, startRunnerHeapWatch as Xt, UdsLocalTransportServer as Y, startHeapWatch as Yt, localEndpointPath as Z, strandedMb as Zt, AddonDepsManager as _, socketDirectionBytes as _n, DeviceRegistry as _t, createUdsAddonContext as a, readHeapSpaces as an, mountNativeCapService as at, CapUsageRegistry as b, createEventPlaneMeter as bn, copyBundledNativeModules as bt, getOrInitReadinessRegistryForClient as c, SOCKET_PLANE_TOP_N as cn, validateProviderRegistrations as ct, clusterEventTopic as d, diffSocketPlane as dn, capActionSuffix as dt, RUNNER_HEAP_SNAPSHOT_ENV as en, buildNativeCapProxy as et, getBrokerEventBus as f, formatSocketPlane as fn, capBareAction as ft, subscribePassthrough as g, sampleSocketDirection as gn, serializeTypedArrays as gt, setNodeEventInterest as h, recordSocketFrame as hn, deserializeTypedArrays as ht, createAddonContext as i, heapSpaceField as in, getWorkerNativeCapSnapshot as it, createParentUnownedCallHandler as j, HUB_MAIN_HEAP_WATCH_LABEL as jt, HUB_CAP_FWD_SERVICE as k, HEAP_WATCH_INTERVAL_MS as kt, setHubConnected as l, createSocketPlaneMeter as ln, NATIVE_PROVIDER_SERVICE_INFIX as lt, registerEventBusService as m, createSocketDirectionCounters as mn, parseCapAction as mt, installManifestPythonDeps as n, formatHeapSpaces as nn, createBrokerDeviceManagerApi as nt, getWorkerDeviceRegistry as o, selectReportedSpaces as on, setWorkerNativeCapsChangeListener as ot, getMoleculerEventStats as p, EMPTY_SOCKET_DIRECTION as pn, capServiceName as pt, createLocalTransport as q, reclaimIntervalMs as qt, adaptBrokerToCluster as r, heapSnapshotAuthorised as rn, getWorkerNativeCapProvider as rt, getOrInitReadinessRegistry as s, EMPTY_SOCKET_PLANE_BASELINE as sn, createAddonService as st, installManifestSystemDeps as t, emitHeapDiagnosticReport as tn, buildUdsNativeCapProxy as tt, EVENT_TOPIC_PREFIX as u, createSocketPlaneReader as un, capActionName as ut, createKernelHwAccel as v, socketDirectionMessages as vn, CapabilityHandle as vt, brokerTransportLink as w, runNpm as wt, __resetCapUsageRegistryForTests as x, createEventPlaneReader as xn, installManifestNativeDeps as xt, resolveHwAccel as y, EVENT_PLANE_TOP_N as yn, CapabilityUnavailableError as yt, UDS_NO_ROUTE_PREFIX as z, buildHeapSample as zt };
9886
+ export { FrameDecoder as $, DECISIVE_HEAP_SPACES as $t, createUdsEventBus as A, HEAP_WATCH_INTERVAL_MS as At, CapRouteError as B, buildHeapSample as Bt, ipcParentLink as C, diffEventPlane as Cn, resolveAddonClass as Ct, createHubCapForwardService as D, HEAP_RECLAIM_MIN_INTERVAL_MS as Dt, HUB_CAP_FWD_SERVICE as E, createAddonDataPlaneFacility as Et, LocalChildRegistry as F, RSS_BUDGET_REANNOUNCE_MIN_MS as Ft, PARENT_ROUTED_PROVIDER_ADDON_ID as G, hubMainRssBudget as Gt, callWithServiceDiscovery as H, createLoopDelayMeter as Ht, UDS_NO_ROUTE_PREFIX as I, RSS_BUDGET_RELEASE_RATIO as It, createLocalTransport as J, reclaimIntervalMs as Jt, __resetCapUsageRegistryForTests as K, nextRssBudgetState as Kt, AGENT_CAP_FWD_ACTION as L, RUNNER_HEAP_WATCH_INTERVAL_MS as Lt, createUdsLogger as M, HUB_MAIN_HEAP_WATCH_LABEL as Mt, createUdsLoggerWithControl as N, HUB_MAIN_RSS_BUDGET_MB as Nt, createParentUnownedCallHandler as O, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Ot, LocalChildClient as P, HUB_RSS_BUDGET_ENV as Pt, localEndpointPath as Q, strandedMb as Qt, AGENT_CAP_FWD_SERVICE as R, RUNNER_RSS_BUDGET_ENV as Rt, buildLinkChain as S, createEventPlaneReader as Sn, installManifestNativeDeps as St, HUB_CAP_FWD_ACTION as T, runNpm as Tt, readMoleculerFanoutMode as U, createV8Reclaimer as Ut, classifyCapRoute as V, createDeferredHeapWatchSink as Vt, CapUsageRegistry as W, describeRss as Wt, UdsLocalTransportServer as X, startHeapWatch as Xt, UdsLocalTransportClient as Y, shouldReclaim as Yt, SocketChannel as Z, startRunnerHeapWatch as Zt, AddonDepsManager as _, sampleSocketDirection as _n, serializeTypedArrays as _t, createUdsAddonContext as a, heapSpaceField as an, getWorkerNativeCapSnapshot as at, brokerCallForCap as b, EVENT_PLANE_TOP_N as bn, CapabilityUnavailableError as bt, getOrInitReadinessRegistryForClient as c, EMPTY_SOCKET_PLANE_BASELINE as cn, createAddonService as ct, clusterEventTopic as d, createSocketPlaneReader as dn, capActionName as dt, HEAP_SPACE_REPORT_MIN_MB as en, encodeFrame as et, getBrokerEventBus as f, diffSocketPlane as fn, capActionSuffix as ft, subscribePassthrough as g, recordSocketFrame as gn, deserializeTypedArrays as gt, setNodeEventInterest as h, createSocketDirectionCounters as hn, parseCapAction as ht, createAddonContext as i, heapSnapshotAuthorised as in, getWorkerNativeCapProvider as it, udsChildLogToWorkerEntry as j, HEAP_WATCH_WARN_RATIO as jt, createUdsEventBridge as k, HEAP_RECLAIM_TRIGGER_MB as kt, setHubConnected as l, SOCKET_PLANE_TOP_N as ln, validateProviderRegistrations as lt, registerEventBusService as m, EMPTY_SOCKET_DIRECTION as mn, capServiceName as mt, installManifestPythonDeps as n, emitHeapDiagnosticReport as nn, buildUdsNativeCapProxy as nt, getWorkerDeviceRegistry as o, readHeapSpaces as on, mountNativeCapService as ot, getMoleculerEventStats as p, formatSocketPlane as pn, capBareAction as pt, getCapUsageRegistry as q, parseRssBudgetMb as qt, adaptBrokerToCluster as r, formatHeapSpaces as rn, createBrokerDeviceManagerApi as rt, getOrInitReadinessRegistry as s, selectReportedSpaces as sn, setWorkerNativeCapsChangeListener as st, installManifestSystemDeps as t, RUNNER_HEAP_SNAPSHOT_ENV as tn, buildNativeCapProxy as tt, EVENT_TOPIC_PREFIX as u, createSocketPlaneMeter as un, NATIVE_PROVIDER_SERVICE_INFIX as ut, createKernelHwAccel as v, socketDirectionBytes as vn, DeviceRegistry as vt, localProviderLink as w, formatEventPlane as wn, resolveNpmInvocation as wt, brokerTransportLink as x, createEventPlaneMeter as xn, copyBundledNativeModules as xt, resolveHwAccel as y, socketDirectionMessages as yn, CapabilityHandle as yt, CapRouteResolver as z, ZERO_LOOP_DELAY as zt };