@camstack/addon-pipeline 1.2.196 → 1.2.198

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-B5v8qEI0.js} +1 -1
  4. package/dist/{default-detection-model-BGBAmtEp.mjs → default-detection-model-CASpAESz.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-BxP0SvPz.js} +215 -58
  8. package/dist/{dist-mYDK58Jp.mjs → dist-oFIHnD88.mjs} +198 -59
  9. package/dist/{lazy-sharp-DmAwZ0s9.js → lazy-sharp-VKOG3WW4.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-B4zETsJ9.mjs} +1 -1
  13. package/dist/{node--my8JTOW.js → node-BIKrNEtS.js} +1 -1
  14. package/dist/pipeline-runner/index.js +363 -6
  15. package/dist/pipeline-runner/index.mjs +362 -5
  16. package/dist/{retire-root-keys-KE6D6Xh_.js → prebuffer-DmBYRQWg.mjs} +83 -12
  17. package/dist/{retire-root-keys-DMolfhsP.mjs → prebuffer-DqdeLSsk.js} +118 -1
  18. package/dist/{process-memory-CoCSce63.js → process-memory-CMHOmj75.js} +1 -1
  19. package/dist/{process-memory-BDHR3lxe.mjs → process-memory-D9xYwm6L.mjs} +1 -1
  20. package/dist/recorder/index.js +152 -31
  21. package/dist/recorder/index.mjs +148 -27
  22. package/dist/{segment-demux-js-CBynn9Gh.js → segment-demux-js-DLwL1vmq.js} +1 -1
  23. package/dist/{segment-demux-js-CXvgKfsn.mjs → segment-demux-js-DZysrgdC.mjs} +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 +29 -32
  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-C2eTdxvd.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-BcjLrxhF.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-C8tu7NS4.mjs} +3 -3
  33. package/dist/stream-broker/index.js +452 -541
  34. package/dist/stream-broker/index.mjs +448 -537
  35. package/dist/stream-broker/remoteEntry.js +1 -1
  36. package/dist/{worker-protocol-DCsi4YS1.js → worker-protocol-D2mMjUXv.js} +1 -1
  37. package/dist/{worker-protocol-C0R1DDC0.mjs → worker-protocol-DFlZm_wr.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-BxP0SvPz.js");
6
+ const require_node = require("../node-BIKrNEtS.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_prebuffer = require("../prebuffer-DqdeLSsk.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()).transform((config) => require_dist.stripRetiredBandPreBufferSec(config).config),
3897
3922
  columns: [
3898
3923
  {
3899
3924
  name: "deviceId",
@@ -3925,7 +3950,7 @@ var DEFAULT_DEVICE_CONFIG = {
3925
3950
  };
3926
3951
  /** Build the handle. `declare()` MUST run once at boot before any read/write. */
3927
3952
  function recordingConfigsState(store, logger) {
3928
- return new require_retire_root_keys.RowMapStore({
3953
+ return new require_prebuffer.RowMapStore({
3929
3954
  spec: RECORDING_CONFIGS_SPEC,
3930
3955
  store,
3931
3956
  logger
@@ -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.
@@ -5171,7 +5241,7 @@ var RECORDING_EXPORTS_SPEC = {
5171
5241
  };
5172
5242
  /** Build the handle. `declare()` MUST run once at boot before any read/write. */
5173
5243
  function recordingExportsState(store, logger) {
5174
- return new require_retire_root_keys.RowMapStore({
5244
+ return new require_prebuffer.RowMapStore({
5175
5245
  spec: RECORDING_EXPORTS_SPEC,
5176
5246
  store,
5177
5247
  logger
@@ -11014,7 +11084,7 @@ var RECORDING_OPS_LOG_SPEC = {
11014
11084
  };
11015
11085
  /** Build the handle. `declare()` MUST run once at boot before any read/write. */
11016
11086
  function recordingOpsLogState(store, logger) {
11017
- return new require_retire_root_keys.RowMapStore({
11087
+ return new require_prebuffer.RowMapStore({
11018
11088
  spec: RECORDING_OPS_LOG_SPEC,
11019
11089
  store,
11020
11090
  logger
@@ -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,18 +12563,57 @@ function activeBandAt(config, date) {
12486
12563
  * unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
12487
12564
  */
12488
12565
  /**
12489
- * THE one place a band's pre/post buffers are read. An ABSENT field takes
12490
- * {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
12491
- * is honoured verbatim.
12492
- *
12493
- * Both callers below used to inline `?? 0`, which read "the operator omitted the
12494
- * field" as "the operator asked for a zero-length window". For the live gate
12495
- * that is not a policy, it is an impossibility: nothing can ever be recorded.
12496
- * Two cameras sat in exactly that state.
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
+ postBufferSec: require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec
12582
+ });
12583
+ /**
12584
+ * THE bands the recorder decides on for a config — the ONE place
12585
+ * `config.bands` is turned into what the gates read.
12586
+ *
12587
+ * `deviceDecision` resolves to {@link DEVICE_DECISION_BAND} and to nothing
12588
+ * else; every other config is its own authored bands, byte for byte. Because
12589
+ * the schema makes the two exclusive, this is a choice and never a merge.
12590
+ */
12591
+ function effectiveBands(config) {
12592
+ if (config.deviceDecision === true) return [DEVICE_DECISION_BAND];
12593
+ return config.bands ?? [];
12594
+ }
12595
+ /**
12596
+ * THE one place a band's buffers are read.
12597
+ *
12598
+ * `postMs` is the LIVE demand window and stays a per-band setting: it decides
12599
+ * how long a writer keeps running, which is a policy about this camera at this
12600
+ * hour. An ABSENT field takes {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an
12601
+ * explicit value — including `0` — is honoured verbatim, because both callers
12602
+ * used to inline `?? 0` and read "the operator omitted the field" as "the
12603
+ * operator asked for a zero-length window", which for the live gate is not a
12604
+ * policy but an impossibility. Two cameras sat in exactly that state.
12605
+ *
12606
+ * `preMs` is NOT a setting any more (D381). It is the retroactive KEEP bound —
12607
+ * which already-written segments survive a trigger — and its only correct
12608
+ * value was ever "at least as wide as the widest history the broker can hand
12609
+ * the writer". So it IS that ceiling, read from the one place that defines it:
12610
+ * a keep gate can no longer be configured narrower than the media it is
12611
+ * supposed to keep, which is the disagreement D191 was written against and
12612
+ * which a per-band number could only ever warn about.
12497
12613
  */
12498
12614
  function resolveBandBufferMs(band) {
12499
12615
  return {
12500
- preMs: (band.preBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec) * 1e3,
12616
+ preMs: require_prebuffer.PREBUFFER_MAX_MS,
12501
12617
  postMs: (band.postBufferSec ?? require_dist.DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec) * 1e3
12502
12618
  };
12503
12619
  }
@@ -12526,6 +12642,9 @@ function triggerKey(source) {
12526
12642
  case "signal": return "signal";
12527
12643
  }
12528
12644
  }
12645
+ /** The single source {@link DEVICE_DECISION_BAND} listens to, shared so the
12646
+ * band never allocates a new one per call. */
12647
+ var SIGNAL_SOURCES = Object.freeze([{ kind: "signal" }]);
12529
12648
  /**
12530
12649
  * LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
12531
12650
  * una banda diventano `TriggerSource[]`.
@@ -12541,6 +12660,7 @@ function triggerKey(source) {
12541
12660
  * denuncia invece di restare zitto.
12542
12661
  */
12543
12662
  function bandTriggerSources(band) {
12663
+ if (band.deviceSignal === true) return SIGNAL_SOURCES;
12544
12664
  const t = band.triggers;
12545
12665
  if (t === void 0) return [];
12546
12666
  const sources = [];
@@ -12554,7 +12674,6 @@ function bandTriggerSources(band) {
12554
12674
  kind: "sensor",
12555
12675
  sourceDeviceId
12556
12676
  });
12557
- if (t.deviceSignal === true) sources.push({ kind: "signal" });
12558
12677
  return sources;
12559
12678
  }
12560
12679
  /**
@@ -12740,8 +12859,8 @@ function eventsBandDemanded(band, triggers, atMs) {
12740
12859
  */
12741
12860
  function shouldRecordAt(config, triggers, at) {
12742
12861
  if (!config.enabled) return false;
12743
- const bands = config.bands;
12744
- if (!bands || bands.length === 0) return false;
12862
+ const bands = effectiveBands(config);
12863
+ if (bands.length === 0) return false;
12745
12864
  const band = activeBandAt({ bands }, at);
12746
12865
  if (!band) return false;
12747
12866
  if (band.mode === "continuous") return true;
@@ -12996,8 +13115,8 @@ function candidateEdgesMs(bands, nowMs) {
12996
13115
  * costs one wasted wake-up and never costs footage.
12997
13116
  */
12998
13117
  function nextBandEdgeMs(config, nowMs) {
12999
- const bands = config.bands;
13000
- if (!bands || bands.length === 0) return null;
13118
+ const bands = effectiveBands(config);
13119
+ if (bands.length === 0) return null;
13001
13120
  const current = activeBandAt({ bands }, new Date(nowMs));
13002
13121
  const candidates = [...new Set(candidateEdgesMs(bands, nowMs))].filter((ms) => ms > nowMs).toSorted((a, b) => a - b);
13003
13122
  for (const ms of candidates) if (activeBandAt({ bands }, new Date(ms)) !== current) return ms;
@@ -13032,8 +13151,8 @@ function eventsWindowCloseMs(band, triggers, nowMs) {
13032
13151
  function nextDecisionChangeAt(input) {
13033
13152
  const { config, triggers, nowMs } = input;
13034
13153
  if (!config.enabled) return null;
13035
- const bands = config.bands;
13036
- if (!bands || bands.length === 0) return null;
13154
+ const bands = effectiveBands(config);
13155
+ if (bands.length === 0) return null;
13037
13156
  const band = activeBandAt({ bands }, new Date(nowMs));
13038
13157
  const edgeMs = nextBandEdgeMs(config, nowMs);
13039
13158
  const closeMs = band === null ? null : eventsWindowCloseMs(band, triggers, nowMs);
@@ -14503,7 +14622,7 @@ var RecordingController = class {
14503
14622
  const config = await this.deps.loadConfig(deviceId);
14504
14623
  if (config.enabled !== true) return;
14505
14624
  const nowMs = this.now();
14506
- const band = activeBandAt({ bands: config.bands ?? [] }, new Date(nowMs));
14625
+ const band = activeBandAt({ bands: effectiveBands(config) }, new Date(nowMs));
14507
14626
  if (!band || band.mode !== "events") return;
14508
14627
  const { postMs } = resolveBandBufferMs(band);
14509
14628
  const sinceMs = nowMs - postMs - TRIGGER_HISTORY_MARGIN_MS;
@@ -14910,7 +15029,7 @@ var RecordingController = class {
14910
15029
  if (this.stopped) return;
14911
15030
  let band;
14912
15031
  try {
14913
- band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15032
+ band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14914
15033
  } catch (err) {
14915
15034
  this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
14916
15035
  tags: { deviceId },
@@ -14935,7 +15054,7 @@ var RecordingController = class {
14935
15054
  */
14936
15055
  async postBufferMsFor(deviceId, atMs) {
14937
15056
  try {
14938
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15057
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14939
15058
  if (band) return resolveBandBufferMs(band).postMs;
14940
15059
  } catch (err) {
14941
15060
  this.deps.logger.warn("recorder: trigger history using the DEFAULT post-buffer — config read failed", {
@@ -15026,7 +15145,7 @@ var RecordingController = class {
15026
15145
  reportUnrecordable(deviceId, config) {
15027
15146
  if (!config.enabled) return;
15028
15147
  if (this.unrecordableReported.has(deviceId)) return;
15029
- const bands = config.bands ?? [];
15148
+ const bands = effectiveBands(config);
15030
15149
  if (bands.length === 0) {
15031
15150
  this.unrecordableReported.add(deviceId);
15032
15151
  this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
@@ -15363,7 +15482,7 @@ var RecordingController = class {
15363
15482
  */
15364
15483
  async handleFinalizedSegment(deviceId, profile, placement, attach, startMs, durMs, flatAbsPath) {
15365
15484
  try {
15366
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(startMs + durMs));
15485
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(startMs + durMs));
15367
15486
  const triggers = this.triggersFor(deviceId);
15368
15487
  if (band?.mode === "events" && !segmentRetainedForEvents(startMs, startMs + durMs, band, triggers, attach)) {
15369
15488
  this.reportDiscardedSegment(deviceId, profile, attach, band, triggers, startMs, durMs);
@@ -17367,7 +17486,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17367
17486
  watchIntervalMs: this.config.watchIntervalMs
17368
17487
  }
17369
17488
  });
17370
- warnLostBands(this.configStore(), this.ctx.logger);
17489
+ migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
17490
+ this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: require_dist.errMsg(err) } });
17491
+ }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
17371
17492
  const providers = [
17372
17493
  {
17373
17494
  capability: require_dist.recordingCapability,
@@ -18289,7 +18410,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
18289
18410
  */
18290
18411
  async purgeFlattenedSettingsKeys() {
18291
18412
  try {
18292
- await require_retire_root_keys.retireRootKeys({
18413
+ await require_prebuffer.retireRootKeys({
18293
18414
  store: this.ctx.api.settingsStore,
18294
18415
  addonId: RECORDER_ADDON_ID,
18295
18416
  logger: this.ctx.logger,
@@ -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 { Bt as hydrateSchema, Ct as storageEvictableCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Ft as BaseAddon, H as deriveBatteryPresence, Jt as sleep$1, Lt as DeviceFeature, Ot as errMsg, P as batteryCapability, R as customAction, Rt as DeviceType, T as RECORDING_EXPORT_MAX_READ_BYTES, Tt as stripRetiredBandPreBufferSec, V as defineCustomActions, W as deriveRecordingMode, Wt as nodePin, Xt as array, Y as evaluateSensorEdge, Yt as _enum, Zt as boolean, _t as recordingSignalCapability, an as string, at as migrateLegacyDeviceSignalConfig, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, et as isFirstLevelMacroClass, g as ExportRecordSchema, gt as recordingExportCapability, ht as recordingCapability, i as BatteryStatusSchema, in as record, nn as object, qt as selectAssignedProfileSlots, rn as preprocess, sn as EventCategory, tn as number, tt as loadContributionCapability, x as OpsLogEntrySchema, xt as resolveRecordingProfiles } from "../dist-oFIHnD88.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B4zETsJ9.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 { a as retireRootKeys, o as RowMapStore, t as PREBUFFER_MAX_MS } from "../prebuffer-DmBYRQWg.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()).transform((config) => stripRetiredBandPreBufferSec(config).config),
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,18 +12557,57 @@ function activeBandAt(config, date) {
12480
12557
  * unit-testable without ffmpeg, the broker, the event bus, or a real wall clock.
12481
12558
  */
12482
12559
  /**
12483
- * THE one place a band's pre/post buffers are read. An ABSENT field takes
12484
- * {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an explicit value — including `0` —
12485
- * is honoured verbatim.
12486
- *
12487
- * Both callers below used to inline `?? 0`, which read "the operator omitted the
12488
- * field" as "the operator asked for a zero-length window". For the live gate
12489
- * that is not a policy, it is an impossibility: nothing can ever be recorded.
12490
- * Two cameras sat in exactly that state.
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
+ postBufferSec: DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec
12576
+ });
12577
+ /**
12578
+ * THE bands the recorder decides on for a config — the ONE place
12579
+ * `config.bands` is turned into what the gates read.
12580
+ *
12581
+ * `deviceDecision` resolves to {@link DEVICE_DECISION_BAND} and to nothing
12582
+ * else; every other config is its own authored bands, byte for byte. Because
12583
+ * the schema makes the two exclusive, this is a choice and never a merge.
12584
+ */
12585
+ function effectiveBands(config) {
12586
+ if (config.deviceDecision === true) return [DEVICE_DECISION_BAND];
12587
+ return config.bands ?? [];
12588
+ }
12589
+ /**
12590
+ * THE one place a band's buffers are read.
12591
+ *
12592
+ * `postMs` is the LIVE demand window and stays a per-band setting: it decides
12593
+ * how long a writer keeps running, which is a policy about this camera at this
12594
+ * hour. An ABSENT field takes {@link DEFAULT_EVENTS_BAND_BUFFER_SEC}; an
12595
+ * explicit value — including `0` — is honoured verbatim, because both callers
12596
+ * used to inline `?? 0` and read "the operator omitted the field" as "the
12597
+ * operator asked for a zero-length window", which for the live gate is not a
12598
+ * policy but an impossibility. Two cameras sat in exactly that state.
12599
+ *
12600
+ * `preMs` is NOT a setting any more (D381). It is the retroactive KEEP bound —
12601
+ * which already-written segments survive a trigger — and its only correct
12602
+ * value was ever "at least as wide as the widest history the broker can hand
12603
+ * the writer". So it IS that ceiling, read from the one place that defines it:
12604
+ * a keep gate can no longer be configured narrower than the media it is
12605
+ * supposed to keep, which is the disagreement D191 was written against and
12606
+ * which a per-band number could only ever warn about.
12491
12607
  */
12492
12608
  function resolveBandBufferMs(band) {
12493
12609
  return {
12494
- preMs: (band.preBufferSec ?? DEFAULT_EVENTS_BAND_BUFFER_SEC.preBufferSec) * 1e3,
12610
+ preMs: PREBUFFER_MAX_MS,
12495
12611
  postMs: (band.postBufferSec ?? DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec) * 1e3
12496
12612
  };
12497
12613
  }
@@ -12520,6 +12636,9 @@ function triggerKey(source) {
12520
12636
  case "signal": return "signal";
12521
12637
  }
12522
12638
  }
12639
+ /** The single source {@link DEVICE_DECISION_BAND} listens to, shared so the
12640
+ * band never allocates a new one per call. */
12641
+ var SIGNAL_SOURCES = Object.freeze([{ kind: "signal" }]);
12523
12642
  /**
12524
12643
  * LE sorgenti che questa banda ascolta — l'unico posto dove i `triggers` di
12525
12644
  * una banda diventano `TriggerSource[]`.
@@ -12535,6 +12654,7 @@ function triggerKey(source) {
12535
12654
  * denuncia invece di restare zitto.
12536
12655
  */
12537
12656
  function bandTriggerSources(band) {
12657
+ if (band.deviceSignal === true) return SIGNAL_SOURCES;
12538
12658
  const t = band.triggers;
12539
12659
  if (t === void 0) return [];
12540
12660
  const sources = [];
@@ -12548,7 +12668,6 @@ function bandTriggerSources(band) {
12548
12668
  kind: "sensor",
12549
12669
  sourceDeviceId
12550
12670
  });
12551
- if (t.deviceSignal === true) sources.push({ kind: "signal" });
12552
12671
  return sources;
12553
12672
  }
12554
12673
  /**
@@ -12734,8 +12853,8 @@ function eventsBandDemanded(band, triggers, atMs) {
12734
12853
  */
12735
12854
  function shouldRecordAt(config, triggers, at) {
12736
12855
  if (!config.enabled) return false;
12737
- const bands = config.bands;
12738
- if (!bands || bands.length === 0) return false;
12856
+ const bands = effectiveBands(config);
12857
+ if (bands.length === 0) return false;
12739
12858
  const band = activeBandAt({ bands }, at);
12740
12859
  if (!band) return false;
12741
12860
  if (band.mode === "continuous") return true;
@@ -12990,8 +13109,8 @@ function candidateEdgesMs(bands, nowMs) {
12990
13109
  * costs one wasted wake-up and never costs footage.
12991
13110
  */
12992
13111
  function nextBandEdgeMs(config, nowMs) {
12993
- const bands = config.bands;
12994
- if (!bands || bands.length === 0) return null;
13112
+ const bands = effectiveBands(config);
13113
+ if (bands.length === 0) return null;
12995
13114
  const current = activeBandAt({ bands }, new Date(nowMs));
12996
13115
  const candidates = [...new Set(candidateEdgesMs(bands, nowMs))].filter((ms) => ms > nowMs).toSorted((a, b) => a - b);
12997
13116
  for (const ms of candidates) if (activeBandAt({ bands }, new Date(ms)) !== current) return ms;
@@ -13026,8 +13145,8 @@ function eventsWindowCloseMs(band, triggers, nowMs) {
13026
13145
  function nextDecisionChangeAt(input) {
13027
13146
  const { config, triggers, nowMs } = input;
13028
13147
  if (!config.enabled) return null;
13029
- const bands = config.bands;
13030
- if (!bands || bands.length === 0) return null;
13148
+ const bands = effectiveBands(config);
13149
+ if (bands.length === 0) return null;
13031
13150
  const band = activeBandAt({ bands }, new Date(nowMs));
13032
13151
  const edgeMs = nextBandEdgeMs(config, nowMs);
13033
13152
  const closeMs = band === null ? null : eventsWindowCloseMs(band, triggers, nowMs);
@@ -14497,7 +14616,7 @@ var RecordingController = class {
14497
14616
  const config = await this.deps.loadConfig(deviceId);
14498
14617
  if (config.enabled !== true) return;
14499
14618
  const nowMs = this.now();
14500
- const band = activeBandAt({ bands: config.bands ?? [] }, new Date(nowMs));
14619
+ const band = activeBandAt({ bands: effectiveBands(config) }, new Date(nowMs));
14501
14620
  if (!band || band.mode !== "events") return;
14502
14621
  const { postMs } = resolveBandBufferMs(band);
14503
14622
  const sinceMs = nowMs - postMs - TRIGGER_HISTORY_MARGIN_MS;
@@ -14904,7 +15023,7 @@ var RecordingController = class {
14904
15023
  if (this.stopped) return;
14905
15024
  let band;
14906
15025
  try {
14907
- band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15026
+ band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14908
15027
  } catch (err) {
14909
15028
  this.deps.logger.warn("recorder: audio trigger qualification skipped — config read failed", {
14910
15029
  tags: { deviceId },
@@ -14929,7 +15048,7 @@ var RecordingController = class {
14929
15048
  */
14930
15049
  async postBufferMsFor(deviceId, atMs) {
14931
15050
  try {
14932
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(atMs));
15051
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(atMs));
14933
15052
  if (band) return resolveBandBufferMs(band).postMs;
14934
15053
  } catch (err) {
14935
15054
  this.deps.logger.warn("recorder: trigger history using the DEFAULT post-buffer — config read failed", {
@@ -15020,7 +15139,7 @@ var RecordingController = class {
15020
15139
  reportUnrecordable(deviceId, config) {
15021
15140
  if (!config.enabled) return;
15022
15141
  if (this.unrecordableReported.has(deviceId)) return;
15023
- const bands = config.bands ?? [];
15142
+ const bands = effectiveBands(config);
15024
15143
  if (bands.length === 0) {
15025
15144
  this.unrecordableReported.add(deviceId);
15026
15145
  this.deps.logger.warn("recorder: device is enabled but has no recording bands — nothing will ever be recorded", { tags: { deviceId } });
@@ -15357,7 +15476,7 @@ var RecordingController = class {
15357
15476
  */
15358
15477
  async handleFinalizedSegment(deviceId, profile, placement, attach, startMs, durMs, flatAbsPath) {
15359
15478
  try {
15360
- const band = activeBandAt({ bands: (await this.deps.loadConfig(deviceId)).bands ?? [] }, new Date(startMs + durMs));
15479
+ const band = activeBandAt({ bands: effectiveBands(await this.deps.loadConfig(deviceId)) }, new Date(startMs + durMs));
15361
15480
  const triggers = this.triggersFor(deviceId);
15362
15481
  if (band?.mode === "events" && !segmentRetainedForEvents(startMs, startMs + durMs, band, triggers, attach)) {
15363
15482
  this.reportDiscardedSegment(deviceId, profile, attach, band, triggers, startMs, durMs);
@@ -17361,7 +17480,9 @@ var RecorderV2Addon = class extends BaseAddon {
17361
17480
  watchIntervalMs: this.config.watchIntervalMs
17362
17481
  }
17363
17482
  });
17364
- warnLostBands(this.configStore(), this.ctx.logger);
17483
+ migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
17484
+ this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: errMsg(err) } });
17485
+ }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
17365
17486
  const providers = [
17366
17487
  {
17367
17488
  capability: recordingCapability,
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-HG5geL9t.js");
1
+ const require_dist = require("./dist-BxP0SvPz.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) => {