@camstack/system 1.2.217 → 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-C85OOASj.js → dist-BOlMCOeT.js} +116 -1
  53. package/dist/{dist-DtDKMSeG.mjs → dist-DigmGsQS.mjs} +111 -2
  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-Cx-lYIbU.mjs → retired-settings-keys-vK_U-CG2.mjs} +1 -1
  65. package/dist/{retired-settings-keys-Dfj9LA-i.js → retired-settings-keys-xEAlYnv7.js} +1 -1
  66. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { CustomActionCaller, IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
1
+ import { ChildCustomActionCatalogs, CustomActionCaller, IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
2
2
  /**
3
3
  * Routing descriptor a child sends so the parent can route cap calls to it.
4
4
  * The provider implementation never crosses the wire — only these keys do.
@@ -35,6 +35,18 @@ export interface RegisterMessage {
35
35
  * `EventSubUpdateMessage` for post-boot updates.
36
36
  */
37
37
  readonly eventPatterns?: readonly string[];
38
+ /**
39
+ * Custom-action catalogs of the addons this child hosts, keyed by addon id,
40
+ * with the Zod schemas stripped — see `CustomActionDescriptor`. Carried IN
41
+ * the register frame for the same reason as `eventPatterns`: the parent
42
+ * learns it race-free, at the post-init re-register, FROM THE PROCESS THAT
43
+ * OWNS THE ADDON. It is never learned by evaluating the addon's bundle in the
44
+ * parent (D444: ten 9 MB evaluations in hub-main, none of which produced a
45
+ * catalog, and every one pinned the evicted graph's module-scope state).
46
+ * OPTIONAL: absent from the pre-init register (the catalogs do not exist yet)
47
+ * and from a legacy child; absent means "not described yet", never "none".
48
+ */
49
+ readonly customActions?: ChildCustomActionCatalogs;
38
50
  }
39
51
  /**
40
52
  * Child → parent, fire-and-forget: the child's category-pattern subscription
@@ -248,6 +260,8 @@ export interface RegisteredChild {
248
260
  * one. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
249
261
  */
250
262
  readonly incarnation: number;
263
+ /** See {@link RegisterMessage.customActions}. Absent = not described yet. */
264
+ readonly customActions?: ChildCustomActionCatalogs;
251
265
  }
252
266
  /** Arguments to route a cap method call (the `cap-call` message minus its discriminant). */
253
267
  export type CapCallInput = Omit<CapCallMessage, 'kind'>;
