@camstack/system 1.2.150 → 1.2.153

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 +14 -15
  2. package/dist/addon-runner.mjs +14 -14
  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.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +3 -3
  26. package/dist/builtins/local-network/local-network.addon.mjs +3 -3
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/cgroup-memory.d.ts +193 -0
  30. package/dist/builtins/native-metrics/container-memory-series.d.ts +226 -0
  31. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +26 -0
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1536 -661
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1536 -660
  34. package/dist/builtins/platform-probe/index.js +3 -3
  35. package/dist/builtins/platform-probe/index.mjs +2 -2
  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 +2 -2
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +2 -2
  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/{dist-CbRciAbu.mjs → dist-BLhrrxIs.mjs} +63 -4
  51. package/dist/{dist-CImxMt5h.js → dist-C7uZ_fOb.js} +63 -4
  52. package/dist/index.d.ts +6 -2
  53. package/dist/index.js +26 -5
  54. package/dist/index.mjs +5 -5
  55. package/dist/kernel/event-plane-report.d.ts +202 -0
  56. package/dist/kernel/heap-spaces.d.ts +138 -0
  57. package/dist/kernel/heap-watch.d.ts +213 -5
  58. package/dist/kernel/index.d.ts +2 -0
  59. package/dist/kernel/transport/local-child-registry.d.ts +22 -0
  60. package/dist/{manifest-python-deps-BaVekjRY.js → manifest-python-deps-CRTx_j4k.js} +706 -15
  61. package/dist/{manifest-python-deps-B5rrxUQv.mjs → manifest-python-deps-QompQxhG.mjs} +580 -15
  62. package/dist/{retired-settings-keys-C7gLUS3x.js → retired-settings-keys-CBU4-TJO.js} +1 -1
  63. package/dist/{retired-settings-keys-DhFPXIRD.mjs → retired-settings-keys-q9mt_DLX.mjs} +1 -1
  64. package/package.json +1 -1
  65. package/dist/{tls-u8QCJCFE.js → tls-BxQlomxd.js} +1 -1
  66. package/dist/{tls-CQhPGSJm.mjs → tls-DOTmtLCW.mjs} +1 -1
@@ -1,4 +1,4 @@
1
- import "./dist-CbRciAbu.mjs";
1
+ import "./dist-BLhrrxIs.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";
@@ -7,16 +7,358 @@ import * as path$1 from "node:path";
7
7
  import { isAbsolute, join } from "node:path";
8
8
  import { execFile } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
+ import * as fs from "node:fs";
10
11
  import * as os from "node:os";
11
12
  import { tmpdir } from "node:os";
12
- import * as fs from "node:fs";
13
13
  import { createServer } from "node:http";
14
- import { monitorEventLoopDelay } from "node:perf_hooks";
15
14
  import * as v8 from "node:v8";
15
+ import { monitorEventLoopDelay } from "node:perf_hooks";
16
16
  import * as vm from "node:vm";
17
17
  import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, createEventBusSliceSource, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
18
18
  import { connect, createServer as createServer$1 } from "node:net";
19
19
  import { TRPCClientError, createTRPCClient } from "@trpc/client";
