@camstack/addon-pipeline 1.2.135 → 1.2.136

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.
@@ -8734,6 +8734,38 @@ var RESTART_MAX_MS = 1e4;
8734
8734
  /** A process that ran at least this long is "stable" → reset the restart count,
8735
8735
  * so sporadic blips over a long lifetime never accumulate into a give-up. */
8736
8736
  var STABLE_RUN_MS = 3e4;
8737
+ /**
8738
+ * The window the SECOND budget counts over — and it counts EVERY restart,
8739
+ * whatever that run lasted.
8740
+ *
8741
+ * `STABLE_RUN_MS` above is a reset, and a reset is a blind spot: measured on
8742
+ * 2026-08-27, `SegmentWriter restarting` fired **1 814 times in 12 h** and
8743
+ * almost every `ranMs` was over 30 s (122 620 / 165 255 / 345 420 / 394 522 ms
8744
+ * are four consecutive real samples), so `attempt` never left 1, `onGaveUp`
8745
+ * never fired, and `recoverWriterGaveUp` never ran. The storm could have lasted
8746
+ * forever without a single line saying anything was wrong.
8747
+ *
8748
+ * A window cannot be zeroed by a long run, which is the whole point. Modelled
8749
+ * on `CrashSupervisor` (kernel, D6): timestamps age out of `windowMs`, and the
8750
+ * count inside it is the budget.
8751
+ */
8752
+ var RESTART_WINDOW_MS = 60 * 6e4;
8753
+ /**
8754
+ * Restarts inside {@link RESTART_WINDOW_MS} before the writer stops respawning
8755
+ * and escalates to the controller.
8756
+ *
8757
+ * 15/hour, from the measured distribution. Per writer over the same 12 h:
8758
+ * 1441/high 549 (**45.8/h**), 615/high 241 (20.1/h), 618/high 217 (18.1/h),
8759
+ * 592/high 211 (17.6/h), 590/high 195 (16.3/h) — every one of them a storm and
8760
+ * every one of them silent — against `low` profiles at 4–15 per 12 h (≤1.3/h),
8761
+ * which is what a healthy writer looks like. 15 separates them with room for a
8762
+ * camera that blips hourly and is otherwise fine.
8763
+ *
8764
+ * It never shadows {@link MAX_RESTARTS}: 10 CONSECUTIVE rapid failures happen
8765
+ * inside ~90 s, so the rapid breaker still trips first on a hard failure. This
8766
+ * one exists for the slow storm the rapid breaker structurally cannot see.
8767
+ */
8768
+ var MAX_RESTARTS_IN_WINDOW = 15;
8737
8769
  /** Grace between the SIGTERM in stop() and the escalated SIGKILL. */
8738
8770
  var KILL_GRACE_MS = 500;
8739
8771
  /** Supervises one passthrough ffmpeg for a single (camera, profile). */
