@camstack/system 1.2.216 → 1.2.218

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 (66) hide show
  1. package/dist/addon-runner.js +9 -6
  2. package/dist/addon-runner.mjs +9 -6
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +0 -7
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -8
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -8
  34. package/dist/builtins/platform-probe/index.js +1 -1
  35. package/dist/builtins/platform-probe/index.mjs +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{custom-action-registry-jY0NOZK8.js → custom-action-registry-BDwM5qAK.js} +19 -0
  51. package/dist/{custom-action-registry-F__gp_VX.mjs → custom-action-registry-CsONUlhC.mjs} +19 -0
  52. package/dist/{dist-DYWqXngV.js → dist-BOlMCOeT.js} +157 -5
  53. package/dist/{dist-MtqqwJVI.mjs → dist-DigmGsQS.mjs} +152 -6
  54. package/dist/index.d.ts +3 -1
  55. package/dist/index.js +11 -3
  56. package/dist/index.mjs +4 -4
  57. package/dist/kernel/array-buffer-census.d.ts +122 -0
  58. package/dist/kernel/custom-action-registry.d.ts +10 -1
  59. package/dist/kernel/heap-watch.d.ts +15 -0
  60. package/dist/kernel/transport/child-cap-protocol.d.ts +15 -1
  61. package/dist/kernel/transport/local-child-client.d.ts +18 -2
  62. package/dist/{manifest-system-deps-DvY1bY_n.js → manifest-system-deps-Bo92lg5u.js} +350 -14
  63. package/dist/{manifest-system-deps-726RBOX7.mjs → manifest-system-deps-D--wyMD9.mjs} +303 -15
  64. package/dist/{retired-settings-keys-BBrgMHDy.mjs → retired-settings-keys-vK_U-CG2.mjs} +1 -1
  65. package/dist/{retired-settings-keys-CXLIB19R.js → retired-settings-keys-xEAlYnv7.js} +1 -1
  66. package/package.json +1 -1
@@ -13,6 +13,7 @@ import * as os from "node:os";
13
13
  import { tmpdir } from "node:os";
14
14
  import { createServer } from "node:http";
15
15
  import * as v8 from "node:v8";
16
+ import { Session } from "node:inspector/promises";
16
17
  import { monitorEventLoopDelay } from "node:perf_hooks";
17
18
  import * as vm from "node:vm";
18
19
  import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DEVICE_CHILDREN_BATCH_MAX, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, createEventBusSliceSource, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
@@ -511,7 +512,7 @@ var DECISIVE_HEAP_SPACES = ["old_space", "large_object_space"];
511
512
  var HEAP_SPACE_REPORT_MIN_MB = 32;
512
513
  var BYTES_PER_MB = 1048576;
513
514
  /** Whole MB, matching the rest of the `[mem]` line. */
