@camstack/addon-pipeline 1.2.127 → 1.2.129

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 (35) hide show
  1. package/dist/{addon-utils-BnprivSs.js → addon-utils-CKB-2oM9.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +241 -95
  5. package/dist/detection-pipeline/index.mjs +239 -93
  6. package/dist/{dist-HLAOw6Pe.mjs → dist-CMio-1qq.mjs} +47 -31
  7. package/dist/{dist-BZUXb5S5.js → dist-D9x2_csL.js} +47 -31
  8. package/dist/{event-loop-stall-monitor-BH-cL_hg.mjs → event-loop-stall-monitor-B9dxYxyf.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-BdArVUlx.js → event-loop-stall-monitor-UW8nsiMb.js} +1 -1
  10. package/dist/{lazy-sharp-DTDXrhMG.js → lazy-sharp-DrLwLKpg.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 +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-CDPRLalN.mjs → process-memory-C6es8dMA.mjs} +1 -1
  16. package/dist/{process-memory-Dh1SyzdJ.js → process-memory-CSJ7-Wlp.js} +1 -1
  17. package/dist/recorder/index.js +121 -33
  18. package/dist/recorder/index.mjs +120 -32
  19. package/dist/{segment-demux-js-D-TLcHW5.mjs → segment-demux-js-BRCjXbI0.mjs} +1 -1
  20. package/dist/{segment-demux-js-DRutggd6.js → segment-demux-js-DiLDoV9_.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +1 -1
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BDR5z0Z9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-1O99xPTr.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs +26 -0
  26. package/dist/stream-broker/demux-worker-child.js +1 -1
  27. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  28. package/dist/stream-broker/{hostInit-Dmw5LWmP.mjs → hostInit-8scvCWua.mjs} +2 -2
  29. package/dist/stream-broker/index.js +530 -154
  30. package/dist/stream-broker/index.mjs +530 -154
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/dist/{worker-protocol-h7Smb_0i.mjs → worker-protocol-BHNWB9Ve.mjs} +1 -1
  33. package/dist/{worker-protocol-BxkCoVjw.js → worker-protocol-BmBYCioC.js} +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cs8bmrWP.mjs +0 -26
@@ -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-BZUXb5S5.js");
6
- const require_lazy_sharp = require("../lazy-sharp-DTDXrhMG.js");
5
+ const require_dist = require("../dist-D9x2_csL.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-DrLwLKpg.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 { Dt as hydrateSchema, G as evaluateZoneRules, St as BaseAddon, Tt as DeviceType, X as motionDetectionCapability } from "../dist-HLAOw6Pe.mjs";
1
+ import { Dt as hydrateSchema, G as evaluateZoneRules, St as BaseAddon, Tt as DeviceType, X as motionDetectionCapability } from "../dist-CMio-1qq.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
3
3
  import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BZUXb5S5.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-BdArVUlx.js");
7
- const require_worker_protocol = require("../worker-protocol-BxkCoVjw.js");
5
+ const require_dist = require("../dist-D9x2_csL.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-UW8nsiMb.js");
7
+ const require_worker_protocol = require("../worker-protocol-BmBYCioC.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
- const require_lazy_sharp = require("../lazy-sharp-DTDXrhMG.js");
9
+ const require_lazy_sharp = require("../lazy-sharp-DrLwLKpg.js");
10
10
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
11
11
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
12
12
  let sharp = require("sharp");
@@ -1,6 +1,6 @@
1
- import { Bt as lazy, Et as createEvent, F as customAction, Gt as string, Ht as number, It as _enum, L as defineCustomActions, Lt as array, Rt as boolean, St as BaseAddon, Ut as object, Vt as literal, f as DEFAULT_NATIVE_LEASE_SETTINGS, gt as errMsg, it as pipelineRunnerCapability, jt as nodePin, nt as pickNativeLeaseOverride, p as DEVICE_BACKEND_TO_FORMAT, qt as EventCategory, tt as pickDetailCropConvention, u as DEFAULT_DETAIL_CROP_CONVENTION, z as deriveDetailCropRect } from "../dist-HLAOw6Pe.mjs";
2
- import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-BH-cL_hg.mjs";
3
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-h7Smb_0i.mjs";
1
+ import { Bt as lazy, Et as createEvent, F as customAction, Gt as string, Ht as number, It as _enum, L as defineCustomActions, Lt as array, Rt as boolean, St as BaseAddon, Ut as object, Vt as literal, f as DEFAULT_NATIVE_LEASE_SETTINGS, gt as errMsg, it as pipelineRunnerCapability, jt as nodePin, nt as pickNativeLeaseOverride, p as DEVICE_BACKEND_TO_FORMAT, qt as EventCategory, tt as pickDetailCropConvention, u as DEFAULT_DETAIL_CROP_CONVENTION, z as deriveDetailCropRect } from "../dist-CMio-1qq.mjs";
2
+ import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-B9dxYxyf.mjs";
3
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BHNWB9Ve.mjs";
4
4
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
6
6
  import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
@@ -1,4 +1,4 @@
1
- import { Q as parseProcStatus } from "./dist-HLAOw6Pe.mjs";
1
+ import { Q as parseProcStatus } from "./dist-CMio-1qq.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BZUXb5S5.js");
1
+ const require_dist = require("./dist-D9x2_csL.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_dist.__toESM(node_fs);
@@ -1,7 +1,7 @@
1
- const require_dist = require("../dist-BZUXb5S5.js");
1
+ const require_dist = require("../dist-D9x2_csL.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
3
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
4
- const require_addon_utils = require("../addon-utils-BnprivSs.js");
4
+ const require_addon_utils = require("../addon-utils-CKB-2oM9.js");
5
5
  const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
@@ -6061,13 +6061,20 @@ async function readPlacementState(state) {
6061
6061
  * Replace the assignment map, preserving everything else in the blob. The WHOLE
6062
6062
  * blob round-trips on every write (the DurableState contract), so the remembered
6063
6063
  * volume ids cannot be dropped by an assignment write.
6064
+ *
6065
+ * `base` is REQUIRED, and it is the blob the caller already read. This used to
6066
+ * re-read the store itself, which made every write two round trips instead of
6067
+ * one — and every one of them goes over the UDS door to `hub/sqlite-settings`
6068
+ * with the 60 s default bound. The placement pass read the blob, then wrote it,
6069
+ * and paid for the read TWICE: 4 × 60 s = the `durationMs: 240003` pass measured
6070
+ * on the live hub against a 30 s budget. Taking the blob as an argument makes
6071
+ * "one plan, one read, one write" a shape the type system enforces.
6064
6072
  */
6065
- async function writeAssignments(state, assignments) {
6066
- const blob = await state.get();
6073
+ async function writeAssignments(state, base, assignments) {
6067
6074
  const next = {};
6068
6075
  for (const [key, locationId] of assignments) next[key] = locationId;
6069
6076
  await state.set({
6070
- ...blob,
6077
+ ...base,
6071
6078
  assignments: next,
6072
6079
  updatedAt: Date.now()
6073
6080
  });
@@ -6583,9 +6590,45 @@ var fsVolumeProbeDeps = {
6583
6590
  newVolumeId: () => (0, node_crypto.randomUUID)()
6584
6591
  };
6585
6592
  //#endregion
6593
+ //#region src/recorder/addon/with-deadline.ts
6594
+ /**
6595
+ * Race a promise against a deadline.
6596
+ *
6597
+ * Every cross-process call the recorder makes goes over a UDS door whose
6598
+ * DEFAULT timeout is 60 s (`DEFAULT_UDS_REQUEST_TIMEOUT_MS`). That default is a
6599
+ * backstop, not a budget: a pass that makes four of them serially and bounds
6600
+ * none of them costs four minutes, and the recorder's periodic passes SKIP
6601
+ * every fire while a run is in flight — so the unbounded call does not make one
6602
+ * pass late, it cancels the seven behind it. Measured on the live hub as a
6603
+ * `durationMs: 240003` placement pass against a 30 s budget (2026-08-18/19).
6604
+ *
6605
+ * Handlers stay attached to the losing promise, so a post-deadline settlement
6606
+ * can never surface as a process-level `unhandledRejection`.
6607
+ */
6608
+ function withDeadline(promise, ms, label) {
6609
+ return new Promise((resolve, reject) => {
6610
+ const timer = setTimeout(() => {
6611
+ reject(/* @__PURE__ */ new Error(`${label} timed out after ${ms}ms`));
6612
+ }, ms);
6613
+ timer.unref?.();
6614
+ promise.then((value) => {
6615
+ clearTimeout(timer);
6616
+ resolve(value);
6617
+ }, (err) => {
6618
+ clearTimeout(timer);
6619
+ reject(err instanceof Error ? err : new Error(String(err)));
6620
+ });
6621
+ });
6622
+ }
6623
+ //#endregion
6586
6624
  //#region src/recorder/addon/placement-service.ts
6587
6625
  /** Free space a location keeps in reserve when its config names no floor. */
6588
6626
  var DEFAULT_MIN_FREE_PERCENT = 10;
6627
+ /** Nothing planned, nothing to persist. */
6628
+ var NO_PLAN = {
6629
+ assignments: /* @__PURE__ */ new Map(),
6630
+ changes: []
6631
+ };
6589
6632
  var PlacementService = class {
6590
6633
  deps;
6591
6634
  /** Locations whose volume has been probed since the last location change. */
@@ -6614,6 +6657,9 @@ var PlacementService = class {
6614
6657
  get reprobeMs() {
6615
6658
  return this.deps.reprobeMs ?? 6e4;
6616
6659
  }
6660
+ get storeTimeoutMs() {
6661
+ return this.deps.storeTimeoutMs ?? 5e3;
6662
+ }
6617
6663
  /**
6618
6664
  * Forget every probe verdict. Called whenever the location set is re-resolved
6619
6665
  * (rescan, storage migration) — a remount is exactly the event the probe
@@ -6756,6 +6802,10 @@ var PlacementService = class {
6756
6802
  * controller's existing 30s tick — it changes the PLAN only; a running writer
6757
6803
  * keeps its root until it next attaches. Returns the plan so a caller can log
6758
6804
  * or assert on it.
6805
+ *
6806
+ * ONE read and, at most, ONE write. Both bounded
6807
+ * ({@link PLACEMENT_STORE_TIMEOUT_MS}) and both against the same blob: the
6808
+ * plan the pass reads is the plan it writes back.
6759
6809
  */
6760
6810
  async recompute(work) {
6761
6811
  const pool = this.pool();
@@ -6765,12 +6815,17 @@ var PlacementService = class {
6765
6815
  type: item.profile === "low" ? "recordingsLow" : "recordings",
6766
6816
  dailyBytes: this.deps.dailyBytesFor?.(item.deviceId, item.profile) ?? 0
6767
6817
  }));
6768
- let current = /* @__PURE__ */ new Map();
6818
+ let blob;
6769
6819
  try {
6770
- current = new Map(Object.entries((await readPlacementState(this.deps.state)).assignments));
6820
+ blob = await this.bounded(readPlacementState(this.deps.state), "placement plan read");
6771
6821
  } catch (err) {
6772
- this.deps.logger.warn("recorder placement: plan read failed — planning from scratch", { meta: { error: require_dist.errMsg(err) } });
6822
+ this.deps.logger.warn("recorder placement: plan read failed — this tick plans NOTHING and the stored plan stands", { meta: {
6823
+ error: require_dist.errMsg(err),
6824
+ timeoutMs: this.storeTimeoutMs
6825
+ } });
6826
+ return NO_PLAN;
6773
6827
  }
6828
+ const current = new Map(Object.entries(blob.assignments));
6774
6829
  const evacuate = new Set(candidates.filter((c) => c.headroomBytes <= 0).map((c) => c.locationId));
6775
6830
  for (const location of pool) if (location.readOnly) evacuate.add(location.id);
6776
6831
  const plan = planPlacement({
@@ -6794,12 +6849,19 @@ var PlacementService = class {
6794
6849
  });
6795
6850
  }
6796
6851
  try {
6797
- await writeAssignments(this.deps.state, plan.assignments);
6852
+ await this.bounded(writeAssignments(this.deps.state, blob, plan.assignments), "placement plan write");
6798
6853
  } catch (err) {
6799
- this.deps.logger.warn("recorder placement: plan persist failed", { meta: { error: require_dist.errMsg(err) } });
6854
+ this.deps.logger.warn("recorder placement: plan persist failed", { meta: {
6855
+ error: require_dist.errMsg(err),
6856
+ timeoutMs: this.storeTimeoutMs
6857
+ } });
6800
6858
  }
6801
6859
  return plan;
6802
6860
  }
6861
+ /** Bound one addon-store round trip. See {@link PLACEMENT_STORE_TIMEOUT_MS}. */
6862
+ bounded(promise, label) {
6863
+ return withDeadline(promise, this.storeTimeoutMs, label);
6864
+ }
6803
6865
  /**
6804
6866
  * The write targets and what each can still absorb — the SAME pool and the
6805
6867
  * SAME headroom `recompute` plans over.
@@ -6970,7 +7032,7 @@ var PlacementService = class {
6970
7032
  const blob = await readPlacementState(this.deps.state);
6971
7033
  const next = new Map(Object.entries(blob.assignments));
6972
7034
  next.set(key, location.id);
6973
- await writeAssignments(this.deps.state, next);
7035
+ await writeAssignments(this.deps.state, blob, next);
6974
7036
  } catch (err) {
6975
7037
  this.deps.logger.warn("recorder placement: assignment persist failed", {
6976
7038
  tags: { deviceId },
@@ -8282,6 +8344,40 @@ var PLACEMENT_TICK_MS = 3e4;
8282
8344
  */
8283
8345
  var RECONCILE_TICK_MS = 12e4;
8284
8346
  /**
8347
+ * Bound on the ONE store read at the head of every placement and reconcile pass
8348
+ * (`enabledDeviceIds` → `readDeviceConfigs`).
8349
+ *
8350
+ * It is a cross-process read of `hub/sqlite-settings` and it was unbounded, so
8351
+ * it fell through to the 60 s UDS default. Sixty seconds is TWICE the placement
8352
+ * pass's whole budget spent before the pass has looked at a single location, and
8353
+ * it is a third of the reconcile budget spent before a single camera is
8354
+ * evaluated. Measured on the live hub on 2026-08-25: the pass that logged
8355
+ * `durationMs: 181056` spent its first 59 952 ms here and then logged
8356
+ * `could not enumerate persisted devices`.
8357
+ *
8358
+ * Failing this read is already a handled, LOGGED degradation — the pass covers
8359
+ * the active devices only — so a bound costs one pass a narrower scope and buys
8360
+ * back 55 s. Five seconds is three orders of magnitude above the healthy read.
8361
+ */
8362
+ var TRACKED_DEVICES_READ_TIMEOUT_MS = 5e3;
8363
+ /**
8364
+ * A placement pass slower than this is reported WITH ITS PHASES, long before it
8365
+ * reaches the 30 s budget that only says "it was slow".
8366
+ *
8367
+ * The budget line is a tombstone: by the time a pass trips it, it has already
8368
+ * skipped the ticks behind it, and it names no phase — every 240 s pass in this
8369
+ * repo's history had to be attributed by hand-correlating adjacent lines. This
8370
+ * says which half was slow, so a store that is degrading is visible while it is
8371
+ * still only degrading.
8372
+ *
8373
+ * Deliberately BELOW every bound the pass applies (5 s), so a pass that trips
8374
+ * one of them always reports its phases — a threshold equal to the bound misses
8375
+ * the trip by a rounding error. A healthy pass is single-digit milliseconds
8376
+ * (`placementMs` 3–18 ms on the live hub), so 2 s is still two orders of
8377
+ * magnitude of headroom.
8378
+ */
8379
+ var PLACEMENT_PHASE_WARN_MS = 2e3;
8380
+ /**
8285
8381
  * Bound on the broker lease-release RPC (`releaseStreamWithCodec`) during
8286
8382
  * teardown. The UDS request default is 60s, but the runner supervisor's
8287
8383
  * SIGTERM→SIGKILL grace (`$process.stop`/`restart` in the kernel
@@ -8329,11 +8425,6 @@ function passSlowAfterMs(intervalMs) {
8329
8425
  return intervalMs;
8330
8426
  }
8331
8427
  /**
8332
- * Race `promise` against a deadline. Handlers stay attached to the losing
8333
- * promise, so a post-deadline settlement can never surface as a process-level
8334
- * `unhandledRejection`.
8335
- */
8336
- /**
8337
8428
  * Bound on the two registry reads the battery-wake policy makes. They run
8338
8429
  * before any broker call on every attach, battery camera or not, so they are
8339
8430
  * the cheapest place for an unresponsive durable store to cost a recording.
@@ -8348,21 +8439,6 @@ var BATTERY_READ_TIMEOUT_MS = 3e3;
8348
8439
  * the shipped 30 s post-buffer.
8349
8440
  */
8350
8441
  var TRIGGER_LAG_WARN_MS = 2e3;
8351
- function withDeadline(promise, ms, label) {
8352
- return new Promise((resolve, reject) => {
8353
- const timer = setTimeout(() => {
8354
- reject(/* @__PURE__ */ new Error(`${label} timed out after ${ms}ms`));
8355
- }, ms);
8356
- timer.unref?.();
8357
- promise.then((value) => {
8358
- clearTimeout(timer);
8359
- resolve(value);
8360
- }, (err) => {
8361
- clearTimeout(timer);
8362
- reject(err instanceof Error ? err : new Error(String(err)));
8363
- });
8364
- });
8365
- }
8366
8442
  /**
8367
8443
  * Floor for "this writer is pinned but producing nothing".
8368
8444
  *
@@ -8609,7 +8685,8 @@ var RecordingController = class {
8609
8685
  async trackedDeviceIds() {
8610
8686
  const ids = new Set(this.active.keys());
8611
8687
  try {
8612
- for (const id of await this.deps.enabledDeviceIds()) ids.add(id);
8688
+ const enabled = await withDeadline(this.deps.enabledDeviceIds(), TRACKED_DEVICES_READ_TIMEOUT_MS, "recorder: enabledDeviceIds");
8689
+ for (const id of enabled) ids.add(id);
8613
8690
  } catch (err) {
8614
8691
  this.deps.logger.warn("recorder controller: could not enumerate persisted devices — this pass covers the active ones only", { meta: {
8615
8692
  error: require_dist.errMsg(err),
@@ -8627,12 +8704,22 @@ var RecordingController = class {
8627
8704
  async replanPlacement() {
8628
8705
  if (this.stopped || this.paused) return;
8629
8706
  if (this.deps.onPlacementTick === void 0) return;
8707
+ const startedAt = this.now();
8630
8708
  const ids = await this.trackedDeviceIds();
8709
+ const enumeratedAt = this.now();
8631
8710
  try {
8632
8711
  await this.deps.onPlacementTick(ids);
8633
8712
  } catch (err) {
8634
8713
  this.deps.logger.warn("recorder controller: placement recompute failed", { meta: { error: require_dist.errMsg(err) } });
8635
8714
  }
8715
+ const finishedAt = this.now();
8716
+ if (finishedAt - startedAt > PLACEMENT_PHASE_WARN_MS) this.deps.logger.warn("recorder: placement pass ran long — phase breakdown", { meta: {
8717
+ totalMs: finishedAt - startedAt,
8718
+ enumerateMs: enumeratedAt - startedAt,
8719
+ recomputeMs: finishedAt - enumeratedAt,
8720
+ devices: ids.length,
8721
+ budgetMs: PLACEMENT_TICK_MS
8722
+ } });
8636
8723
  }
8637
8724
  /**
8638
8725
  * The backstop pass: re-evaluate every tracked-or-configured device.
@@ -11808,7 +11895,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
11808
11895
  */
11809
11896
  async resetPlacementPlan() {
11810
11897
  try {
11811
- await writeAssignments(this.placementStore(), /* @__PURE__ */ new Map());
11898
+ const store = this.placementStore();
11899
+ await writeAssignments(store, await readPlacementState(store), /* @__PURE__ */ new Map());
11812
11900
  this.ctx.logger.info("recorder: placement plan reset after storage migration");
11813
11901
  } catch (err) {
11814
11902
  this.ctx.logger.warn("recorder: placement plan reset failed", { meta: { error: require_dist.errMsg(err) } });
@@ -1,4 +1,4 @@
1
- import { B as deriveRecordingMode, Dt as hydrateSchema, Gt as string, Ht as number, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-HLAOw6Pe.mjs";
1
+ import { B as deriveRecordingMode, Dt as hydrateSchema, Gt as string, Ht as number, M as batteryCapability, Pt as selectAssignedProfileSlots, R as deriveBatteryPresence, St as BaseAddon, T as RecordingConfigSchema, Tt as DeviceType, Ut as object, Wt as record, at as recordingCapability, b as OpsLogEntrySchema, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, ft as storageEvictableCapability, g as ExportRecordSchema, gt as errMsg, i as BatteryStatusSchema, jt as nodePin, m as EVENT_PAD_MS, ot as recordingExportCapability, qt as EventCategory, ut as resolveRecordingProfiles, w as RECORDING_EXPORT_MAX_READ_BYTES, wt as DeviceFeature } from "../dist-CMio-1qq.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
4
4
  import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-CgBCF-mE.mjs";
@@ -6060,13 +6060,20 @@ async function readPlacementState(state) {
6060
6060
  * Replace the assignment map, preserving everything else in the blob. The WHOLE
6061
6061
  * blob round-trips on every write (the DurableState contract), so the remembered
6062
6062
  * volume ids cannot be dropped by an assignment write.
6063
+ *
6064
+ * `base` is REQUIRED, and it is the blob the caller already read. This used to
6065
+ * re-read the store itself, which made every write two round trips instead of
6066
+ * one — and every one of them goes over the UDS door to `hub/sqlite-settings`
6067
+ * with the 60 s default bound. The placement pass read the blob, then wrote it,
6068
+ * and paid for the read TWICE: 4 × 60 s = the `durationMs: 240003` pass measured
6069
+ * on the live hub against a 30 s budget. Taking the blob as an argument makes
6070
+ * "one plan, one read, one write" a shape the type system enforces.
6063
6071
  */
6064
- async function writeAssignments(state, assignments) {
6065
- const blob = await state.get();
6072
+ async function writeAssignments(state, base, assignments) {
6066
6073
  const next = {};
6067
6074
  for (const [key, locationId] of assignments) next[key] = locationId;
6068
6075
  await state.set({
6069
- ...blob,
6076
+ ...base,
6070
6077
  assignments: next,
6071
6078
  updatedAt: Date.now()
6072
6079
  });
@@ -6582,9 +6589,45 @@ var fsVolumeProbeDeps = {
6582
6589
  newVolumeId: () => randomUUID()
6583
6590
  };
6584
6591
  //#endregion
6592
+ //#region src/recorder/addon/with-deadline.ts
6593
+ /**
6594
+ * Race a promise against a deadline.
6595
+ *
6596
+ * Every cross-process call the recorder makes goes over a UDS door whose
6597
+ * DEFAULT timeout is 60 s (`DEFAULT_UDS_REQUEST_TIMEOUT_MS`). That default is a
6598
+ * backstop, not a budget: a pass that makes four of them serially and bounds
6599
+ * none of them costs four minutes, and the recorder's periodic passes SKIP
6600
+ * every fire while a run is in flight — so the unbounded call does not make one
6601
+ * pass late, it cancels the seven behind it. Measured on the live hub as a
6602
+ * `durationMs: 240003` placement pass against a 30 s budget (2026-08-18/19).
6603
+ *
6604
+ * Handlers stay attached to the losing promise, so a post-deadline settlement
6605
+ * can never surface as a process-level `unhandledRejection`.
6606
+ */
6607
+ function withDeadline(promise, ms, label) {
6608
+ return new Promise((resolve, reject) => {
6609
+ const timer = setTimeout(() => {
6610
+ reject(/* @__PURE__ */ new Error(`${label} timed out after ${ms}ms`));
6611
+ }, ms);
6612
+ timer.unref?.();
6613
+ promise.then((value) => {
6614
+ clearTimeout(timer);
6615
+ resolve(value);
6616
+ }, (err) => {
6617
+ clearTimeout(timer);
6618
+ reject(err instanceof Error ? err : new Error(String(err)));
6619
+ });
6620
+ });
6621
+ }
6622
+ //#endregion
6585
6623
  //#region src/recorder/addon/placement-service.ts
6586
6624
  /** Free space a location keeps in reserve when its config names no floor. */
6587
6625
  var DEFAULT_MIN_FREE_PERCENT = 10;
6626
+ /** Nothing planned, nothing to persist. */
6627
+ var NO_PLAN = {
6628
+ assignments: /* @__PURE__ */ new Map(),
6629
+ changes: []
6630
+ };
6588
6631
  var PlacementService = class {
6589
6632
  deps;
6590
6633
  /** Locations whose volume has been probed since the last location change. */
@@ -6613,6 +6656,9 @@ var PlacementService = class {
6613
6656
  get reprobeMs() {
6614
6657
  return this.deps.reprobeMs ?? 6e4;
6615
6658
  }
6659
+ get storeTimeoutMs() {
6660
+ return this.deps.storeTimeoutMs ?? 5e3;
6661
+ }
6616
6662
  /**
6617
6663
  * Forget every probe verdict. Called whenever the location set is re-resolved
6618
6664
  * (rescan, storage migration) — a remount is exactly the event the probe
@@ -6755,6 +6801,10 @@ var PlacementService = class {
6755
6801
  * controller's existing 30s tick — it changes the PLAN only; a running writer
6756
6802
  * keeps its root until it next attaches. Returns the plan so a caller can log
6757
6803
  * or assert on it.
6804
+ *
6805
+ * ONE read and, at most, ONE write. Both bounded
6806
+ * ({@link PLACEMENT_STORE_TIMEOUT_MS}) and both against the same blob: the
6807
+ * plan the pass reads is the plan it writes back.
6758
6808
  */
6759
6809
  async recompute(work) {
6760
6810
  const pool = this.pool();
@@ -6764,12 +6814,17 @@ var PlacementService = class {
6764
6814
  type: item.profile === "low" ? "recordingsLow" : "recordings",
6765
6815
  dailyBytes: this.deps.dailyBytesFor?.(item.deviceId, item.profile) ?? 0
6766
6816
  }));
6767
- let current = /* @__PURE__ */ new Map();
6817
+ let blob;
6768
6818
  try {
6769
- current = new Map(Object.entries((await readPlacementState(this.deps.state)).assignments));
6819
+ blob = await this.bounded(readPlacementState(this.deps.state), "placement plan read");
6770
6820
  } catch (err) {
6771
- this.deps.logger.warn("recorder placement: plan read failed — planning from scratch", { meta: { error: errMsg(err) } });
6821
+ this.deps.logger.warn("recorder placement: plan read failed — this tick plans NOTHING and the stored plan stands", { meta: {
6822
+ error: errMsg(err),
6823
+ timeoutMs: this.storeTimeoutMs
6824
+ } });
6825
+ return NO_PLAN;
6772
6826
  }
6827
+ const current = new Map(Object.entries(blob.assignments));
6773
6828
  const evacuate = new Set(candidates.filter((c) => c.headroomBytes <= 0).map((c) => c.locationId));
6774
6829
  for (const location of pool) if (location.readOnly) evacuate.add(location.id);
6775
6830
  const plan = planPlacement({
@@ -6793,12 +6848,19 @@ var PlacementService = class {
6793
6848
  });
6794
6849
  }
6795
6850
  try {
6796
- await writeAssignments(this.deps.state, plan.assignments);
6851
+ await this.bounded(writeAssignments(this.deps.state, blob, plan.assignments), "placement plan write");
6797
6852
  } catch (err) {
6798
- this.deps.logger.warn("recorder placement: plan persist failed", { meta: { error: errMsg(err) } });
6853
+ this.deps.logger.warn("recorder placement: plan persist failed", { meta: {
6854
+ error: errMsg(err),
6855
+ timeoutMs: this.storeTimeoutMs
6856
+ } });
6799
6857
  }
6800
6858
  return plan;
6801
6859
  }
6860
+ /** Bound one addon-store round trip. See {@link PLACEMENT_STORE_TIMEOUT_MS}. */
6861
+ bounded(promise, label) {
6862
+ return withDeadline(promise, this.storeTimeoutMs, label);
6863
+ }
6802
6864
  /**
6803
6865
  * The write targets and what each can still absorb — the SAME pool and the
6804
6866
  * SAME headroom `recompute` plans over.
@@ -6969,7 +7031,7 @@ var PlacementService = class {
6969
7031
  const blob = await readPlacementState(this.deps.state);
6970
7032
  const next = new Map(Object.entries(blob.assignments));
6971
7033
  next.set(key, location.id);
6972
- await writeAssignments(this.deps.state, next);
7034
+ await writeAssignments(this.deps.state, blob, next);
6973
7035
  } catch (err) {
6974
7036
  this.deps.logger.warn("recorder placement: assignment persist failed", {
6975
7037
  tags: { deviceId },
@@ -8281,6 +8343,40 @@ var PLACEMENT_TICK_MS = 3e4;
8281
8343
  */
8282
8344
  var RECONCILE_TICK_MS = 12e4;
8283
8345
  /**
8346
+ * Bound on the ONE store read at the head of every placement and reconcile pass
8347
+ * (`enabledDeviceIds` → `readDeviceConfigs`).
8348
+ *
8349
+ * It is a cross-process read of `hub/sqlite-settings` and it was unbounded, so
8350
+ * it fell through to the 60 s UDS default. Sixty seconds is TWICE the placement
8351
+ * pass's whole budget spent before the pass has looked at a single location, and
8352
+ * it is a third of the reconcile budget spent before a single camera is
8353
+ * evaluated. Measured on the live hub on 2026-08-25: the pass that logged
8354
+ * `durationMs: 181056` spent its first 59 952 ms here and then logged
8355
+ * `could not enumerate persisted devices`.
8356
+ *
8357
+ * Failing this read is already a handled, LOGGED degradation — the pass covers
8358
+ * the active devices only — so a bound costs one pass a narrower scope and buys
8359
+ * back 55 s. Five seconds is three orders of magnitude above the healthy read.
8360
+ */
8361
+ var TRACKED_DEVICES_READ_TIMEOUT_MS = 5e3;
8362
+ /**
8363
+ * A placement pass slower than this is reported WITH ITS PHASES, long before it
8364
+ * reaches the 30 s budget that only says "it was slow".
8365
+ *
8366
+ * The budget line is a tombstone: by the time a pass trips it, it has already
8367
+ * skipped the ticks behind it, and it names no phase — every 240 s pass in this
8368
+ * repo's history had to be attributed by hand-correlating adjacent lines. This
8369
+ * says which half was slow, so a store that is degrading is visible while it is
8370
+ * still only degrading.
8371
+ *
8372
+ * Deliberately BELOW every bound the pass applies (5 s), so a pass that trips
8373
+ * one of them always reports its phases — a threshold equal to the bound misses
8374
+ * the trip by a rounding error. A healthy pass is single-digit milliseconds
8375
+ * (`placementMs` 3–18 ms on the live hub), so 2 s is still two orders of
8376
+ * magnitude of headroom.
8377
+ */
8378
+ var PLACEMENT_PHASE_WARN_MS = 2e3;
8379
+ /**
8284
8380
  * Bound on the broker lease-release RPC (`releaseStreamWithCodec`) during
8285
8381
  * teardown. The UDS request default is 60s, but the runner supervisor's
8286
8382
  * SIGTERM→SIGKILL grace (`$process.stop`/`restart` in the kernel
@@ -8328,11 +8424,6 @@ function passSlowAfterMs(intervalMs) {
8328
8424
  return intervalMs;
8329
8425
  }
8330
8426
  /**
8331
- * Race `promise` against a deadline. Handlers stay attached to the losing
8332
- * promise, so a post-deadline settlement can never surface as a process-level
8333
- * `unhandledRejection`.
8334
- */
8335
- /**
8336
8427
  * Bound on the two registry reads the battery-wake policy makes. They run
8337
8428
  * before any broker call on every attach, battery camera or not, so they are
8338
8429
  * the cheapest place for an unresponsive durable store to cost a recording.
@@ -8347,21 +8438,6 @@ var BATTERY_READ_TIMEOUT_MS = 3e3;
8347
8438
  * the shipped 30 s post-buffer.
8348
8439
  */
8349
8440
  var TRIGGER_LAG_WARN_MS = 2e3;
8350
- function withDeadline(promise, ms, label) {
8351
- return new Promise((resolve, reject) => {
8352
- const timer = setTimeout(() => {
8353
- reject(/* @__PURE__ */ new Error(`${label} timed out after ${ms}ms`));
8354
- }, ms);
8355
- timer.unref?.();
8356
- promise.then((value) => {
8357
- clearTimeout(timer);
8358
- resolve(value);
8359
- }, (err) => {
8360
- clearTimeout(timer);
8361
- reject(err instanceof Error ? err : new Error(String(err)));
8362
- });
8363
- });
8364
- }
8365
8441
  /**
8366
8442
  * Floor for "this writer is pinned but producing nothing".
8367
8443
  *
@@ -8608,7 +8684,8 @@ var RecordingController = class {
8608
8684
  async trackedDeviceIds() {
8609
8685
  const ids = new Set(this.active.keys());
8610
8686
  try {
8611
- for (const id of await this.deps.enabledDeviceIds()) ids.add(id);
8687
+ const enabled = await withDeadline(this.deps.enabledDeviceIds(), TRACKED_DEVICES_READ_TIMEOUT_MS, "recorder: enabledDeviceIds");
8688
+ for (const id of enabled) ids.add(id);
8612
8689
  } catch (err) {
8613
8690
  this.deps.logger.warn("recorder controller: could not enumerate persisted devices — this pass covers the active ones only", { meta: {
8614
8691
  error: errMsg(err),
@@ -8626,12 +8703,22 @@ var RecordingController = class {
8626
8703
  async replanPlacement() {
8627
8704
  if (this.stopped || this.paused) return;
8628
8705
  if (this.deps.onPlacementTick === void 0) return;
8706
+ const startedAt = this.now();
8629
8707
  const ids = await this.trackedDeviceIds();
8708
+ const enumeratedAt = this.now();
8630
8709
  try {
8631
8710
  await this.deps.onPlacementTick(ids);
8632
8711
  } catch (err) {
8633
8712
  this.deps.logger.warn("recorder controller: placement recompute failed", { meta: { error: errMsg(err) } });
8634
8713
  }
8714
+ const finishedAt = this.now();
8715
+ if (finishedAt - startedAt > PLACEMENT_PHASE_WARN_MS) this.deps.logger.warn("recorder: placement pass ran long — phase breakdown", { meta: {
8716
+ totalMs: finishedAt - startedAt,
8717
+ enumerateMs: enumeratedAt - startedAt,
8718
+ recomputeMs: finishedAt - enumeratedAt,
8719
+ devices: ids.length,
8720
+ budgetMs: PLACEMENT_TICK_MS
8721
+ } });
8635
8722
  }
8636
8723
  /**
8637
8724
  * The backstop pass: re-evaluate every tracked-or-configured device.
@@ -11807,7 +11894,8 @@ var RecorderV2Addon = class extends BaseAddon {
11807
11894
  */
11808
11895
  async resetPlacementPlan() {
11809
11896
  try {
11810
- await writeAssignments(this.placementStore(), /* @__PURE__ */ new Map());
11897
+ const store = this.placementStore();
11898
+ await writeAssignments(store, await readPlacementState(store), /* @__PURE__ */ new Map());
11811
11899
  this.ctx.logger.info("recorder: placement plan reset after storage migration");
11812
11900
  } catch (err) {
11813
11901
  this.ctx.logger.warn("recorder: placement plan reset failed", { meta: { error: errMsg(err) } });
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { gt as errMsg } from "./dist-HLAOw6Pe.mjs";
2
+ import { gt as errMsg } from "./dist-CMio-1qq.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BZUXb5S5.js");
1
+ const require_dist = require("./dist-D9x2_csL.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
4
4
  var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-BxkCoVjw.js");
3
- const require_lazy_sharp = require("../lazy-sharp-DTDXrhMG.js");
2
+ const require_worker_protocol = require("../worker-protocol-BmBYCioC.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-DrLwLKpg.js");
4
4
  //#region src/session-decode/color-conversion.ts
5
5
  /**
6
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -1,4 +1,4 @@
1
- import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-h7Smb_0i.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-BHNWB9Ve.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -1,7 +1,7 @@
1
1
  import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Do2DEtRt.mjs";
2
2
  import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
3
3
  import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
4
- import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cs8bmrWP.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs";
5
5
  import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
6
6
  //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
7
7
  var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.105",
21
+ version: "1.2.106",
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.71",
36
+ version: "1.2.72",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_stream_broker_widgets",