@camstack/system 1.2.102 → 1.2.104

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 (62) hide show
  1. package/dist/addon-runner.js +15 -1
  2. package/dist/addon-runner.mjs +15 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.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 +1 -1
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  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 +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  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/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +150 -22
  36. package/dist/builtins/snapshot/index.mjs +150 -22
  37. package/dist/builtins/snapshot/snapshot-cache.d.ts +63 -0
  38. package/dist/builtins/snapshot/snapshot-resize.d.ts +33 -1
  39. package/dist/builtins/snapshot/snapshot.addon.d.ts +6 -0
  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 +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  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-3wh5LCad.mjs → dist-CAzHQ03i.mjs} +144 -21
  51. package/dist/{dist-9sC0wZw6.js → dist-DR1VmGz6.js} +144 -21
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +243 -328
  54. package/dist/index.mjs +233 -319
  55. package/dist/kernel/config-schema.d.ts +14 -10
  56. package/dist/kernel/heap-watch.d.ts +106 -7
  57. package/dist/kernel/moleculer/process-service.d.ts +34 -0
  58. package/dist/logging/log-manager.d.ts +7 -5
  59. package/dist/logging/partitioned-log-buffer.d.ts +95 -16
  60. package/dist/{manifest-python-deps-Bt_J04b0.mjs → manifest-python-deps-5ei2xVUh.mjs} +355 -1
  61. package/dist/{manifest-python-deps-BtVjvfvj.js → manifest-python-deps-GejnH--L.js} +422 -0
  62. package/package.json +1 -1
@@ -10,9 +10,363 @@ import * as os from "node:os";
10
10
  import { tmpdir } from "node:os";
11
11
  import * as fs from "node:fs";
12
12
  import { createServer } from "node:http";
13
+ import * as v8 from "node:v8";
14
+ import * as vm from "node:vm";
13
15
  import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
14
16
  import { TRPCClientError, createTRPCClient } from "@trpc/client";
15
17
  import { connect, createServer as createServer$1 } from "node:net";