@@ -1,4 +1,4 @@
1
- import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
1
+ import { ChildCustomActionCatalogs, IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
2
2
  import { AddonCallInput, CapCallInput, ChildCapDescriptor, ChildLogMessage } from './child-cap-protocol.js';
3
3
  export interface LocalChildClientOptions {
4
4
  /** Parent node id — selects the UDS endpoint this child connects to. */
@@ -56,6 +56,15 @@ export declare class LocalChildClient {
56
56
  * subscriptions do), the register carries the real union — even if empty.
57
57
  */
58
58
  private hasDeclaredEventPatterns;
59
+ /**
60
+ * The custom-action catalogs of every hosted addon, once the runner's init
61
+ * loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
62
+ * until then, and OMITTED from the register frame while null: the parent
63
+ * reads absence as "not described yet", and a pre-init register must never
64
+ * look like "this child declares no actions". Kept on the client so every
65
+ * later re-register (native-cap change, reconnect) re-carries it unchanged.
66
+ */
67
+ private latestCustomActions;
59
68
  /** Events and logs queued while the channel is not yet open. */
60
69
  private readonly pendingEmits;
61
70
  /** Handler for parent→child events. Registered via `onEvent`. */
@@ -138,7 +147,14 @@ export declare class LocalChildClient {
138
147
  * it (device-restore can race ahead of the UDS connect). After `start()`,
139
148
  * the set is sent immediately.
140
149
  */
141
- updateCaps(caps: readonly ChildCapDescriptor[]): Promise<void>;
150
+ updateCaps(caps: readonly ChildCapDescriptor[], customActions?: ChildCustomActionCatalogs): Promise<void>;
151
+ /**
152
+ * The register frame: caps, plus everything a re-register must atomically
153
+ * re-carry so no separate re-sync can be missed — the subscription union
154
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
155
+ * custom-action catalogs (omitted until the init loop produced them).
156
+ */
157
+ private registerFrame;
142
158
  /**
143
159
  * Fire-and-forget: send a system event to the parent for forwarding to the
144
160
  * hub event bus. Safe to call before `start()` — events are buffered and
@@ -15,6 +15,7 @@ node_os = require_chunk.__toESM(node_os);
15
15
  let node_http = require("node:http");
16
16
  let node_v8 = require("node:v8");
17
17
  node_v8 = require_chunk.__toESM(node_v8);
18
+ let node_inspector_promises = require("node:inspector/promises");
18
19
  let node_perf_hooks = require("node:perf_hooks");
19
20
  let node_vm = require("node:vm");
20
21
  node_vm = require_chunk.__toESM(node_vm);
@@ -514,7 +515,7 @@ var DECISIVE_HEAP_SPACES = ["old_space", "large_object_space"];
514
515
  var HEAP_SPACE_REPORT_MIN_MB = 32;
515
516
  var BYTES_PER_MB = 1048576;
516
517
  /** Whole MB, matching the rest of the `[mem]` line. */
517
- function mb(bytes) {
518
+ function mb$1(bytes) {
518
519
  return Math.round(bytes / BYTES_PER_MB);
519
520
  }
520
521
  /** Tenths of an MB — the resolution the SIGUSR2 report has always printed. */
@@ -561,7 +562,7 @@ function heapSpaceField(v8Name) {
561
562
  */
562
563
  function selectReportedSpaces(spaces, minMb = 32) {
563
564
  const decisive = DECISIVE_HEAP_SPACES.map((name) => spaces.find((space) => space.name === name)).filter((space) => space !== void 0);
564
- 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));
565
+ 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));
565
566
  return [...decisive, ...rest];
566
567
  }
567
568
  /**
@@ -576,7 +577,7 @@ function selectReportedSpaces(spaces, minMb = 32) {
576
577
  function formatHeapSpaces(spaces, minMb = 32) {
577
578
  const selected = selectReportedSpaces(spaces, minMb);
578
579
  if (selected.length === 0) return "";
579
- return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb(s.usedBytes)}MB`).join(" ")}`;
580
+ return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb$1(s.usedBytes)}MB`).join(" ")}`;
580
581
  }
581
582
  var defaultDeps = {
582
583
  readMemory: () => process.memoryUsage(),
@@ -606,6 +607,251 @@ function emitHeapDiagnosticReport(request, deps = defaultDeps) {
606
607
  }
607
608
  }
608
609
  //#endregion
610
+ //#region src/kernel/array-buffer-census.ts
611
+ /**
612
+ * array-buffer-census — WHAT SHAPE the live ArrayBuffers have, taken by the
613
+ * heartbeat itself when they grow, because nobody can be awake for it.
614
+ *
615
+ * ## The episodes this exists for
616
+ *
617
+ * hub-main, 2026-09-10 (CEST): `arrayBuffers` climbed linearly and released in
618
+ * one instant, twice, with not one line from the process at either boundary —
619
+ * 17:26→18:31 at ~20 MB/min to 1 264 MB, released in one heartbeat; 22:20→23:36
620
+ * at 60–85 MB/min to **4 968 MB**, released between two probes at 23:38. Both
621
+ * were LIVE: a forced compacting GC at 23:36:40 returned 4 968→4 954 MB. The
622
+ * measurement that names their shape had been prepared (D443) and was never
623
+ * run, because it needed a human to run it during the episode, and the
624
+ * episode does not wait. Two hours after the second release the process died
625
+ * on `Ineffective mark-compacts near heap limit`.
626
+ *
627
+ * ## What this is, and what it is NOT
628
+ *
629
+ * `Runtime.queryObjects(ArrayBuffer.prototype)` over an IN-PROCESS inspector
630
+ * session, followed by `Runtime.callFunctionOn` computing a byteLength
631
+ * histogram over the result array, returned BY VALUE. It walks the heap in
632
+ * memory and returns a few hundred bytes of JSON. Nothing is serialised,
633
+ * nothing is written, no file exists afterwards.
634
+ *
635
+ * It is NOT a heap snapshot. `scripts/check-no-heap-dump-command.ts` forbids
636
+ * `writeHeapSnapshot` because a multi-GB serialisation of the heap took the
637
+ * whole host down twice; this module contains no such call and must never
638
+ * grow one. The two look alike only in that both need a GC first.
639
+ *
640
+ * ## What it costs, and why the line says so
641
+ *
642
+ * `queryObjects` forces ONE full garbage collection before it walks — the same
643
+ * stop-the-world the reclaim pass already pays every 10 minutes on this
644
+ * process (~1 s at a 2.5 GB heap). The cost is printed on the line it
645
+ * produces, because a pause with no owner in the log is exactly the kind of
646
+ * stall D181 warns about on the process that mediates every cross-runner call.
647
+ * The gate ({@link createArrayBufferCensusGate}) bounds it: a threshold, held
648
+ * for a sustain window so a burst that dies in the next scavenge never pays,
649
+ * and a generous cooldown so an hour-long episode buys a handful of readings,
650
+ * not one per probe.
651
+ *
652
+ * ## Why the byteLength histogram, and not "who holds them"
653
+ *
654
+ * `queryObjects` returns the objects, not their retainers; a retainer walk is
655
+ * a heap snapshot by another name. But the SIZE distribution already
656
+ * discriminates the candidates that were argued about for a night: 65 536-byte
657
+ * buffers are socket chunks, 8 192-byte ones are Buffer-pool slabs, a few
658
+ * hundred equal multi-megabyte ones are frames or JPEGs, and the count says
659
+ * whether it is thousands of small or dozens of huge. The exact top sizes are
660
+ * printed as well as power-of-two buckets, because "1 462 272 B × 880" names
661
+ * a producer where "≤2 MB: 1.2 GB" does not.
662
+ */
663
+ /**
664
+ * Take the census once `arrayBuffers` has been at or above this for
665
+ * {@link ARRAY_BUFFER_CENSUS_SUSTAIN_MS}.
666
+ *
667
+ * 512 MB. The two 2026-09-10 episodes crossed it 25 and 8 minutes in; the
668
+ * routine reading in six hours of 2-second samples around them was 18–100 MB
669
+ * with single-sample spikes to 479 MB, and the process's floor after every
670
+ * compaction is 12–18 MB. A dead-buffer sawtooth between major GCs (measured
671
+ * up to 3.65 GB on 2026-08-08 when the heap had 4 GB of headroom) cannot reach
672
+ * it while GC runs every few seconds — and if one did, the census would SAY
673
+ * so: it reports the live total after its own GC next to the raw reading.
674
+ */
675
+ var ARRAY_BUFFER_CENSUS_THRESHOLD_MB = 512;
676
+ /**
677
+ * How long the reading must stay above the threshold before a census is taken.
678
+ *
679
+ * 60 s = 30 probes at the 2 s cadence. A burst that a scavenge or the next
680
+ * major GC returns never pays a forced GC; an episode climbing at the slowest
681
+ * rate measured (20 MB/min) is still 20 MB higher by the time it is measured,
682
+ * which is nothing against the threshold.
683
+ */
684
+ var ARRAY_BUFFER_CENSUS_SUSTAIN_MS = 6e4;
685
+ /**
686
+ * Minimum spacing between two censuses.
687
+ *
688
+ * 30 minutes: the second episode ran 76 minutes, so it would have produced
689
+ * three readings — enough to see the shape change (or not) as it grew — at a
690
+ * cost the router already pays six times an hour for the reclaim pass.
691
+ */
692
+ var ARRAY_BUFFER_CENSUS_COOLDOWN_MS = 30 * 6e4;
693
+ /**
694
+ * A drop of at least this share of the threshold between two consecutive
695
+ * probes, from a reading that had been SUSTAINED above the threshold, is a
696
+ * RELEASE — the boundary both episodes crossed with no line, and the one line
697
+ * that lets the release be correlated with whatever else the process logged
698
+ * that second. Sustained, not merely above: a one-probe spike that the next
699
+ * scavenge sweeps (a 1.3 GB burst in 11 s was measured on 2026-08-29) is not
700
+ * an episode ending, and a WARN for every one of those would bury the line
701
+ * this exists to produce.
702
+ */
703
+ var ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO = .5;
704
+ /**
705
+ * The pure decision: threshold, sustain, cooldown, release — testable without
706
+ * allocating a gigabyte or forcing a GC.
707
+ */
708
+ function createArrayBufferCensusGate(options = {}) {
709
+ const thresholdMb = options.thresholdMb ?? 512;
710
+ const sustainMs = options.sustainMs ?? 6e4;
711
+ const cooldownMs = options.cooldownMs ?? 18e5;
712
+ let aboveSince = null;
713
+ let lastTakenAt = Number.NEGATIVE_INFINITY;
714
+ let previousMb = null;
715
+ let previousAt = null;
716
+ return { observe(arrayBuffersMb, at) {
717
+ const previous = previousMb;
718
+ const previousHeldForMs = previous !== null && previousAt !== null && aboveSince !== null ? previousAt - aboveSince : null;
719
+ previousMb = arrayBuffersMb;
720
+ previousAt = at;
721
+ if (previous !== null && previousHeldForMs !== null && previousHeldForMs >= sustainMs && previous - arrayBuffersMb >= thresholdMb * .5) {
722
+ aboveSince = null;
723
+ return {
724
+ kind: "released",
725
+ fromMb: previous,
726
+ toMb: arrayBuffersMb
727
+ };
728
+ }
729
+ if (arrayBuffersMb < thresholdMb) {
730
+ aboveSince = null;
731
+ return { kind: "hold" };
732
+ }
733
+ aboveSince ??= at;
734
+ const heldForMs = at - aboveSince;
735
+ if (heldForMs < sustainMs) return { kind: "hold" };
736
+ if (at - lastTakenAt < cooldownMs) return { kind: "hold" };
737
+ lastTakenAt = at;
738
+ return {
739
+ kind: "take",
740
+ reason: `arrayBuffers=${arrayBuffersMb}MB has been >=${thresholdMb}MB for ${heldForMs}ms`
741
+ };
742
+ } };
743
+ }
744
+ /**
745
+ * The function evaluated INSIDE the inspector against the `queryObjects`
746
+ * result array (`this`). Kept as source text because that is what the
747
+ * protocol takes; kept small because it runs with the world stopped.
748
+ */
749
+ var HISTOGRAM_FN = `function () {
750
+ var byBucket = new Map(), bySize = new Map()
751
+ var count = 0, bytes = 0, detached = 0
752
+ for (var i = 0; i < this.length; i++) {
753
+ var b = this[i].byteLength
754
+ count++
755
+ bytes += b
756
+ if (b === 0) { detached++; continue }
757
+ var bucket = 1
758
+ while (bucket < b) bucket *= 2
759
+ var e = byBucket.get(bucket); if (!e) { e = [0, 0]; byBucket.set(bucket, e) }
760
+ e[0]++; e[1] += b
761
+ var s = bySize.get(b); if (!s) { s = [0, 0]; bySize.set(b, s) }
762
+ s[0]++; s[1] += b
763
+ }
764
+ var buckets = []
765
+ byBucket.forEach(function (v, k) { buckets.push({ upToBytes: k, count: v[0], bytes: v[1] }) })
766
+ buckets.sort(function (a, b) { return b.bytes - a.bytes })
767
+ var topSizes = []
768
+ bySize.forEach(function (v, k) { topSizes.push({ byteLength: k, count: v[0], bytes: v[1] }) })
769
+ topSizes.sort(function (a, b) { return b.bytes - a.bytes })
770
+ return { count: count, bytes: bytes, detached: detached,
771
+ buckets: buckets.slice(0, 6), topSizes: topSizes.slice(0, 5) }
772
+ }`;
773
+ function isFiniteNumber(value) {
774
+ return typeof value === "number" && Number.isFinite(value);
775
+ }
776
+ function isBucket(value) {
777
+ return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "upToBytes")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
778
+ }
779
+ function isExactSize(value) {
780
+ return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "byteLength")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
781
+ }
782
+ /** The inspector answers `unknown`; the report is trusted only once it has this shape. */
783
+ function isArrayBufferCensusReport(value) {
784
+ if (typeof value !== "object" || value === null) return false;
785
+ const buckets = Reflect.get(value, "buckets");
786
+ const topSizes = Reflect.get(value, "topSizes");
787
+ 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);
788
+ }
789
+ function readObjectId(result, path) {
790
+ let cursor = result;
791
+ for (const key of path) {
792
+ if (typeof cursor !== "object" || cursor === null) throw new Error(`inspector reply has no ${path.join(".")}`);
793
+ cursor = Reflect.get(cursor, key);
794
+ }
795
+ if (typeof cursor !== "string") throw new Error(`inspector reply has no ${path.join(".")}`);
796
+ return cursor;
797
+ }
798
+ /**
799
+ * The real census: an in-process inspector session, opened for the call and
800
+ * closed after it, so no session is left listening between censuses.
801
+ *
802
+ * Every remote object it creates is released before the session closes; a
803
+ * failure anywhere still disconnects. The session is in-process — nothing
804
+ * listens on a port, nothing outside this process can reach it.
805
+ */
806
+ function createInspectorArrayBufferCensus() {
807
+ return async () => {
808
+ const session = new node_inspector_promises.Session();
809
+ session.connect();
810
+ try {
811
+ const prototypeObjectId = readObjectId(await session.post("Runtime.evaluate", { expression: "ArrayBuffer.prototype" }), ["result", "objectId"]);
812
+ try {
813
+ const objectId = readObjectId(await session.post("Runtime.queryObjects", { prototypeObjectId }), ["objects", "objectId"]);
814
+ try {
815
+ const evaluated = await session.post("Runtime.callFunctionOn", {
816
+ objectId,
817
+ functionDeclaration: HISTOGRAM_FN,
818
+ returnByValue: true
819
+ });
820
+ const value = typeof evaluated === "object" && evaluated !== null ? Reflect.get(Reflect.get(evaluated, "result") ?? {}, "value") : void 0;
821
+ if (!isArrayBufferCensusReport(value)) throw new Error("inspector returned a histogram of an unexpected shape");
822
+ return value;
823
+ } finally {
824
+ await session.post("Runtime.releaseObject", { objectId });
825
+ }
826
+ } finally {
827
+ await session.post("Runtime.releaseObject", { objectId: prototypeObjectId });
828
+ }
829
+ } finally {
830
+ session.disconnect();
831
+ }
832
+ };
833
+ }
834
+ var MB$1 = 1048576;
835
+ function mb(bytes) {
836
+ return Math.round(bytes / MB$1);
837
+ }
838
+ function formatBucket(bucket) {
839
+ 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`;
840
+ }
841
+ function formatExact(size) {
842
+ return `${size.byteLength}Bx${size.count}=${mb(size.bytes)}MB`;
843
+ }
844
+ /**
845
+ * The census line. `live` is what survived the census's own GC; `raw` is the
846
+ * reading that triggered it — when they disagree by a lot, the growth was dead
847
+ * buffers awaiting a sweep, not a retention, and the line says which.
848
+ */
849
+ function formatArrayBufferCensus(label, report, context) {
850
+ const live = mb(report.bytes);
851
+ const verdict = live < context.rawMb * .5 ? "mostly DEAD buffers awaiting a sweep, not a retention" : "LIVE — something holds them";
852
+ 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}`;
853
+ }
854
+ //#endregion
609
855
  //#region src/kernel/heap-watch.ts