20
+ //#region src/kernel/event-plane-report.ts
21
+ /**
22
+ * Build a reader over the two registries.
23
+ *
24
+ * Returns `undefined` until the child registry exists — the heartbeat starts as
25
+ * the FIRST statement of `bootstrap()` (a process that dies early still leaves
26
+ * samples) and `LocalChildRegistry` is built hundreds of lines later inside
27
+ * `MoleculerService.onModuleInit`. Printing zeros in that window would report a
28
+ * silent plane where there is no plane at all.
29
+ *
30
+ * Wrapped: a registry mid-teardown may throw, and a diagnostic that can crash
31
+ * the process it watches is worse than none.
32
+ */
33
+ function createEventPlaneReader(sources) {
34
+ return () => {
35
+ try {
36
+ const registry = sources.registry();
37
+ if (registry === null) return void 0;
38
+ const children = [];
39
+ const listed = registry.listChildren();
40
+ for (const child of listed) {
41
+ const stats = registry.getChildEventStats(child.childId);
42
+ if (stats === null) continue;
43
+ children.push({
44
+ childId: child.childId,
45
+ sent: stats.sent,
46
+ suppressed: stats.suppressed
47
+ });
48
+ }
49
+ const crossNode = sources.crossNodeStats();
50
+ return {
51
+ udsMode: registry.eventFanoutMode,
52
+ crossNodeMode: sources.crossNodeMode(),
53
+ children,
54
+ childrenConnected: listed.length,
55
+ childrenUndeclared: registry.undeclaredChildCount(),
56
+ crossNodeDelivered: crossNode?.delivered ?? 0,
57
+ crossNodeSuppressed: crossNode?.suppressed ?? 0
58
+ };
59
+ } catch {
60
+ return;
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * How many children get named on the line.
66
+ *
67
+ * Five, because the question is which peer DOMINATES and the measured
68
+ * distribution is not flat: one peer accounted for 78–82% of outbound bytes and
69
+ * the runner-up for ~6%. Five names the dominant peer, its challenger and enough
70
+ * of the tail to see the shape change; naming all ~38 would put a kilobyte-long
71
+ * line in the hub's shared log every minute, which is how a log stops being read.
72
+ */
73
+ var EVENT_PLANE_TOP_N = 5;
74
+ /** A process that has never read a plane. */
75
+ var EMPTY_EVENT_PLANE_BASELINE = {
76
+ children: /* @__PURE__ */ new Map(),
77
+ crossNodeDelivered: 0,
78
+ crossNodeSuppressed: 0
79
+ };
80
+ /**
81
+ * Subtract the previous cumulative reading from the current one.
82
+ *
83
+ * A child that RESTARTED has counters that went backwards — the registry keys by
84
+ * childId and a respawned runner reconnects under the same id with a fresh
85
+ * entry. Clamping at 0 reports the restart as a quiet window rather than as a
86
+ * negative rate: the window's traffic is genuinely unknown, and a negative
87
+ * number on a rate reads as a broken meter.
88
+ */
89
+ function diffEventPlane(baseline, current, topN = 5) {
90
+ const deltas = [];
91
+ let fanoutSent = 0;
92
+ let fanoutSuppressed = 0;
93
+ for (const sample of current.children) {
94
+ const before = baseline.children.get(sample.childId);
95
+ const sent = Math.max(0, sample.sent - (before?.sent ?? 0));
96
+ const suppressed = Math.max(0, sample.suppressed - (before?.suppressed ?? 0));
97
+ fanoutSent += sent;
98
+ fanoutSuppressed += suppressed;
99
+ if (sent > 0 || suppressed > 0) deltas.push({
100
+ childId: sample.childId,
101
+ sent,
102
+ suppressed
103
+ });
104
+ }
105
+ return {
106
+ udsMode: current.udsMode,
107
+ crossNodeMode: current.crossNodeMode,
108
+ childrenConnected: current.childrenConnected,
109
+ childrenUndeclared: current.childrenUndeclared,
110
+ fanoutSent,
111
+ fanoutSuppressed,
112
+ crossNodeDelivered: Math.max(0, current.crossNodeDelivered - baseline.crossNodeDelivered),
113
+ crossNodeSuppressed: Math.max(0, current.crossNodeSuppressed - baseline.crossNodeSuppressed),
114
+ top: deltas.toSorted((a, b) => b.sent - a.sent || a.childId.localeCompare(b.childId)).slice(0, topN)
115
+ };
116
+ }
117
+ function createEventPlaneMeter(reader, topN = 5) {
118
+ let baseline = EMPTY_EVENT_PLANE_BASELINE;
119
+ return { read: () => {
120
+ const current = reader();
121
+ if (current === void 0) return void 0;
122
+ const window = diffEventPlane(baseline, current, topN);
123
+ baseline = {
124
+ children: new Map(current.children.map((s) => [s.childId, s])),
125
+ crossNodeDelivered: current.crossNodeDelivered,
126
+ crossNodeSuppressed: current.crossNodeSuppressed
127
+ };
128
+ return window;
129
+ } };
130
+ }
131
+ /**
132
+ * The suffix appended to a `[mem]` line — leading space included, empty when
133
+ * this process has no event plane.
134
+ *
135
+ * Only two processes in the cluster have one (hub-main, and an agent's main), so
136
+ * these fields never appear on the ~38 runner lines that share the same log
137
+ * stream. On the two that do, every field is printed on every line INCLUDING at
138
+ * zero: the modes because a suppressed count without its regime is unreadable,
139
+ * `childrenUndeclared` because one undeclared child silently disables both
140
+ * gates, and the deltas because "the plane went quiet for a minute" is a
141
+ * reading, not a blank.
142
+ *
143
+ * `fanoutTop` is the one conditional field — a ranked list of nothing is noise —
144
+ * and it is a single comma-separated value so the field COUNT does not change
145
+ * from line to line: `childId:sent/suppressed`, largest first.
146
+ */
147
+ function formatEventPlane(window) {
148
+ if (window === void 0) return "";
149
+ const top = window.top.map((d) => `${d.childId}:${d.sent}/${d.suppressed}`).join(",");
150
+ return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
151
+ }
152
+ //#endregion
153
+ //#region src/kernel/heap-spaces.ts
154
+ /**
155
+ * heap-spaces — WHERE in the V8 heap the bytes are, on the ordinary heartbeat.
156
+ *
157
+ * ## The question this exists to answer, and why it was unobtainable
158
+ *
159
+ * `hub/recorder` holds ~1.4 GB of live, retained JS heap that nobody can name.
160
+ * That is measured, not inferred: a forced compacting
161
+ * `gc({ flavor: 'last-resort' })` returned 507 MB of RSS and left V8 with
162
+ * 1870 MB committed and 1439 MB of it LIVE forty seconds later. Everything the
163
+ * 2026-08-29 pass could price — index rows 60–67 MB, runner shell ~50 MB, boot
164
+ * parse 35–50 MB transient, write path 0 retained — sums to 115–125 MB, under
165
+ * 9%. The same addon build running as `recorder` on an agent with no cameras
166
+ * and no archive sits at 52 MB.
167
+ *
168
+ * The next question is cheap and binary, and it halves the search:
169
+ *
170
+ * - **`old_space` holds it** ⇒ millions of small objects. The hunt becomes
171
+ * "what allocates ~7 M objects", and the row counters the recorder's index
172
+ * already exposes are the next thing to log.
173
+ * - **`large_object_space` holds it** ⇒ a handful of objects over V8's
174
+ * ~128 KB regular-object ceiling. The hunt becomes "who holds one huge array
175
+ * or string", which is a very short list in any one addon.
176
+ *
177
+ * V8 answers it for free, and this repository has been computing the answer
178
+ * since the SIGUSR2 handler was written — and then throwing it away, because
179
+ * `addon-runner.ts` welded the free read to `v8.writeHeapSnapshot`. One signal,
180
+ * both actions, so the cheap answer could not be had without the expensive one
181
+ * an operator has to authorise on a production hub. {@link emitHeapDiagnosticReport}
182
+ * splits them; this module puts the same breakdown on the periodic `[mem]` line
183
+ * so no signal is needed at all.
184
+ *
185
+ * ## Why bytes and not MB in the model
186
+ *
187
+ * The two consumers want different resolution — the heartbeat wants whole MB
188
+ * next to `heapUsed`, the SIGUSR2 report keeps the 0.1 MB it has always
189
+ * printed — so the reading stays raw and rounding happens at the edge. A model
190
+ * that pre-rounds cannot serve both without lying to one of them.
191
+ */
192
+ /**
193
+ * The two spaces that ANSWER the question, printed always — including at zero.
194
+ *
195
+ * Node 24 reports thirteen spaces (measured on 24.17: `read_only_space`,
196
+ * `new_space`, `old_space`, `code_space`, `shared_space`, `trusted_space`,
197
+ * `shared_trusted_space`, `new_large_object_space`, `large_object_space`,
198
+ * `code_large_object_space`, `shared_large_object_space`,
199
+ * `shared_trusted_large_object_space`, `trusted_large_object_space`). Printing
200
+ * all thirteen once a minute per process would add ~13 fields to a line that
201
+ * already carries eleven, on every runner, forever — and eleven of the thirteen
202
+ * cannot be the answer to a 1.4 GB retained-set question.
203
+ *
204
+ * These two are printed unconditionally because **absence must never be
205
+ * ambiguous**. A threshold-only rule would drop `large_object_space` at 20 MB,
206
+ * and a reader would not be able to tell "small" from "this build does not
207
+ * report it" — which is the same class of mistake as a heartbeat with no
208
+ * destination (D285). `largeObjectSpace=0MB` is a load-bearing reading: it says
209
+ * the retainer is NOT a handful of huge objects.
210
+ */
211
+ var DECISIVE_HEAP_SPACES = ["old_space", "large_object_space"];
212
+ /**
213
+ * A non-decisive space is printed only once it holds at least this many MB.
214
+ *
215
+ * 32 MB, and the number comes from the smallest heap this line is ever asked to
216
+ * explain: the control in the 2026-08-29 pass is an idle `recorder` runner at
217
+ * **52 MB** of `heapUsed`. A threshold below that means no space that could
218
+ * dominate even the smallest watched process is ever hidden, while a quiet
219
+ * process — measured on a bare Node 24.17: `new_space` 0.7 MB, `code_space`
220
+ * 0.25 MB, `trusted_space` 0.9 MB — prints nothing but the decisive pair.
221
+ *
222
+ * What this drops, and why it is right to drop it: `new_space` is the scavenger's
223
+ * semi-space and is bounded by `--max-semi-space-size`, so it can never be a
224
+ * retained set; `code_space` and `read_only_space` are JIT output and V8's own
225
+ * immutable roots, both bounded by the code that exists rather than by the data
226
+ * it touches; the `shared_*` spaces are empty outside a worker-threads
227
+ * deployment, which this cluster does not run. None of them has ever been the
228
+ * answer to "what is this process holding", and each would cost a field on
229
+ * every `[mem]` line in the fleet.
230
+ *
231
+ * They are not censored, though — a space over the threshold prints, whatever it
232
+ * is. If `code_space` ever reaches 32 MB that is itself the finding.
233
+ */
234
+ var HEAP_SPACE_REPORT_MIN_MB = 32;
235
+ var BYTES_PER_MB = 1048576;
236
+ /** Whole MB, matching the rest of the `[mem]` line. */
237
+ function mb(bytes) {
238
+ return Math.round(bytes / BYTES_PER_MB);
239
+ }
240
+ /** Tenths of an MB — the resolution the SIGUSR2 report has always printed. */
241
+ function tenthsMb(bytes) {
242
+ return Math.round(bytes / BYTES_PER_MB * 10) / 10;
243
+ }
244
+ /**
245
+ * Read every space from V8.
246
+ *
247
+ * Wrapped: a diagnostic that can crash the process it watches is worse than
248
+ * none, and this is called from a heartbeat whose whole contract is that it
249
+ * survives whatever it is measuring. An empty reading prints no fields, which
250
+ * is exactly what a heartbeat that could not read the heap should say.
251
+ */
252
+ function readHeapSpaces() {
253
+ try {
254
+ return v8.getHeapSpaceStatistics().map((space) => ({
255
+ name: space.space_name,
256
+ usedBytes: space.space_used_size
257
+ }));
258
+ } catch {
259
+ return [];
260
+ }
261
+ }
262
+ /**
263
+ * `old_space` → `oldSpace`. Mechanical and total, so a space V8 adds tomorrow
264
+ * gets a field name without anyone editing a table.
265
+ *
266
+ * camelCase because that is the vocabulary of every other field on this line
267
+ * (`heapUsed`, `heapTotal`, `arrayBuffers`, `rssBudget`, `loopP99`). A line that
268
+ * mixed `heapUsed=` with `old_space=` would be two conventions for one record.
269
+ */
270
+ function heapSpaceField(v8Name) {
271
+ return v8Name.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
272
+ }
273
+ /**
274
+ * The spaces worth a field on this line: the decisive pair (in that order,
275
+ * always), then every other space at or above `minMb`, largest first.
276
+ *
277
+ * Largest-first for the tail because the tail only ever appears when something
278
+ * unexpected is holding memory, and the biggest surprise should be the first
279
+ * thing read. The decisive pair keeps a FIXED position so the two numbers a
280
+ * reader is comparing are always in the same place on every line.
281
+ */
282
+ function selectReportedSpaces(spaces, minMb = 32) {
283
+ const decisive = DECISIVE_HEAP_SPACES.map((name) => spaces.find((space) => space.name === name)).filter((space) => space !== void 0);
284
+ 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));
285
+ return [...decisive, ...rest];
286
+ }
287
+ /**
288
+ * The suffix appended to a `[mem]` line — leading space included, empty when
289
+ * there is nothing to say.
290
+ *
291
+ * APPENDED, never interleaved: every existing field keeps its name AND its
292
+ * position, so a `hl loki-query` pipeline written against the old line still
293
+ * matches. Prefix stability is the only change to a log line that cannot break
294
+ * a query someone already wrote.
295
+ */
296
+ function formatHeapSpaces(spaces, minMb = 32) {
297
+ const selected = selectReportedSpaces(spaces, minMb);
298
+ if (selected.length === 0) return "";
299
+ return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb(s.usedBytes)}MB`).join(" ")}`;
300
+ }
301
+ /**
302
+ * Env var that authorises a heap snapshot on SIGUSR2.
303
+ *
304
+ * A snapshot is an admin mutation on a production hub: `writeHeapSnapshot`
305
+ * streams a file the size of the heap to `/tmp` and stops the world while it
306
+ * walks it — on the recorder that is a multi-GB file and a stall measured in
307
+ * seconds. The per-space breakdown is ~1.2 µs and changes nothing. Welding them
308
+ * to one signal priced the free answer at the cost of the expensive one.
309
+ */
310
+ var RUNNER_HEAP_SNAPSHOT_ENV = "CAMSTACK_RUNNER_HEAP_SNAPSHOT";
311
+ /**
312
+ * Whether the operator has authorised the snapshot half of SIGUSR2.
313
+ *
314
+ * Explicit `'on'` only. Anything else — unset, empty, `'true'`, `'1'` — is NOT
315
+ * authorisation: a multi-GB stop-the-world write must never be reachable by a
316
+ * value someone set for a different purpose.
317
+ */
318
+ function heapSnapshotAuthorised(env = process.env) {
319
+ return env[RUNNER_HEAP_SNAPSHOT_ENV] === "on";
320
+ }
321
+ var defaultDeps = {
322
+ readMemory: () => process.memoryUsage(),
323
+ readSpaces: readHeapSpaces,
324
+ writeSnapshot: (path) => {
325
+ v8.writeHeapSnapshot(path);
326
+ },
327
+ env: process.env
328
+ };
329
+ /**
330
+ * The SIGUSR2 report — memory + the FULL per-space breakdown, always; the
331
+ * snapshot only when {@link heapSnapshotAuthorised}.
332
+ *
333
+ * The breakdown here keeps every non-empty space at 0.1 MB, unlike the
334
+ * heartbeat's filtered whole-MB pair: this is a hand-fired diagnostic read once
335
+ * by a human who asked for it, so completeness costs nothing. The heartbeat runs
336
+ * unattended on every runner forever, so it pays for every field it prints.
337
+ *
338
+ * A refused snapshot SAYS SO. A branch that drops work silently reads as "it was
339
+ * never requested", and this one is fired by an operator who is standing there
340
+ * waiting for a file.
341
+ */
342
+ function emitHeapDiagnosticReport(request, deps = defaultDeps) {
343
+ try {
344
+ const mu = deps.readMemory();
345
+ const spaces = deps.readSpaces().filter((space) => space.usedBytes > 0).map((space) => `${space.name}=${tenthsMb(space.usedBytes)}`).join(" ");
346
+ request.sink.info(`[mem] node="${request.label}" addons=[${request.addons.join(",")}] rss=${tenthsMb(mu.rss)} heapUsed=${tenthsMb(mu.heapUsed)} heapTotal=${tenthsMb(mu.heapTotal)} external=${tenthsMb(mu.external)} arrayBuffers=${tenthsMb(mu.arrayBuffers)} | ${spaces}`);
347
+ } catch (error) {
348
+ request.sink.warn(`[mem] node="${request.label}" space report failed: ${error instanceof Error ? error.message : String(error)}`);
349
+ }
350
+ if (!heapSnapshotAuthorised(deps.env)) {
351
+ request.sink.info(`[mem] node="${request.label}" heap snapshot NOT taken — it is an admin mutation (a multi-GB write and a stop-the-world walk of the heap). The per-space breakdown above is the free half and needs no authorisation. Set ${RUNNER_HEAP_SNAPSHOT_ENV}=on for this runner and signal again to take one.`);
352
+ return;
353
+ }
354
+ try {
355
+ deps.writeSnapshot(request.snapshotPath);
356
+ request.sink.info(`[mem] node="${request.label}" heap snapshot written -> ${request.snapshotPath}`);
357
+ } catch (error) {
358
+ request.sink.warn(`[mem] node="${request.label}" snapshot failed: ${error instanceof Error ? error.message : String(error)}`);
359
+ }
360
+ }
361
+ //#endregion
20
362
  //#region src/kernel/heap-watch.ts