@@ -8743,6 +8775,12 @@ var SegmentWriter = class {
8743
8775
  proc = null;
8744
8776
  stopped = false;
8745
8777
  restarts = 0;
8778
+ /**
8779
+ * Epoch-ms of every termination still inside {@link RESTART_WINDOW_MS}.
8780
+ * Replaced, never mutated in place, so a reader can never observe a
8781
+ * half-pruned window.
8782
+ */
8783
+ restartWindow = [];
8746
8784
  restartTimer = null;
8747
8785
  startedAt = 0;
8748
8786
  exitWait = null;
@@ -8776,10 +8814,11 @@ var SegmentWriter = class {
8776
8814
  settled = true;
8777
8815
  this.onTermination(termination, stderrTail);
8778
8816
  };
8779
- proc.on("exit", (code) => {
8817
+ proc.on("exit", (code, signal) => {
8780
8818
  terminate({
8781
8819
  reason: "exit",
8782
- exitCode: typeof code === "number" ? code : null
8820
+ exitCode: typeof code === "number" ? code : null,
8821
+ signal: typeof signal === "string" ? signal : null
8783
8822
  });
8784
8823
  });
8785
8824
  proc.on("error", (raw) => {
@@ -8795,32 +8834,51 @@ var SegmentWriter = class {
8795
8834
  /**
8796
8835
  * The ONE restart policy, reached from both `exit` and `error`.
8797
8836
  *
8798
- * `deps.logger` is the device-scoped child the controller binds
8799
- * (`logger.withTags({ deviceId })`), so every line below carries
8800
- * `tags: { deviceId }` "why is 617 worse than 615" is always asked per
8801
- * camera, and a writer that stops recording one camera must be greppable by
8802
- * that camera.
8837
+ * Every line below carries `tags: { deviceId }` from `deps.deviceId` "why
8838
+ * is 617 worse than 615" is always asked per camera, and a writer that stops
8839
+ * recording one camera must be greppable by that camera.
8803
8840
  */
8804
8841
  onTermination(termination, stderrTail) {
8805
8842
  this.resolveExit?.();
8806
8843
  this.resolveExit = null;
8807
8844
  this.proc = null;
8808
8845
  if (this.stopped) return;
8809
- const ranMs = Date.now() - this.startedAt;
8846
+ const now = Date.now();
8847
+ const ranMs = now - this.startedAt;
8810
8848
  if (ranMs >= STABLE_RUN_MS) this.restarts = 0;
8811
- if (termination.reason === "spawn-error") {
8812
- this.deps.logger.warn("SegmentWriter ffmpeg spawn failed", { meta: {
8849
+ this.restartWindow = [...this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS), now];
8850
+ const restartsInWindow = this.restartWindow.length;
8851
+ if (termination.reason === "exit") this.deps.logger.warn("SegmentWriter ffmpeg exited", {
8852
+ tags: { deviceId: this.deps.deviceId },
8853
+ meta: {
8813
8854
  outDir: this.cfg.outDir,
8814
- errorCode: termination.errorCode,
8815
- error: termination.message,
8816
- permanent: termination.permanent
8817
- } });
8818
- if (termination.permanent) {
8819
- this.deps.logger.error("SegmentWriter giving up: ffmpeg cannot be executed on this node", { meta: {
8855
+ exitCode: termination.exitCode,
8856
+ signal: termination.signal,
8857
+ ranMs,
8858
+ restartsInWindow,
8859
+ windowMs: RESTART_WINDOW_MS,
8860
+ stderrTail: stderrTail.join(" | ")
8861
+ }
8862
+ });
8863
+ if (termination.reason === "spawn-error") {
8864
+ this.deps.logger.warn("SegmentWriter ffmpeg spawn failed", {
8865
+ tags: { deviceId: this.deps.deviceId },
8866
+ meta: {
8820
8867
  outDir: this.cfg.outDir,
8821
8868
  errorCode: termination.errorCode,
8822
- error: termination.message
8823
- } });
8869
+ error: termination.message,
8870
+ permanent: termination.permanent
8871
+ }
8872
+ });
8873
+ if (termination.permanent) {
8874
+ this.deps.logger.error("SegmentWriter giving up: ffmpeg cannot be executed on this node", {
8875
+ tags: { deviceId: this.deps.deviceId },
8876
+ meta: {
8877
+ outDir: this.cfg.outDir,
8878
+ errorCode: termination.errorCode,
8879
+ error: termination.message
8880
+ }
8881
+ });
8824
8882
  this.stopped = true;
8825
8883
  this.deps.onGaveUp?.();
8826
8884
  return;
@@ -8828,25 +8886,51 @@ var SegmentWriter = class {
8828
8886
  this.deps.onResourcePressure?.();
8829
8887
  }
8830
8888
  if (this.restarts >= MAX_RESTARTS) {
8831
- this.deps.logger.warn("SegmentWriter giving up after max restarts", { meta: {
8832
- outDir: this.cfg.outDir,
8833
- reason: termination.reason,
8834
- code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8835
- stderrTail: stderrTail.join(" | ")
8836
- } });
8889
+ this.deps.logger.warn("SegmentWriter giving up after max restarts", {
8890
+ tags: { deviceId: this.deps.deviceId },
8891
+ meta: {
8892
+ outDir: this.cfg.outDir,
8893
+ reason: termination.reason,
8894
+ code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8895
+ stderrTail: stderrTail.join(" | ")
8896
+ }
8897
+ });
8898
+ this.stopped = true;
8899
+ this.deps.onGaveUp?.();
8900
+ return;
8901
+ }
8902
+ if (restartsInWindow >= MAX_RESTARTS_IN_WINDOW) {
8903
+ this.deps.logger.warn("SegmentWriter giving up: restart storm", {
8904
+ tags: { deviceId: this.deps.deviceId },
8905
+ meta: {
8906
+ outDir: this.cfg.outDir,
8907
+ restartsInWindow,
8908
+ windowMs: RESTART_WINDOW_MS,
8909
+ budget: MAX_RESTARTS_IN_WINDOW,
8910
+ attempt: this.restarts,
8911
+ ranMs,
8912
+ reason: termination.reason,
8913
+ code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8914
+ stderrTail: stderrTail.join(" | ")
8915
+ }
8916
+ });
8837
8917
  this.stopped = true;
8838
8918
  this.deps.onGaveUp?.();
8839
8919
  return;
8840
8920
  }
8841
8921
  this.restarts++;
8842
8922
  const delayMs = Math.min(RESTART_BASE_MS * 2 ** (this.restarts - 1), RESTART_MAX_MS);
8843
- this.deps.logger.info("SegmentWriter restarting", { meta: {
8844
- outDir: this.cfg.outDir,
8845
- attempt: this.restarts,
8846
- delayMs,
8847
- ranMs,
8848
- reason: termination.reason
8849
- } });
8923
+ this.deps.logger.info("SegmentWriter restarting", {
8924
+ tags: { deviceId: this.deps.deviceId },
8925
+ meta: {
8926
+ outDir: this.cfg.outDir,
8927
+ attempt: this.restarts,
8928
+ restartsInWindow,
8929
+ delayMs,
8930
+ ranMs,
8931
+ reason: termination.reason
8932
+ }
8933
+ });
8850
8934
  this.restartTimer = setTimeout(() => {
8851
8935
  this.restartTimer = null;
8852
8936
  this.start();
@@ -10110,6 +10194,7 @@ var RecordingController = class {
10110
10194
  }, {
10111
10195
  spawn: this.deps.spawn,
10112
10196
  logger: deviceLog,
10197
+ deviceId,
10113
10198
  onGaveUp: () => {
10114
10199
  this.recoverWriterGaveUp(deviceId, profile);
10115
10200
  },
@@ -10533,6 +10618,209 @@ function buildRecordingExportProvider(deps) {
10533
10618
  */
10534
10619
  var resolveRecordingHubHostname = require_hub_hostname.resolveHubHostname;
10535
10620
  //#endregion
10621
+ //#region src/recorder/redundant-segments.ts
10622
+ /**
10623
+ * Return the indices (into `segs`) of segments safe to delete: those fully
10624
+ * contained within a kept segment `[coverStart, coverEnd]`. Sorting by start
10625
+ * ascending, then by end DESCENDING, guarantees the containing (longer) segment
10626
+ * is seen first and kept, and any shorter segment nested inside it is removed.
10627
+ */
10628
+ function selectRedundantSegments(segs) {
10629
+ if (segs.length < 2) return [];
10630
+ const order = segs.map((s, i) => ({
10631
+ start: s.startMs,
10632
+ end: s.startMs + s.durMs,
10633
+ i
10634
+ })).toSorted((a, b) => a.start - b.start || b.end - a.end);
10635
+ const remove = [];
10636
+ let coverStart = Number.NEGATIVE_INFINITY;
10637
+ let coverEnd = Number.NEGATIVE_INFINITY;
10638
+ for (const seg of order) if (seg.start >= coverStart && seg.end <= coverEnd && seg.end > seg.start) remove.push(seg.i);
10639
+ else {
10640
+ coverStart = seg.start;
10641
+ coverEnd = seg.end;
10642
+ }
10643
+ return remove;
10644
+ }
10645
+ //#endregion
10646
+ //#region src/recorder/addon/redundancy-sweep.ts
10647
+ /**
10648
+ * Periodic redundancy sweep — the only thing that removes duplicated footage.
10649
+ *
10650
+ * WHY IT EXISTS AGAIN
10651
+ * ───────────────────
10652
+ * Every `SegmentWriter` restart re-dials the broker with `withRecordingIntent`,
10653
+ * and the broker answers a RECORDING dial with its pre-roll ring (≈10 s). So a
10654
+ * restart does not resume at the live edge: it re-writes ~10 s of media that is
10655
+ * already on disk. At the 2026-08-27 measurement — **1 814 restarts in 12 h** —
10656
+ * that is ~5 hours of duplicated `high`-profile footage per day, on a fuse
10657
+ * share that was already at 90 % full.
10658
+ *
10659
+ * The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
10660
+ * from `runFullArchiveWalk`, which was reachable only from
10661
+ * `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
10662
+ * (commit `9308428b4`) all three were deleted together as dead code. That was
10663
+ * correct about the walk and left the system with **no duplicate pruner
10664
+ * whatsoever**. The precedent for letting this class of waste run unattended is
10665
+ * the 179 GB of stranded staging segments in `staging-reconcile.ts`.
10666
+ *
10667
+ * WHAT IS DIFFERENT FROM THE ONE THAT WAS DELETED
10668
+ * ───────────────────────────────────────────────
10669
+ * The old janitor ran once, after a walk of the WHOLE archive, and asked the
10670
+ * in-RAM index for `segments(deviceId)` — a full copy plus a sort of every row
10671
+ * the device owns. At the live shape (7.1 M rows) that copy alone was measured
10672
+ * at ~560 MB of transient heap, which is precisely the cost D248 had just
10673
+ * finished removing from the pressure sweep. Reconnecting it in that shape
10674
+ * would have traded one regression for another.
10675
+ *
10676
+ * So this one is bounded by a LOOKBACK WINDOW, not by the archive: duplicates
10677
+ * are made by a restart, restarts are now, and `segmentsStartingIn` slices the
10678
+ * sorted view instead of copying it. Cost grows with footage RECORDED in the
10679
+ * window — the invariant the recorder is supposed to satisfy — never with rows
10680
+ * already indexed.
10681
+ *
10682
+ * WHAT IT WILL NOT DO
10683
+ * ───────────────────
10684
+ * It never deletes a segment that contributes unique coverage:
10685
+ * {@link selectRedundantSegments} selects only segments fully contained inside
10686
+ * another segment that is KEPT, and staggered partial overlaps are always kept.
10687
+ * The delete itself is `SegmentStore.evict` — the same audited path
10688
+ * disk-pressure eviction uses, so a removed segment is de-indexed exactly as an
10689
+ * evicted one is. A failure for one (device, profile, location) is logged and
10690
+ * never aborts the pass.
10691
+ */
10692
+ /** The profiles a recorder writes. Same set `staging-reconcile` sweeps. */
10693
+ var SWEPT_PROFILES = [
10694
+ "high",
10695
+ "mid",
10696
+ "low"
10697
+ ];
10698
+ /** Group rows by the location that owns them. `evict` is per-location. */
10699
+ function byLocation(rows) {
10700
+ const out = /* @__PURE__ */ new Map();
10701
+ for (const row of rows) {
10702
+ const found = out.get(row.locationId);
10703
+ if (found) found.push(row);
10704
+ else out.set(row.locationId, [row]);
10705
+ }
10706
+ return out;
10707
+ }
10708
+ /**
10709
+ * Owns its timer and a single-flight guard, the same shape the retention sweep
10710
+ * and the export janitor already use — a slow pass is never overlapped by the
10711
+ * next tick.
10712
+ */
10713
+ var RedundancySweeper = class {
10714
+ deps;
10715
+ timer = null;
10716
+ sweeping = false;
10717
+ constructor(deps) {
10718
+ this.deps = deps;
10719
+ }
10720
+ /**
10721
+ * Arm the sweep on its interval. Idempotent.
10722
+ *
10723
+ * Deliberately NOT run immediately: boot is when the writers are attaching,
10724
+ * the staging reconcile is relocating thousands of files over shfs and the
10725
+ * first viewer is painting. A duplicate that has waited an hour can wait
10726
+ * another one; the write path cannot.
10727
+ */
10728
+ start() {
10729
+ if (this.timer !== null) return;
10730
+ this.timer = setInterval(() => {
10731
+ this.sweep();
10732
+ }, this.deps.intervalMs);
10733
+ this.timer.unref?.();
10734
+ }
10735
+ /** Stop the timer. Idempotent. An in-flight pass still completes. */
10736
+ stop() {
10737
+ if (this.timer !== null) {
10738
+ clearInterval(this.timer);
10739
+ this.timer = null;
10740
+ }
10741
+ }
10742
+ /**
10743
+ * One pass. Never throws: a device-list read failure aborts THIS pass
10744
+ * (logged), a per-location evict failure is logged and the pass continues.
10745
+ */
10746
+ async sweep() {
10747
+ const empty = {
10748
+ files: 0,
10749
+ bytes: 0,
10750
+ devices: 0
10751
+ };
10752
+ if (this.sweeping) {
10753
+ this.deps.logger.debug("recorder: redundancy sweep already running — skipping this tick");
10754
+ return empty;
10755
+ }
10756
+ this.sweeping = true;
10757
+ try {
10758
+ let deviceIds;
10759
+ try {
10760
+ deviceIds = await this.deps.deviceIds();
10761
+ } catch (err) {
10762
+ this.deps.logger.warn("recorder: redundancy sweep could not read the device list", { meta: { error: require_dist.errMsg(err) } });
10763
+ return empty;
10764
+ }
10765
+ const toMs = (this.deps.now ?? Date.now)();
10766
+ const fromMs = toMs - this.deps.lookbackMs;
10767
+ let files = 0;
10768
+ let bytes = 0;
10769
+ let devices = 0;
10770
+ for (const deviceId of deviceIds) {
10771
+ let removedHere = 0;
10772
+ for (const profile of SWEPT_PROFILES) {
10773
+ const rows = this.deps.segmentsInWindow(deviceId, profile, fromMs, toMs);
10774
+ const redundant = selectRedundantSegments(rows);
10775
+ if (redundant.length === 0) continue;
10776
+ const victims = redundant.map((i) => rows[i]).filter((row) => row !== void 0);
10777
+ for (const [locationId, group] of byLocation(victims)) try {
10778
+ const reclaimed = await this.deps.evict(locationId, group);
10779
+ files += group.length;
10780
+ bytes += reclaimed;
10781
+ removedHere += group.length;
10782
+ } catch (err) {
10783
+ this.deps.logger.warn("recorder: redundancy sweep could not evict duplicates", {
10784
+ tags: { deviceId },
10785
+ meta: {
10786
+ profile,
10787
+ locationId,
10788
+ count: group.length,
10789
+ error: require_dist.errMsg(err)
10790
+ }
10791
+ });
10792
+ }
10793
+ }
10794
+ if (removedHere > 0) {
10795
+ devices += 1;
10796
+ this.deps.logger.info("recorder: removed duplicated segments for device", {
10797
+ tags: { deviceId },
10798
+ meta: {
10799
+ files: removedHere,
10800
+ fromMs,
10801
+ toMs
10802
+ }
10803
+ });
10804
+ }
10805
+ }
10806
+ this.deps.logger.info("recorder: redundancy sweep complete", { meta: {
10807
+ devices,
10808
+ files,
10809
+ bytes,
10810
+ lookbackMs: this.deps.lookbackMs,
10811
+ deviceCount: deviceIds.length
10812
+ } });
10813
+ return {
10814
+ files,
10815
+ bytes,
10816
+ devices
10817
+ };
10818
+ } finally {
10819
+ this.sweeping = false;
10820
+ }
10821
+ }
10822
+ };
10823
+ //#endregion
10536
10824
  //#region src/recorder/addon/retention-sweep.ts
10537
10825
  /**
10538
10826
  * Periodic footage-retention sweep (recording-spec §6).
@@ -12093,6 +12381,18 @@ var EXPORT_SWEEP_INTERVAL_MS = 5 * 6e4;
12093
12381
  * hourly cadence keeps the storage.list/evict cost negligible while honouring
12094
12382
  * the recording-spec §6 periodic retention. */
12095
12383
  var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
12384
+ /** How often the redundancy sweep removes footage a writer restart duplicated. */
12385
+ var REDUNDANCY_SWEEP_INTERVAL_MS = 60 * 6e4;
12386
+ /**
12387
+ * How far back each redundancy pass looks.
12388
+ *
12389
+ * Twice the interval, so a pass that is skipped (single-flight) or delayed by a
12390
+ * slow predecessor still covers the window its predecessor was supposed to.
12391
+ * It is deliberately NOT "the whole archive": duplicates are made by a writer
12392
+ * restart, restarts are recent, and a full-archive selection is the ~560 MB
12393
+ * transient copy D248 removed. See `redundancy-sweep.ts`.
12394
+ */
12395
+ var REDUNDANCY_SWEEP_LOOKBACK_MS = 2 * REDUNDANCY_SWEEP_INTERVAL_MS;
12096
12396
  /** What the per-volume usage row reports when the hour ledger is unavailable:
12097
12397
  * nothing measured, rather than a total counted off a partial RAM index. */
12098
12398
  var EMPTY_ARCHIVE_ACCOUNTING = {
@@ -12245,6 +12545,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
12245
12545
  /** Periodic footage-retention sweep (owns its timer + single-flight guard).
12246
12546
  * Constructed + started in `onHubReachable`, stopped on shutdown. */
12247
12547
  retentionSweeper = null;
12548
+ /** Periodic duplicate-footage sweep — the ONLY pruner of media a writer
12549
+ * restart re-wrote. Constructed + started in `onHubReachable`. */
12550
+ redundancySweeper = null;
12248
12551
  /** Reversible migration pause lease; never reflected in RecordingConfig. */
12249
12552
  storageMigrationLeaseId = null;
12250
12553
  /**
@@ -12733,6 +13036,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
12733
13036
  }
12734
13037
  this.retentionSweeper?.stop();
12735
13038
  this.retentionSweeper = null;
13039
+ this.redundancySweeper?.stop();
13040
+ this.redundancySweeper = null;
12736
13041
  this.exportEngine = null;
12737
13042
  this.recordingProvider = null;
12738
13043
  }
@@ -12872,6 +13177,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
12872
13177
  intervalMs: RETENTION_SWEEP_INTERVAL_MS
12873
13178
  });
12874
13179
  this.retentionSweeper?.start();
13180
+ const store = this.segmentStore;
13181
+ if (store && this.redundancySweeper === null) this.redundancySweeper = new RedundancySweeper({
13182
+ deviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
13183
+ segmentsInWindow: (deviceId, profile, fromMs, toMs) => this.index.segmentsStartingIn(deviceId, profile, fromMs, toMs),
13184
+ evict: (locationId, rows) => store.evict(locationId, rows),
13185
+ logger: this.ctx.logger,
13186
+ intervalMs: REDUNDANCY_SWEEP_INTERVAL_MS,
13187
+ lookbackMs: REDUNDANCY_SWEEP_LOOKBACK_MS
13188
+ });
13189
+ this.redundancySweeper?.start();
12875
13190
  }
12876
13191
  /**
12877
13192
  * Rebuild the sensor trigger's reverse index from the persisted configs.
@@ -8732,6 +8732,38 @@ var RESTART_MAX_MS = 1e4;
8732
8732
  /** A process that ran at least this long is "stable" → reset the restart count,
8733
8733
  * so sporadic blips over a long lifetime never accumulate into a give-up. */
8734
8734
  var STABLE_RUN_MS = 3e4;
8735
+ /**
8736
+ * The window the SECOND budget counts over — and it counts EVERY restart,
8737
+ * whatever that run lasted.
8738
+ *
8739
+ * `STABLE_RUN_MS` above is a reset, and a reset is a blind spot: measured on
8740
+ * 2026-08-27, `SegmentWriter restarting` fired **1 814 times in 12 h** and
8741
+ * almost every `ranMs` was over 30 s (122 620 / 165 255 / 345 420 / 394 522 ms
8742
+ * are four consecutive real samples), so `attempt` never left 1, `onGaveUp`
8743
+ * never fired, and `recoverWriterGaveUp` never ran. The storm could have lasted
8744
+ * forever without a single line saying anything was wrong.
8745
+ *
8746
+ * A window cannot be zeroed by a long run, which is the whole point. Modelled
8747
+ * on `CrashSupervisor` (kernel, D6): timestamps age out of `windowMs`, and the
8748
+ * count inside it is the budget.
8749
+ */
8750
+ var RESTART_WINDOW_MS = 60 * 6e4;
8751
+ /**
8752
+ * Restarts inside {@link RESTART_WINDOW_MS} before the writer stops respawning
8753
+ * and escalates to the controller.
8754
+ *
8755
+ * 15/hour, from the measured distribution. Per writer over the same 12 h:
8756
+ * 1441/high 549 (**45.8/h**), 615/high 241 (20.1/h), 618/high 217 (18.1/h),
8757
+ * 592/high 211 (17.6/h), 590/high 195 (16.3/h) — every one of them a storm and
8758
+ * every one of them silent — against `low` profiles at 4–15 per 12 h (≤1.3/h),
8759
+ * which is what a healthy writer looks like. 15 separates them with room for a
8760
+ * camera that blips hourly and is otherwise fine.
8761
+ *
8762
+ * It never shadows {@link MAX_RESTARTS}: 10 CONSECUTIVE rapid failures happen
8763
+ * inside ~90 s, so the rapid breaker still trips first on a hard failure. This
8764
+ * one exists for the slow storm the rapid breaker structurally cannot see.
8765
+ */
8766
+ var MAX_RESTARTS_IN_WINDOW = 15;
8735
8767
  /** Grace between the SIGTERM in stop() and the escalated SIGKILL. */
8736
8768
  var KILL_GRACE_MS = 500;
8737
8769
  /** Supervises one passthrough ffmpeg for a single (camera, profile). */
@@ -8741,6 +8773,12 @@ var SegmentWriter = class {
8741
8773
  proc = null;
8742
8774
  stopped = false;
8743
8775
  restarts = 0;
8776
+ /**
8777
+ * Epoch-ms of every termination still inside {@link RESTART_WINDOW_MS}.
8778
+ * Replaced, never mutated in place, so a reader can never observe a
8779
+ * half-pruned window.
8780
+ */
8781
+ restartWindow = [];
8744
8782
  restartTimer = null;
8745
8783
  startedAt = 0;
8746
8784
  exitWait = null;
@@ -8774,10 +8812,11 @@ var SegmentWriter = class {
8774
8812
  settled = true;
8775
8813
  this.onTermination(termination, stderrTail);
8776
8814
  };
8777
- proc.on("exit", (code) => {
8815
+ proc.on("exit", (code, signal) => {
8778
8816
  terminate({
8779
8817
  reason: "exit",
8780
- exitCode: typeof code === "number" ? code : null
8818
+ exitCode: typeof code === "number" ? code : null,
8819
+ signal: typeof signal === "string" ? signal : null
8781
8820
  });
8782
8821
  });
8783
8822
  proc.on("error", (raw) => {
@@ -8793,32 +8832,51 @@ var SegmentWriter = class {
8793
8832
  /**
8794
8833
  * The ONE restart policy, reached from both `exit` and `error`.
8795
8834
  *
8796
- * `deps.logger` is the device-scoped child the controller binds
8797
- * (`logger.withTags({ deviceId })`), so every line below carries
8798
- * `tags: { deviceId }` "why is 617 worse than 615" is always asked per
8799
- * camera, and a writer that stops recording one camera must be greppable by
8800
- * that camera.
8835
+ * Every line below carries `tags: { deviceId }` from `deps.deviceId` "why
8836
+ * is 617 worse than 615" is always asked per camera, and a writer that stops
8837
+ * recording one camera must be greppable by that camera.
8801
8838
  */
8802
8839
  onTermination(termination, stderrTail) {
8803
8840
  this.resolveExit?.();
8804
8841
  this.resolveExit = null;
8805
8842
  this.proc = null;
8806
8843
  if (this.stopped) return;
8807
- const ranMs = Date.now() - this.startedAt;
8844
+ const now = Date.now();
8845
+ const ranMs = now - this.startedAt;
8808
8846
  if (ranMs >= STABLE_RUN_MS) this.restarts = 0;
8809
- if (termination.reason === "spawn-error") {
8810
- this.deps.logger.warn("SegmentWriter ffmpeg spawn failed", { meta: {
8847
+ this.restartWindow = [...this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS), now];
8848
+ const restartsInWindow = this.restartWindow.length;
8849
+ if (termination.reason === "exit") this.deps.logger.warn("SegmentWriter ffmpeg exited", {
8850
+ tags: { deviceId: this.deps.deviceId },
8851
+ meta: {
8811
8852
  outDir: this.cfg.outDir,
8812
- errorCode: termination.errorCode,
8813
- error: termination.message,
8814
- permanent: termination.permanent
8815
- } });
8816
- if (termination.permanent) {
8817
- this.deps.logger.error("SegmentWriter giving up: ffmpeg cannot be executed on this node", { meta: {
8853
+ exitCode: termination.exitCode,
8854
+ signal: termination.signal,
8855
+ ranMs,
8856
+ restartsInWindow,
8857
+ windowMs: RESTART_WINDOW_MS,
8858
+ stderrTail: stderrTail.join(" | ")
8859
+ }
8860
+ });
8861
+ if (termination.reason === "spawn-error") {
8862
+ this.deps.logger.warn("SegmentWriter ffmpeg spawn failed", {
8863
+ tags: { deviceId: this.deps.deviceId },
8864
+ meta: {
8818
8865
  outDir: this.cfg.outDir,
8819
8866
  errorCode: termination.errorCode,
8820
- error: termination.message
8821
- } });
8867
+ error: termination.message,
8868
+ permanent: termination.permanent
8869
+ }
8870
+ });
8871
+ if (termination.permanent) {
8872
+ this.deps.logger.error("SegmentWriter giving up: ffmpeg cannot be executed on this node", {
8873
+ tags: { deviceId: this.deps.deviceId },
8874
+ meta: {
8875
+ outDir: this.cfg.outDir,
8876
+ errorCode: termination.errorCode,
8877
+ error: termination.message
8878
+ }
8879
+ });
8822
8880
  this.stopped = true;
8823
8881
  this.deps.onGaveUp?.();
8824
8882
  return;
@@ -8826,25 +8884,51 @@ var SegmentWriter = class {
8826
8884
  this.deps.onResourcePressure?.();
8827
8885
  }
8828
8886
  if (this.restarts >= MAX_RESTARTS) {
8829
- this.deps.logger.warn("SegmentWriter giving up after max restarts", { meta: {
8830
- outDir: this.cfg.outDir,
8831
- reason: termination.reason,
8832
- code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8833
- stderrTail: stderrTail.join(" | ")
8834
- } });
8887
+ this.deps.logger.warn("SegmentWriter giving up after max restarts", {
8888
+ tags: { deviceId: this.deps.deviceId },
8889
+ meta: {
8890
+ outDir: this.cfg.outDir,
8891
+ reason: termination.reason,
8892
+ code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8893
+ stderrTail: stderrTail.join(" | ")
8894
+ }
8895
+ });
8896
+ this.stopped = true;
8897
+ this.deps.onGaveUp?.();
8898
+ return;
8899
+ }
8900
+ if (restartsInWindow >= MAX_RESTARTS_IN_WINDOW) {
8901
+ this.deps.logger.warn("SegmentWriter giving up: restart storm", {
8902
+ tags: { deviceId: this.deps.deviceId },
8903
+ meta: {
8904
+ outDir: this.cfg.outDir,
8905
+ restartsInWindow,
8906
+ windowMs: RESTART_WINDOW_MS,
8907
+ budget: MAX_RESTARTS_IN_WINDOW,
8908
+ attempt: this.restarts,
8909
+ ranMs,
8910
+ reason: termination.reason,
8911
+ code: termination.reason === "exit" ? termination.exitCode : termination.errorCode,
8912
+ stderrTail: stderrTail.join(" | ")
8913
+ }
8914
+ });
8835
8915
  this.stopped = true;
8836
8916
  this.deps.onGaveUp?.();
8837
8917
  return;
8838
8918
  }
8839
8919
  this.restarts++;
8840
8920
  const delayMs = Math.min(RESTART_BASE_MS * 2 ** (this.restarts - 1), RESTART_MAX_MS);
8841
- this.deps.logger.info("SegmentWriter restarting", { meta: {
8842
- outDir: this.cfg.outDir,
8843
- attempt: this.restarts,
8844
- delayMs,
8845
- ranMs,
8846
- reason: termination.reason
8847
- } });
8921
+ this.deps.logger.info("SegmentWriter restarting", {
8922
+ tags: { deviceId: this.deps.deviceId },
8923
+ meta: {
8924
+ outDir: this.cfg.outDir,
8925
+ attempt: this.restarts,
8926
+ restartsInWindow,
8927
+ delayMs,
8928
+ ranMs,
8929
+ reason: termination.reason
8930
+ }
8931
+ });
8848
8932
  this.restartTimer = setTimeout(() => {
8849
8933
  this.restartTimer = null;
8850
8934
  this.start();
@@ -10108,6 +10192,7 @@ var RecordingController = class {
10108
10192
  }, {
10109
10193
  spawn: this.deps.spawn,
10110
10194
  logger: deviceLog,
10195
+ deviceId,
10111
10196
  onGaveUp: () => {
10112
10197
  this.recoverWriterGaveUp(deviceId, profile);
10113
10198
  },
@@ -10531,6 +10616,209 @@ function buildRecordingExportProvider(deps) {
10531
10616
  */
10532
10617
  var resolveRecordingHubHostname = resolveHubHostname;
10533
10618
  //#endregion
10619
+ //#region src/recorder/redundant-segments.ts
10620
+ /**
10621
+ * Return the indices (into `segs`) of segments safe to delete: those fully
10622
+ * contained within a kept segment `[coverStart, coverEnd]`. Sorting by start
10623
+ * ascending, then by end DESCENDING, guarantees the containing (longer) segment
10624
+ * is seen first and kept, and any shorter segment nested inside it is removed.
10625
+ */
10626
+ function selectRedundantSegments(segs) {
10627
+ if (segs.length < 2) return [];
10628
+ const order = segs.map((s, i) => ({
10629
+ start: s.startMs,
10630
+ end: s.startMs + s.durMs,
10631
+ i
10632
+ })).toSorted((a, b) => a.start - b.start || b.end - a.end);
10633
+ const remove = [];
10634
+ let coverStart = Number.NEGATIVE_INFINITY;
10635
+ let coverEnd = Number.NEGATIVE_INFINITY;
10636
+ for (const seg of order) if (seg.start >= coverStart && seg.end <= coverEnd && seg.end > seg.start) remove.push(seg.i);
10637
+ else {
10638
+ coverStart = seg.start;
10639
+ coverEnd = seg.end;
10640
+ }
10641
+ return remove;
10642
+ }
10643
+ //#endregion
10644
+ //#region src/recorder/addon/redundancy-sweep.ts
10645
+ /**
10646
+ * Periodic redundancy sweep — the only thing that removes duplicated footage.
10647
+ *
10648
+ * WHY IT EXISTS AGAIN
10649
+ * ───────────────────
10650
+ * Every `SegmentWriter` restart re-dials the broker with `withRecordingIntent`,
10651
+ * and the broker answers a RECORDING dial with its pre-roll ring (≈10 s). So a
10652
+ * restart does not resume at the live edge: it re-writes ~10 s of media that is
10653
+ * already on disk. At the 2026-08-27 measurement — **1 814 restarts in 12 h** —
10654
+ * that is ~5 hours of duplicated `high`-profile footage per day, on a fuse
10655
+ * share that was already at 90 % full.
10656
+ *
10657
+ * The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
10658
+ * from `runFullArchiveWalk`, which was reachable only from
10659
+ * `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
10660
+ * (commit `9308428b4`) all three were deleted together as dead code. That was
10661
+ * correct about the walk and left the system with **no duplicate pruner
10662
+ * whatsoever**. The precedent for letting this class of waste run unattended is
10663
+ * the 179 GB of stranded staging segments in `staging-reconcile.ts`.
10664
+ *
10665
+ * WHAT IS DIFFERENT FROM THE ONE THAT WAS DELETED
10666
+ * ───────────────────────────────────────────────
10667
+ * The old janitor ran once, after a walk of the WHOLE archive, and asked the
10668
+ * in-RAM index for `segments(deviceId)` — a full copy plus a sort of every row
10669
+ * the device owns. At the live shape (7.1 M rows) that copy alone was measured
10670
+ * at ~560 MB of transient heap, which is precisely the cost D248 had just
10671
+ * finished removing from the pressure sweep. Reconnecting it in that shape
10672
+ * would have traded one regression for another.
10673
+ *
10674
+ * So this one is bounded by a LOOKBACK WINDOW, not by the archive: duplicates
10675
+ * are made by a restart, restarts are now, and `segmentsStartingIn` slices the
10676
+ * sorted view instead of copying it. Cost grows with footage RECORDED in the
10677
+ * window — the invariant the recorder is supposed to satisfy — never with rows
10678
+ * already indexed.
10679
+ *
10680
+ * WHAT IT WILL NOT DO
10681
+ * ───────────────────
10682
+ * It never deletes a segment that contributes unique coverage:
10683
+ * {@link selectRedundantSegments} selects only segments fully contained inside
10684
+ * another segment that is KEPT, and staggered partial overlaps are always kept.
10685
+ * The delete itself is `SegmentStore.evict` — the same audited path
10686
+ * disk-pressure eviction uses, so a removed segment is de-indexed exactly as an
10687
+ * evicted one is. A failure for one (device, profile, location) is logged and
10688
+ * never aborts the pass.
10689
+ */
10690
+ /** The profiles a recorder writes. Same set `staging-reconcile` sweeps. */
10691
+ var SWEPT_PROFILES = [
10692
+ "high",
10693
+ "mid",
10694
+ "low"
10695
+ ];
10696
+ /** Group rows by the location that owns them. `evict` is per-location. */
10697
+ function byLocation(rows) {
10698
+ const out = /* @__PURE__ */ new Map();
10699
+ for (const row of rows) {
10700
+ const found = out.get(row.locationId);
10701
+ if (found) found.push(row);
10702
+ else out.set(row.locationId, [row]);
10703
+ }
10704
+ return out;
10705
+ }
10706
+ /**
10707
+ * Owns its timer and a single-flight guard, the same shape the retention sweep
10708
+ * and the export janitor already use — a slow pass is never overlapped by the
10709
+ * next tick.
10710
+ */
10711
+ var RedundancySweeper = class {
10712
+ deps;
10713
+ timer = null;
10714
+ sweeping = false;
10715
+ constructor(deps) {
10716
+ this.deps = deps;
10717
+ }
10718
+ /**
10719
+ * Arm the sweep on its interval. Idempotent.
10720
+ *
10721
+ * Deliberately NOT run immediately: boot is when the writers are attaching,
10722
+ * the staging reconcile is relocating thousands of files over shfs and the
10723
+ * first viewer is painting. A duplicate that has waited an hour can wait
10724
+ * another one; the write path cannot.
10725
+ */
10726
+ start() {
10727
+ if (this.timer !== null) return;
10728
+ this.timer = setInterval(() => {
10729
+ this.sweep();
10730
+ }, this.deps.intervalMs);
10731
+ this.timer.unref?.();
10732
+ }
10733
+ /** Stop the timer. Idempotent. An in-flight pass still completes. */
10734
+ stop() {
10735
+ if (this.timer !== null) {
10736
+ clearInterval(this.timer);
10737
+ this.timer = null;
10738
+ }
10739
+ }
10740
+ /**
10741
+ * One pass. Never throws: a device-list read failure aborts THIS pass
10742
+ * (logged), a per-location evict failure is logged and the pass continues.
10743
+ */
10744
+ async sweep() {
10745
+ const empty = {
10746
+ files: 0,
10747
+ bytes: 0,
10748
+ devices: 0
10749
+ };
10750
+ if (this.sweeping) {
10751
+ this.deps.logger.debug("recorder: redundancy sweep already running — skipping this tick");
10752
+ return empty;
10753
+ }
10754
+ this.sweeping = true;
10755
+ try {
10756
+ let deviceIds;
10757
+ try {
10758
+ deviceIds = await this.deps.deviceIds();
10759
+ } catch (err) {
10760
+ this.deps.logger.warn("recorder: redundancy sweep could not read the device list", { meta: { error: errMsg(err) } });
10761
+ return empty;
10762
+ }
10763
+ const toMs = (this.deps.now ?? Date.now)();
10764
+ const fromMs = toMs - this.deps.lookbackMs;
10765
+ let files = 0;
10766
+ let bytes = 0;
10767
+ let devices = 0;
10768
+ for (const deviceId of deviceIds) {
10769
+ let removedHere = 0;
10770
+ for (const profile of SWEPT_PROFILES) {
10771
+ const rows = this.deps.segmentsInWindow(deviceId, profile, fromMs, toMs);
10772
+ const redundant = selectRedundantSegments(rows);
10773
+ if (redundant.length === 0) continue;
10774
+ const victims = redundant.map((i) => rows[i]).filter((row) => row !== void 0);
10775
+ for (const [locationId, group] of byLocation(victims)) try {
10776
+ const reclaimed = await this.deps.evict(locationId, group);
10777
+ files += group.length;
10778
+ bytes += reclaimed;
10779
+ removedHere += group.length;
10780
+ } catch (err) {
10781
+ this.deps.logger.warn("recorder: redundancy sweep could not evict duplicates", {
10782
+ tags: { deviceId },
10783
+ meta: {
10784
+ profile,
10785
+ locationId,
10786
+ count: group.length,
10787
+ error: errMsg(err)
10788
+ }
10789
+ });
10790
+ }
10791
+ }
10792
+ if (removedHere > 0) {
10793
+ devices += 1;
10794
+ this.deps.logger.info("recorder: removed duplicated segments for device", {
10795
+ tags: { deviceId },
10796
+ meta: {
10797
+ files: removedHere,
10798
+ fromMs,
10799
+ toMs
10800
+ }
10801
+ });
10802
+ }
10803
+ }
10804
+ this.deps.logger.info("recorder: redundancy sweep complete", { meta: {
10805
+ devices,
10806
+ files,
10807
+ bytes,
10808
+ lookbackMs: this.deps.lookbackMs,
10809
+ deviceCount: deviceIds.length
10810
+ } });
10811
+ return {
10812
+ files,
10813
+ bytes,
10814
+ devices
10815
+ };
10816
+ } finally {
10817
+ this.sweeping = false;
10818
+ }
10819
+ }
10820
+ };
10821
+ //#endregion
10534
10822
  //#region src/recorder/addon/retention-sweep.ts
10535
10823
  /**
10536
10824
  * Periodic footage-retention sweep (recording-spec §6).
@@ -12091,6 +12379,18 @@ var EXPORT_SWEEP_INTERVAL_MS = 5 * 6e4;
12091
12379
  * hourly cadence keeps the storage.list/evict cost negligible while honouring
12092
12380
  * the recording-spec §6 periodic retention. */
12093
12381
  var RETENTION_SWEEP_INTERVAL_MS = 60 * 6e4;
12382
+ /** How often the redundancy sweep removes footage a writer restart duplicated. */
12383
+ var REDUNDANCY_SWEEP_INTERVAL_MS = 60 * 6e4;
12384
+ /**
12385
+ * How far back each redundancy pass looks.
12386
+ *
12387
+ * Twice the interval, so a pass that is skipped (single-flight) or delayed by a
12388
+ * slow predecessor still covers the window its predecessor was supposed to.
12389
+ * It is deliberately NOT "the whole archive": duplicates are made by a writer
12390
+ * restart, restarts are recent, and a full-archive selection is the ~560 MB
12391
+ * transient copy D248 removed. See `redundancy-sweep.ts`.
12392
+ */
12393
+ var REDUNDANCY_SWEEP_LOOKBACK_MS = 2 * REDUNDANCY_SWEEP_INTERVAL_MS;
12094
12394
  /** What the per-volume usage row reports when the hour ledger is unavailable:
12095
12395
  * nothing measured, rather than a total counted off a partial RAM index. */
12096
12396
  var EMPTY_ARCHIVE_ACCOUNTING = {
@@ -12243,6 +12543,9 @@ var RecorderV2Addon = class extends BaseAddon {
12243
12543
  /** Periodic footage-retention sweep (owns its timer + single-flight guard).
12244
12544
  * Constructed + started in `onHubReachable`, stopped on shutdown. */
12245
12545
  retentionSweeper = null;
12546
+ /** Periodic duplicate-footage sweep — the ONLY pruner of media a writer
12547
+ * restart re-wrote. Constructed + started in `onHubReachable`. */
12548
+ redundancySweeper = null;
12246
12549
  /** Reversible migration pause lease; never reflected in RecordingConfig. */
12247
12550
  storageMigrationLeaseId = null;
12248
12551
  /**
@@ -12731,6 +13034,8 @@ var RecorderV2Addon = class extends BaseAddon {
12731
13034
  }
12732
13035
  this.retentionSweeper?.stop();
12733
13036
  this.retentionSweeper = null;
13037
+ this.redundancySweeper?.stop();
13038
+ this.redundancySweeper = null;
12734
13039
  this.exportEngine = null;
12735
13040
  this.recordingProvider = null;
12736
13041
  }
@@ -12870,6 +13175,16 @@ var RecorderV2Addon = class extends BaseAddon {
12870
13175
  intervalMs: RETENTION_SWEEP_INTERVAL_MS
12871
13176
  });
12872
13177
  this.retentionSweeper?.start();
13178
+ const store = this.segmentStore;
13179
+ if (store && this.redundancySweeper === null) this.redundancySweeper = new RedundancySweeper({
13180
+ deviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
13181
+ segmentsInWindow: (deviceId, profile, fromMs, toMs) => this.index.segmentsStartingIn(deviceId, profile, fromMs, toMs),
13182
+ evict: (locationId, rows) => store.evict(locationId, rows),
13183
+ logger: this.ctx.logger,
13184
+ intervalMs: REDUNDANCY_SWEEP_INTERVAL_MS,
13185
+ lookbackMs: REDUNDANCY_SWEEP_LOOKBACK_MS
13186
+ });
13187
+ this.redundancySweeper?.start();
12873
13188
  }
12874
13189
  /**
12875
13190
  * Rebuild the sensor trigger's reverse index from the persisted configs.
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.37",
6
+ version: "1.2.38",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_stream_broker_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.115",
21
+ version: "1.2.116",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.78",
36
+ version: "1.2.79",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.115",
39
+ version: "1.2.116",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.37",
48
+ version: "1.2.38",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.78",
84
+ version: "1.2.79",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C14Qqrqn.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLtMmF1E.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.135",
3
+ "version": "1.2.136",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -168,7 +168,8 @@
168
168
  "entry": "./dist/stream-broker/index.js",
169
169
  "execution": {
170
170
  "placement": "any-node",
171
- "heapProfile": "heavy"
171
+ "heapProfile": "heavy",
172
+ "rssBudgetMb": 2048
172
173
  },
173
174
  "capabilities": [
174
175
  {
@@ -201,7 +202,8 @@
201
202
  "execution": {
202
203
  "placement": "any-node",
203
204
  "heapProfile": "heavy",
204
- "maxOldSpaceMb": 3072
205
+ "maxOldSpaceMb": 3072,
206
+ "rssBudgetMb": 3584
205
207
  },
206
208
  "capabilities": [
207
209
  {