610
856
  /**
611
857
  * heap-watch — hub-main's own memory, on the record, and returned to the OS.
@@ -1291,8 +1537,35 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
1291
1537
  if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
1292
1538
  checkRssBudget(sample, at);
1293
1539
  maybeReclaim(sample);
1540
+ judgeArrayBuffers(sample, at);
1294
1541
  } catch {}
1295
1542
  };
1543
+ const censusProbe = probes?.arrayBufferCensus;
1544
+ const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
1545
+ let censusInFlight = false;
1546
+ const judgeArrayBuffers = (sample, at) => {
1547
+ if (censusProbe === void 0 || censusGate === void 0) return;
1548
+ const verdict = censusGate.observe(sample.arrayBuffersMb, at);
1549
+ if (verdict.kind === "released") {
1550
+ 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.`);
1551
+ return;
1552
+ }
1553
+ if (verdict.kind !== "take" || censusInFlight) return;
1554
+ censusInFlight = true;
1555
+ const startedAt = now();
1556
+ const rawMb = sample.arrayBuffersMb;
1557
+ censusProbe.census().then((report) => {
1558
+ sink.warn(formatArrayBufferCensus(label, report, {
1559
+ rawMb,
1560
+ tookMs: now() - startedAt,
1561
+ reason: verdict.reason
1562
+ }));
1563
+ }).catch((error) => {
1564
+ sink.warn(`[mem] ${label} arrayBuffer census FAILED after ${now() - startedAt}ms (trigger: ${verdict.reason}) — ${error instanceof Error ? error.message : String(error)}`);
1565
+ }).finally(() => {
1566
+ censusInFlight = false;
1567
+ });
1568
+ };
1296
1569
  const timer = setInterval(tick, probeIntervalMs);
1297
1570
  timer.unref?.();
1298
1571
  tick();
@@ -6871,7 +7144,8 @@ var LocalChildRegistry = class {
6871
7144
  this.registeredHandler({
6872
7145
  childId: msg.childId,
6873
7146
  caps: msg.caps,
6874
- incarnation
7147
+ incarnation,
7148
+ ...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
6875
7149
  });
6876
7150
  return { ok: true };
6877
7151
  }
@@ -6973,6 +7247,15 @@ var LocalChildClient = class {
6973
7247
  * subscriptions do), the register carries the real union — even if empty.
6974
7248
  */