21
363
  /**
22
364
  * heap-watch — hub-main's own memory, on the record, and returned to the OS.
@@ -41,6 +383,13 @@ import { TRPCClientError, createTRPCClient } from "@trpc/client";
41
383
  * `addon-runner.ts`) — hub-main, the one process that actually OOMs, was the
42
384
  * one process with no telemetry at all.
43
385
  *
386
+ * **And for 27 days after that it still had none where anyone could read it.**
387
+ * The watch was started correctly and passed NO SINK, so it fell back to
388
+ * `console.log`, which is not an `ILogDestination` — 42 hours of Loki carried a
389
+ * line for every runner and none for hub-main. Read
390
+ * {@link createDeferredHeapWatchSink} before trusting anything below: a
391
+ * heartbeat with no destination is indistinguishable from no heartbeat (D285).
392
+ *
44
393
  * ## What it gives
45
394
  *
46
395
  * A heartbeat carrying the numbers that separate the two possible stories, and
@@ -51,6 +400,14 @@ import { TRPCClientError, createTRPCClient } from "@trpc/client";
51
400
  * - `heapUsedMb` flat with a sharp jump under load → a burst allocation, and
52
401
  * the timestamp says what was being served.
53
402
  *
403
+ * Since 2026-08-29 the line also says **WHERE in the V8 heap** the bytes are —
404
+ * `oldSpace` and `largeObjectSpace`, appended after everything already on it.
405
+ * `heapUsed` alone cannot separate "millions of small objects" from "a handful
406
+ * of huge ones", and those lead to opposite searches; the recorder's unnamed
407
+ * ~1.4 GB retained set is the case that proved it. See
408
+ * {@link formatHeapSpaces} for which spaces are printed and why the rest are
409
+ * not.
410
+ *
54
411
  * `external`/`arrayBuffers` ride along because they are NOT bounded by
55
412
  * `--max-old-space-size` and are most of the gap between a 1.5GB cap and the
56
413
  * ~2.05GB RSS observed live: a diagnosis that only looks at the JS heap
@@ -92,6 +449,30 @@ import { TRPCClientError, createTRPCClient } from "@trpc/client";
92
449
  * `HEAP_RECLAIM_MIN_INTERVAL_MS`. The alternative, measured, is a ratchet to
93
450
  * 7.8GB and the OOM that ends it.
94
451
  */
