@camstack/addon-pipeline 1.2.79 → 1.2.81

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 (30) hide show
  1. package/dist/{addon-utils-BZQxAJ1G.js → addon-utils-CQFlZXSn.js} +1 -1
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/detection-pipeline/index.js +4 -4
  5. package/dist/detection-pipeline/index.mjs +2 -2
  6. package/dist/{dist-BUIpqu3E.js → dist-B57MztMq.js} +276 -14
  7. package/dist/{dist-BfTf6doa.mjs → dist-DGp0dRiq.mjs} +259 -15
  8. package/dist/{event-loop-stall-monitor-CmrHj0i9.mjs → event-loop-stall-monitor-8vYH44kg.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-CGLQP0by.js → event-loop-stall-monitor-DPA6Pu_h.js} +1 -1
  10. package/dist/{lazy-sharp-BZVhL9Lw.js → lazy-sharp-BMgRSV9x.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 +26 -4
  14. package/dist/pipeline-runner/index.mjs +25 -3
  15. package/dist/recorder/index.js +62 -5
  16. package/dist/recorder/index.mjs +61 -4
  17. package/dist/session-decode/decode-worker-child.js +2 -2
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BohtrVou.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cb8ynph1.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B2scqOH7.mjs +26 -0
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dxr14lIu.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DEps-9qh.mjs} +1 -1
  23. package/dist/stream-broker/{hostInit-pDANS0x0.mjs → hostInit-GJipMxBe.mjs} +2 -2
  24. package/dist/stream-broker/index.js +224 -1
  25. package/dist/stream-broker/index.mjs +224 -1
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/dist/{worker-protocol-BaLcYcOo.js → worker-protocol-4L4SbSAo.js} +1 -1
  28. package/dist/{worker-protocol-BdOv9Ujl.mjs → worker-protocol-_e84lGuR.mjs} +1 -1
  29. package/package.json +1 -1
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-y8ehJPoY.mjs +0 -26
@@ -13704,6 +13704,18 @@ var LlmGenerateBaseInputSchema = object({
13704
13704
  * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
13705
13705
  * watchdog — operator decision #3).
13706
13706
  */
13707
+ /**
13708
+ * A companion artifact that MUST land beside the main GGUF: the `mmproj`
13709
+ * projector of a vision model, or shards 2..N of a split GGUF. Carried on the
13710
+ * REF rather than looked up at install time, so what the operator approved in
13711
+ * the preview is exactly what the node downloads.
13712
+ */
13713
+ var ManagedModelExtraFileSchema = object({
13714
+ url: string(),
13715
+ filename: string(),
13716
+ sizeBytes: number(),
13717
+ sha256: string().optional()
13718
+ });
13707
13719
  var ManagedModelRefSchema = discriminatedUnion("kind", [
13708
13720
  object({
13709
13721
  kind: literal("catalog"),
@@ -13712,7 +13724,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
13712
13724
  object({
13713
13725
  kind: literal("url"),
13714
13726
  url: string(),
13715
- sha256: string().optional()
13727
+ sha256: string().optional(),
13728
+ /** Picker/status label; the file basename when absent. */
13729
+ label: string().optional(),
13730
+ sizeBytes: number().optional(),
13731
+ extraFiles: array(ManagedModelExtraFileSchema).optional()
13716
13732
  }),
13717
13733
  object({
13718
13734
  kind: literal("path"),
@@ -13773,11 +13789,39 @@ var ManagedRuntimeConfigSchema = object({
13773
13789
  "q4_1",
13774
13790
  "q4_0"
13775
13791
  ]).optional(),
13792
+ /**
13793
+ * Escape hatch for llama-server flags this schema does NOT model — `--jinja`
13794
+ * (which most vision chat templates need and some language-only models
13795
+ * dislike), `--cont-batching`, `--rope-scaling`, …
13796
+ *
13797
+ * It is NOT a second place to set the flags above. A token that collides
13798
+ * with a typed field is REJECTED at start, naming the field that owns it
13799
+ * (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
13800
+ * the "two switches that disagree" failure this repo has already shipped
13801
+ * twice (D62).
13802
+ */
13803
+ extraArgs: array(string()).default([]),
13776
13804
  /** Else lazy: first generate boots it. */
13777
13805
  autoStart: boolean().default(false),
13778
13806
  /** 0 = never; frees RAM after quiet periods. */
13779
13807
  idleStopMinutes: number().int().default(30)
13780
13808
  });
13809
+ /**
13810
+ * Where a multi-GB install currently is. A single 0..1 fraction cannot answer
13811
+ * "is it stuck?" for an install that is three files (shards + mmproj) followed
13812
+ * by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
13813
+ * node looked hung. Phase + file + bytes is the smallest shape that does.
13814
+ */
13815
+ var LlmDownloadProgressSchema = object({
13816
+ phase: _enum(["downloading", "verifying"]),
13817
+ /** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
13818
+ file: string(),
13819
+ fileIndex: number().int(),
13820
+ fileCount: number().int(),
13821
+ /** Across the WHOLE install, not the current file. */
13822
+ downloadedBytes: number(),
13823
+ totalBytes: number().optional()
13824
+ });
13781
13825
  var LlmRuntimeStatusSchema = object({
13782
13826
  /** Status is ALWAYS node-qualified. */
13783
13827
  nodeId: string(),
@@ -13794,6 +13838,8 @@ var LlmRuntimeStatusSchema = object({
13794
13838
  modelPath: string().optional(),
13795
13839
  modelId: string().optional(),
13796
13840
  downloadProgress: number().min(0).max(1).optional(),
13841
+ /** Detail behind `downloadProgress`; present for the same lifetime. */
13842
+ download: LlmDownloadProgressSchema.optional(),
13797
13843
  lastError: string().optional(),
13798
13844
  crashesInWindow: number(),
13799
13845
  /** Child RSS (sampled best-effort). */
@@ -13804,7 +13850,14 @@ var LlmNodeModelSchema = object({
13804
13850
  file: string(),
13805
13851
  sizeBytes: number(),
13806
13852
  catalogId: string().optional(),
13807
- installedAt: number().optional()
13853
+ installedAt: number().optional(),
13854
+ /**
13855
+ * Absolute path on the node. Present so a file that is on disk but matches
13856
+ * no catalog entry — a custom Hugging Face install, or a GGUF the operator
13857
+ * copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
13858
+ * it the picker could list such a file and do nothing with it.
13859
+ */
13860
+ path: string().optional()
13808
13861
  });
13809
13862
  var LlmRuntimeDiskUsageSchema = object({
13810
13863
  nodeId: string(),
@@ -13950,6 +14003,36 @@ var ManagedModelCatalogEntrySchema = object({
13950
14003
  /** Vision models: companion projector file. */
13951
14004
  mmprojUrl: string().optional()
13952
14005
  });
14006
+ /**
14007
+ * The outcome of turning one operator-typed Hugging Face reference into a
14008
+ * download plan. A RESULT, never a throw: "this repo has 24 quantizations and
14009
+ * I will not pick for you" is a normal answer the UI has to render, not an
14010
+ * exception.
14011
+ *
14012
+ * `candidates` is the whole reason the refusal is usable — every string in it
14013
+ * is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
14014
+ */
14015
+ var HfModelResolutionSchema = discriminatedUnion("ok", [object({
14016
+ ok: literal(true),
14017
+ /** Ready to hand to `installModel` unchanged. */
14018
+ model: ManagedModelRefSchema,
14019
+ label: string(),
14020
+ repo: string(),
14021
+ quantization: string(),
14022
+ purpose: _enum(["text", "vision"]),
14023
+ totalBytes: number(),
14024
+ /** mmproj + shards, for the preview: an operator approving 23 GB should
14025
+ * see that 0.9 GB of it is a projector they did not name. */
14026
+ extraFilenames: array(string())
14027
+ }), object({
14028
+ ok: literal(false),
14029
+ code: string(),
14030
+ message: string(),
14031
+ candidates: array(string()).optional(),
14032
+ /** Set when the refusal was only the ceiling: re-calling with
14033
+ * `maxBytes: requiredBytes` is the operator's explicit override. */
14034
+ requiredBytes: number().optional()
14035
+ })]);
13953
14036
  var LlmRuntimeNodeSchema = object({
13954
14037
  nodeId: string(),
13955
14038
  reachable: boolean(),
@@ -13986,6 +14069,15 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13986
14069
  consumer: string().optional(),
13987
14070
  profileId: string().optional()
13988
14071
  }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
14072
+ /** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
14073
+ * `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
14074
+ ref: string(),
14075
+ /** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
14076
+ maxBytes: number().positive().optional()
14077
+ }), HfModelResolutionSchema, {
14078
+ kind: "mutation",
14079
+ auth: "admin"
14080
+ }), method(object({
13989
14081
  nodeId: string(),
13990
14082
  model: ManagedModelRefSchema
13991
14083
  }), _void(), {
@@ -19613,6 +19705,17 @@ var maxSessionHoldMsField = {
19613
19705
  default: 12e4,
19614
19706
  step: 5e3
19615
19707
  };
19708
+ /**
19709
+ * Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
19710
+ * 5s so a rearm can never degenerate into per-event stream churn; default 90s
19711
+ * comfortably outlives the gap between two PIR wakes on a battery camera.
19712
+ */
19713
+ var audioMotionWindowMsField = {
19714
+ min: 5e3,
19715
+ max: 6e5,
19716
+ default: 9e4,
19717
+ step: 5e3
19718
+ };
19616
19719
  var motionFpsField = {
19617
19720
  min: 1,
19618
19721
  max: 30,
@@ -19789,6 +19892,27 @@ var RunnerCameraConfigSchema = object({
19789
19892
  * resolved `CameraDetectionConfig`.
19790
19893
  */
19791
19894
  maxSessionHoldMs: number().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
19895
+ /**
19896
+ * Orchestrator-side quiet period (ms) that closes an `audioMode:
19897
+ * 'on-motion'` audio window, measured from the LAST motion event.
19898
+ *
19899
+ * This exists because the falling edge cannot be relied on. Camera-native
19900
+ * providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
19901
+ * its email-push SMTP path both emit `detected: true` and never the
19902
+ * counterpart); only the frame-diff analyzer emits falls. So on an
19903
+ * onboard-only camera a window that closed only on `detected: false` never
19904
+ * closed at all, and `on-motion` silently behaved as `always-on` — on a
19905
+ * battery camera, the one failure mode the mode exists to prevent.
19906
+ *
19907
+ * Every motion event rearms this timer WITHOUT restarting the stream, so a
19908
+ * burst of re-fires costs nothing. A falling edge, when one does arrive,
19909
+ * still closes earlier via `motionCooldownMs` — whichever comes first wins.
19910
+ *
19911
+ * Not consumed by the runner: carried here so it shares the per-camera
19912
+ * device-settings surface with `motionCooldownMs`, exactly like
19913
+ * `maxSessionHoldMs`.
19914
+ */
19915
+ audioMotionWindowMs: number().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
19792
19916
  motionFps: number().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
19793
19917
  detectionFps: number().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
19794
19918
  motionStreamId: string(),
@@ -19884,7 +20008,7 @@ var RunnerCameraConfigSchema = object({
19884
20008
  */
19885
20009
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
19886
20010
  });
19887
- motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
20011
+ motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
19888
20012
  /**
19889
20013
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
19890
20014
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -22659,6 +22783,25 @@ var BatteryStatusSchema = object({
22659
22783
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
22660
22784
  lastUpdated: number(),
22661
22785
  /**
22786
+ * Ms epoch of the last time the device PROVED it was reachable — a
22787
+ * completed firmware round-trip, an observed wake, or an inbound push
22788
+ * (firmware event, email). `0`/absent = never since this slice was born.
22789
+ *
22790
+ * This is the ONLY input that separates "asleep" from "gone", and it is
22791
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
22792
+ * for the radio, because a poll that confirms reachability is the same
22793
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
22794
+ * single derivation every consumer must use; no surface computes its own.
22795
+ *
22796
+ * It is deliberately NOT a clock in the
22797
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
22798
+ * observation itself, and it is the only thing a 30-hour silence is
22799
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
22800
+ * Reolink provider) so a value that means "recently" cannot cost a
22801
+ * SQLite commit per round-trip.
22802
+ */
22803
+ lastContactAt: number().optional(),
22804
+ /**
22662
22805
  * True when the source is a BINARY low-battery indicator (HA
22663
22806
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
22664
22807
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -22667,17 +22810,84 @@ var BatteryStatusSchema = object({
22667
22810
  */
22668
22811
  binary: boolean().optional()
22669
22812
  });
22670
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
22671
- deviceId: number(),
22672
- /** Bound on the wait. Sensible range 3000–10000ms. */
22673
- timeoutMs: number().int().min(500).max(3e4).default(8e3)
22674
- }), object({
22675
- awoke: boolean(),
22676
- durationMs: number()
22677
- }), { kind: "mutation" }), object({
22678
- deviceId: number(),
22679
- status: BatteryStatusSchema
22680
- });
22813
+ var batteryCapability = {
22814
+ name: "battery",
22815
+ scope: "device",
22816
+ deviceNative: true,
22817
+ mode: "singleton",
22818
+ deviceTypes: [
22819
+ DeviceType.Camera,
22820
+ DeviceType.Sensor,
22821
+ DeviceType.Button,
22822
+ DeviceType.Switch
22823
+ ],
22824
+ methods: {
22825
+ /**
22826
+ * Explicitly wake the camera from low-power sleep ahead of a
22827
+ * streaming session start. Consumers that initiate a stream
22828
+ * against a sleeping battery cam (HomeKit Secure Video, Alexa
22829
+ * RTCSession, snapshot wrappers) call this with a short timeout
22830
+ * before establishing the media pipeline — the broker's own
22831
+ * passive wake-on-dial works but adds 5–7 seconds to first-frame,
22832
+ * during which the consumer renders a black screen. Pre-waking
22833
+ * compresses that gap.
22834
+ *
22835
+ * Returns `awoke: true` when the firmware acknowledged the wake
22836
+ * before `timeoutMs`. Returns `awoke: false` when it timed out OR
22837
+ * the cap surface is unavailable (no Baichuan / firmware
22838
+ * channel); the caller should still attempt the stream — the
22839
+ * passive broker wake remains as fallback.
22840
+ */
22841
+ wakeForStream: method(object({
22842
+ deviceId: number(),
22843
+ /** Bound on the wait. Sensible range 3000–10000ms. */
22844
+ timeoutMs: number().int().min(500).max(3e4).default(8e3)
22845
+ }), object({
22846
+ awoke: boolean(),
22847
+ durationMs: number()
22848
+ }), { kind: "mutation" }) },
22849
+ events: {
22850
+ /**
22851
+ * Emitted whenever the cached status changes (firmware push OR
22852
+ * poll observes a delta). The DeviceEventPropagator mirrors this
22853
+ * event on the parent chain — subscribing to a camera's source
22854
+ * receives battery events from child accessories automatically.
22855
+ */
22856
+ onStatusChanged: { data: object({
22857
+ deviceId: number(),
22858
+ status: BatteryStatusSchema
22859
+ }) } },
22860
+ status: {
22861
+ schema: BatteryStatusSchema,
22862
+ kind: "push",
22863
+ empty: {
22864
+ percentage: 0,
22865
+ charging: "none",
22866
+ sleeping: false,
22867
+ lastUpdated: 0
22868
+ }
22869
+ },
22870
+ /**
22871
+ * Runtime-state slice — every provider that registers this cap
22872
+ * stores the same shape under `device.runtimeState[battery]`.
22873
+ * Cross-provider uniformity: a Reolink Argus, a Frigate sensor
22874
+ * proxy, an ONVIF battery cam all read/write the same keys.
22875
+ * Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
22876
+ * via `device.runtimeState.getCapState('battery')` regardless of
22877
+ * the underlying driver.
22878
+ */
22879
+ runtimeState: BatteryStatusSchema,
22880
+ /**
22881
+ * Runtime-state durability: **restored** — a sleeping battery camera may not report for hours; the restored percentage is the only thing the UI and the sleep gate have. Zero churn once `lastUpdated` is excluded — 526 writes, 0 value changes, in 25 minutes.
22882
+ *
22883
+ * See `RuntimeStateDurability`. Enforced by
22884
+ * `scripts/check-runtime-state-durability.ts`.
22885
+ */
22886
+ durability: "restored",
22887
+ /** Clock fields: written, but excluded from the compare that decides
22888
+ * whether persisting is worth a SQLite commit. */
22889
+ volatileStateFields: ["lastUpdated"]
22890
+ };
22681
22891
  object({
22682
22892
  on: boolean(),
22683
22893
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -27088,6 +27298,16 @@ var CamStreamDescriptorSchema = object({
27088
27298
  /** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
27089
27299
  metadata: record(string(), unknown()).optional()
27090
27300
  });
27301
+ object({
27302
+ /** The descriptors as last built from a real camera response. Never a guess:
27303
+ * a failed or refused build writes NOTHING, so a restored catalog is always
27304
+ * one the camera itself once produced. */
27305
+ descriptors: array(CamStreamDescriptorSchema),
27306
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
27307
+ * path decide whether the camera's own awake window is worth spending on a
27308
+ * re-read. */
27309
+ lastFetchedAt: number()
27310
+ });
27091
27311
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly());
27092
27312
  /** One of the camera's stream profiles. */
27093
27313
  var StreamProfileSchema = _enum([
@@ -27972,6 +28192,24 @@ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, Accessor
27972
28192
  DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
27973
28193
  new Set(Object.values(DeviceType));
27974
28194
  DeviceFeature.BatteryOperated;
28195
+ /**
28196
+ * THE derivation. One crop rectangle, one presence verdict — see D52 for why
28197
+ * this repo insists a derived value has exactly one implementation.
28198
+ *
28199
+ * `undefined` status ⇒ `sleeping`. A device with no slice has made no claim,
28200
+ * and the caller decides separately whether it is even battery-operated
28201
+ * (`DeviceFeature.BatteryOperated`); this function never answers that
28202
+ * question, only what a battery device is doing.
28203
+ */
28204
+ function deriveBatteryPresence(input) {
28205
+ const status = input.status;
28206
+ if (!status) return "sleeping";
28207
+ if (status.sleeping !== true) return "awake";
28208
+ const lastContactAt = status.lastContactAt;
28209
+ if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
28210
+ const budget = input.unreachableAfterMs ?? 216e5;
28211
+ return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
28212
+ }
27975
28213
  Object.freeze({
27976
28214
  "accessories.setChildHidden": {
27977
28215
  capName: "accessories",
@@ -30121,6 +30359,12 @@ Object.freeze({
30121
30359
  addonId: null,
30122
30360
  access: "view"
30123
30361
  },
30362
+ "llm.resolveModelRef": {
30363
+ capName: "llm",
30364
+ capScope: "system",
30365
+ addonId: null,
30366
+ access: "create"
30367
+ },
30124
30368
  "llm.setDefault": {
30125
30369
  capName: "llm",
30126
30370
  capScope: "system",
@@ -36277,4 +36521,4 @@ function enumerateInferenceDevices(hw) {
36277
36521
  return out;
36278
36522
  }
36279
36523
  //#endregion
36280
- export { supportedRuntimes as $, defaultDeviceFor as A, string as At, mapAudioLabelToMacro as B, YAMNET_TO_MACRO as C, boolean as Ct, cameraStreamsCapability as D, number as Dt, audioAnalyzerCapability as E, literal as Et, egressTranscodeSharingKey as F, pipelineExecutorCapability as G, motionDetectionCapability as H, egressTransportFromRequest as I, recordingExportCapability as J, pipelineRunnerCapability as K, enumerateInferenceDevices as L, deriveDetailCropRect as M, EventCategory as Mt, deriveRecordingMode as N, createHwAccelCache as O, object as Ot, detectionPipelineCapability as P, streamBrokerCapability as Q, evaluateZoneRules as R, RingBuffer as S, array as St, audioAnalysisCapability as T, lazy as Tt, pickDetailCropConvention as U, maskUrlCredentials as V, pickNativeLeaseOverride as W, runtimeDevices as X, resolveEgressDecodeHwAccel as Y, storageEvictableCapability as Z, OpsLogEntrySchema as _, parseJsonUnknown as _t, COCO_TO_MACRO as a, invocationFromEncodeProfile as at, RECORDING_EXPORT_MAX_READ_BYTES as b, sleep as bt, DEFAULT_EVENTS_BAND_BUFFER_SEC as c, CAM_PROFILE_ORDER as ct, EVENT_PAD_MS as d, createEvent as dt, webrtcSessionCapability as et, EncodeProfileSchema as f, hydrateSchema as ft, NativeLeaseSettingsSchema as g, nodePin as gt, NativeLeaseAdmissionSchema as h, makeSourceBrokerId as ht, COCO_80_LABELS as i, buildFfmpegArgs as it, defineCustomActions as j, union as jt, customAction as k, record as kt, DEFAULT_NATIVE_LEASE_SETTINGS as l, DeviceFeature as lt, HF_BASE_URL as m, makeProfileBrokerId as mt, AUDIO_BACKEND_CHOICES as n, AUDIO_PRESETS as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, isSoftwareDecode as ot, ExportRecordSchema as p, isEvent as pt, recordingCapability as q, AUDIO_MACRO_LABELS as r, Fmp4BoxSplitter as rt, DEFAULT_DETAIL_CROP_CONVENTION as s, BaseAddon as st, APPLE_SA_TO_MACRO as t, errMsg as tt, DEVICE_BACKEND_TO_FORMAT as u, DeviceType as ut, RATE_CONTROL_RELAXED as v, parseProfileBrokerId as vt, addonWidgetsSourceCapability as w, discriminatedUnion as wt, RecordingConfigSchema as x, _enum as xt, RATE_CONTROL_TIGHT as y, selectAssignedProfileSlots as yt, hfModelUrl as z };
36524
+ export { runtimeDevices as $, createHwAccelCache as A, number as At, enumerateInferenceDevices as B, RingBuffer as C, sleep as Ct, audioAnalyzerCapability as D, discriminatedUnion as Dt, audioAnalysisCapability as E, boolean as Et, deriveDetailCropRect as F, EventCategory as Ft, motionDetectionCapability as G, hfModelUrl as H, deriveRecordingMode as I, pipelineExecutorCapability as J, pickDetailCropConvention as K, detectionPipelineCapability as L, defaultDeviceFor as M, record as Mt, defineCustomActions as N, string as Nt, batteryCapability as O, lazy as Ot, deriveBatteryPresence as P, union as Pt, resolveEgressDecodeHwAccel as Q, egressTranscodeSharingKey as R, RecordingConfigSchema as S, selectAssignedProfileSlots as St, addonWidgetsSourceCapability as T, array as Tt, mapAudioLabelToMacro as U, evaluateZoneRules as V, maskUrlCredentials as W, recordingCapability as X, pipelineRunnerCapability as Y, recordingExportCapability as Z, NativeLeaseSettingsSchema as _, makeProfileBrokerId as _t, COCO_80_LABELS as a, AUDIO_PRESETS as at, RATE_CONTROL_TIGHT as b, parseJsonUnknown as bt, DEFAULT_DETAIL_CROP_CONVENTION as c, invocationFromEncodeProfile as ct, DEVICE_BACKEND_TO_FORMAT as d, CAM_PROFILE_ORDER as dt, storageEvictableCapability as et, EVENT_PAD_MS as f, DeviceFeature as ft, NativeLeaseAdmissionSchema as g, isEvent as gt, HF_BASE_URL as h, hydrateSchema as ht, BatteryStatusSchema as i, errMsg as it, customAction as j, object as jt, cameraStreamsCapability as k, literal as kt, DEFAULT_EVENTS_BAND_BUFFER_SEC as l, isSoftwareDecode as lt, ExportRecordSchema as m, createEvent as mt, AUDIO_BACKEND_CHOICES as n, supportedRuntimes as nt, COCO_TO_MACRO as o, Fmp4BoxSplitter as ot, EncodeProfileSchema as p, DeviceType as pt, pickNativeLeaseOverride as q, AUDIO_MACRO_LABELS as r, webrtcSessionCapability as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, buildFfmpegArgs as st, APPLE_SA_TO_MACRO as t, streamBrokerCapability as tt, DEFAULT_NATIVE_LEASE_SETTINGS as u, BaseAddon as ut, OpsLogEntrySchema as v, makeSourceBrokerId as vt, YAMNET_TO_MACRO as w, _enum as wt, RECORDING_EXPORT_MAX_READ_BYTES as x, parseProfileBrokerId as xt, RATE_CONTROL_RELAXED as y, nodePin as yt, egressTransportFromRequest as z };
@@ -1,4 +1,4 @@
1
- import { a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS, z as hfModelUrl } from "./dist-BfTf6doa.mjs";
1
+ import { H as hfModelUrl, a as COCO_80_LABELS, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-DGp0dRiq.mjs";
2
2
  import { PerformanceObserver } from "node:perf_hooks";
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BUIpqu3E.js");
1
+ const require_dist = require("./dist-B57MztMq.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BUIpqu3E.js");
1
+ const require_dist = require("./dist-B57MztMq.js");
2
2
  let node_url = require("node:url");
3
3
  let node_module = require("node:module");
4
4
  let node_path = require("node:path");
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BUIpqu3E.js");
6
- const require_lazy_sharp = require("../lazy-sharp-BZVhL9Lw.js");
5
+ const require_dist = require("../dist-B57MztMq.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.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 { H as motionDetectionCapability, R as evaluateZoneRules, ft as hydrateSchema, st as BaseAddon, ut as DeviceType } from "../dist-BfTf6doa.mjs";
1
+ import { G as motionDetectionCapability, V as evaluateZoneRules, ht as hydrateSchema, pt as DeviceType, ut as BaseAddon } from "../dist-DGp0dRiq.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
3
3
  import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";
@@ -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-BUIpqu3E.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-CGLQP0by.js");
7
- const require_worker_protocol = require("../worker-protocol-BaLcYcOo.js");
5
+ const require_dist = require("../dist-B57MztMq.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DPA6Pu_h.js");
7
+ const require_worker_protocol = require("../worker-protocol-4L4SbSAo.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
- const require_lazy_sharp = require("../lazy-sharp-BZVhL9Lw.js");
9
+ const require_lazy_sharp = require("../lazy-sharp-BMgRSV9x.js");
10
10
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
11
11
  let sharp = require("sharp");
12
12
  sharp = require_dist.__toESM(sharp);
@@ -4350,6 +4350,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4350
4350
  nodeId = "unknown";
4351
4351
  metricsSnapshotTimer = null;
4352
4352
  unsubMotionEvents = null;
4353
+ /** Devices we have positively observed reporting `battery.sleeping: true`.
4354
+ * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
4355
+ * `BatteryOnStatusChanged`, so consulting it costs nothing and can never
4356
+ * wake the camera it protects. See `enqueueOccupancyBurst`. */
4357
+ sleepingBatteryDevices = /* @__PURE__ */ new Set();
4358
+ unsubBatteryEvents = null;
4353
4359
  /** Last analyzer-detected state per device — gates the
4354
4360
  * `MotionOnMotionChanged` emit in `runMotionAnalysis` to transitions
4355
4361
  * only (otherwise we'd emit on every analyzer frame). */
@@ -4557,6 +4563,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4557
4563
  if (!attachment.config.motionSources.includes(source)) return;
4558
4564
  this.runner?.reportMotion(deviceId, data.detected, source, data.regions ? [...data.regions] : void 0);
4559
4565
  });
4566
+ if (this.ctx.eventBus) this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.BatteryOnStatusChanged }, (event) => {
4567
+ const data = event.data;
4568
+ const deviceId = data.deviceId;
4569
+ if (typeof deviceId !== "number") return;
4570
+ if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
4571
+ else this.sleepingBatteryDevices.delete(deviceId);
4572
+ });
4560
4573
  this.metricsSnapshotTimer = setInterval(() => this.emitMetricsSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
4561
4574
  return {
4562
4575
  providers: [{
@@ -4590,6 +4603,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4590
4603
  this.unsubMotionEvents();
4591
4604
  this.unsubMotionEvents = null;
4592
4605
  }
4606
+ if (this.unsubBatteryEvents) {
4607
+ this.unsubBatteryEvents();
4608
+ this.unsubBatteryEvents = null;
4609
+ }
4610
+ this.sleepingBatteryDevices.clear();
4593
4611
  this.lastAnalyzerDetected.clear();
4594
4612
  this.packageGovernor.reset();
4595
4613
  this.packageGateSkips.clear();
@@ -6660,6 +6678,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6660
6678
  * parallel decoder dials.
6661
6679
  */
6662
6680
  enqueueOccupancyBurst(deviceId, frames) {
6681
+ if (this.sleepingBatteryDevices.has(deviceId)) {
6682
+ this.ctx.logger.debug("occupancy recheck skipped — battery camera is sleeping", { tags: { deviceId } });
6683
+ return;
6684
+ }
6663
6685
  this.occupancyFrames.set(deviceId, frames);
6664
6686
  if (this.occupancyBurstInFlight.has(deviceId) || this.occupancyQueued.has(deviceId)) return;
6665
6687
  this.occupancyQueued.add(deviceId);
@@ -1,6 +1,6 @@
1
- import { At as string, Ct as boolean, Dt as number, Et as literal, K as pipelineRunnerCapability, M as deriveDetailCropRect, Mt as EventCategory, Ot as object, St as array, Tt as lazy, U as pickDetailCropConvention, W as pickNativeLeaseOverride, dt as createEvent, gt as nodePin, j as defineCustomActions, k as customAction, s as DEFAULT_DETAIL_CROP_CONVENTION, st as BaseAddon, tt as errMsg, u as DEVICE_BACKEND_TO_FORMAT, xt as _enum } from "../dist-BfTf6doa.mjs";
2
- import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-CmrHj0i9.mjs";
3
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BdOv9Ujl.mjs";
1
+ import { At as number, Et as boolean, F as deriveDetailCropRect, Ft as EventCategory, K as pickDetailCropConvention, N as defineCustomActions, Nt as string, Ot as lazy, Tt as array, Y as pipelineRunnerCapability, c as DEFAULT_DETAIL_CROP_CONVENTION, d as DEVICE_BACKEND_TO_FORMAT, it as errMsg, j as customAction, jt as object, kt as literal, mt as createEvent, q as pickNativeLeaseOverride, ut as BaseAddon, wt as _enum, yt as nodePin } from "../dist-DGp0dRiq.mjs";
2
+ import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-8vYH44kg.mjs";
3
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-_e84lGuR.mjs";
4
4
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
6
6
  import { i as withDetectionIntent, n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-Ci7RXNGb.mjs";
@@ -4343,6 +4343,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
4343
4343
  nodeId = "unknown";
4344
4344
  metricsSnapshotTimer = null;
4345
4345
  unsubMotionEvents = null;
4346
+ /** Devices we have positively observed reporting `battery.sleeping: true`.
4347
+ * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
4348
+ * `BatteryOnStatusChanged`, so consulting it costs nothing and can never
4349
+ * wake the camera it protects. See `enqueueOccupancyBurst`. */
4350
+ sleepingBatteryDevices = /* @__PURE__ */ new Set();
4351
+ unsubBatteryEvents = null;
4346
4352
  /** Last analyzer-detected state per device — gates the
4347
4353
  * `MotionOnMotionChanged` emit in `runMotionAnalysis` to transitions
4348
4354
  * only (otherwise we'd emit on every analyzer frame). */
@@ -4550,6 +4556,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
4550
4556
  if (!attachment.config.motionSources.includes(source)) return;
4551
4557
  this.runner?.reportMotion(deviceId, data.detected, source, data.regions ? [...data.regions] : void 0);
4552
4558
  });
4559
+ if (this.ctx.eventBus) this.unsubBatteryEvents = this.ctx.eventBus.subscribe({ category: EventCategory.BatteryOnStatusChanged }, (event) => {
4560
+ const data = event.data;
4561
+ const deviceId = data.deviceId;
4562
+ if (typeof deviceId !== "number") return;
4563
+ if (data.status?.sleeping === true) this.sleepingBatteryDevices.add(deviceId);
4564
+ else this.sleepingBatteryDevices.delete(deviceId);
4565
+ });
4553
4566
  this.metricsSnapshotTimer = setInterval(() => this.emitMetricsSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
4554
4567
  return {
4555
4568
  providers: [{
@@ -4583,6 +4596,11 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
4583
4596
  this.unsubMotionEvents();
4584
4597
  this.unsubMotionEvents = null;
4585
4598
  }
4599
+ if (this.unsubBatteryEvents) {
4600
+ this.unsubBatteryEvents();
4601
+ this.unsubBatteryEvents = null;
4602
+ }
4603
+ this.sleepingBatteryDevices.clear();
4586
4604
  this.lastAnalyzerDetected.clear();
4587
4605
  this.packageGovernor.reset();
4588
4606
  this.packageGateSkips.clear();
@@ -6653,6 +6671,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
6653
6671
  * parallel decoder dials.
6654
6672
  */
6655
6673
  enqueueOccupancyBurst(deviceId, frames) {
6674
+ if (this.sleepingBatteryDevices.has(deviceId)) {
6675
+ this.ctx.logger.debug("occupancy recheck skipped — battery camera is sleeping", { tags: { deviceId } });
6676
+ return;
6677
+ }
6656
6678
  this.occupancyFrames.set(deviceId, frames);
6657
6679
  if (this.occupancyBurstInFlight.has(deviceId) || this.occupancyQueued.has(deviceId)) return;
6658
6680
  this.occupancyQueued.add(deviceId);
@@ -1,6 +1,6 @@
1
- const require_dist = require("../dist-BUIpqu3E.js");
1
+ const require_dist = require("../dist-B57MztMq.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
- const require_addon_utils = require("../addon-utils-BZQxAJ1G.js");
3
+ const require_addon_utils = require("../addon-utils-CQFlZXSn.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_child_process = require("node:child_process");
6
6
  let node_path = require("node:path");
@@ -3764,6 +3764,7 @@ async function buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs) {
3764
3764
  if (base === null) return noPlayback(deviceId);
3765
3765
  const readPrefix = deps.readFilePrefix ?? readPrefixFromDisk;
3766
3766
  const playbackDir = playbackDirFor(deps.dataDir, deviceId);
3767
+ if (deps.index.hydrationOf(deviceId, fromMs, toMs) !== "hydrated") await deps.hydrateWindow?.(deviceId, fromMs, toMs);
3767
3768
  const profiles = [...new Set(deps.index.segments(deviceId).map((s) => s.profile))];
3768
3769
  const variants = [];
3769
3770
  for (const profile of profiles) {
@@ -5016,6 +5017,61 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
5016
5017
  }
5017
5018
  /** Entries classified between yields in {@link hydrateDeviceFromStorage}. */
5018
5019
  var HYDRATE_CHUNK = 2e4;
5020
+ /**
5021
+ * Device walks in flight at once in {@link hydrateDevicesFromStorage}.
5022
+ *
5023
+ * The walk is `readdir` latency on a network/FUSE share, not CPU: measured on
5024
+ * the live hub, 24 cold hour directories cost 31 s one at a time and 16.7 s
5025
+ * eight at a time. Four is the conservative point on that curve — enough to
5026
+ * hide most of the latency, few enough that the boot walk never becomes the
5027
+ * reason a live SegmentWriter waits. libuv's default threadpool is 4, so a
5028
+ * higher number here mostly queues.
5029
+ */
5030
+ var HYDRATE_DEVICE_CONCURRENCY = 4;
5031
+ /**
5032
+ * Walk the archive for a whole fleet: bounded-parallel, and each camera becomes
5033
+ * authoritative the moment ITS OWN walk lands (D167).
5034
+ *
5035
+ * Two things this fixes, both measured on the live hub on 2026-08-15:
5036
+ *
5037
+ * - The caller was `for (const id of ids) await hydrateDeviceFromStorage(...)`,
5038
+ * so seven cameras' 357 s of `readdir` WAS the 362 s wall clock. Nothing in
5039
+ * the walk contends for CPU — it is share latency — so overlapping is free.
5040
+ * - `markHydratedAll` ran in a SECOND loop after every device. A camera whose
5041
+ * walk finished at minute 1 still reported `hydrationOf → 'unknown'` until
5042
+ * minute 6, and every read of it in that window paid for an on-demand walk
5043
+ * over rows already in the index, competing with the walk still running for
5044
+ * the others.
5045
+ *
5046
+ * A device whose walk throws is NOT marked: `unknown` is the honest answer, and
5047
+ * it is what keeps the read path walking that window on demand.
5048
+ */
5049
+ async function hydrateDevicesFromStorage(api, index, deviceIds, locations, logger, options = {}) {
5050
+ const concurrency = Math.max(1, options.concurrency ?? HYDRATE_DEVICE_CONCURRENCY);
5051
+ const queue = [...deviceIds];
5052
+ const walkOptions = {
5053
+ ...options.yieldBetween ? { yieldBetween: options.yieldBetween } : {},
5054
+ ...options.chunkSize !== void 0 ? { chunkSize: options.chunkSize } : {}
5055
+ };
5056
+ const lane = async () => {
5057
+ for (;;) {
5058
+ const deviceId = queue.shift();
5059
+ if (deviceId === void 0) return;
5060
+ try {
5061
+ options.onDeviceStarted?.(deviceId);
5062
+ await hydrateDeviceFromStorage(api, index, deviceId, locations, logger, walkOptions);
5063
+ index.markHydratedAll(deviceId);
5064
+ options.onDeviceHydrated?.(deviceId);
5065
+ } catch (err) {
5066
+ logger.warn("recorder: device hydrate failed — window stays unknown, reads will walk it", {
5067
+ tags: { deviceId },
5068
+ meta: { error: require_dist.errMsg(err) }
5069
+ });
5070
+ }
5071
+ }
5072
+ };
5073
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, lane));
5074
+ }
5019
5075
  var macrotask$1 = () => new Promise((resolve) => {
5020
5076
  setImmediate(resolve);
5021
5077
  });
@@ -8108,11 +8164,12 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
8108
8164
  return;
8109
8165
  }
8110
8166
  this.warmToday([...configs.keys()]);
8111
- for (const deviceId of configs.keys()) await hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, this.resolvedLocations, this.ctx.logger);
8112
- for (const deviceId of configs.keys()) this.index.markHydratedAll(deviceId);
8167
+ const hydrateStartedMs = Date.now();
8168
+ await hydrateDevicesFromStorage(this.ctx.api, this.index, [...configs.keys()], this.resolvedLocations, this.ctx.logger);
8113
8169
  this.ctx.logger.info("recorder: hydrated index from storage", { meta: {
8114
8170
  deviceCount: configs.size,
8115
- locationCount: this.resolvedLocations.length
8171
+ locationCount: this.resolvedLocations.length,
8172
+ ms: Date.now() - hydrateStartedMs
8116
8173
  } });
8117
8174
  const loggedDomains = /* @__PURE__ */ new Set();
8118
8175
  for (const loc of this.resolvedLocations) {