6975
7249
  hasDeclaredEventPatterns = false;
7250
+ /**
7251
+ * The custom-action catalogs of every hosted addon, once the runner's init
7252
+ * loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
7253
+ * until then, and OMITTED from the register frame while null: the parent
7254
+ * reads absence as "not described yet", and a pre-init register must never
7255
+ * look like "this child declares no actions". Kept on the client so every
7256
+ * later re-register (native-cap change, reconnect) re-carries it unchanged.
7257
+ */
7258
+ latestCustomActions = null;
6976
7259
  /** Events and logs queued while the channel is not yet open. */
6977
7260
  pendingEmits = [];
6978
7261
  /** Handler for parent→child events. Registered via `onEvent`. */
@@ -7112,12 +7395,7 @@ var LocalChildClient = class {
7112
7395
  }
7113
7396
  if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
7114
7397
  });
7115
- const register = {
7116
- kind: "register",
7117
- childId: this.options.childId,
7118
- caps: this.latestCaps,
7119
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
7120
- };
7398
+ const register = this.registerFrame(this.latestCaps);
7121
7399
  try {
7122
7400
  await channel.request(register);
7123
7401
  } catch (err) {
@@ -7143,16 +7421,26 @@ var LocalChildClient = class {
7143
7421
  * it (device-restore can race ahead of the UDS connect). After `start()`,
7144
7422
  * the set is sent immediately.
7145
7423
  */
7146
- async updateCaps(caps) {
7424
+ async updateCaps(caps, customActions) {
7147
7425
  this.latestCaps = caps;
7426
+ if (customActions !== void 0) this.latestCustomActions = customActions;
7148
7427
  if (this.channel === null) return;
7149
- const register = {
7428
+ await this.channel.request(this.registerFrame(caps));
7429
+ }
7430
+ /**
7431
+ * The register frame: caps, plus everything a re-register must atomically
7432
+ * re-carry so no separate re-sync can be missed — the subscription union
7433
+ * (omitted if nothing ever declared, which keeps the fail-open shape) and the
7434
+ * custom-action catalogs (omitted until the init loop produced them).
7435
+ */
7436
+ registerFrame(caps) {
7437
+ return {
7150
7438
  kind: "register",
7151
7439
  childId: this.options.childId,
7152
7440
  caps,
7153
- ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
7441
+ ...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
7442
+ ...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
7154
7443
  };
7155
- await this.channel.request(register);
7156
7444
  }
7157
7445
  /**
7158
7446
  * Fire-and-forget: send a system event to the parent for forwarding to the
@@ -9901,6 +10189,30 @@ Object.defineProperty(exports, "AGENT_CAP_FWD_SERVICE", {
9901
10189
  return AGENT_CAP_FWD_SERVICE;
9902
10190
  }
9903
10191
  });
10192
+ Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_COOLDOWN_MS", {
10193
+ enumerable: true,
10194
+ get: function() {
10195
+ return ARRAY_BUFFER_CENSUS_COOLDOWN_MS;
10196
+ }
10197
+ });
10198
+ Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_SUSTAIN_MS", {
10199
+ enumerable: true,
10200
+ get: function() {
10201
+ return ARRAY_BUFFER_CENSUS_SUSTAIN_MS;
10202
+ }
10203
+ });
10204
+ Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_THRESHOLD_MB", {
10205
+ enumerable: true,
10206
+ get: function() {
10207
+ return ARRAY_BUFFER_CENSUS_THRESHOLD_MB;
10208
+ }
10209
+ });
10210
+ Object.defineProperty(exports, "ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO", {
10211
+ enumerable: true,
10212
+ get: function() {
10213
+ return ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO;
10214
+ }
10215
+ });
9904
10216
  Object.defineProperty(exports, "AddonDepsManager", {
9905
10217
  enumerable: true,
9906
10218
  get: function() {
@@ -10231,6 +10543,12 @@ Object.defineProperty(exports, "createAddonService", {
10231
10543
  return createAddonService;
10232
10544
  }
10233
10545
  });
10546
+ Object.defineProperty(exports, "createArrayBufferCensusGate", {
10547
+ enumerable: true,
10548
+ get: function() {
10549
+ return createArrayBufferCensusGate;
10550
+ }
10551
+ });
10234
10552
  Object.defineProperty(exports, "createBrokerDeviceManagerApi", {
10235
10553
  enumerable: true,
10236
10554
  get: function() {
@@ -10255,6 +10573,12 @@ Object.defineProperty(exports, "createEventPlaneReader", {
10255
10573
  return createEventPlaneReader;
10256
10574
  }
10257
10575
  });
10576
+ Object.defineProperty(exports, "createInspectorArrayBufferCensus", {
10577
+ enumerable: true,
10578
+ get: function() {
10579
+ return createInspectorArrayBufferCensus;
10580
+ }
10581
+ });
10258
10582
  Object.defineProperty(exports, "createKernelHwAccel", {
10259
10583
  enumerable: true,
10260
10584
  get: function() {
@@ -10369,6 +10693,12 @@ Object.defineProperty(exports, "encodeFrame", {
10369
10693
  return encodeFrame;
10370
10694
  }
10371
10695
  });
10696
+ Object.defineProperty(exports, "formatArrayBufferCensus", {
10697
+ enumerable: true,
10698
+ get: function() {
10699
+ return formatArrayBufferCensus;
10700
+ }
10701
+ });
10372
10702
  Object.defineProperty(exports, "formatEventPlane", {
10373
10703
  enumerable: true,
10374
10704
  get: function() {
@@ -10471,6 +10801,12 @@ Object.defineProperty(exports, "ipcParentLink", {
10471
10801
  return ipcParentLink;
10472
10802
  }
10473
10803
  });
10804
+ Object.defineProperty(exports, "isArrayBufferCensusReport", {
10805
+ enumerable: true,
10806
+ get: function() {
10807
+ return isArrayBufferCensusReport;
10808
+ }
10809
+ });
10474
10810
  Object.defineProperty(exports, "localEndpointPath", {
10475
10811
  enumerable: true,
10476
10812
  get: function() {