514
- function mb(bytes) {
515
+ function mb$1(bytes) {
515
516
  return Math.round(bytes / BYTES_PER_MB);
516
517
  }
517
518
  /** Tenths of an MB — the resolution the SIGUSR2 report has always printed. */
@@ -558,7 +559,7 @@ function heapSpaceField(v8Name) {
558
559
  */
559
560
  function selectReportedSpaces(spaces, minMb = 32) {
560
561
  const decisive = DECISIVE_HEAP_SPACES.map((name) => spaces.find((space) => space.name === name)).filter((space) => space !== void 0);
561
- const rest = spaces.filter((space) => !DECISIVE_HEAP_SPACES.includes(space.name)).filter((space) => mb(space.usedBytes) >= minMb).toSorted((a, b) => b.usedBytes - a.usedBytes || a.name.localeCompare(b.name));
562
+ const rest = spaces.filter((space) => !DECISIVE_HEAP_SPACES.includes(space.name)).filter((space) => mb$1(space.usedBytes) >= minMb).toSorted((a, b) => b.usedBytes - a.usedBytes || a.name.localeCompare(b.name));
562
563
  return [...decisive, ...rest];
563
564
  }
564
565
  /**
@@ -573,7 +574,7 @@ function selectReportedSpaces(spaces, minMb = 32) {
573
574
  function formatHeapSpaces(spaces, minMb = 32) {
574
575
  const selected = selectReportedSpaces(spaces, minMb);
575
576
  if (selected.length === 0) return "";
576
- return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb(s.usedBytes)}MB`).join(" ")}`;
577
+ return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb$1(s.usedBytes)}MB`).join(" ")}`;
577
578
  }
578
579
  var defaultDeps = {
579
580
  readMemory: () => process.memoryUsage(),
@@ -603,6 +604,251 @@ function emitHeapDiagnosticReport(request, deps = defaultDeps) {
603
604
  }
604
605
  }
605
606
  //#endregion
607
+ //#region src/kernel/array-buffer-census.ts
608
+ /**
609
+ * array-buffer-census — WHAT SHAPE the live ArrayBuffers have, taken by the
610
+ * heartbeat itself when they grow, because nobody can be awake for it.
611
+ *
612
+ * ## The episodes this exists for
613
+ *
614
+ * hub-main, 2026-09-10 (CEST): `arrayBuffers` climbed linearly and released in
615
+ * one instant, twice, with not one line from the process at either boundary —
616
+ * 17:26→18:31 at ~20 MB/min to 1 264 MB, released in one heartbeat; 22:20→23:36
617
+ * at 60–85 MB/min to **4 968 MB**, released between two probes at 23:38. Both
618
+ * were LIVE: a forced compacting GC at 23:36:40 returned 4 968→4 954 MB. The
619
+ * measurement that names their shape had been prepared (D443) and was never
620
+ * run, because it needed a human to run it during the episode, and the
621
+ * episode does not wait. Two hours after the second release the process died
622
+ * on `Ineffective mark-compacts near heap limit`.
623
+ *
624
+ * ## What this is, and what it is NOT
625
+ *
626
+ * `Runtime.queryObjects(ArrayBuffer.prototype)` over an IN-PROCESS inspector
627
+ * session, followed by `Runtime.callFunctionOn` computing a byteLength
628
+ * histogram over the result array, returned BY VALUE. It walks the heap in
629
+ * memory and returns a few hundred bytes of JSON. Nothing is serialised,
630
+ * nothing is written, no file exists afterwards.
631
+ *
632
+ * It is NOT a heap snapshot. `scripts/check-no-heap-dump-command.ts` forbids
633
+ * `writeHeapSnapshot` because a multi-GB serialisation of the heap took the
634
+ * whole host down twice; this module contains no such call and must never
635
+ * grow one. The two look alike only in that both need a GC first.
636
+ *
637
+ * ## What it costs, and why the line says so
638
+ *
639
+ * `queryObjects` forces ONE full garbage collection before it walks — the same
640
+ * stop-the-world the reclaim pass already pays every 10 minutes on this
641
+ * process (~1 s at a 2.5 GB heap). The cost is printed on the line it
642
+ * produces, because a pause with no owner in the log is exactly the kind of
643
+ * stall D181 warns about on the process that mediates every cross-runner call.
644
+ * The gate ({@link createArrayBufferCensusGate}) bounds it: a threshold, held
645
+ * for a sustain window so a burst that dies in the next scavenge never pays,
646
+ * and a generous cooldown so an hour-long episode buys a handful of readings,
647
+ * not one per probe.
648
+ *
649
+ * ## Why the byteLength histogram, and not "who holds them"
650
+ *
651
+ * `queryObjects` returns the objects, not their retainers; a retainer walk is
652
+ * a heap snapshot by another name. But the SIZE distribution already
653
+ * discriminates the candidates that were argued about for a night: 65 536-byte
654
+ * buffers are socket chunks, 8 192-byte ones are Buffer-pool slabs, a few
655
+ * hundred equal multi-megabyte ones are frames or JPEGs, and the count says
656
+ * whether it is thousands of small or dozens of huge. The exact top sizes are
657
+ * printed as well as power-of-two buckets, because "1 462 272 B × 880" names
658
+ * a producer where "≤2 MB: 1.2 GB" does not.
659
+ */
660
+ /**
661
+ * Take the census once `arrayBuffers` has been at or above this for
662
+ * {@link ARRAY_BUFFER_CENSUS_SUSTAIN_MS}.
663
+ *
664
+ * 512 MB. The two 2026-09-10 episodes crossed it 25 and 8 minutes in; the
665
+ * routine reading in six hours of 2-second samples around them was 18–100 MB
666
+ * with single-sample spikes to 479 MB, and the process's floor after every
667
+ * compaction is 12–18 MB. A dead-buffer sawtooth between major GCs (measured
668
+ * up to 3.65 GB on 2026-08-08 when the heap had 4 GB of headroom) cannot reach
669
+ * it while GC runs every few seconds — and if one did, the census would SAY
670
+ * so: it reports the live total after its own GC next to the raw reading.
671
+ */
672
+ var ARRAY_BUFFER_CENSUS_THRESHOLD_MB = 512;
673
+ /**
674
+ * How long the reading must stay above the threshold before a census is taken.
675
+ *
676
+ * 60 s = 30 probes at the 2 s cadence. A burst that a scavenge or the next
677
+ * major GC returns never pays a forced GC; an episode climbing at the slowest
678
+ * rate measured (20 MB/min) is still 20 MB higher by the time it is measured,
679
+ * which is nothing against the threshold.
680
+ */
681
+ var ARRAY_BUFFER_CENSUS_SUSTAIN_MS = 6e4;
682
+ /**
683
+ * Minimum spacing between two censuses.
684
+ *
685
+ * 30 minutes: the second episode ran 76 minutes, so it would have produced
686
+ * three readings — enough to see the shape change (or not) as it grew — at a
687
+ * cost the router already pays six times an hour for the reclaim pass.
688
+ */
689
+ var ARRAY_BUFFER_CENSUS_COOLDOWN_MS = 30 * 6e4;
690
+ /**
691
+ * A drop of at least this share of the threshold between two consecutive
692
+ * probes, from a reading that had been SUSTAINED above the threshold, is a
693
+ * RELEASE — the boundary both episodes crossed with no line, and the one line
694
+ * that lets the release be correlated with whatever else the process logged
695
+ * that second. Sustained, not merely above: a one-probe spike that the next
696
+ * scavenge sweeps (a 1.3 GB burst in 11 s was measured on 2026-08-29) is not
697
+ * an episode ending, and a WARN for every one of those would bury the line
698
+ * this exists to produce.
699
+ */
700
+ var ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO = .5;
701
+ /**
702
+ * The pure decision: threshold, sustain, cooldown, release — testable without
703
+ * allocating a gigabyte or forcing a GC.
704
+ */
705
+ function createArrayBufferCensusGate(options = {}) {
706
+ const thresholdMb = options.thresholdMb ?? 512;
707
+ const sustainMs = options.sustainMs ?? 6e4;
708
+ const cooldownMs = options.cooldownMs ?? 18e5;
709
+ let aboveSince = null;
710
+ let lastTakenAt = Number.NEGATIVE_INFINITY;
711
+ let previousMb = null;
712
+ let previousAt = null;
713
+ return { observe(arrayBuffersMb, at) {
714
+ const previous = previousMb;
715
+ const previousHeldForMs = previous !== null && previousAt !== null && aboveSince !== null ? previousAt - aboveSince : null;
716
+ previousMb = arrayBuffersMb;
717
+ previousAt = at;
718
+ if (previous !== null && previousHeldForMs !== null && previousHeldForMs >= sustainMs && previous - arrayBuffersMb >= thresholdMb * .5) {
719
+ aboveSince = null;
720
+ return {
721
+ kind: "released",
722
+ fromMb: previous,
723
+ toMb: arrayBuffersMb
724
+ };
725
+ }
726
+ if (arrayBuffersMb < thresholdMb) {
727
+ aboveSince = null;
728
+ return { kind: "hold" };
729
+ }
730
+ aboveSince ??= at;
731
+ const heldForMs = at - aboveSince;
732
+ if (heldForMs < sustainMs) return { kind: "hold" };
733
+ if (at - lastTakenAt < cooldownMs) return { kind: "hold" };
734
+ lastTakenAt = at;
735
+ return {
736
+ kind: "take",
737
+ reason: `arrayBuffers=${arrayBuffersMb}MB has been >=${thresholdMb}MB for ${heldForMs}ms`
738
+ };
739
+ } };
740
+ }
741
+ /**
742
+ * The function evaluated INSIDE the inspector against the `queryObjects`
743
+ * result array (`this`). Kept as source text because that is what the
744
+ * protocol takes; kept small because it runs with the world stopped.
745
+ */
746
+ var HISTOGRAM_FN = `function () {
747
+ var byBucket = new Map(), bySize = new Map()
748
+ var count = 0, bytes = 0, detached = 0
749
+ for (var i = 0; i < this.length; i++) {
750
+ var b = this[i].byteLength
751
+ count++
752
+ bytes += b
753
+ if (b === 0) { detached++; continue }
754
+ var bucket = 1
755
+ while (bucket < b) bucket *= 2
756
+ var e = byBucket.get(bucket); if (!e) { e = [0, 0]; byBucket.set(bucket, e) }
757
+ e[0]++; e[1] += b
758
+ var s = bySize.get(b); if (!s) { s = [0, 0]; bySize.set(b, s) }
759
+ s[0]++; s[1] += b
760
+ }
761
+ var buckets = []
762
+ byBucket.forEach(function (v, k) { buckets.push({ upToBytes: k, count: v[0], bytes: v[1] }) })
763
+ buckets.sort(function (a, b) { return b.bytes - a.bytes })
764
+ var topSizes = []
765
+ bySize.forEach(function (v, k) { topSizes.push({ byteLength: k, count: v[0], bytes: v[1] }) })
766
+ topSizes.sort(function (a, b) { return b.bytes - a.bytes })
767
+ return { count: count, bytes: bytes, detached: detached,
768
+ buckets: buckets.slice(0, 6), topSizes: topSizes.slice(0, 5) }
769
+ }`;
770
+ function isFiniteNumber(value) {
771
+ return typeof value === "number" && Number.isFinite(value);
772
+ }
773
+ function isBucket(value) {
774
+ return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "upToBytes")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
775
+ }
776
+ function isExactSize(value) {
777
+ return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "byteLength")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
778
+ }
779
+ /** The inspector answers `unknown`; the report is trusted only once it has this shape. */
780
+ function isArrayBufferCensusReport(value) {
781
+ if (typeof value !== "object" || value === null) return false;
782
+ const buckets = Reflect.get(value, "buckets");
783
+ const topSizes = Reflect.get(value, "topSizes");
784
+ return isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes")) && isFiniteNumber(Reflect.get(value, "detached")) && Array.isArray(buckets) && buckets.every(isBucket) && Array.isArray(topSizes) && topSizes.every(isExactSize);
785
+ }
786
+ function readObjectId(result, path) {
787
+ let cursor = result;
788
+ for (const key of path) {
789
+ if (typeof cursor !== "object" || cursor === null) throw new Error(`inspector reply has no ${path.join(".")}`);
790
+ cursor = Reflect.get(cursor, key);
791
+ }
792
+ if (typeof cursor !== "string") throw new Error(`inspector reply has no ${path.join(".")}`);
793
+ return cursor;
794
+ }
795
+ /**
796
+ * The real census: an in-process inspector session, opened for the call and
797
+ * closed after it, so no session is left listening between censuses.
798
+ *
799
+ * Every remote object it creates is released before the session closes; a
800
+ * failure anywhere still disconnects. The session is in-process — nothing
801
+ * listens on a port, nothing outside this process can reach it.
802
+ */
803
+ function createInspectorArrayBufferCensus() {
804
+ return async () => {
805
+ const session = new Session();
806
+ session.connect();
807
+ try {
808
+ const prototypeObjectId = readObjectId(await session.post("Runtime.evaluate", { expression: "ArrayBuffer.prototype" }), ["result", "objectId"]);
809
+ try {
810
+ const objectId = readObjectId(await session.post("Runtime.queryObjects", { prototypeObjectId }), ["objects", "objectId"]);
811
+ try {
812
+ const evaluated = await session.post("Runtime.callFunctionOn", {
813
+ objectId,
814
+ functionDeclaration: HISTOGRAM_FN,
815
+ returnByValue: true
816
+ });
817
+ const value = typeof evaluated === "object" && evaluated !== null ? Reflect.get(Reflect.get(evaluated, "result") ?? {}, "value") : void 0;
818
+ if (!isArrayBufferCensusReport(value)) throw new Error("inspector returned a histogram of an unexpected shape");
819
+ return value;
820
+ } finally {
821
+ await session.post("Runtime.releaseObject", { objectId });
822
+ }
823
+ } finally {
824
+ await session.post("Runtime.releaseObject", { objectId: prototypeObjectId });
825
+ }
826
+ } finally {
827
+ session.disconnect();
828
+ }
829
+ };
830
+ }
831
+ var MB$1 = 1048576;
832
+ function mb(bytes) {
833
+ return Math.round(bytes / MB$1);
834
+ }
835
+ function formatBucket(bucket) {
836
+ return `<=${bucket.upToBytes >= MB$1 ? `${Math.round(bucket.upToBytes / MB$1)}MB` : bucket.upToBytes >= 1024 ? `${Math.round(bucket.upToBytes / 1024)}KB` : `${bucket.upToBytes}B`}:${bucket.count}x/${mb(bucket.bytes)}MB`;
837
+ }
838
+ function formatExact(size) {
839
+ return `${size.byteLength}Bx${size.count}=${mb(size.bytes)}MB`;
840
+ }
841
+ /**
842
+ * The census line. `live` is what survived the census's own GC; `raw` is the
843
+ * reading that triggered it — when they disagree by a lot, the growth was dead
844
+ * buffers awaiting a sweep, not a retention, and the line says which.
845
+ */
846
+ function formatArrayBufferCensus(label, report, context) {
847
+ const live = mb(report.bytes);
848
+ const verdict = live < context.rawMb * .5 ? "mostly DEAD buffers awaiting a sweep, not a retention" : "LIVE — something holds them";
849
+ return `[mem] ${label} arrayBuffer census: live=${live}MB in ${report.count} ArrayBuffers (raw arrayBuffers=${context.rawMb}MB at the trigger → ${verdict}) top sizes: ${report.topSizes.map(formatExact).join(", ") || "(none)"}; buckets: ${report.buckets.map(formatBucket).join(", ") || "(none)"}; detached=${report.detached} took=${context.tookMs}ms — cost: ONE forced full GC (the same pause as a reclaim pass), walked in memory, nothing serialised, nothing written; trigger: ${context.reason}`;
850
+ }
851
+ //#endregion
606
852
  //#region src/kernel/heap-watch.ts