452
+ /**
453
+ * What hub-main calls itself in a `[mem]` line.
454
+ *
455
+ * ## Why not `hub/main`
456
+ *
457
+ * A runner's label is its nodeId, and a nodeId is built in exactly one place as
458
+ * `` `${parentNodeId}/${runnerId}` `` (`kernel/moleculer/addon-runner.ts:222`) —
459
+ * so every runner label contains a `/` and every hub-local one is `hub/<runner>`.
460
+ * A label with NO slash therefore cannot be produced by that rule, which makes
461
+ * `hub-main` unambiguous by construction rather than by convention: no future
462
+ * addon, group or agent can ever claim it.
463
+ *
464
+ * It also sorts where it belongs. `-` (0x2D) precedes `/` (0x2F), so a sorted
465
+ * `[mem]` census puts `hub-main` immediately before `hub/decoder`,
466
+ * `hub/recorder`, `hub/stream-broker` — the composition root first, then the
467
+ * runners it forked. `hub/main` would sort into the middle of its own children
468
+ * and would be one addon id away from a collision.
469
+ *
470
+ * And it is the name the rest of this repository already uses — the docblocks,
471
+ * {@link HUB_MAIN_RSS_BUDGET_MB}, {@link HUB_RSS_BUDGET_ENV}, every ADR. A log
472
+ * label that disagreed with the prose would be a second vocabulary for one
473
+ * process, which is the exact failure this telemetry exists to remove.
474
+ */
475
+ var HUB_MAIN_HEAP_WATCH_LABEL = "hub-main";
95
476
  /** Heartbeat period. Slow enough to be free, fast enough that the sample before
96
477
  * a fatal is recent. */
