@camstack/addon-pipeline 1.2.196 → 1.2.197

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 (38) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-CJdc8j83.js → default-detection-model-CM-8U0Bb.js} +1 -1
  4. package/dist/{default-detection-model-BGBAmtEp.mjs → default-detection-model-CplzzTmN.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-HG5geL9t.js → dist-CBa1j3On.js} +141 -35
  8. package/dist/{dist-mYDK58Jp.mjs → dist-Y4BsF_EC.mjs} +130 -36
  9. package/dist/{lazy-sharp-DmAwZ0s9.js → lazy-sharp-CS5WE9mI.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-xRsaPwBa.mjs → node-B5ouEQYN.mjs} +1 -1
  13. package/dist/{node--my8JTOW.js → node-C-B8VuGi.js} +1 -1
  14. package/dist/pipeline-runner/index.js +363 -6
  15. package/dist/pipeline-runner/index.mjs +362 -5
  16. package/dist/{process-memory-CoCSce63.js → process-memory-CmeKr_MZ.js} +1 -1
  17. package/dist/{process-memory-BDHR3lxe.mjs → process-memory-DeO2OZ3-.mjs} +1 -1
  18. package/dist/recorder/index.js +131 -18
  19. package/dist/recorder/index.mjs +131 -18
  20. package/dist/{retire-root-keys-KE6D6Xh_.js → retire-root-keys-Csb8z2f3.js} +19 -0
  21. package/dist/{retire-root-keys-DMolfhsP.mjs → retire-root-keys-DZvlso2l.mjs} +19 -0
  22. package/dist/{segment-demux-js-CXvgKfsn.mjs → segment-demux-js-DXPgq2cf.mjs} +1 -1
  23. package/dist/{segment-demux-js-CBynn9Gh.js → segment-demux-js-DiIsywGz.js} +1 -1
  24. package/dist/session-decode/decode-worker-child.js +2 -2
  25. package/dist/session-decode/decode-worker-child.mjs +1 -1
  26. package/dist/stream-broker/_stub.js +2 -2
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BMsB2MVD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Q-6HzWJ0.mjs} +3 -3
  28. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cv5McbTI.mjs} +1 -1
  29. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CBxobiZW.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs} +1 -1
  30. package/dist/stream-broker/demux-worker-child.js +1 -1
  31. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  32. package/dist/stream-broker/{hostInit-CZig_taJ.mjs → hostInit-C-2Eo_nr.mjs} +3 -3
  33. package/dist/stream-broker/index.js +4 -4
  34. package/dist/stream-broker/index.mjs +4 -4
  35. package/dist/stream-broker/remoteEntry.js +1 -1
  36. package/dist/{worker-protocol-DCsi4YS1.js → worker-protocol-CXsdriRX.js} +1 -1
  37. package/dist/{worker-protocol-C0R1DDC0.mjs → worker-protocol-_7zrToTo.mjs} +1 -1
  38. package/package.json +1 -1
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-HG5geL9t.js");
6
- const require_node = require("../node--my8JTOW.js");
5
+ const require_dist = require("../dist-CBa1j3On.js");
6
+ const require_node = require("../node-C-B8VuGi.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
9
9
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
10
- const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
10
+ const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
11
  let node_crypto = require("node:crypto");
12
12
  let node_fs = require("node:fs");
13
13
  let node_path = require("node:path");
@@ -3884,6 +3884,31 @@ var CalendarIndex = class {
3884
3884
  //#endregion
3885
3885
  //#region src/recorder/addon/config-store.ts
3886
3886
  /**
3887
+ * Per-device recording-config persistence for the recorder addon.
3888
+ *
3889
+ * ONE ROW PER CAMERA in `recorder:recording-configs`, through
3890
+ * {@link RowMapStore}. Asking what camera 617 records is a primary-key lookup;
3891
+ * saving its config writes that row and touches no other.
3892
+ *
3893
+ * ## What this replaced, and why
3894
+ *
3895
+ * Until the 2026-08-19 flatten every camera's `RecordingConfig` lived in ONE
3896
+ * key of the recorder's `addon-settings` / `root` blob — a row that was 201 833
3897
+ * bytes on the reference hub, because the ops-log and the export history shared
3898
+ * it. So `loadDeviceConfig(617)` read and `JSON.parse`d 201 KB, and
3899
+ * `saveDeviceConfig` read that, merged, and rewrote it. The staging walk did it
3900
+ * once per device and lost 45–267 s of a boot to it (2026-08-18); teaching the
3901
+ * walk to read the map once capped the symptom, and this removes the cause.
3902
+ *
3903
+ * ## Failure direction
3904
+ *
3905
+ * `RecordingConfigSchema` rejects unknown keys so a stale caller fails loudly,
3906
+ * but a row written by an older build may still carry retired ones — `.strip()`
3907
+ * drops them. And validation is now PER ROW: one unparseable camera costs that
3908
+ * camera, where the blob's single `safeParse` took every camera down to the
3909
+ * empty fallback (= the whole fleet silently unconfigured).
3910
+ */
3911
+ /**
3887
3912
  * @durable class=config owner=recorder
3888
3913
  * write="one row per camera, written by `saveDeviceConfig` (the `recording` cap's
3889
3914
  * setConfig / band authoring) — the only writer"
@@ -3893,7 +3918,7 @@ var CalendarIndex = class {
3893
3918
  var RECORDING_CONFIGS_COLLECTION = "recorder:recording-configs";
3894
3919
  var RECORDING_CONFIGS_SPEC = {
3895
3920
  collection: RECORDING_CONFIGS_COLLECTION,
3896
- schema: require_dist.RecordingConfigSchema.strip(),
3921
+ schema: require_dist.preprocess((raw) => require_dist.migrateLegacyDeviceSignalConfig(raw).value, require_dist.RecordingConfigSchema.strip()),
3897
3922
  columns: [
3898
3923
  {
3899
3924
  name: "deviceId",
@@ -3959,7 +3984,10 @@ async function loadDeviceConfig(state, deviceId) {
3959
3984
  */
3960
3985
  function deviceIdsWithLostBands(configs) {
3961
3986
  const lost = [];
3962
- for (const [deviceId, config] of configs) if (config.enabled && config.bands.length === 0 && config.mode && config.mode !== "off") lost.push(deviceId);
3987
+ for (const [deviceId, config] of configs) {
3988
+ if (config.deviceDecision === true) continue;
3989
+ if (config.enabled && config.bands.length === 0 && config.mode && config.mode !== "off") lost.push(deviceId);
3990
+ }
3963
3991
  return lost.toSorted((a, b) => a - b);
3964
3992
  }
3965
3993
  /** Boot-time check: log the {@link deviceIdsWithLostBands} once, if any. */
@@ -3969,6 +3997,48 @@ async function warnLostBands(state, logger) {
3969
3997
  logger.warn(`recording config: device(s) ${lost.join(", ")} claim a recording mode but have NO bands — a pre-retirement config that lost its legacy authoring fields. Nothing is recorded for them until their bands are re-authored.`);
3970
3998
  }
3971
3999
  /**
4000
+ * Persist the D371→D380 shape for every stored row — the migration, in code,
4001
+ * run once per boot and IDEMPOTENT: a second run finds no legacy key and
4002
+ * writes nothing.
4003
+ *
4004
+ * It reads the rows RAW ({@link RowMapStore.readAllRaw}) on purpose. The read
4005
+ * schema already migrates in memory, so by the time `readAll` answers there is
4006
+ * nothing left to recognise — and the retired key is stripped by a non-strict
4007
+ * `z.object`, so the migration would look like it had nothing to do on exactly
4008
+ * the row it exists for.
4009
+ *
4010
+ * Both outcomes are logged, because both change what a camera records and
4011
+ * silence would read as "nothing happened" — which is what a stripped key
4012
+ * looks like from the outside.
4013
+ */
4014
+ async function migrateLegacyDeviceSignalRows(state, logger) {
4015
+ const migrated = [];
4016
+ const stripped = [];
4017
+ for (const [key, raw] of await state.readAllRaw()) {
4018
+ const outcome = require_dist.migrateLegacyDeviceSignalConfig(raw);
4019
+ if (!outcome.migrated && !outcome.strippedOnly) continue;
4020
+ const deviceId = Number(key);
4021
+ if (!Number.isInteger(deviceId)) continue;
4022
+ const parsed = require_dist.RecordingConfigSchema.strip().safeParse(outcome.value);
4023
+ if (!parsed.success) {
4024
+ logger.warn("recording config: legacy device-signal row could not be migrated", {
4025
+ tags: { deviceId },
4026
+ meta: { error: parsed.error.message }
4027
+ });
4028
+ continue;
4029
+ }
4030
+ await saveDeviceConfig(state, deviceId, parsed.data);
4031
+ if (outcome.migrated) migrated.push(deviceId);
4032
+ else stripped.push(deviceId);
4033
+ }
4034
+ if (migrated.length > 0) logger.info("recording config: migrated to the on-device-decision mode (the retired band trigger)", { meta: { deviceIds: migrated } });
4035
+ if (stripped.length > 0) logger.warn("recording config: a band mixed the retired device-signal trigger with others — the key was dropped and the band kept. These cameras no longer follow their own signal until the on-device-decision mode is selected for them.", { meta: { deviceIds: stripped } });
4036
+ return {
4037
+ migrated,
4038
+ stripped
4039
+ };
4040
+ }
4041
+ /**
3972
4042
  * Persist (insert or replace) a device's config — ONE row. Validates against
3973
4043
  * `RecordingConfigSchema` at the boundary (untrusted cap input). Returns the
3974
4044
  * validated config that was persisted.
@@ -12462,6 +12532,13 @@ function bandActiveAt(band, date) {
12462
12532
  if (s > e) return applies(wd) && t >= s || applies(prevWd) && t < e;
12463
12533
  return applies(wd);
12464
12534
  }
12535
+ /**
12536
+ * The band covering `date`, or null. GENERIC over the band type so the caller
12537
+ * gets back exactly what it put in: the recorder decides on `EffectiveBand`s
12538
+ * (`band-decision.ts`), which carry the one non-authored fact — that the DEVICE
12539
+ * decides — and a return type widened to `RecordingBand` would erase it right
12540
+ * where the gates read it.
12541
+ */
12465
12542
  function activeBandAt(config, date) {
12466
12543
  return config.bands.find((b) => bandActiveAt(b, date)) ?? null;
12467
12544
  }
@@ -12486,6 +12563,37 @@ function activeBandAt(config, date) {
12486
12563
  * unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
12487
12564
  */
12488
12565
  /**
12566
+ * THE band a `deviceDecision` config decides through: always on (every day,
12567
+ * `start === end` — the band engine's always-on shape), `events`, and listening
12568
+ * to exactly one source, the camera's own level.
12569
+ *
12570
+ * Its buffers are the canonical pad ({@link DEFAULT_EVENTS_BAND_BUFFER_SEC}),
12571
+ * not per-camera knobs: this mode's whole contract with the operator is ONE
12572
+ * flag. The pad only ever retains footage around an episode the device itself
12573
+ * delimited, so there is nothing here for an operator to tune.
12574
+ */
12575
+ var DEVICE_DECISION_BAND = Object.freeze({
12576
+ days: [],
12577
+ start: "00:00",
12578
+ end: "00:00",
12579
+ mode: "events",
12580
+ deviceSignal: true,
12581
+ preBufferSec: require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec,
12582
+ postBufferSec: require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec
12583
+ });
12584
+ /**
12585
+ * THE bands the recorder decides on for a config — the ONE place
12586
+ * `config.bands` is turned into what the gates read.
12587
+ *
12588
+ * `deviceDecision` resolves to {@link DEVICE_DECISION_BAND} and to nothing
12589
+ * else; every other config is its own authored bands, byte for byte. Because
12590
+ * the schema makes the two exclusive, this is a choice and never a merge.
12591
+ */
12592
+ function effectiveBands(config) {
12593
+ if (config.deviceDecision === true) return [DEVICE_DECISION_BAND];
12594
+ return config.bands ?? [];
12595
+ }
12596
+ /**
12489
12597
  * THE one place a band's pre/post buffers are read. An ABSENT field takes
12490
12598
  * {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
12491
12599
  * is honoured verbatim.
@@ -12526,6 +12634,9 @@ function triggerKey(source) {
12526
12634
  case "signal": return "signal";
12527
12635
  }
12528
12636
  }
12637
+ /** The single source {@link DEVICE_DECISION_BAND} listens to, shared so the
12638
+ * band never allocates a new one per call. */
12639
+ var SIGNAL_SOURCES = Object.freeze([{ kind: "signal" }]);
12529
12640
  /**
12530
12641
  * LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
12531
12642
  * una banda diventano `TriggerSource[]`.
@@ -12541,6 +12652,7 @@ function triggerKey(source) {
12541
12652
  * denuncia invece di restare zitto.
12542
12653
  */
12543
12654
  function bandTriggerSources(band) {
12655
+ if (band.deviceSignal === true) return SIGNAL_SOURCES;
12544
12656
  const t = band.triggers;
12545
12657
  if (t === void 0) return [];
12546
12658
  const sources = [];
@@ -12554,7 +12666,6 @@ function bandTriggerSources(band) {
12554
12666
  kind: "sensor",
12555
12667
  sourceDeviceId
12556
12668
  });
12557
- if (t.deviceSignal === true) sources.push({ kind: "signal" });
12558
12669
  return sources;
12559
12670
  }
12560
12671
  /**
@@ -12740,8 +12851,8 @@ function eventsBandDemanded(band, triggers, atMs) {
12740
12851
  */
12741
12852
  function shouldRecordAt(config, triggers, at) {
12742
12853
  if (!config.enabled) return false;
12743
- const bands = config.bands;
12744
- if (!bands || bands.length === 0) return false;
12854
+ const bands = effectiveBands(config);
12855
+ if (bands.length === 0) return false;
12745
12856
  const band = activeBandAt({ bands }, at);
12746
12857
  if (!band) return false;
12747
12858
  if (band.mode === "continuous") return true;
@@ -12996,8 +13107,8 @@ function candidateEdgesMs(bands, nowMs) {
12996
13107
  * costs one wasted wake-up and never costs footage.
12997
13108
  */
12998
13109
  function nextBandEdgeMs(config, nowMs) {
12999
- const bands = config.bands;
13000
- if (!bands || bands.length === 0) return null;
13110
+ const bands = effectiveBands(config);
13111
+ if (bands.length === 0) return null;
13001
13112
  const current = activeBandAt({ bands }, new Date(nowMs));
13002
13113
  const candidates = [...new Set(candidateEdgesMs(bands, nowMs))].filter((ms) => ms > nowMs).toSorted((a, b) => a - b);
13003
13114
  for (const ms of candidates) if (activeBandAt({ bands }, new Date(ms)) !== current) return ms;
@@ -13032,8 +13143,8 @@ function eventsWindowCloseMs(band, triggers, nowMs) {
13032
13143
  function nextDecisionChangeAt(input) {
13033
13144
  const { config, triggers, nowMs } = input;
13034
13145
  if (!config.enabled) return null;
13035
- const bands = config.bands;
13036
- if (!bands || bands.length === 0) return null;
13146
+ const bands = effectiveBands(config);
13147
+ if (bands.length === 0) return null;
13037
13148
  const band = activeBandAt({ bands }, new Date(nowMs));
13038
13149
  const edgeMs = nextBandEdgeMs(config, nowMs);
13039
13150
  const closeMs = band === null ? null : eventsWindowCloseMs(band, triggers, nowMs);
@@ -14503,7 +14614,7 @@ var RecordingController = class {
14503
14614
  const config = await this.deps.loadConfig(deviceId);
14504
14615
  if (config.enabled !== true) return;
14505
14616
  const nowMs = this.now();
14506
- const band = activeBandAt({ bands: config.bands ?? [] }, new Date(nowMs));
14617
+ const band = activeBandAt({ bands: effectiveBands(config) }, new Date(nowMs));
14507
14618
  if (!band || band.mode !== "events") return;
14508
14619
  const { postMs } = resolveBandBufferMs(band);
14509
14620
  const sinceMs = nowMs - postMs - TRIGGER_HISTORY_MARGIN_MS;
@@ -14910,7 +15021,7 @@ var RecordingController = class {
14910
15021
  if (this.stopped) return;
14911
15022
  let band;
14912
15023
  try {
14913
- band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15024
+ band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14914
15025
  } catch (err) {
14915
15026
  this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
14916
15027
  tags: { deviceId },
@@ -14935,7 +15046,7 @@ var RecordingController = class {
14935
15046
  */
14936
15047
  async postBufferMsFor(deviceId, atMs) {
14937
15048
  try {
14938
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15049
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14939
15050
  if (band) return resolveBandBufferMs(band).postMs;
14940
15051
  } catch (err) {
14941
15052
  this.deps.logger.warn("recorder: trigger history using the DEFAULT post-buffer — config read failed", {
@@ -15026,7 +15137,7 @@ var RecordingController = class {
15026
15137
  reportUnrecordable(deviceId, config) {
15027
15138
  if (!config.enabled) return;
15028
15139
  if (this.unrecordableReported.has(deviceId)) return;
15029
- const bands = config.bands ?? [];
15140
+ const bands = effectiveBands(config);
15030
15141
  if (bands.length === 0) {
15031
15142
  this.unrecordableReported.add(deviceId);
15032
15143
  this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
@@ -15363,7 +15474,7 @@ var RecordingController = class {
15363
15474
  */
15364
15475
  async handleFinalizedSegment(deviceId, profile, placement, attach, startMs, durMs, flatAbsPath) {
15365
15476
  try {
15366
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(startMs + durMs));
15477
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(startMs + durMs));
15367
15478
  const triggers = this.triggersFor(deviceId);
15368
15479
  if (band?.mode === "events" && !segmentRetainedForEvents(startMs, startMs + durMs, band, triggers, attach)) {
15369
15480
  this.reportDiscardedSegment(deviceId, profile, attach, band, triggers, startMs, durMs);
@@ -17367,7 +17478,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17367
17478
  watchIntervalMs: this.config.watchIntervalMs
17368
17479
  }
17369
17480
  });
17370
- warnLostBands(this.configStore(), this.ctx.logger);
17481
+ migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
17482
+ this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: require_dist.errMsg(err) } });
17483
+ }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
17371
17484
  const providers = [
17372
17485
  {
17373
17486
  capability: require_dist.recordingCapability,
@@ -1,9 +1,9 @@
1
- import { $t as number, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as errMsg, Ft as DeviceFeature, Gt as selectAssignedProfileSlots, H as deriveBatteryPresence, Ht as nodePin, It as DeviceType, Jt as array, Kt as sleep$1, Nt as BaseAddon, P as batteryCapability, R as customAction, Rt as hydrateSchema, St as storageEvictableCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Yt as boolean, bt as resolveRecordingProfiles, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as object, et as isFirstLevelMacroClass, g as ExportRecordSchema, gt as recordingSignalCapability, ht as recordingExportCapability, i as BatteryStatusSchema, in as EventCategory, mt as recordingCapability, nn as string, qt as _enum, tn as record, tt as loadContributionCapability, x as OpsLogEntrySchema } from "../dist-mYDK58Jp.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-xRsaPwBa.mjs";
1
+ import { Ct as storageEvictableCapability, D as RecordingSignalStatusSchema, Dt as errMsg, E as RecordingConfigSchema, H as deriveBatteryPresence, It as DeviceFeature, Jt as _enum, Kt as selectAssignedProfileSlots, Lt as DeviceType, P as batteryCapability, Pt as BaseAddon, R as customAction, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as nodePin, V as defineCustomActions, W as deriveRecordingMode, Xt as boolean, Y as evaluateSensorEdge, Yt as array, _t as recordingSignalCapability, at as migrateLegacyDeviceSignalConfig, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, en as number, et as isFirstLevelMacroClass, g as ExportRecordSchema, gt as recordingExportCapability, ht as recordingCapability, i as BatteryStatusSchema, in as string, nn as preprocess, on as EventCategory, qt as sleep$1, rn as record, tn as object, tt as loadContributionCapability, x as OpsLogEntrySchema, xt as resolveRecordingProfiles, zt as hydrateSchema } from "../dist-Y4BsF_EC.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B5ouEQYN.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { r as withRecordingIntent } from "../restream-intent-B4BXZra7.mjs";
5
5
  import { n as startEventLoopStallMonitor } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
6
- import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DMolfhsP.mjs";
6
+ import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { createReadStream, promises, realpathSync } from "node:fs";
9
9
  import path from "node:path";
@@ -3879,6 +3879,31 @@ var CalendarIndex = class {
3879
3879
  //#endregion
3880
3880
  //#region src/recorder/addon/config-store.ts
3881
3881
  /**
3882
+ * Per-device recording-config persistence for the recorder addon.
3883
+ *
3884
+ * ONE ROW PER CAMERA in `recorder:recording-configs`, through
3885
+ * {@link RowMapStore}. Asking what camera 617 records is a primary-key lookup;
3886
+ * saving its config writes that row and touches no other.
3887
+ *
3888
+ * ## What this replaced, and why
3889
+ *
3890
+ * Until the 2026-08-19 flatten every camera's `RecordingConfig` lived in ONE
3891
+ * key of the recorder's `addon-settings` / `root` blob — a row that was 201 833
3892
+ * bytes on the reference hub, because the ops-log and the export history shared
3893
+ * it. So `loadDeviceConfig(617)` read and `JSON.parse`d 201 KB, and
3894
+ * `saveDeviceConfig` read that, merged, and rewrote it. The staging walk did it
3895
+ * once per device and lost 45–267 s of a boot to it (2026-08-18); teaching the
3896
+ * walk to read the map once capped the symptom, and this removes the cause.
3897
+ *
3898
+ * ## Failure direction
3899
+ *
3900
+ * `RecordingConfigSchema` rejects unknown keys so a stale caller fails loudly,
3901
+ * but a row written by an older build may still carry retired ones — `.strip()`
3902
+ * drops them. And validation is now PER ROW: one unparseable camera costs that
3903
+ * camera, where the blob's single `safeParse` took every camera down to the
3904
+ * empty fallback (= the whole fleet silently unconfigured).
3905
+ */
3906
+ /**
3882
3907
  * @durable class=config owner=recorder
3883
3908
  * write="one row per camera, written by `saveDeviceConfig` (the `recording` cap's
3884
3909
  * setConfig / band authoring) — the only writer"
@@ -3888,7 +3913,7 @@ var CalendarIndex = class {
3888
3913
  var RECORDING_CONFIGS_COLLECTION = "recorder:recording-configs";
3889
3914
  var RECORDING_CONFIGS_SPEC = {
3890
3915
  collection: RECORDING_CONFIGS_COLLECTION,
3891
- schema: RecordingConfigSchema.strip(),
3916
+ schema: preprocess((raw) => migrateLegacyDeviceSignalConfig(raw).value, RecordingConfigSchema.strip()),
3892
3917
  columns: [
3893
3918
  {
3894
3919
  name: "deviceId",
@@ -3954,7 +3979,10 @@ async function loadDeviceConfig(state, deviceId) {
3954
3979
  */
3955
3980
  function deviceIdsWithLostBands(configs) {
3956
3981
  const lost = [];
3957
- for (const [deviceId, config] of configs) if (config.enabled && config.bands.length === 0 && config.mode && config.mode !== "off") lost.push(deviceId);
3982
+ for (const [deviceId, config] of configs) {
3983
+ if (config.deviceDecision === true) continue;
3984
+ if (config.enabled && config.bands.length === 0 && config.mode && config.mode !== "off") lost.push(deviceId);
3985
+ }
3958
3986
  return lost.toSorted((a, b) => a - b);
3959
3987
  }
3960
3988
  /** Boot-time check: log the {@link deviceIdsWithLostBands} once, if any. */
@@ -3964,6 +3992,48 @@ async function warnLostBands(state, logger) {
3964
3992
  logger.warn(`recording config: device(s) ${lost.join(", ")} claim a recording mode but have NO bands — a pre-retirement config that lost its legacy authoring fields. Nothing is recorded for them until their bands are re-authored.`);
3965
3993
  }
3966
3994
  /**
3995
+ * Persist the D371→D380 shape for every stored row — the migration, in code,
3996
+ * run once per boot and IDEMPOTENT: a second run finds no legacy key and
3997
+ * writes nothing.
3998
+ *
3999
+ * It reads the rows RAW ({@link RowMapStore.readAllRaw}) on purpose. The read
4000
+ * schema already migrates in memory, so by the time `readAll` answers there is
4001
+ * nothing left to recognise — and the retired key is stripped by a non-strict
4002
+ * `z.object`, so the migration would look like it had nothing to do on exactly
4003
+ * the row it exists for.
4004
+ *
4005
+ * Both outcomes are logged, because both change what a camera records and
4006
+ * silence would read as "nothing happened" — which is what a stripped key
4007
+ * looks like from the outside.
4008
+ */
4009
+ async function migrateLegacyDeviceSignalRows(state, logger) {
4010
+ const migrated = [];
4011
+ const stripped = [];
4012
+ for (const [key, raw] of await state.readAllRaw()) {
4013
+ const outcome = migrateLegacyDeviceSignalConfig(raw);
4014
+ if (!outcome.migrated && !outcome.strippedOnly) continue;
4015
+ const deviceId = Number(key);
4016
+ if (!Number.isInteger(deviceId)) continue;
4017
+ const parsed = RecordingConfigSchema.strip().safeParse(outcome.value);
4018
+ if (!parsed.success) {
4019
+ logger.warn("recording config: legacy device-signal row could not be migrated", {
4020
+ tags: { deviceId },
4021
+ meta: { error: parsed.error.message }
4022
+ });
4023
+ continue;
4024
+ }
4025
+ await saveDeviceConfig(state, deviceId, parsed.data);
4026
+ if (outcome.migrated) migrated.push(deviceId);
4027
+ else stripped.push(deviceId);
4028
+ }
4029
+ if (migrated.length > 0) logger.info("recording config: migrated to the on-device-decision mode (the retired band trigger)", { meta: { deviceIds: migrated } });
4030
+ if (stripped.length > 0) logger.warn("recording config: a band mixed the retired device-signal trigger with others — the key was dropped and the band kept. These cameras no longer follow their own signal until the on-device-decision mode is selected for them.", { meta: { deviceIds: stripped } });
4031
+ return {
4032
+ migrated,
4033
+ stripped
4034
+ };
4035
+ }
4036
+ /**
3967
4037
  * Persist (insert or replace) a device's config — ONE row. Validates against
3968
4038
  * `RecordingConfigSchema` at the boundary (untrusted cap input). Returns the
3969
4039
  * validated config that was persisted.
@@ -12456,6 +12526,13 @@ function bandActiveAt(band, date) {
12456
12526
  if (s > e) return applies(wd) && t >= s || applies(prevWd) && t < e;
12457
12527
  return applies(wd);
12458
12528
  }
12529
+ /**
12530
+ * The band covering `date`, or null. GENERIC over the band type so the caller
12531
+ * gets back exactly what it put in: the recorder decides on `EffectiveBand`s
12532
+ * (`band-decision.ts`), which carry the one non-authored fact — that the DEVICE
12533
+ * decides — and a return type widened to `RecordingBand` would erase it right
12534
+ * where the gates read it.
12535
+ */
12459
12536
  function activeBandAt(config, date) {
12460
12537
  return config.bands.find((b) => bandActiveAt(b, date)) ?? null;
12461
12538
  }
@@ -12480,6 +12557,37 @@ function activeBandAt(config, date) {
12480
12557
  * unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
12481
12558
  */
12482
12559
  /**
12560
+ * THE band a `deviceDecision` config decides through: always on (every day,
12561
+ * `start === end` — the band engine's always-on shape), `events`, and listening
12562
+ * to exactly one source, the camera's own level.
12563
+ *
12564
+ * Its buffers are the canonical pad ({@link DEFAULT_EVENTS_BAND_BUFFER_SEC}),
12565
+ * not per-camera knobs: this mode's whole contract with the operator is ONE
12566
+ * flag. The pad only ever retains footage around an episode the device itself
12567
+ * delimited, so there is nothing here for an operator to tune.
12568
+ */
12569
+ var DEVICE_DECISION_BAND = Object.freeze({
12570
+ days: [],
12571
+ start: "00:00",
12572
+ end: "00:00",
12573
+ mode: "events",
12574
+ deviceSignal: true,
12575
+ preBufferSec: DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec,
12576
+ postBufferSec: DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec
12577
+ });
12578
+ /**
12579
+ * THE bands the recorder decides on for a config — the ONE place
12580
+ * `config.bands` is turned into what the gates read.
12581
+ *
12582
+ * `deviceDecision` resolves to {@link DEVICE_DECISION_BAND} and to nothing
12583
+ * else; every other config is its own authored bands, byte for byte. Because
12584
+ * the schema makes the two exclusive, this is a choice and never a merge.
12585
+ */
12586
+ function effectiveBands(config) {
12587
+ if (config.deviceDecision === true) return [DEVICE_DECISION_BAND];
12588
+ return config.bands ?? [];
12589
+ }
12590
+ /**
12483
12591
  * THE one place a band's pre/post buffers are read. An ABSENT field takes
12484
12592
  * {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
12485
12593
  * is honoured verbatim.
@@ -12520,6 +12628,9 @@ function triggerKey(source) {
12520
12628
  case "signal": return "signal";
12521
12629
  }
12522
12630
  }
12631
+ /** The single source {@link DEVICE_DECISION_BAND} listens to, shared so the
12632
+ * band never allocates a new one per call. */
12633
+ var SIGNAL_SOURCES = Object.freeze([{ kind: "signal" }]);
12523
12634
  /**
12524
12635
  * LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
12525
12636
  * una banda diventano `TriggerSource[]`.
@@ -12535,6 +12646,7 @@ function triggerKey(source) {
12535
12646
  * denuncia invece di restare zitto.
12536
12647
  */
12537
12648
  function bandTriggerSources(band) {
12649
+ if (band.deviceSignal === true) return SIGNAL_SOURCES;
12538
12650
  const t = band.triggers;
12539
12651
  if (t === void 0) return [];
12540
12652
  const sources = [];
@@ -12548,7 +12660,6 @@ function bandTriggerSources(band) {
12548
12660
  kind: "sensor",
12549
12661
  sourceDeviceId
12550
12662
  });
12551
- if (t.deviceSignal === true) sources.push({ kind: "signal" });
12552
12663
  return sources;
12553
12664
  }
12554
12665
  /**
@@ -12734,8 +12845,8 @@ function eventsBandDemanded(band, triggers, atMs) {
12734
12845
  */
12735
12846
  function shouldRecordAt(config, triggers, at) {
12736
12847
  if (!config.enabled) return false;
12737
- const bands = config.bands;
12738
- if (!bands || bands.length === 0) return false;
12848
+ const bands = effectiveBands(config);
12849
+ if (bands.length === 0) return false;
12739
12850
  const band = activeBandAt({ bands }, at);
12740
12851
  if (!band) return false;
12741
12852
  if (band.mode === "continuous") return true;
@@ -12990,8 +13101,8 @@ function candidateEdgesMs(bands, nowMs) {
12990
13101
  * costs one wasted wake-up and never costs footage.
12991
13102
  */
12992
13103
  function nextBandEdgeMs(config, nowMs) {
12993
- const bands = config.bands;
12994
- if (!bands || bands.length === 0) return null;
13104
+ const bands = effectiveBands(config);
13105
+ if (bands.length === 0) return null;
12995
13106
  const current = activeBandAt({ bands }, new Date(nowMs));
12996
13107
  const candidates = [...new Set(candidateEdgesMs(bands, nowMs))].filter((ms) => ms > nowMs).toSorted((a, b) => a - b);
12997
13108
  for (const ms of candidates) if (activeBandAt({ bands }, new Date(ms)) !== current) return ms;
@@ -13026,8 +13137,8 @@ function eventsWindowCloseMs(band, triggers, nowMs) {
13026
13137
  function nextDecisionChangeAt(input) {
13027
13138
  const { config, triggers, nowMs } = input;
13028
13139
  if (!config.enabled) return null;
13029
- const bands = config.bands;
13030
- if (!bands || bands.length === 0) return null;
13140
+ const bands = effectiveBands(config);
13141
+ if (bands.length === 0) return null;
13031
13142
  const band = activeBandAt({ bands }, new Date(nowMs));
13032
13143
  const edgeMs = nextBandEdgeMs(config, nowMs);
13033
13144
  const closeMs = band === null ? null : eventsWindowCloseMs(band, triggers, nowMs);
@@ -14497,7 +14608,7 @@ var RecordingController = class {
14497
14608
  const config = await this.deps.loadConfig(deviceId);
14498
14609
  if (config.enabled !== true) return;
14499
14610
  const nowMs = this.now();
14500
- const band = activeBandAt({ bands: config.bands ?? [] }, new Date(nowMs));
14611
+ const band = activeBandAt({ bands: effectiveBands(config) }, new Date(nowMs));
14501
14612
  if (!band || band.mode !== "events") return;
14502
14613
  const { postMs } = resolveBandBufferMs(band);
14503
14614
  const sinceMs = nowMs - postMs - TRIGGER_HISTORY_MARGIN_MS;
@@ -14904,7 +15015,7 @@ var RecordingController = class {
14904
15015
  if (this.stopped) return;
14905
15016
  let band;
14906
15017
  try {
14907
- band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15018
+ band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14908
15019
  } catch (err) {
14909
15020
  this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
14910
15021
  tags: { deviceId },
@@ -14929,7 +15040,7 @@ var RecordingController = class {
14929
15040
  */
14930
15041
  async postBufferMsFor(deviceId, atMs) {
14931
15042
  try {
14932
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15043
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14933
15044
  if (band) return resolveBandBufferMs(band).postMs;
14934
15045
  } catch (err) {
14935
15046
  this.deps.logger.warn("recorder: trigger history using the DEFAULT post-buffer — config read failed", {
@@ -15020,7 +15131,7 @@ var RecordingController = class {
15020
15131
  reportUnrecordable(deviceId, config) {
15021
15132
  if (!config.enabled) return;
15022
15133
  if (this.unrecordableReported.has(deviceId)) return;
15023
- const bands = config.bands ?? [];
15134
+ const bands = effectiveBands(config);
15024
15135
  if (bands.length === 0) {
15025
15136
  this.unrecordableReported.add(deviceId);
15026
15137
  this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
@@ -15357,7 +15468,7 @@ var RecordingController = class {
15357
15468
  */
15358
15469
  async handleFinalizedSegment(deviceId, profile, placement, attach, startMs, durMs, flatAbsPath) {
15359
15470
  try {
15360
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(startMs + durMs));
15471
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(startMs + durMs));
15361
15472
  const triggers = this.triggersFor(deviceId);
15362
15473
  if (band?.mode === "events" && !segmentRetainedForEvents(startMs, startMs + durMs, band, triggers, attach)) {
15363
15474
  this.reportDiscardedSegment(deviceId, profile, attach, band, triggers, startMs, durMs);
@@ -17361,7 +17472,9 @@ var RecorderV2Addon = class extends BaseAddon {
17361
17472
  watchIntervalMs: this.config.watchIntervalMs
17362
17473
  }
17363
17474
  });
17364
- warnLostBands(this.configStore(), this.ctx.logger);
17475
+ migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
17476
+ this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: errMsg(err) } });
17477
+ }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
17365
17478
  const providers = [
17366
17479
  {
17367
17480
  capability: recordingCapability,
@@ -91,6 +91,25 @@ var RowMapStore = class RowMapStore {
91
91
  } });
92
92
  return out;
93
93
  }
94
+ /**
95
+ * Every entry as it was WRITTEN, unvalidated and unparsed — the read a
96
+ * MIGRATION needs.
97
+ *
98
+ * {@link readAll} answers through the CURRENT schema, which is exactly what a
99
+ * migration must not do: a retired key is stripped by a non-strict `z.object`
100
+ * before the migration could see it, so the row it is there to rescue looks
101
+ * like a row that never needed rescuing. Only a migration should call this;
102
+ * everything else wants the validated read.
103
+ */
104
+ async readAllRaw() {
105
+ const records = await this.store.query.query({
106
+ collection: this.spec.collection,
107
+ filter: { limit: this.spec.loadLimit }
108
+ });
109
+ const out = /* @__PURE__ */ new Map();
110
+ for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
111
+ return out;
112
+ }
94
113
  /** Insert or replace ONE entry. Validates at the boundary. */
95
114
  async write(key, value) {
96
115
  const validated = this.spec.schema.parse(value);
@@ -91,6 +91,25 @@ var RowMapStore = class RowMapStore {
91
91
  } });
92
92
  return out;
93
93
  }
94
+ /**
95
+ * Every entry as it was WRITTEN, unvalidated and unparsed — the read a
96
+ * MIGRATION needs.
97
+ *
98
+ * {@link readAll} answers through the CURRENT schema, which is exactly what a
99
+ * migration must not do: a retired key is stripped by a non-strict `z.object`
100
+ * before the migration could see it, so the row it is there to rescue looks
101
+ * like a row that never needed rescuing. Only a migration should call this;
102
+ * everything else wants the validated read.
103
+ */
104
+ async readAllRaw() {
105
+ const records = await this.store.query.query({
106
+ collection: this.spec.collection,
107
+ filter: { limit: this.spec.loadLimit }
108
+ });
109
+ const out = /* @__PURE__ */ new Map();
110
+ for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
111
+ return out;
112
+ }
94
113
  /** Insert or replace ONE entry. Validates at the boundary. */
95
114
  async write(key, value) {
96
115
  const validated = this.spec.schema.parse(value);
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { Et as errMsg } from "./dist-mYDK58Jp.mjs";
2
+ import { Dt as errMsg } from "./dist-Y4BsF_EC.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-HG5geL9t.js");
1
+ const require_dist = require("./dist-CBa1j3On.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-DCsi4YS1.js");
3
- const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.js");
2
+ const require_worker_protocol = require("../worker-protocol-CXsdriRX.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-CS5WE9mI.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-C0R1DDC0.mjs";
1
+ import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-_7zrToTo.mjs";
2
2
  import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  //#region src/session-decode/color-conversion.ts
4
4
  /**
@@ -1,7 +1,7 @@
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-CBxobiZW.mjs";
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-Dj_qoc1e.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-B_NVJXAy.mjs";
4
+ import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cv5McbTI.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) => {