@camstack/addon-pipeline 1.2.93 → 1.2.95

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 (36) hide show
  1. package/dist/{addon-utils-CTRpEiSd.js → addon-utils-UMWiMAxR.js} +1 -1
  2. package/dist/audio-analyzer/index.js +58 -4
  3. package/dist/audio-analyzer/index.mjs +56 -2
  4. package/dist/detection-pipeline/index.js +194 -6
  5. package/dist/detection-pipeline/index.mjs +191 -3
  6. package/dist/{dist-6POVIYmC.mjs → dist-C49o1k7u.mjs} +359 -21
  7. package/dist/{dist-D9To_r_d.js → dist-hyryByiw.js} +376 -20
  8. package/dist/{event-loop-stall-monitor-C-VvWOxS.mjs → event-loop-stall-monitor-CfxZBgNi.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-CGacbzXq.js → event-loop-stall-monitor-DPNTzGOT.js} +1 -1
  10. package/dist/{lazy-sharp-C3zOnmiU.js → lazy-sharp-Dxg6UeW-.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +195 -14
  14. package/dist/pipeline-runner/index.mjs +194 -13
  15. package/dist/process-memory-B9-UD7Rp.js +78 -0
  16. package/dist/process-memory-DXOB6eHX.mjs +66 -0
  17. package/dist/recorder/index.js +943 -109
  18. package/dist/recorder/index.mjs +942 -108
  19. package/dist/session-decode/decode-worker-child.js +4 -4
  20. package/dist/session-decode/decode-worker-child.mjs +3 -3
  21. package/dist/stream-broker/_stub.js +1 -1
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2woB6bY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CF-c0cDZ.mjs} +3 -3
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BjvmJK_d.mjs +26 -0
  24. package/dist/stream-broker/{hostInit-VCURcZLt.mjs → hostInit-Cpcf2hDB.mjs} +3 -3
  25. package/dist/stream-broker/index.js +25 -7
  26. package/dist/stream-broker/index.mjs +25 -7
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-Bq28qobd.js → worker-protocol-DR_a77D5.js} +1 -1
  29. package/dist/{worker-protocol-PzAKg8mC.mjs → worker-protocol-DqQ2GI4M.mjs} +1 -1
  30. package/package.json +13 -7
  31. package/python/inference_pool.py +178 -3
  32. package/python/test_inference_pool_backpressure.py +7 -1
  33. package/python/test_inference_pool_memstats.py +146 -0
  34. package/dist/node-topology-platform-BkR_k6WT.mjs +0 -15
  35. package/dist/node-topology-platform-CFZ7F4xW.js +0 -20
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dut56EeQ.mjs +0 -26
@@ -22,7 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  let node_crypto = require("node:crypto");
25
- //#region ../types/dist/event-category-C0lyLd5U.mjs
25
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
26
26
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
27
27
  EventCategory["SystemBoot"] = "system.boot";
28
28
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -61,6 +61,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
61
61
  EventCategory["AddonInstalled"] = "addon.installed";
62
62
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
63
63
  EventCategory["AddonCrashed"] = "addon.crashed";
64
+ /**
65
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
66
+ *
67
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
68
+ * respawned"); this is the one that never resolves itself. Emitted exactly
69
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
70
+ * addons it hosted and the crash count that tripped the breaker.
71
+ *
72
+ * It exists because a runner marked terminally `failed` used to be SILENT:
73
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
74
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
75
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
76
+ * operator's phone told him. The Notification Center's system-event intake
77
+ * consumes this category and maps it to the `addon-crash-loop` kind.
78
+ */
79
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
64
80
  EventCategory["AddonError"] = "addon.error";
65
81
  EventCategory["AddonPageReady"] = "addon.page-ready";