97
478
  var HEAP_WATCH_INTERVAL_MS = 6e4;
@@ -168,6 +549,86 @@ var HEAP_RECLAIM_TRIGGER_MB = 1024;
168
549
  * router's time, against a ~1.5GiB peak it removes each pass.
169
550
  */
170
551
  var HEAP_RECLAIM_MIN_INTERVAL_MS = 12e4;
552
+ /**
553
+ * Floor on the pass once it has PROVEN it is steady state, not a rescue.
554
+ *
555
+ * ## Why `stranded` cannot discriminate on hub-main
556
+ *
557
+ * {@link strandedMb} is `rss − heapUsed − external`: everything the process
558
+ * holds that those two numbers do not name. That lumps together two things with
559
+ * opposite fixes —
560
+ *
561
+ * - V8 pages committed at a sawtooth peak, which `gc(last-resort)` RETURNS;
562
+ * - glibc arena memory already freed and never handed back, which **no V8
563
+ * flavor can reach** (nothing in this repository calls `malloc_trim`).
564
+ *
565
+ * {@link HEAP_RECLAIM_TRIGGER_MB} was calibrated against a hub-main whose strand
566
+ * was mostly the first kind, and a pass then took it back under the trigger. Once
567
+ * the second kind grows past 1024 MB the quantity can never fall back under it,
568
+ * and the gate degenerates. Measured on the live hub 2026-08-29, first 25
569
+ * minutes of hub-main telemetry ever collected:
570
+ *
571
+ * - `stranded` sat at **1031–2313 MB for the entire boot**, i.e.
572
+ * {@link shouldReclaim} never once said no. **What is written as a threshold
573
+ * is, on this process, an unconditional 120-second timer.**
574
+ * - Nine minutes out of nine that contained a reclaim show `loopMax` ≈ the
575
+ * reclaim's own duration (555–869 ms). Every minute without one is
576
+ * 107–435 ms. **The largest recurring event-loop stall on the process that
577
+ * mediates every cross-runner call is the memory patch it applies to itself.**
578
+ * - Each pass freed less than the last (1193 → 462 MB) while the post-pass RSS
579
+ * floor ratcheted **1940 → 3510 MB in fourteen minutes**, with `heapUsed`
580
+ * never moving. It is compacting a heap that is not the problem.
581
+ *
582
+ * ## The fix is not a bigger trigger
583
+ *
584
+ * Raising {@link HEAP_RECLAIM_TRIGGER_MB} would move the number at which the same
585
+ * degeneration happens, and it would silence `hub/recorder` — whose single pass
586
+ * in six hours is the only cheap proof anyone has that the recorder's 1.4 GB is
587
+ * RETAINED (a forced compaction freed 507 MB and V8 still held 1.44 GB live).
588
+ * The discriminator is not the level of the strand; it is **whether the strand
589
+ * ever falls back under the trigger between passes**, which is precisely what
590
+ * {@link RECLAIM_STEADY_STATE_PASSES} already counts and used only to talk about.
591
+ *
592
+ * So the counter becomes load-bearing: once the pass has fired at the floor
593
+ * {@link RECLAIM_STEADY_STATE_PASSES} times running, the trigger has demonstrably
594
+ * stopped discriminating on this process, and the cadence falls back to a period
595
+ * chosen for its COST rather than to a threshold pretending to be a gate.
596
+ *
597
+ * ## Why 600_000, and why that is not a regression
598
+ *
599
+ * It is the value {@link HEAP_RECLAIM_MIN_INTERVAL_MS} held before 2026-08-17,
600
+ * and the reason it came down is recorded there: *"the ten-minute floor let a
601
+ * peak that re-formed in seconds stand for ten minutes"*. That argument is about
602
+ * a BURST — and a burst subsides, which resets the run and restores the 120 s
603
+ * floor. It never applied to a process in proven steady state. So the burst
604
+ * bound is untouched and the steady state gets the period the burst argument was
605
+ * never about.
606
+ *
607
+ * ## What each process does after this change
608
+ *
609
+ * - **hub-main**: first six passes at 120 s, unchanged. From the seventh
610
+ * consecutive floor-to-floor pass, every 600 s. Stop-the-world cost on the
611
+ * router falls from ~0.58% of wall clock to ~0.12%. The ArrayBuffer probe
612
+ * survives at six passes an hour — the finding it produced needed ten passes
613
+ * total, not ten in twenty-five minutes.
614
+ * - **`hub/recorder`**: zero change, by construction. `passesAtFloor` only
615
+ * increments when a pass fires within `minIntervalMs * 2` of the previous
616
+ * one; the recorder's strand takes six hours to re-form, so the counter is
617
+ * reset to 1 on every pass and can never reach six. The 507 MB probe is
618
+ * intact.
619
+ * - **Anything that recovers**: the run resets the moment the strand falls back
620
+ * under the trigger, and the 120 s floor returns with it.
621
+ */
622
+ var HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS = 6e5;
623
+ /**
624
+ * The floor in force for the NEXT pass, given how many have fired at the floor.
625
+ *
626
+ * Pure, so the cadence change is testable without stranding a gigabyte to reach
627
+ * it — and so the one place that decides it can be read on its own.
628
+ */
629
+ function reclaimIntervalMs(passesAtFloor, minIntervalMs = HEAP_RECLAIM_MIN_INTERVAL_MS, steadyStateIntervalMs = HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, steadyStatePasses = 6) {
630
+ return passesAtFloor >= steadyStatePasses ? Math.max(minIntervalMs, steadyStateIntervalMs) : minIntervalMs;
631
+ }
171
632
  /** What a loop with nothing recorded yet reports. */