607
853
  /**
608
854
  * heap-watch — hub-main's own memory, on the record, and returned to the OS.
@@ -1288,8 +1534,35 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1288
1534
  if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
1289
1535
  checkRssBudget(sample, at);
1290
1536
  maybeReclaim(sample);
1537
+ judgeArrayBuffers(sample, at);
1291
1538
  } catch {}
1292
1539
  };
1540
+ const censusProbe = probes?.arrayBufferCensus;
1541
+ const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
1542
+ let censusInFlight = false;
1543
+ const judgeArrayBuffers = (sample, at) => {
1544
+ if (censusProbe === void 0 || censusGate === void 0) return;
1545
+ const verdict = censusGate.observe(sample.arrayBuffersMb, at);
1546
+ if (verdict.kind === "released") {
1547
+ sink.warn(`[mem] ${label} arrayBuffers RELEASED ${verdict.fromMb}MB→${verdict.toMb}MB between two probes ${probeIntervalMs}ms apart — no owner logged this release; whatever held them let go in one instant (a socket that died, a queue that drained, a map that was cleared). Correlate with every line of this process in that window.`);
1548
+ return;
1549
+ }
1550
+ if (verdict.kind !== "take" || censusInFlight) return;
1551
+ censusInFlight = true;
1552
+ const startedAt = now();
1553
+ const rawMb = sample.arrayBuffersMb;
1554
+ censusProbe.census().then((report) => {
1555
+ sink.warn(formatArrayBufferCensus(label, report, {
1556
+ rawMb,
1557
+ tookMs: now() - startedAt,
1558
+ reason: verdict.reason
1559
+ }));
1560
+ }).catch((error) => {
1561
+ sink.warn(`[mem] ${label} arrayBuffer census FAILED after ${now() - startedAt}ms (trigger: ${verdict.reason}) — ${error instanceof Error ? error.message : String(error)}`);
1562
+ }).finally(() => {
1563
+ censusInFlight = false;
1564
+ });
1565
+ };
1293
1566
  const timer = setInterval(tick, probeIntervalMs);
1294
1567
  timer.unref?.();
1295
1568
  tick();
@@ -6868,7 +7141,8 @@ var LocalChildRegistry = class {
6868
7141
  this.registeredHandler({
6869
7142
  childId: msg.childId,
6870
7143
  caps: msg.caps,
6871
- incarnation
7144
+ incarnation,
7145
+ ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
6872
7146
  });
6873
7147
  return { ok: true };
6874
7148
  }
@@ -6970,6 +7244,15 @@ var LocalChildClient = class {
6970
7244
  * subscriptions do), the register carries the real union — even if empty.
6971
7245
  */