66
82
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -16094,6 +16110,7 @@ var NcSystemEventKindSchema = _enum([
16094
16110
  "node-offline",
16095
16111
  "node-inference-unavailable",
16096
16112
  "detection-blind",
16113
+ "addon-crash-loop",
16097
16114
  "addon-update-available",
16098
16115
  "server-update-available",
16099
16116
  "alarm-triggered",
@@ -21996,20 +22013,6 @@ var VectorStatsResultSchema = object({
21996
22013
  exact: boolean()
21997
22014
  });
21998
22015
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
21999
- /**
22000
- * `videoclips` — the unified, navigable-clip surface for a camera.
22001
- *
22002
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
22003
- * provider per device, substitutable. The DEFAULT provider (registered by
22004
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
22005
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
22006
- * is a time-WINDOW over existing footage, never a separate file. A camera that
22007
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
22008
- * wrapper provider for its device and serve its own clip catalog + URLs.
22009
- *
22010
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
22011
- * temporal overlap, so the API never decides `continuous` vs `events`.
22012
- */
22013
22016
  var ClipSchema = object({
22014
22017
  /** Opaque, provider-namespaced id. The default provider encodes the time
22015
22018
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -22035,12 +22038,41 @@ var ClipSchema = object({
22035
22038
  * of the event that owns `kind` (object > motion > audio). Do not extract
22036
22039
  * a keyframe from the recorded segments. Other providers (onboard, HKSV)
22037
22040
  * mint their own stills.
22041
+ *
22042
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
22043
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
22044
+ * Absent is meaningful — "this visit has no event still" — never "we did not
22045
+ * look". Stamping it from a URL template made 35% of one camera's clips point
22046
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
22047
+ * A read that FAILS drops the claim; it never invents it.
22038
22048
  */
22039
22049
  thumbnail: string().optional(),
22040
- /** Analytics event ids that overlap this visit. Empty on footage-only clips.
22041
- * The default provider's visit grain puts many motion heartbeats on one clip
22042
- * instead of minting one clip per marker. */
22050
+ /**
22051
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
22052
+ * can be decoded. Present whenever the visit came from recorded availability;
22053
+ * absent on a per-event padded window (there is no footage to promise).
22054
+ *
22055
+ * This is not a thumbnail and not a second byte path: it is the argument to
22056
+ * the recorder's existing still route. The surface — never the provider —
22057
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
22058
+ * contiguous range, not of the visit: a visit spans its holes by
22059
+ * construction, so a naive midpoint lands in dead air.
22060
+ */
22061
+ stillAtMs: number().optional(),
22062
+ /**
22063
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
22064
+ * first within kind (object → motion → audio), capped at
22065
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
22066
+ *
22067
+ * Bounded because it is not a payload the surface pages through: one visit on
22068
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
22069
+ * camera-day. Read {@link eventCount} for the true total.
22070
+ */
22043
22071
  eventIds: array(string()).optional(),
22072
+ /** How many analytics events actually overlap this visit. Differs from
22073
+ * `eventIds.length` exactly when the sample was capped — so a truncated
22074
+ * list is never mistaken for a quiet visit. */
22075
+ eventCount: number().int().nonnegative().optional(),
22044
22076
  /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
22045
22077
  * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
22046
22078
  * bar keeps showing them via `recording.getAvailability`. */
@@ -22591,7 +22623,14 @@ var SearchResultSchema = object({
22591
22623
  });
22592
22624
  var AutoUpdateSettingsSchema = object({
22593
22625
  channel: ChannelSchema,
22594
- intervalSeconds: number()
22626
+ intervalSeconds: number(),
22627
+ /**
22628
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
22629
+ * `channel`: the poller runs while auto-apply is `off`, because being told
22630
+ * about a publish and installing it are different decisions. Clamped
22631
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
22632
+ */
22633
+ updateCheckIntervalSeconds: number()
22595
22634
  });
22596
22635
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
22597
22636
  var RestartAddonResultSchema = unknown();
@@ -22732,7 +22771,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
22732
22771
  auth: "admin"
22733
22772
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
22734
22773
  channel: ChannelSchema,
22735
- intervalSeconds: number().min(300).max(86400).optional()
22774
+ intervalSeconds: number().min(300).max(86400).optional(),
22775
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
22776
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
22736
22777
  }), unknown(), {
22737
22778
  kind: "mutation",
22738
22779
  auth: "admin"
@@ -36424,6 +36465,303 @@ function maskUrlCredentials(rawUrl) {
36424
36465
  return rawUrl;
36425
36466
  }
36426
36467
  }
36468
+ var MB = 1024 * 1024;
36469
+ var DEFAULT_POOL_MEMORY_POLICY = {
36470
+ sampleIntervalMs: 6e4,
36471
+ baselineSettleSamples: 5,
36472
+ baselineSampleCount: 3,
36473
+ restartMultiple: 4,
36474
+ floorBytes: 1024 * MB,
36475
+ ceilingBytes: 3072 * MB,
36476
+ cooldownMs: 30 * 6e4,
36477
+ maxRestartsPerWindow: 6,
36478
+ restartWindowMs: 1440 * 6e4
36479
+ };
36480
+ /**
36481
+ * Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
36482
+ * absent values keep the default — an operator typo must never disable the
36483
+ * bound or set it to zero.
36484
+ */
36485
+ function resolvePoolMemoryPolicy(env) {
36486
+ const num = (key, fallback, min) => {
36487
+ const raw = env[key];
36488
+ if (raw === void 0) return fallback;
36489
+ const parsed = Number(raw);
36490
+ return Number.isFinite(parsed) && parsed >= min ? parsed : fallback;
36491
+ };
36492
+ const d = DEFAULT_POOL_MEMORY_POLICY;
36493
+ return {
36494
+ sampleIntervalMs: num("CAMSTACK_POOL_MEM_INTERVAL_MS", d.sampleIntervalMs, 5e3),
36495
+ baselineSettleSamples: d.baselineSettleSamples,
36496
+ baselineSampleCount: d.baselineSampleCount,
36497
+ restartMultiple: num("CAMSTACK_POOL_MEM_MULTIPLE", d.restartMultiple, 1.5),
36498
+ floorBytes: num("CAMSTACK_POOL_MEM_FLOOR_MB", d.floorBytes / MB, 128) * MB,
36499
+ ceilingBytes: num("CAMSTACK_POOL_MEM_CEILING_MB", d.ceilingBytes / MB, 256) * MB,
36500
+ cooldownMs: num("CAMSTACK_POOL_MEM_COOLDOWN_MS", d.cooldownMs, 6e4),
36501
+ maxRestartsPerWindow: num("CAMSTACK_POOL_MEM_MAX_RESTARTS", d.maxRestartsPerWindow, 1),
36502
+ restartWindowMs: num("CAMSTACK_POOL_MEM_RESTART_WINDOW_MS", d.restartWindowMs, 6e4)
36503
+ };
36504
+ }
36505
+ /** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
36506
+ * Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
36507
+ function parseProcStatus(text) {
36508
+ const kb = (label) => {
36509
+ const match = text.match(new RegExp(`^${label}:\\s+(\\d+)\\s*kB`, "m"));
36510
+ return match ? Number(match[1]) * 1024 : null;
36511
+ };
36512
+ const rssBytes = kb("VmRSS");
36513
+ if (rssBytes === null) return null;
36514
+ const threadsMatch = text.match(/^Threads:\s+(\d+)/m);
36515
+ return {
36516
+ rssBytes,
36517
+ vmBytes: kb("VmSize") ?? 0,
36518
+ hwmBytes: kb("VmHWM") ?? 0,
36519
+ swapBytes: kb("VmSwap") ?? 0,
36520
+ threads: threadsMatch ? Number(threadsMatch[1]) : 0
36521
+ };
36522
+ }
36523
+ function initialPoolMemoryState() {
36524
+ return {
36525
+ settleSeen: 0,
36526
+ baselineWindow: [],
36527
+ baselineBytes: null,
36528
+ restartsAt: [],
36529
+ lastRestartAt: null
36530
+ };
36531
+ }
36532
+ function median(values) {
36533
+ const sorted = [...values].sort((a, b) => a - b);
36534
+ return sorted[Math.floor(sorted.length / 2)];
36535
+ }
36536
+ function advanceBaseline(state, rssBytes, policy) {
36537
+ if (state.baselineBytes !== null) return state;
36538
+ if (state.settleSeen < policy.baselineSettleSamples) return {
36539
+ ...state,
36540
+ settleSeen: state.settleSeen + 1
36541
+ };
36542
+ const window = [...state.baselineWindow, rssBytes];
36543
+ if (window.length < policy.baselineSampleCount) return {
36544
+ ...state,
36545
+ baselineWindow: window
36546
+ };
36547
+ return {
36548
+ ...state,
36549
+ baselineWindow: window,
36550
+ baselineBytes: median(window)
36551
+ };
36552
+ }
36553
+ /** The one place a pool's restart threshold is computed. */
36554
+ function poolMemoryThreshold(baselineBytes, policy) {
36555
+ if (baselineBytes === null) return policy.ceilingBytes;
36556
+ return Math.min(policy.ceilingBytes, Math.max(policy.floorBytes, policy.restartMultiple * baselineBytes));
36557
+ }
36558
+ function pruneRestarts(restartsAt, nowMs, policy) {
36559
+ return restartsAt.filter((t) => nowMs - t < policy.restartWindowMs);
36560
+ }
36561
+ /**
36562
+ * Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
36563
+ * The caller performs (and logs) the restart, then commits it via
36564
+ * {@link commitWatchdogRestart}.
36565
+ */
36566
+ function evaluatePoolMemory(state, rssBytes, nowMs, policy) {
36567
+ const next = advanceBaseline(state, rssBytes, policy);
36568
+ const thresholdBytes = poolMemoryThreshold(next.baselineBytes, policy);
36569
+ if (rssBytes <= thresholdBytes) return {
36570
+ state: next,
36571
+ action: next.baselineBytes === null ? "baseline-pending" : "ok",
36572
+ baselineBytes: next.baselineBytes,
36573
+ thresholdBytes
36574
+ };
36575
+ const inWindow = pruneRestarts(next.restartsAt, nowMs, policy);
36576
+ const pruned = {
36577
+ ...next,
36578
+ restartsAt: inWindow
36579
+ };
36580
+ if (inWindow.length >= policy.maxRestartsPerWindow) return {
36581
+ state: pruned,
36582
+ action: "exhausted",
36583
+ baselineBytes: next.baselineBytes,
36584
+ thresholdBytes
36585
+ };
36586
+ if (pruned.lastRestartAt !== null && nowMs - pruned.lastRestartAt < policy.cooldownMs) return {
36587
+ state: pruned,
36588
+ action: "cooldown",
36589
+ baselineBytes: next.baselineBytes,
36590
+ thresholdBytes
36591
+ };
36592
+ return {
36593
+ state: pruned,
36594
+ action: "restart",
36595
+ baselineBytes: next.baselineBytes,
36596
+ thresholdBytes
36597
+ };
36598
+ }
36599
+ /** Record a performed watchdog restart: budget consumed, baseline reset —
36600
+ * the recreated pool is a NEW process and gets a fresh settle window. */
36601
+ function commitWatchdogRestart(state, nowMs) {
36602
+ return {
36603
+ ...initialPoolMemoryState(),
36604
+ restartsAt: [...state.restartsAt, nowMs],
36605
+ lastRestartAt: nowMs
36606
+ };
36607
+ }
36608
+ /** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
36609
+ * kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
36610
+ * restart budget is kept: it bounds the WATCHDOG, not the pool. */
36611
+ function resetPoolBaseline(state) {
36612
+ return {
36613
+ ...initialPoolMemoryState(),
36614
+ restartsAt: state.restartsAt,
36615
+ lastRestartAt: state.lastRestartAt
36616
+ };
36617
+ }
36618
+ /**
36619
+ * Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
36620
+ * several pools at once (e.g. after a model rollout grew every baseline)
36621
+ * would take detection down on every camera simultaneously; one per sweep
36622
+ * means the survivors keep serving while the worst offender reloads, and the
36623
+ * next sweep (one interval later) takes the next one.
36624
+ */
36625
+ function pickRestartCandidate(candidates) {
36626
+ if (candidates.length === 0) return null;
36627
+ let worst = candidates[0];
36628
+ for (const c of candidates) if (c.rssBytes / c.thresholdBytes > worst.rssBytes / worst.thresholdBytes) worst = c;
36629
+ return worst.key;
36630
+ }
36631
+ var PoolMemoryWatchdog = class {
36632
+ pools = /* @__PURE__ */ new Map();
36633
+ timer = null;
36634
+ sweeping = false;
36635
+ stopped = false;
36636
+ opts;
36637
+ now;
36638
+ setTimer;
36639
+ clearTimer;
36640
+ constructor(opts) {
36641
+ this.opts = opts;
36642
+ this.now = opts.now ?? (() => Date.now());
36643
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
36644
+ this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
36645
+ }
36646
+ start() {
36647
+ if (this.timer || this.stopped) return;
36648
+ this.arm();
36649
+ }
36650
+ stop() {
36651
+ this.stopped = true;
36652
+ if (this.timer) {
36653
+ this.clearTimer(this.timer);
36654
+ this.timer = null;
36655
+ }
36656
+ }
36657
+ arm() {
36658
+ const timer = this.setTimer(() => {
36659
+ this.sweep().finally(() => {
36660
+ if (!this.stopped) this.arm();
36661
+ });
36662
+ }, this.opts.policy.sampleIntervalMs);
36663
+ if (typeof timer.unref === "function") timer.unref();
36664
+ this.timer = timer;
36665
+ }
36666
+ /** One sweep: sample → log every pool → restart at most one. Public so the
36667
+ * loop is testable without fake global timers. */
36668
+ async sweep() {
36669
+ if (this.sweeping) return;
36670
+ this.sweeping = true;
36671
+ try {
36672
+ await this.doSweep();
36673
+ } catch (err) {
36674
+ this.opts.log.warn("pool memory sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
36675
+ } finally {
36676
+ this.sweeping = false;
36677
+ }
36678
+ }
36679
+ async doSweep() {
36680
+ const nowMs = this.now();
36681
+ const samples = await this.opts.sample();
36682
+ const candidates = [];
36683
+ const verdictByKey = /* @__PURE__ */ new Map();
36684
+ for (const t of samples) {
36685
+ const tracked = this.pools.get(t.key);
36686
+ let state = tracked?.state ?? initialPoolMemoryState();
36687
+ if (tracked && tracked.pids.length > 0 && !samePids(tracked.pids, t.pids)) {
36688
+ this.opts.log.warn("pool process changed outside the watchdog — re-baselining", { meta: {
36689
+ poolKey: t.key,
36690
+ previousPids: tracked.pids,
36691
+ pids: t.pids
36692
+ } });
36693
+ state = resetPoolBaseline(state);
36694
+ }
36695
+ const verdict = evaluatePoolMemory(state, t.rssBytes, nowMs, this.opts.policy);
36696
+ this.pools.set(t.key, {
36697
+ state: verdict.state,
36698
+ pids: t.pids
36699
+ });
36700
+ verdictByKey.set(t.key, verdict);
36701
+ this.opts.log.info("pool memory", { meta: {
36702
+ poolKey: t.key,
36703
+ pids: t.pids,
36704
+ rssMb: Math.round(t.rssBytes / MB),
36705
+ baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
36706
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
36707
+ action: verdict.action,
36708
+ ...t.meta
36709
+ } });
36710
+ if (verdict.action === "restart") candidates.push({
36711
+ key: t.key,
36712
+ rssBytes: t.rssBytes,
36713
+ thresholdBytes: verdict.thresholdBytes
36714
+ });
36715
+ else if (verdict.action === "cooldown") this.opts.log.warn("pool over memory threshold but inside restart cooldown", { meta: {
36716
+ poolKey: t.key,
36717
+ rssMb: Math.round(t.rssBytes / MB),
36718
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
36719
+ cooldownMs: this.opts.policy.cooldownMs
36720
+ } });
36721
+ else if (verdict.action === "exhausted") this.opts.log.error("pool memory watchdog gave up — restart budget exhausted, pool leaks faster than restarts can pay for; needs a heap profile (py-spy / tracemalloc)", { meta: {
36722
+ poolKey: t.key,
36723
+ rssMb: Math.round(t.rssBytes / MB),
36724
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
36725
+ maxRestartsPerWindow: this.opts.policy.maxRestartsPerWindow,
36726
+ restartWindowMs: this.opts.policy.restartWindowMs
36727
+ } });
36728
+ }
36729
+ for (const key of [...this.pools.keys()]) if (!samples.some((t) => t.key === key)) this.pools.delete(key);
36730
+ const pickedKey = pickRestartCandidate(candidates);
36731
+ if (pickedKey === null) return;
36732
+ const picked = candidates.find((c) => c.key === pickedKey);
36733
+ const verdict = verdictByKey.get(pickedKey);
36734
+ const tracked = this.pools.get(pickedKey);
36735
+ const restartsInWindow = tracked.state.restartsAt.length + 1;
36736
+ this.opts.log.warn("pool memory watchdog restarting pool — RSS over threshold", { meta: {
36737
+ poolKey: pickedKey,
36738
+ pids: tracked.pids,
36739
+ rssMb: Math.round(picked.rssBytes / MB),
36740
+ thresholdMb: Math.round(picked.thresholdBytes / MB),
36741
+ baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
36742
+ restartsInWindow,
36743
+ deferredCandidates: candidates.filter((c) => c.key !== pickedKey).map((c) => c.key)
36744
+ } });
36745
+ try {
36746
+ await this.opts.restart(pickedKey);
36747
+ this.pools.set(pickedKey, {
36748
+ state: commitWatchdogRestart(tracked.state, this.now()),
36749
+ pids: []
36750
+ });
36751
+ } catch (err) {
36752
+ this.opts.log.error("pool memory watchdog restart failed", { meta: {
36753
+ poolKey: pickedKey,
36754
+ error: err instanceof Error ? err.message : String(err)
36755
+ } });
36756
+ }
36757
+ }
36758
+ };
36759
+ function samePids(a, b) {
36760
+ if (a.length !== b.length) return false;
36761
+ const sortedA = [...a].sort((x, y) => x - y);
36762
+ const sortedB = [...b].sort((x, y) => x - y);
36763
+ return sortedA.every((v, i) => v === sortedB[i]);
36764
+ }
36427
36765
  /**
36428
36766
  * Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
36429
36767
  * drain() returns up to maxCount items in FIFO order and removes them.
@@ -36911,6 +37249,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
36911
37249
  return OpsLogEntrySchema;
36912
37250
  }
36913
37251
  });
37252
+ Object.defineProperty(exports, "PoolMemoryWatchdog", {
37253
+ enumerable: true,
37254
+ get: function() {
37255
+ return PoolMemoryWatchdog;
37256
+ }
37257
+ });
36914
37258
  Object.defineProperty(exports, "RATE_CONTROL_RELAXED", {
36915
37259
  enumerable: true,
36916
37260
  get: function() {
@@ -37199,6 +37543,12 @@ Object.defineProperty(exports, "parseJsonUnknown", {
37199
37543
  return parseJsonUnknown;
37200
37544
  }
37201
37545
  });
37546
+ Object.defineProperty(exports, "parseProcStatus", {
37547
+ enumerable: true,
37548
+ get: function() {
37549
+ return parseProcStatus;
37550
+ }
37551
+ });
37202
37552
  Object.defineProperty(exports, "parseProfileBrokerId", {
37203
37553
  enumerable: true,
37204
37554
  get: function() {
@@ -37253,6 +37603,12 @@ Object.defineProperty(exports, "resolveEgressDecodeHwAccel", {
37253
37603
  return resolveEgressDecodeHwAccel;
37254
37604
  }
37255
37605
  });
37606
+ Object.defineProperty(exports, "resolvePoolMemoryPolicy", {
37607
+ enumerable: true,
37608
+ get: function() {
37609
+ return resolvePoolMemoryPolicy;
37610
+ }
37611
+ });
37256
37612
  Object.defineProperty(exports, "resolveRecordingProfiles", {
37257
37613
  enumerable: true,
37258
37614
  get: function() {
@@ -1,4 +1,4 @@
1
- import { H as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-6POVIYmC.mjs";
1
+ import { U as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-C49o1k7u.mjs";
2
2
  import { PerformanceObserver } from "node:perf_hooks";
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-D9To_r_d.js");
1
+ const require_dist = require("./dist-hyryByiw.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-D9To_r_d.js");
1
+ const require_dist = require("./dist-hyryByiw.js");
2
2
  let node_url = require("node:url");
3
3
  let node_module = require("node:module");
4
4
  let node_path = require("node:path");
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-D9To_r_d.js");
6
- const require_lazy_sharp = require("../lazy-sharp-C3zOnmiU.js");
5
+ const require_dist = require("../dist-hyryByiw.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-Dxg6UeW-.js");
7
7
  let node_path = require("node:path");
8
8
  let node_fs = require("node:fs");
9
9
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { G as motionDetectionCapability, V as evaluateZoneRules, dt as BaseAddon, gt as hydrateSchema, mt as DeviceType } from "../dist-6POVIYmC.mjs";
1
+ import { H as evaluateZoneRules, K as motionDetectionCapability, _t as DeviceType, mt as BaseAddon, yt as hydrateSchema } from "../dist-C49o1k7u.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
3
3
  import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";