172
633
  var ZERO_LOOP_DELAY = {
173
634
  p50Ms: 0,
@@ -187,6 +648,37 @@ var ZERO_LOOP_DELAY = {
187
648
  * naively would print `loopMax=9223372036854ms` and be read as a catastrophe on
188
649
  * every boot. `count === 0` short-circuits to {@link ZERO_LOOP_DELAY}.
189
650
  *
651
+ * ## `loopP50=20ms` was the SAMPLER, not the loop (fixed 2026-08-29)
652
+ *
653
+ * `monitorEventLoopDelay` records the whole INTERVAL between its own timer
654
+ * firings, not the excess over the interval it asked for. So on a loop with
655
+ * nothing wrong the histogram reads `resolution`, and every `[mem]` line this
656
+ * process has ever emitted said `loopP50=20ms` — which was read, repeatedly in
657
+ * one session, as "hub-main is chronically 20 ms behind". It is not; 20 was the
658
+ * default `resolutionMs` of this very function.
659
+ *
660
+ * The delay is `interval − resolution`, and that is not a fudge factor — it is
661
+ * the only reading the four resolutions agree on. Measured here, one 120 ms
662
+ * block observed by four histograms at once:
663
+ *
664
+ * | resolution | raw p50 | raw max | max − resolution |
665
+ * | --- | --- | --- | --- |
666
+ * | 20 ms | 20.40 ms | 137.63 ms | 117.6 ms |
667
+ * | 10 ms | 10.22 ms | 123.60 ms | 113.6 ms |
668
+ * | 5 ms | 5.11 ms | 123.60 ms | 118.6 ms |
669
+ * | 1 ms | 1.27 ms | 121.04 ms | 120.0 ms |
670
+ *
671
+ * Raw, the four disagree by 16 ms about the same event and none of them reports
672
+ * the block. Corrected, they agree within 7 ms and all four name it. The
673
+ * subtraction costs nothing and needs no faster sampler — a 1 ms resolution
674
+ * would be 1 000 extra timer wakeups per second on the process that already
675
+ * serves ~44 000 socket operations per second, to buy 2 ms of accuracy on a
676
+ * quantity nobody reads below 100 ms.
677
+ *
678
+ * **Readings from before this change are 20 ms high.** `loopP99=21–28ms` in the
679
+ * 2026-08-29 census is really 1–8 ms; `loopMax=2804ms` is really 2784 ms. Do not
680
+ * compare a pre-fix series with a post-fix one without shifting it.
681
+ *
190
682
  * Returns undefined if the platform declines, so the caller keeps a heartbeat
191
683
  * either way.
192
684
  */
@@ -194,14 +686,15 @@ function createLoopDelayMeter(resolutionMs = 20) {
194
686
  try {
195
687
  const histogram = monitorEventLoopDelay({ resolution: resolutionMs });
196
688
  histogram.enable();
689
+ const delay = (nanoseconds) => Math.max(0, Math.round(nanoseconds / 1e6) - resolutionMs);
197
690
  return {
198
691
  read: () => {
199
692
  try {
200
693
  if (histogram.count === 0) return ZERO_LOOP_DELAY;
201
694
  const sample = {
202
- p50Ms: Math.round(histogram.percentile(50) / 1e6),
203
- p99Ms: Math.round(histogram.percentile(99) / 1e6),
204
- maxMs: Math.round(histogram.max / 1e6)
695
+ p50Ms: delay(histogram.percentile(50)),
696
+ p99Ms: delay(histogram.percentile(99)),
697
+ maxMs: delay(histogram.max)
205
698
  };
206
699
  histogram.reset();
207
700
  return sample;
@@ -257,6 +750,33 @@ var consoleSink = {
257
750
  warn: (line) => console.warn(line)
258
751
  };
259
752
  /**
753
+ * Build a {@link DeferredHeapWatchSink}.
754
+ *
755
+ * The handover announces itself, once, through the NEW target: that line is the
756
+ * only in-band evidence that hub-main's telemetry actually reached the log
757
+ * manager on a given boot. Its absence from Loki means the wiring is broken —
758
+ * which is precisely the state this module was in, undetected, for 42 hours.
759
+ */
760
+ function createDeferredHeapWatchSink(label = HUB_MAIN_HEAP_WATCH_LABEL, fallback = consoleSink) {
761
+ let target = fallback;
762
+ const sink = {
763
+ info: (line) => {
764
+ target.info(line);
765
+ },
766
+ warn: (line) => {
767
+ target.warn(line);
768
+ }
769
+ };
770
+ const attach = (next) => {
771
+ target = next;
772
+ next.info(`[mem] ${label} heartbeat attached to the log manager — earlier lines went to stdout only and are not in the log buffer, the rotated file or Loki.`);
773
+ };
774
+ return {
775
+ sink,
776
+ attach
777
+ };
778
+ }
779
+ /**
260
780
  * RSS the process is holding but not using.
261
781
  *
262
782
  * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
@@ -385,10 +905,22 @@ function createV8Reclaimer() {
385
905
  function formatOverBudget(label, b, source) {
386
906
  return `[mem] ${label} OVER RSS BUDGET rss=${b.rssMb}MB budget=${b.budgetMb}MB over=+${b.overMb}MB shape=${b.shape} heapUsed=${b.heapUsedMb}MB(${share(b.heapUsedMb, b.rssMb)}%) external=${b.externalMb}MB(${share(b.externalMb, b.rssMb)}%) nativeResidue=${b.nativeResidueMb}MB(${share(b.nativeResidueMb, b.rssMb)}%) arrayBuffers=${b.arrayBuffersMb}MB budgetFrom="${source}" — ${SHAPE_ADVICE[b.shape]}`;
387
907
  }
388
- function format(label, s, loop, budgetMb) {
908
+ /**
909
+ * The heartbeat line.
910
+ *
911
+ * ## Nothing here may be renamed or moved
912
+ *
913
+ * `heapUsed` says HOW MUCH; the per-space suffix says WHERE, which is the half
914
+ * that names a retained set. The suffix is APPENDED after the loop fields for
915
+ * one reason: every existing field then keeps its name and its position, so the
916
+ * `hl loki-query` pipelines written against this line keep matching. A rename or
917
+ * a reorder is the one change to a log format that silently breaks a query
918
+ * nobody will re-run until they need it.
919
+ */
920
+ function format(label, s, loop, budgetMb, suffix) {
389
921
  const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB` + (budgetMb === void 0 ? "" : ` rssBudget=${budgetMb}MB`);
390
- if (loop === void 0) return line;
391
- return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms`;
922
+ if (loop === void 0) return `${line}${suffix}`;
923
+ return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms${suffix}`;
392
924
  }
393
925
  /**
394
926
  * Start the heartbeat. Returns a stop function.
@@ -396,11 +928,14 @@ function format(label, s, loop, budgetMb) {
396
928
  * `unref`ed so it can never hold the process open, and every read is wrapped:
397
929
  * a diagnostic that can crash the process it watches is worse than none.
398
930
  */
399
- function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true, rssBudget) {
931
+ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true, rssBudget, probes) {
932
+ const readSpaces = probes?.readSpaces ?? readHeapSpaces;
933
+ const eventPlane = probes?.eventPlane === void 0 ? void 0 : createEventPlaneMeter(probes.eventPlane);
400
934
  const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
401
935
  const now = reclaimOptions?.now ?? (() => Date.now());
402
936
  const triggerMb = reclaimOptions?.triggerMb ?? 1024;
403
937
  const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
938
+ const steadyStateIntervalMs = reclaimOptions?.steadyStateIntervalMs ?? 6e5;
404
939
  const fastIntervalMs = reclaimOptions?.fastIntervalMs ?? 2e3;
405
940
  const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
406
941
  const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
@@ -420,9 +955,10 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
420
955
  steadyStateAnnounced = false;
421
956
  return;
422
957
  }
958
+ const floorMs = reclaimIntervalMs(passesAtFloor, minIntervalMs, steadyStateIntervalMs);
423
959
  const sinceLast = now() - lastReclaimAt;
424
- if (sinceLast < minIntervalMs) return;
425
- passesAtFloor = sinceLast <= minIntervalMs * 2 ? passesAtFloor + 1 : 1;
960
+ if (sinceLast < floorMs) return;
961
+ passesAtFloor = sinceLast <= floorMs * 2 ? passesAtFloor + 1 : 1;
426
962
  lastReclaimAt = now();
427
963
  const startedAt = now();
428
964
  try {
@@ -435,7 +971,8 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
435
971
  sink.info(`[mem] reclaim ${label} stranded=${strandedMb(sample)}MB rss=${sample.rssMb}MB→${after.rssMb}MB freed=${sample.rssMb - after.rssMb}MB arrayBuffers=${sample.arrayBuffersMb}MB→${after.arrayBuffersMb}MB took=${now() - startedAt}ms`);
436
972
  if (passesAtFloor >= 6 && !steadyStateAnnounced) {
437
973
  steadyStateAnnounced = true;
438
- sink.warn(`[mem] reclaim ${label} has run at the floor for ${passesAtFloor} consecutive passes (every ${minIntervalMs}ms) — the stop-the-world is now STEADY STATE, not a rescue. The allocation source is what needs the fix; this pass only bounds its peak.`);
974
+ const nextFloorMs = reclaimIntervalMs(passesAtFloor, minIntervalMs, steadyStateIntervalMs);
975
+ sink.warn(`[mem] reclaim ${label} has run at the floor for ${passesAtFloor} consecutive passes (every ${minIntervalMs}ms) — the stop-the-world is now STEADY STATE, not a rescue: stranded never fell back under ${triggerMb}MB between passes, so the trigger is not discriminating on this process and what is left deciding is the timer. BACKING OFF to every ${nextFloorMs}ms — the pass keeps running (it is still the only measurement of this process's live ArrayBuffer set) at a cost the router can pay. The allocation source is what needs the fix; this pass only bounds its peak.`);
439
976
  }
440
977
  };
441
978
  let mode = "steady";
@@ -498,7 +1035,7 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
498
1035
  const due = at - lastLoggedAt >= intervalMs;
499
1036
  if (mode === "escalated" || due) {
500
1037
  lastLoggedAt = at;
501
- const line = format(label, sample, loopDelay?.read(), budgetMb);
1038
+ const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}`);
502
1039
  if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
503
1040
  else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
504
1041
  else sink.info(line);
@@ -5417,6 +5954,34 @@ var LocalChildRegistry = class {
5417
5954
  };
5418
5955
  }
5419
5956
  /**
5957
+ * The regime the counters above were produced under, read once from
5958
+ * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
5959
+ *
5960
+ * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
5961
+ * WOULD have been dropped" under `shadow`. Those are different claims, and
5962
+ * until this getter existed the mode was invisible at runtime — the counters
5963
+ * were readable and the regime that produced them was not.
5964
+ */
5965
+ get eventFanoutMode() {
5966
+ return this.fanoutMode;
5967
+ }
5968
+ /**
5969
+ * Connected children that never declared a subscription pattern set.
5970
+ *
5971
+ * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
5972
+ * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
5973
+ * `aggregateEventInterest` returns `null` — accept everything — if ANY
5974
+ * connected child is undeclared, which disables the node's inbound cross-node
5975
+ * gate as well. **One legacy runner silently turns off the filtering for the
5976
+ * whole node**, and nothing reported it, so a zero here is as load-bearing as
5977
+ * a non-zero one.
5978
+ */
5979
+ undeclaredChildCount() {
5980
+ let count = 0;
5981
+ for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
5982
+ return count;
5983
+ }
5984
+ /**
5420
5985
  * Does `entry` want to receive an event in `category`? Honours the fan-out
5421
5986
  * mode: `broadcast` → always; undeclared patterns (`null`) → always
5422
5987
  * (fail-open); else the child's declared set must match via `matchesPattern`
@@ -8582,4 +9147,4 @@ async function installManifestPythonDeps(declaration, addonDir, deps, logger) {
8582
9147
  await deps.installPythonRequirements(reqAbs);
8583
9148
  }
8584
9149
  //#endregion
8585
- export { buildUdsNativeCapProxy as $, createParentUnownedCallHandler as A, RSS_BUDGET_REANNOUNCE_MIN_MS as At, AGENT_CAP_FWD_SERVICE as B, shouldReclaim as Bt, brokerTransportLink as C, createAddonDataPlaneFacility as Ct, HUB_CAP_FWD_ACTION as D, HEAP_WATCH_WARN_RATIO as Dt, localProviderLink as E, HEAP_WATCH_INTERVAL_MS as Et, createUdsLoggerWithControl as F, createV8Reclaimer as Ft, createLocalTransport as G, CapRouteError as H, startRunnerHeapWatch as Ht, LocalChildClient as I, describeRss as It, SocketChannel as J, UdsLocalTransportClient as K, LocalChildRegistry as L, hubMainRssBudget as Lt, createUdsEventBus as M, RUNNER_HEAP_WATCH_INTERVAL_MS as Mt, udsChildLogToWorkerEntry as N, RUNNER_RSS_BUDGET_ENV as Nt, HUB_CAP_FWD_SERVICE as O, HUB_MAIN_RSS_BUDGET_MB as Ot, createUdsLogger as P, buildHeapSample as Pt, buildNativeCapProxy as Q, UDS_NO_ROUTE_PREFIX as R, nextRssBudgetState as Rt, brokerCallForCap as S, runNpm as St, ipcParentLink as T, HEAP_RECLAIM_TRIGGER_MB as Tt, classifyCapRoute as U, strandedMb as Ut, CapRouteResolver as V, startHeapWatch as Vt, callWithServiceDiscovery as W, FrameDecoder as X, localEndpointPath as Y, encodeFrame as Z, createKernelHwAccel as _, CapabilityUnavailableError as _t, getWorkerDeviceRegistry as a, createAddonService as at, __resetCapUsageRegistryForTests as b, resolveAddonClass as bt, setHubConnected as c, capActionName as ct, getBrokerEventBus as d, capServiceName as dt, createBrokerDeviceManagerApi as et, getMoleculerEventStats as f, parseCapAction as ft, AddonDepsManager as g, CapabilityHandle as gt, subscribePassthrough as h, DeviceRegistry as ht, createUdsAddonContext as i, setWorkerNativeCapsChangeListener as it, createUdsEventBridge as j, RSS_BUDGET_RELEASE_RATIO as jt, createHubCapForwardService as k, HUB_RSS_BUDGET_ENV as kt, EVENT_TOPIC_PREFIX as l, capActionSuffix as lt, setNodeEventInterest as m, serializeTypedArrays as mt, adaptBrokerToCluster as n, getWorkerNativeCapSnapshot as nt, getOrInitReadinessRegistry as o, validateProviderRegistrations as ot, registerEventBusService as p, deserializeTypedArrays as pt, UdsLocalTransportServer as q, createAddonContext as r, mountNativeCapService as rt, getOrInitReadinessRegistryForClient as s, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestPythonDeps as t, getWorkerNativeCapProvider as tt, clusterEventTopic as u, capBareAction as ut, resolveHwAccel as v, copyBundledNativeModules as vt, buildLinkChain as w, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, getCapUsageRegistry as x, resolveNpmInvocation as xt, CapUsageRegistry as y, installManifestNativeDeps as yt, AGENT_CAP_FWD_ACTION as z, parseRssBudgetMb as zt };
9150
+ export { buildNativeCapProxy as $, RUNNER_HEAP_SNAPSHOT_ENV as $t, createParentUnownedCallHandler as A, HUB_MAIN_HEAP_WATCH_LABEL as At, AGENT_CAP_FWD_SERVICE as B, createLoopDelayMeter as Bt, brokerTransportLink as C, runNpm as Ct, HUB_CAP_FWD_ACTION as D, HEAP_RECLAIM_TRIGGER_MB as Dt, localProviderLink as E, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Et, createUdsLoggerWithControl as F, RUNNER_HEAP_WATCH_INTERVAL_MS as Ft, readMoleculerFanoutMode as G, parseRssBudgetMb as Gt, CapRouteError as H, describeRss as Ht, LocalChildClient as I, RUNNER_RSS_BUDGET_ENV as It, UdsLocalTransportServer as J, startHeapWatch as Jt, createLocalTransport as K, reclaimIntervalMs as Kt, LocalChildRegistry as L, ZERO_LOOP_DELAY as Lt, createUdsEventBus as M, HUB_RSS_BUDGET_ENV as Mt, udsChildLogToWorkerEntry as N, RSS_BUDGET_REANNOUNCE_MIN_MS as Nt, HUB_CAP_FWD_SERVICE as O, HEAP_WATCH_INTERVAL_MS as Ot, createUdsLogger as P, RSS_BUDGET_RELEASE_RATIO as Pt, encodeFrame as Q, HEAP_SPACE_REPORT_MIN_MB as Qt, UDS_NO_ROUTE_PREFIX as R, buildHeapSample as Rt, brokerCallForCap as S, resolveNpmInvocation as St, ipcParentLink as T, HEAP_RECLAIM_MIN_INTERVAL_MS as Tt, classifyCapRoute as U, hubMainRssBudget as Ut, CapRouteResolver as V, createV8Reclaimer as Vt, callWithServiceDiscovery as W, nextRssBudgetState as Wt, localEndpointPath as X, strandedMb as Xt, SocketChannel as Y, startRunnerHeapWatch as Yt, FrameDecoder as Z, DECISIVE_HEAP_SPACES as Zt, createKernelHwAccel as _, CapabilityHandle as _t, getWorkerDeviceRegistry as a, selectReportedSpaces as an, setWorkerNativeCapsChangeListener as at, __resetCapUsageRegistryForTests as b, installManifestNativeDeps as bt, setHubConnected as c, createEventPlaneReader as cn, NATIVE_PROVIDER_SERVICE_INFIX as ct, getBrokerEventBus as d, capBareAction as dt, emitHeapDiagnosticReport as en, buildUdsNativeCapProxy as et, getMoleculerEventStats as f, capServiceName as ft, AddonDepsManager as g, DeviceRegistry as gt, subscribePassthrough as h, serializeTypedArrays as ht, createUdsAddonContext as i, readHeapSpaces as in, mountNativeCapService as it, createUdsEventBridge as j, HUB_MAIN_RSS_BUDGET_MB as jt, createHubCapForwardService as k, HEAP_WATCH_WARN_RATIO as kt, EVENT_TOPIC_PREFIX as l, diffEventPlane as ln, capActionName as lt, setNodeEventInterest as m, deserializeTypedArrays as mt, adaptBrokerToCluster as n, heapSnapshotAuthorised as nn, getWorkerNativeCapProvider as nt, getOrInitReadinessRegistry as o, EVENT_PLANE_TOP_N as on, createAddonService as ot, registerEventBusService as p, parseCapAction as pt, UdsLocalTransportClient as q, shouldReclaim as qt, createAddonContext as r, heapSpaceField as rn, getWorkerNativeCapSnapshot as rt, getOrInitReadinessRegistryForClient as s, createEventPlaneMeter as sn, validateProviderRegistrations as st, installManifestPythonDeps as t, formatHeapSpaces as tn, createBrokerDeviceManagerApi as tt, clusterEventTopic as u, formatEventPlane as un, capActionSuffix as ut, resolveHwAccel as v, CapabilityUnavailableError as vt, buildLinkChain as w, createAddonDataPlaneFacility as wt, getCapUsageRegistry as x, resolveAddonClass as xt, CapUsageRegistry as y, copyBundledNativeModules as yt, AGENT_CAP_FWD_ACTION as z, createDeferredHeapWatchSink as zt };