6972
7246
  hasDeclaredEventPatterns = false;
7247
+ /**
7248
+ * The custom-action catalogs of every hosted addon, once the runner's init
7249
+ * loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
7250
+ * until then, and OMITTED from the register frame while null: the parent
7251
+ * reads absence as "not described yet", and a pre-init register must never
7252
+ * look like "this child declares no actions". Kept on the client so every
7253
+ * later re-register (native-cap change, reconnect) re-carries it unchanged.
7254
+ */
7255
+ latestCustomActions = null;
6973
7256
  /** Events and logs queued while the channel is not yet open. */
6974
7257
  pendingEmits = [];
6975
7258
  /** Handler for parent→child events. Registered via `onEvent`. */
@@ -7109,12 +7392,7 @@ var LocalChildClient = class {
7109
7392
  }
7110
7393
  if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
7111
7394
  });
7112
- const register = {
7113
- kind: "register",
7114
- childId: this.options.childId,
7115
- caps: this.latestCaps,
7116
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
7117
- };
7395
+ const register = this.registerFrame(this.latestCaps);
7118
7396
  try {
7119
7397
  await channel.request(register);
7120
7398
  } catch (err) {
@@ -7140,16 +7418,26 @@ var LocalChildClient = class {
7140
7418
  * it (device-restore can race ahead of the UDS connect). After `start()`,
7141
7419
  * the set is sent immediately.
7142
7420
  */
7143
- async updateCaps(caps) {
7421
+ async updateCaps(caps, customActions) {
7144
7422
  this.latestCaps = caps;
7423
+ if (customActions !== void 0) this.latestCustomActions = customActions;
7145
7424
  if (this.channel === null) return;
7146
- const register = {
7425
+ await this.channel.request(this.registerFrame(caps));
7426
+ }
7427
+ /**
7428
+ * The register frame: caps, plus everything a re-register must atomically
7429
+ * re-carry so no separate re-sync can be missed — the subscription union
7430
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7431
+ * custom-action catalogs (omitted until the init loop produced them).
7432
+ */
7433
+ registerFrame(caps) {
7434
+ return {
7147
7435
  kind: "register",
7148
7436
  childId: this.options.childId,
7149
7437
  caps,
7150
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
7438
+ ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7439
+ ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
7151
7440
  };
7152
- await this.channel.request(register);
7153
7441
  }
7154
7442
  /**
7155
7443
  * Fire-and-forget: send a system event to the parent for forwarding to the
@@ -9886,4 +10174,4 @@ async function defaultRun(command, args) {
9886
10174
  await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
9887
10175
  }
9888
10176
  //#endregion
9889
- export { buildUdsNativeCapProxy as $, formatHeapSpaces as $t, createUdsLoggerWithControl as A, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, createV8Reclaimer as Bt, ipcParentLink as C, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, HEAP_RECLAIM_TRIGGER_MB as Et, AGENT_CAP_FWD_SERVICE as F, RUNNER_RSS_BUDGET_ENV as Ft, createLocalTransport as G, reclaimIntervalMs as Gt, PARENT_ROUTED_PROVIDER_ADDON_ID as H, hubMainRssBudget as Ht, CapRouteResolver as I, ZERO_LOOP_DELAY as It, SocketChannel as J, startRunnerHeapWatch as Jt, UdsLocalTransportClient as K, shouldReclaim as Kt, CapRouteError as L, buildHeapSample as Lt, LocalChildRegistry as M, RSS_BUDGET_REANNOUNCE_MIN_MS as Mt, UDS_NO_ROUTE_PREFIX as N, RSS_BUDGET_RELEASE_RATIO as Nt, udsChildLogToWorkerEntry as O, HEAP_WATCH_WARN_RATIO as Ot, AGENT_CAP_FWD_ACTION as P, RUNNER_HEAP_WATCH_INTERVAL_MS as Pt, buildNativeCapProxy as Q, emitHeapDiagnosticReport as Qt, classifyCapRoute as R, createDeferredHeapWatchSink as Rt, buildLinkChain as S, runNpm as St, createParentUnownedCallHandler as T, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Tt, __resetCapUsageRegistryForTests as U, nextRssBudgetState as Ut, CapUsageRegistry as V, describeRss as Vt, getCapUsageRegistry as W, parseRssBudgetMb as Wt, FrameDecoder as X, DECISIVE_HEAP_SPACES as Xt, localEndpointPath as Y, strandedMb as Yt, encodeFrame as Z, HEAP_SPACE_REPORT_MIN_MB as Zt, AddonDepsManager as _, createEventPlaneReader as _n, CapabilityUnavailableError as _t, createUdsAddonContext as a, createSocketPlaneMeter as an, createAddonService as at, brokerCallForCap as b, resolveAddonClass as bt, getOrInitReadinessRegistryForClient as c, formatSocketPlane as cn, capActionName as ct, clusterEventTopic as d, recordSocketFrame as dn, capServiceName as dt, heapSpaceField as en, createBrokerDeviceManagerApi as et, getBrokerEventBus as f, sampleSocketDirection as fn, parseCapAction as ft, subscribePassthrough as g, createEventPlaneMeter as gn, CapabilityHandle as gt, setNodeEventInterest as h, EVENT_PLANE_TOP_N as hn, DeviceRegistry as ht, createAddonContext as i, SOCKET_PLANE_TOP_N as in, setWorkerNativeCapsChangeListener as it, LocalChildClient as j, HUB_RSS_BUDGET_ENV as jt, createUdsLogger as k, HUB_MAIN_HEAP_WATCH_LABEL as kt, setHubConnected as l, EMPTY_SOCKET_DIRECTION as ln, capActionSuffix as lt, registerEventBusService as m, socketDirectionMessages as mn, serializeTypedArrays as mt, installManifestPythonDeps as n, selectReportedSpaces as nn, getWorkerNativeCapSnapshot as nt, getWorkerDeviceRegistry as o, createSocketPlaneReader as on, validateProviderRegistrations as ot, getMoleculerEventStats as p, socketDirectionBytes as pn, deserializeTypedArrays as pt, UdsLocalTransportServer as q, startHeapWatch as qt, adaptBrokerToCluster as r, EMPTY_SOCKET_PLANE_BASELINE as rn, mountNativeCapService as rt, getOrInitReadinessRegistry as s, diffSocketPlane as sn, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestSystemDeps as t, readHeapSpaces as tn, getWorkerNativeCapProvider as tt, EVENT_TOPIC_PREFIX as u, createSocketDirectionCounters as un, capBareAction as ut, createKernelHwAccel as v, diffEventPlane as vn, copyBundledNativeModules as vt, localProviderLink as w, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, brokerTransportLink as x, resolveNpmInvocation as xt, resolveHwAccel as y, formatEventPlane as yn, installManifestNativeDeps as yt, callWithServiceDiscovery as z, createLoopDelayMeter as zt };
10177
+ export { buildUdsNativeCapProxy as $, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as $t, createUdsLoggerWithControl as A, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, createV8Reclaimer as Bt, ipcParentLink as C, EVENT_PLANE_TOP_N as Cn, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, formatEventPlane as Dn, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, diffEventPlane as En, HEAP_RECLAIM_TRIGGER_MB as Et, AGENT_CAP_FWD_SERVICE as F, RUNNER_RSS_BUDGET_ENV as Ft, createLocalTransport as G, reclaimIntervalMs as Gt, PARENT_ROUTED_PROVIDER_ADDON_ID as H, hubMainRssBudget as Ht, CapRouteResolver as I, ZERO_LOOP_DELAY as It, SocketChannel as J, startRunnerHeapWatch as Jt, UdsLocalTransportClient as K, shouldReclaim as Kt, CapRouteError as L, buildHeapSample as Lt, LocalChildRegistry as M, RSS_BUDGET_REANNOUNCE_MIN_MS as Mt, UDS_NO_ROUTE_PREFIX as N, RSS_BUDGET_RELEASE_RATIO as Nt, udsChildLogToWorkerEntry as O, HEAP_WATCH_WARN_RATIO as Ot, AGENT_CAP_FWD_ACTION as P, RUNNER_HEAP_WATCH_INTERVAL_MS as Pt, buildNativeCapProxy as Q, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as Qt, classifyCapRoute as R, createDeferredHeapWatchSink as Rt, buildLinkChain as S, socketDirectionMessages as Sn, runNpm as St, createParentUnownedCallHandler as T, createEventPlaneReader as Tn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Tt, __resetCapUsageRegistryForTests as U, nextRssBudgetState as Ut, CapUsageRegistry as V, describeRss as Vt, getCapUsageRegistry as W, parseRssBudgetMb as Wt, FrameDecoder as X, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as Xt, localEndpointPath as Y, strandedMb as Yt, encodeFrame as Z, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as Zt, AddonDepsManager as _, EMPTY_SOCKET_DIRECTION as _n, CapabilityUnavailableError as _t, createUdsAddonContext as a, HEAP_SPACE_REPORT_MIN_MB as an, createAddonService as at, brokerCallForCap as b, sampleSocketDirection as bn, resolveAddonClass as bt, getOrInitReadinessRegistryForClient as c, heapSpaceField as cn, capActionName as ct, clusterEventTopic as d, EMPTY_SOCKET_PLANE_BASELINE as dn, capServiceName as dt, createArrayBufferCensusGate as en, createBrokerDeviceManagerApi as et, getBrokerEventBus as f, SOCKET_PLANE_TOP_N as fn, parseCapAction as ft, subscribePassthrough as g, formatSocketPlane as gn, CapabilityHandle as gt, setNodeEventInterest as h, diffSocketPlane as hn, DeviceRegistry as ht, createAddonContext as i, DECISIVE_HEAP_SPACES as in, setWorkerNativeCapsChangeListener as it, LocalChildClient as j, HUB_RSS_BUDGET_ENV as jt, createUdsLogger as k, HUB_MAIN_HEAP_WATCH_LABEL as kt, setHubConnected as l, readHeapSpaces as ln, capActionSuffix as lt, registerEventBusService as m, createSocketPlaneReader as mn, serializeTypedArrays as mt, installManifestPythonDeps as n, formatArrayBufferCensus as nn, getWorkerNativeCapSnapshot as nt, getWorkerDeviceRegistry as o, emitHeapDiagnosticReport as on, validateProviderRegistrations as ot, getMoleculerEventStats as p, createSocketPlaneMeter as pn, deserializeTypedArrays as pt, UdsLocalTransportServer as q, startHeapWatch as qt, adaptBrokerToCluster as r, isArrayBufferCensusReport as rn, mountNativeCapService as rt, getOrInitReadinessRegistry as s, formatHeapSpaces as sn, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestSystemDeps as t, createInspectorArrayBufferCensus as tn, getWorkerNativeCapProvider as tt, EVENT_TOPIC_PREFIX as u, selectReportedSpaces as un, capBareAction as ut, createKernelHwAccel as v, createSocketDirectionCounters as vn, copyBundledNativeModules as vt, localProviderLink as w, createEventPlaneMeter as wn, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, brokerTransportLink as x, socketDirectionBytes as xn, resolveNpmInvocation as xt, resolveHwAccel as y, recordSocketFrame as yn, installManifestNativeDeps as yt, callWithServiceDiscovery as z, createLoopDelayMeter as zt };
@@ -1,4 +1,4 @@
1
- import { in as asJsonObject } from "./dist-MtqqwJVI.mjs";
1
+ import { an as asJsonObject } from "./dist-DigmGsQS.mjs";
2
2
  //#region src/builtins/sqlite-storage/retired-settings-keys.ts
3
3
  /**
4
4
  * Is THIS node the one whose settings store is the cluster's authority?
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DYWqXngV.js");
1
+ const require_dist = require("./dist-BOlMCOeT.js");
2
2
  //#region src/builtins/sqlite-storage/retired-settings-keys.ts
3
3
  /**
4
4
  * Is THIS node the one whose settings store is the cluster's authority?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.216",
3
+ "version": "1.2.218",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",