18
+ //#region src/kernel/heap-watch.ts
19
+ /**
20
+ * heap-watch — hub-main's own memory, on the record, and returned to the OS.
21
+ *
22
+ * ## Why this exists
23
+ *
24
+ * hub-main used to be launched with `--max-old-space-size=1536` (docker-entrypoint;
25
+ * the cap was removed on 2026-08-02, so the live ceiling is now V8's own default —
26
+ * 4288MB in this container — and this module reads it at runtime rather than
27
+ * assuming a number). It logged NOTHING about its own heap. When it died on
28
+ * 2026-08-01/02 the only trace was V8's own stderr line:
29
+ *
30
+ * FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
31
+ *
32
+ * which carries no process name, no heap number and no history — the whole
33
+ * server restarts and the evidence is gone. Four of those in ~15h, and the one
34
+ * at 06:57:57 landed nine seconds after a viewer opened a WebRTC session, which
35
+ * is why the app sat in a permanent "loading": the hub was dying as it
36
+ * connected.
37
+ *
38
+ * `[mem]` reports already exist for forked addon runners (SIGUSR2 in
39
+ * `addon-runner.ts`) — hub-main, the one process that actually OOMs, was the
40
+ * one process with no telemetry at all.
41
+ *
42
+ * ## What it gives
43
+ *
44
+ * A heartbeat carrying the numbers that separate the two possible stories, and
45
+ * a WARN as the heap approaches the ceiling — the line that will exist BEFORE
46
+ * the next fatal instead of after it:
47
+ *
48
+ * - `heapUsedMb` climbing steadily between restarts → a leak;
49
+ * - `heapUsedMb` flat with a sharp jump under load → a burst allocation, and
50
+ * the timestamp says what was being served.
51
+ *
52
+ * `external`/`arrayBuffers` ride along because they are NOT bounded by
53
+ * `--max-old-space-size` and are most of the gap between a 1.5GB cap and the
54
+ * ~2.05GB RSS observed live: a diagnosis that only looks at the JS heap
55
+ * misreads that process by 500MB.
56
+ *
57
+ * ## And then the heartbeat explained the "off-heap leak", which was not one
58
+ *
59
+ * 2026-08-08, measured on the live hub over 1019 consecutive samples and seven
60
+ * separate boots: **hub-main's RSS is a HIGH-WATER MARK, not a leak.** In every
61
+ * one of those 1019 samples RSS was >= the running maximum of
62
+ * `heapTotal + external`, never once below it, and
63
+ * `RSS_peak - peak(heapTotal + external)` landed between 874MB and 1639MB on
64
+ * every boot — a roughly constant native floor on top of a number that only
65
+ * ever goes up. RSS tracked the highest point the process ever reached and
66
+ * stayed there: 3.3GB -> 4.5GB -> 5.2GB, and 7.8GB on the worst boot.
67
+ *
68
+ * Two things push that peak, and neither is a retained object:
69
+ *
70
+ * - the JS heap sawtooths to 2.0-2.8GB because the ceiling is 4288MB and
71
+ * nothing creates pressure before then;
72
+ * - `arrayBuffers` accumulates DEAD Buffers — up to 3.65GB — because V8 only
73
+ * sweeps them on a major GC. Eleven collapses were captured, and every
74
+ * single one coincides with `heapUsed` dropping to its ~650MB floor. No code
75
+ * released a reference at 13:23:03; a mark-compact ran.
76
+ *
77
+ * At the peak V8 has committed the pages, and **an ordinary major GC does not
78
+ * give them back.** Proven on the production hub: `smaps` showed 10,819 mapped
79
+ * 256KB pages (V8's `kRegularPageSize`) holding 2704MB while V8 itself reported
80
+ * `total_physical_size` of only 846MB — ~7,800 pages committed, resident, and
81
+ * outside V8's own accounting. One `gc({ flavor: 'last-resort' })` took 417ms
82
+ * and returned RSS 3355MB -> 1237MB, pages 10,819 -> 2,963.
83
+ *
84
+ * So the fix for the "~3GB/2h off-heap growth" is not a retainer hunt. It is to
85
+ * ask V8 for the pages back when the process is visibly holding more than it is
86
+ * using — which is what `shouldReclaim` decides and what the heartbeat now does.
87
+ *
88
+ * The cost is a stop-the-world on the hub's router process — 250-450ms when
89
+ * first measured, 654ms on the largest pass seen since — bounded to once per
90
+ * `HEAP_RECLAIM_MIN_INTERVAL_MS`. The alternative, measured, is a ratchet to
91
+ * 7.8GB and the OOM that ends it.
92
+ */
93
+ /** Heartbeat period. Slow enough to be free, fast enough that the sample before
94
+ * a fatal is recent. */
95
+ var HEAP_WATCH_INTERVAL_MS = 6e4;
96
+ /** Warn above this share of the heap ceiling. Chosen so the warning precedes
97
+ * the fatal by more than one interval under realistic growth. */
98
+ var HEAP_WATCH_WARN_RATIO = .8;
99
+ /**
100
+ * Escalate above this share of the ceiling.
101
+ *
102
+ * 0.70 is 3002MB: clear of the measured routine max (2847MB) so steady state
103
+ * never escalates, and below {@link HEAP_WATCH_WARN_RATIO} so the fast cadence
104
+ * is already running by the time the warn is reachable.
105
+ */
106
+ var HEAP_WATCH_ESCALATE_RATIO = .7;
107
+ /**
108
+ * Release escalation below this share.
109
+ *
110
+ * Hysteresis, not a second threshold: hub-main's heap sawtooths continuously,
111
+ * so a single ratio would flap the mode on every GC. 0.60 is 2573MB — a real
112
+ * retreat rather than the trough of one collection.
113
+ */
114
+ var HEAP_WATCH_DEESCALATE_RATIO = .6;
115
+ /**
116
+ * The cadence state machine, pure so the thresholds are testable without
117
+ * allocating three gigabytes to reach them.
118
+ */
119
+ function nextMode(current, usedRatio, escalateRatio = HEAP_WATCH_ESCALATE_RATIO, deescalateRatio = HEAP_WATCH_DEESCALATE_RATIO) {
120
+ if (current === "escalated") return usedRatio < deescalateRatio ? "steady" : "escalated";
121
+ return usedRatio >= escalateRatio ? "escalated" : "steady";
122
+ }
123
+ /**
124
+ * Reclaim once the process holds this many MB beyond what it is USING.
125
+ *
126
+ * Live calibration, both readings from the same hub minutes apart: 2491MB
127
+ * stranded before the reclaim, 438MB after. 438MB is this process's irreducible
128
+ * native floor (sqlite pages, libvips, sharp, thread stacks, node's own text) —
129
+ * collecting against it would buy nothing and cost a stall every minute.
130
+ *
131
+ * ## Why this came down from 1536 (2026-08-17)
132
+ *
133
+ * The trigger was originally set to fire only on an unmistakable strand. It
134
+ * does, and the pass on the record works exactly as designed:
135
+ *
136
+ * [mem] reclaim hub-main stranded=2365MB rss=4016MB→2368MB freed=1648MB took=654ms
137
+ *
138
+ * But the number it was protecting against changed. The host now runs with a
139
+ * 24GiB cgroup ceiling and **no swap**, and is OOM-killing a neighbouring VM.
140
+ * RSS this process is holding and not using is no longer merely untidy — it is
141
+ * the headroom something else needed, and the OOM is triggered by the PEAK, not
142
+ * by the average. A trigger of 1536 means the peak is allowed to stand 1.5GiB
143
+ * above the floor before anything happens.
144
+ *
145
+ * 1024 is 2.3x the measured 438MB floor, so ordinary movement around the floor
146
+ * can never trip it, and every pass that does fire still returns roughly
147
+ * `1024 - 438` ≈ **580MB minimum** — a real return for a 250-650ms stall,
148
+ * not a stall spent collecting noise.
149
+ */
150
+ var HEAP_RECLAIM_TRIGGER_MB = 1024;
151
+ /**
152
+ * Floor on how often the stop-the-world may run.
153
+ *
154
+ * Ten minutes was chosen to keep the worst case under 0.1% of the router's
155
+ * time. The worst case is not what governs the cost, though: the reclaim only
156
+ * runs when {@link shouldReclaim} says the process is genuinely stranded, and
157
+ * after a pass it is at the ~438MB floor. Live off-heap growth was measured at
158
+ * roughly 3GB/2h ≈ 25MB/min, so re-stranding the ~580MB needed to qualify again
159
+ * takes on the order of **20 minutes** — well outside any floor considered
160
+ * here. In steady state this constant therefore costs nothing at all.
161
+ *
162
+ * What it does govern is a BURST, which is the only case that matters on a host
163
+ * with no swap: the ten-minute floor let a peak that re-formed in seconds stand
164
+ * for ten minutes. Two minutes bounds that, and even the pathological case of a
165
+ * process that re-strands instantly costs 654ms per 120s — 0.55% of the
166
+ * router's time, against a ~1.5GiB peak it removes each pass.
167
+ */
168
+ var HEAP_RECLAIM_MIN_INTERVAL_MS = 12e4;
169
+ var MB = (bytes) => Math.round(bytes / 1048576);
170
+ /**
171
+ * Build a sample from raw readings. Pure so the thresholds are testable without
172
+ * allocating a gigabyte to reach them.
173
+ */
174
+ function buildHeapSample(mem, heapLimitBytes, warnRatio = HEAP_WATCH_WARN_RATIO) {
175
+ const usedRatio = heapLimitBytes > 0 ? mem.heapUsed / heapLimitBytes : 0;
176
+ return {
177
+ rssMb: MB(mem.rss),
178
+ heapUsedMb: MB(mem.heapUsed),
179
+ heapTotalMb: MB(mem.heapTotal),
180
+ heapLimitMb: MB(heapLimitBytes),
181
+ externalMb: MB(mem.external),
182
+ arrayBuffersMb: MB(mem.arrayBuffers),
183
+ usedRatio: Math.round(usedRatio * 100) / 100,
184
+ nearLimit: usedRatio >= warnRatio
185
+ };
186
+ }
187
+ var consoleSink = {
188
+ info: (line) => console.log(line),
189
+ warn: (line) => console.warn(line)
190
+ };
191
+ /**
192
+ * RSS the process is holding but not using.
193
+ *
194
+ * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
195
+ * committed at a sawtooth peak and no longer needs are counted inside heapTotal,
196
+ * and they are precisely what this number exists to see.
197
+ */
198
+ function strandedMb(s) {
199
+ return s.rssMb - s.heapUsedMb - s.externalMb;
200
+ }
201
+ /** Whether it is worth paying a stop-the-world to get those pages back. */
202
+ function shouldReclaim(s, triggerMb = HEAP_RECLAIM_TRIGGER_MB) {
203
+ return strandedMb(s) > triggerMb;
204
+ }
205
+ function isGcFunction(value) {
206
+ return typeof value === "function";
207
+ }
208
+ /**
209
+ * Build the reclaimer without requiring `--expose-gc` on argv.
210
+ *
211
+ * The flag is turned on, the function captured, and the flag turned straight
212
+ * back off — a global `gc` left reachable by application code is a footgun, and
213
+ * requiring an argv change would mean this cannot ship without a new image.
214
+ * Verified against the live hub before being written: 266ms, RSS 1453 -> 1237.
215
+ *
216
+ * Returns undefined if V8 declines, so the caller keeps a heartbeat either way.
217
+ */
218
+ function createV8Reclaimer() {
219
+ try {
220
+ v8.setFlagsFromString("--expose-gc");
221
+ const gc = vm.runInNewContext("gc");
222
+ v8.setFlagsFromString("--no-expose-gc");
223
+ if (!isGcFunction(gc)) return void 0;
224
+ return () => gc({
225
+ execution: "sync",
226
+ flavor: "last-resort"
227
+ });
228
+ } catch {
229
+ return;
230
+ }
231
+ }
232
+ function format(label, s) {
233
+ return `[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`;
234
+ }
235
+ /**
236
+ * Start the heartbeat. Returns a stop function.
237
+ *
238
+ * `unref`ed so it can never hold the process open, and every read is wrapped:
239
+ * a diagnostic that can crash the process it watches is worse than none.
240
+ */
241
+ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions) {
242
+ const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
243
+ const now = reclaimOptions?.now ?? (() => Date.now());
244
+ const triggerMb = reclaimOptions?.triggerMb ?? 1024;
245
+ const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
246
+ const fastIntervalMs = reclaimOptions?.fastIntervalMs ?? 2e3;
247
+ const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
248
+ const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
249
+ let lastReclaimAt = Number.NEGATIVE_INFINITY;
250
+ const read = () => {
251
+ const limit = reclaimOptions?.heapLimitBytes ?? v8.getHeapStatistics().heap_size_limit;
252
+ return buildHeapSample(readMemory(), limit);
253
+ };
254
+ const maybeReclaim = (sample) => {
255
+ if (reclaimOptions === void 0) return;
256
+ if (!shouldReclaim(sample, triggerMb)) return;
257
+ if (now() - lastReclaimAt < minIntervalMs) return;
258
+ lastReclaimAt = now();
259
+ const startedAt = now();
260
+ try {
261
+ reclaimOptions.reclaim();
262
+ } catch (error) {
263
+ sink.warn(`[mem] reclaim failed — ${error instanceof Error ? error.message : String(error)}`);
264
+ return;
265
+ }
266
+ const after = read();
267
+ 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`);
268
+ };
269
+ let mode = "steady";
270
+ let lastLoggedAt = Number.NEGATIVE_INFINITY;
271
+ /**
272
+ * PROBING and LOGGING are separate cadences, and that separation is the whole
273
+ * point.
274
+ *
275
+ * The 2026-08-08 fatal escaped a 60s heartbeat because the burst began and
276
+ * ended between two samples. Deciding "is the heap elevated?" only when a
277
+ * line is due would reproduce that exactly: the process would still be read
278
+ * once a minute, and a 62-second climb would still be invisible. So the timer
279
+ * runs at the FAST period always — `process.memoryUsage()` is tens of
280
+ * microseconds, i.e. free at this rate — and the slow period governs only how
281
+ * often a steady process is allowed to say so.
282
+ *
283
+ * Never probe slower than the log cadence, or a caller passing a short
284
+ * `intervalMs` would silently get the long one.
285
+ */
286
+ const probeIntervalMs = Math.min(fastIntervalMs, intervalMs);
287
+ const tick = () => {
288
+ try {
289
+ const sample = read();
290
+ const previous = mode;
291
+ mode = nextMode(previous, sample.usedRatio, escalateRatio, deescalateRatio);
292
+ const at = now();
293
+ const due = at - lastLoggedAt >= intervalMs;
294
+ if (mode === "escalated" || due) {
295
+ lastLoggedAt = at;
296
+ const line = format(label, sample);
297
+ if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
298
+ else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
299
+ else sink.info(line);
300
+ }
301
+ if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
302
+ maybeReclaim(sample);
303
+ } catch {}
304
+ };
305
+ const timer = setInterval(tick, probeIntervalMs);
306
+ timer.unref?.();
307
+ tick();
308
+ let stopped = false;
309
+ return () => {
310
+ if (stopped) return;
311
+ stopped = true;
312
+ clearInterval(timer);
313
+ };
314
+ }
315
+ /**
316
+ * Steady heartbeat period for a HEAVY addon runner.
317
+ *
318
+ * hub-main logs once a minute because it is ONE process. The heavy runners are
319
+ * ~7 per node sharing the same hub log stream; at 60s each they would put 7
320
+ * routine lines a minute in it forever, which is how a log stops being read.
321
+ * Five minutes keeps the per-runner record (a leak is still a visible climb
322
+ * across samples) at ~1.4 lines/min across the class.
323
+ *
324
+ * This slows LOGGING only. The probe keeps running at
325
+ * {@link HEAP_WATCH_FAST_INTERVAL_MS} (probe and log cadences are separate —
326
+ * see the comment inside {@link startHeapWatch}), so the reclaim gate and the
327
+ * escalated burst-sampling react exactly as fast as hub-main's.
328
+ */
329
+ var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
330
+ /**
331
+ * Start the heap watch + stranded-page reclaimer for ONE addon runner, iff it
332
+ * is a heavy one. Returns the stop function, or undefined when nothing started.
333
+ *
334
+ * ## Why heavy runners get hub-main's mechanism
335
+ *
336
+ * The media-path runners deliberately run with NO old-space ceiling
337
+ * (`maxOldSpaceMb: 0` — a flat cap turned a disk-stall backlog into
338
+ * `Ineffective mark-compacts near heap limit` and killed recording, twice on
339
+ * 2026-08-17). Uncapped, V8 feels no pressure, so a one-off burst becomes the
340
+ * runner's permanent RSS: the recorder measured 2122MB holding a ~50MB index.
341
+ * hub-main had exactly this shape and its reclaimer is proven live — 11 passes
342
+ * in 4h, 375–1652MB returned per pass at 550–680ms each. Same mechanism, same
343
+ * `shouldReclaim` stranded-over-trigger gate; never a second implementation.
344
+ *
345
+ * ## The stall, priced for the runners that get it
346
+ *
347
+ * A pass is a sub-second stop-the-world at most once per
348
+ * {@link HEAP_RECLAIM_MIN_INTERVAL_MS}, and only while >1GiB is stranded. For
349
+ * the recorder — finalizing a segment every ~300ms across 34 profile-writers —
350
+ * the pause delays WATCHER work, not video: ffmpeg children keep writing to
351
+ * disk independently, the playlist tail is the durable queue, and the next
352
+ * tick drains what accrued. For stream-broker a pass can hitch live frames
353
+ * once per qualifying window; after a pass the runner sits at its floor and
354
+ * re-stranding the trigger takes tens of minutes, so in practice the class
355
+ * pays a few passes per hour against the multi-GiB peak that was OOM-killing
356
+ * a neighbouring VM on a host with no swap.
357
+ */
358
+ function startRunnerHeapWatch(options) {
359
+ if (options.heapProfile !== "heavy") return void 0;
360
+ const intervalMs = options.intervalMs ?? 3e5;
361
+ if (options.reclaimSwitch === "off") return startHeapWatch(options.label, options.sink, intervalMs);
362
+ let reclaimOptions = options.reclaimOptions;
363
+ if (reclaimOptions === void 0) {
364
+ const reclaimer = createV8Reclaimer();
365
+ reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
366
+ }
367
+ return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions);
368
+ }
369
+ //#endregion
16
370
  //#region src/kernel/moleculer/addon-data-plane-facility.ts
17
371
  /**
18
372
  * Kernel side of the addon HTTP **data-plane** (see {@link AddonDataPlane}).
@@ -7694,4 +8048,4 @@ async function installManifestPythonDeps(declaration, addonDir, deps, logger) {
7694
8048
  await deps.installPythonRequirements(reqAbs);
7695
8049
  }
7696
8050
  //#endregion
7697
- export { buildNativeCapProxy as $, createHubCapForwardService as A, AGENT_CAP_FWD_ACTION as B, brokerTransportLink as C, runNpm as Ct, localProviderLink as D, ipcParentLink as E, createUdsLogger as F, callWithServiceDiscovery as G, CapRouteResolver as H, createUdsLoggerWithControl as I, UdsLocalTransportServer as J, createLocalTransport as K, LocalChildClient as L, createUdsEventBridge as M, createUdsEventBus as N, HUB_CAP_FWD_ACTION as O, udsChildLogToWorkerEntry as P, encodeFrame as Q, LocalChildRegistry as R, brokerCallForCap as S, resolveNpmInvocation as St, ipcChildLink as T, CapRouteError as U, AGENT_CAP_FWD_SERVICE as V, classifyCapRoute as W, localEndpointPath as X, SocketChannel as Y, FrameDecoder as Z, createKernelHwAccel as _, CapabilityHandle as _t, getWorkerDeviceRegistry as a, setWorkerNativeCapsChangeListener as at, __resetCapUsageRegistryForTests as b, installManifestNativeDeps as bt, setHubConnected as c, NATIVE_PROVIDER_SERVICE_INFIX as ct, getBrokerEventBus as d, capBareAction as dt, buildUdsNativeCapProxy as et, getMoleculerEventStats as f, capServiceName as ft, AddonDepsManager as g, DeviceRegistry as gt, subscribePassthrough as h, serializeTypedArrays as ht, createUdsAddonContext as i, mountNativeCapService as it, createParentUnownedCallHandler as j, HUB_CAP_FWD_SERVICE as k, EVENT_TOPIC_PREFIX as l, capActionName as lt, setNodeEventInterest as m, deserializeTypedArrays as mt, adaptBrokerToCluster as n, getWorkerNativeCapProvider as nt, getOrInitReadinessRegistry as o, createAddonService as ot, registerEventBusService as p, parseCapAction as pt, UdsLocalTransportClient as q, createAddonContext as r, getWorkerNativeCapSnapshot as rt, getOrInitReadinessRegistryForClient as s, validateProviderRegistrations as st, installManifestPythonDeps as t, createBrokerDeviceManagerApi as tt, clusterEventTopic as u, 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, UDS_NO_ROUTE_PREFIX as z };
8051
+ export { buildNativeCapProxy as $, createHubCapForwardService as A, buildHeapSample as At, AGENT_CAP_FWD_ACTION as B, brokerTransportLink as C, runNpm as Ct, localProviderLink as D, HEAP_WATCH_INTERVAL_MS as Dt, ipcParentLink as E, HEAP_RECLAIM_TRIGGER_MB as Et, createUdsLogger as F, strandedMb as Ft, callWithServiceDiscovery as G, CapRouteResolver as H, createUdsLoggerWithControl as I, UdsLocalTransportServer as J, createLocalTransport as K, LocalChildClient as L, createUdsEventBridge as M, shouldReclaim as Mt, createUdsEventBus as N, startHeapWatch as Nt, HUB_CAP_FWD_ACTION as O, HEAP_WATCH_WARN_RATIO as Ot, udsChildLogToWorkerEntry as P, startRunnerHeapWatch as Pt, encodeFrame as Q, LocalChildRegistry as R, brokerCallForCap as S, resolveNpmInvocation as St, ipcChildLink as T, HEAP_RECLAIM_MIN_INTERVAL_MS as Tt, CapRouteError as U, AGENT_CAP_FWD_SERVICE as V, classifyCapRoute as W, localEndpointPath as X, SocketChannel as Y, FrameDecoder as Z, createKernelHwAccel as _, CapabilityHandle as _t, getWorkerDeviceRegistry as a, setWorkerNativeCapsChangeListener as at, __resetCapUsageRegistryForTests as b, installManifestNativeDeps as bt, setHubConnected as c, NATIVE_PROVIDER_SERVICE_INFIX as ct, getBrokerEventBus as d, capBareAction as dt, buildUdsNativeCapProxy as et, getMoleculerEventStats as f, capServiceName as ft, AddonDepsManager as g, DeviceRegistry as gt, subscribePassthrough as h, serializeTypedArrays as ht, createUdsAddonContext as i, mountNativeCapService as it, createParentUnownedCallHandler as j, createV8Reclaimer as jt, HUB_CAP_FWD_SERVICE as k, RUNNER_HEAP_WATCH_INTERVAL_MS as kt, EVENT_TOPIC_PREFIX as l, capActionName as lt, setNodeEventInterest as m, deserializeTypedArrays as mt, adaptBrokerToCluster as n, getWorkerNativeCapProvider as nt, getOrInitReadinessRegistry as o, createAddonService as ot, registerEventBusService as p, parseCapAction as pt, UdsLocalTransportClient as q, createAddonContext as r, getWorkerNativeCapSnapshot as rt, getOrInitReadinessRegistryForClient as s, validateProviderRegistrations as st, installManifestPythonDeps as t, createBrokerDeviceManagerApi as tt, clusterEventTopic as u, 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, UDS_NO_ROUTE_PREFIX as z };