@camstack/system 1.2.92 → 1.2.94

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 (53) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +207 -75
  34. package/dist/builtins/snapshot/index.mjs +207 -75
  35. package/dist/builtins/snapshot/snapshot-courtesy.d.ts +24 -2
  36. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +7 -1
  37. package/dist/builtins/snapshot/snapshot-resize.d.ts +4 -4
  38. package/dist/builtins/snapshot/snapshot.addon.d.ts +35 -26
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-BH2tZ9ZN.js → dist-BspfS8Q7.js} +105 -2
  50. package/dist/{dist-DXDvrBi0.mjs → dist-COd223t-.mjs} +94 -3
  51. package/dist/index.js +1 -1
  52. package/dist/index.mjs +1 -1
  53. package/package.json +1 -1
@@ -40,9 +40,14 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
40
40
  * A courtesy frame is a pure function of those three, so it is rendered once
41
41
  * and reused for as long as the process lives — a disabled camera must not
42
42
  * cost an ffmpeg run per poll. Unbounded on purpose: the key space is
43
- * (3 reasons × this node's own cameras), not user input.
43
+ * (state reasons × this node's own cameras), not user input.
44
44
  */
45
45
  private readonly courtesyFrames;
46
+ /** State overlays over real cached frames, grouped by device so invalidation
47
+ * can discard every generation in O(1). */
48
+ private readonly stateFrames;
49
+ /** Devices inside the explicit battery wake-in-progress window. */
50
+ private readonly wakingDevices;
46
51
  /**
47
52
  * De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
48
53
  * settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
@@ -201,9 +206,7 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
201
206
  * the Viewer's 15-second currency contract and the link-mint wait bound.
202
207
  */
203
208
  private awaitFreshEnough;
204
- /** True only for a BATTERY device that is currently asleep. Kept as one
205
- * question so the link path cannot accidentally treat "battery" as "asleep"
206
- * and stop refreshing a camera that is awake and streaming. */
209
+ /** True only for a battery device that must not be snapshot-refreshed. */
207
210
  private isSleepingBatteryDevice;
208
211
  /**
209
212
  * Resolve a device (+ optional stream, + force) to renderable JPEG bytes for
@@ -225,8 +228,8 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
225
228
  */
226
229
  private peekFreshFrame;
227
230
  /**
228
- * The frame at a card-sized width, derived once per (device, stream, width)
229
- * per capture.
231
+ * The frame at a card-sized width, derived once per
232
+ * (device, stream, width, state treatment) per capture.
230
233
  *
231
234
  * A failed resize falls back to the FULL frame — a visible card beats a
232
235
  * broken one — but never silently: the card would otherwise keep costing a
@@ -355,25 +358,26 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
355
358
  */
356
359
  private deviceStillExists;
357
360
  /**
358
- * Sleep state from the device-state MIRROR, not from a cap round-trip.
361
+ * Resolve the state that must be visible on a snapshot.
359
362
  *
360
- * `fetchDevice(id).state.battery` is a `SliceHandle` over the hub's
361
- * runtime-state mirror: a local read, no RPC, no failure branch to get
362
- * wrong. It is the same mirror that feeds the battery badge elsewhere
363
- * in the UI, so if the operator can see "sleeping" on screen, this
364
- * sees it too. (Both spec harnesses already model exactly this wiring
365
- * — like the orphaned doc comment above, it outlived the gate it was
366
- * built for.)
363
+ * Uses the same two canonical sources as wake-on-play:
364
+ * 1. `deviceManager.getDevice` for the registry's BatteryOperated feature;
365
+ * 2. `deviceState.getCapSlice('battery')` for the durable runtime mirror.
367
366
  *
368
- * `undefined` slice the device has no battery state we know of →
369
- * treated as awake. The caller has already established the device is
370
- * battery-operated from its feature flags, so this only decides
371
- * WHETHER IT IS ASLEEP, never whether it has a battery.
367
+ * A valid battery slice is itself positive evidence that the device is
368
+ * battery-operated. That closes the field failure where "Baby monitor" had a
369
+ * sleeping battery slice but a stale persisted feature projection, so the
370
+ * snapshot wrapper classified it as mains and a Viewer thumbnail logged in
371
+ * to the sleeping camera. The reverse is deliberately not guessed: no
372
+ * feature + no valid slice remains a mains/unknown camera and follows the
373
+ * ordinary capture path.
372
374
  *
373
- * Only consulted on the path that would otherwise CAPTURE — a fresh
374
- * cache hit returns before we get here.
375
+ * Once battery-operated is established, a missing slice fails safe toward
376
+ * `sleeping` through the shared `deriveBatteryPresence` function. This avoids
377
+ * a background wake without inventing an `unreachable` fault.
375
378
  */
376
- private isDeviceSleeping;
379
+ private resolveSnapshotState;
380
+ private isDeviceWaking;
377
381
  /**
378
382
  * True when at least one of the device's brokers is actively
379
383
  * streaming (status === 'streaming'). Used by the battery-cam guard
@@ -421,13 +425,18 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
421
425
  private courtesyFrame;
422
426
  /** A courtesy frame shaped as the cap's response, or null when unrenderable. */
423
427
  private courtesyImage;
428
+ /**
429
+ * Render a state-labelled answer. A cached real frame is the background, not
430
+ * the answer by itself; if it cannot be decoded, fall back to the generated
431
+ * courtesy background rather than leaking an unlabelled stale frame.
432
+ */
433
+ private stateImage;
424
434
  /**
425
435
  * Single-trip device lookup against device-manager. Returns the
426
- * fields the wrapper actually consults name (logging) + battery
427
- * flag (cache window + broker-fallback gate). Sourced from the
428
- * device-manager registry rather than the battery cap so the answer
429
- * survives a momentarily-unreachable provider (the very condition
430
- * we're trying to be resilient to).
436
+ * stable registry fields the wrapper consults. Battery classification is
437
+ * completed by `resolveSnapshotState` from this feature flag PLUS the
438
+ * canonical runtime-state mirror, so a stale persisted feature projection
439
+ * cannot authorize a snapshot dial against a known sleeping battery camera.
431
440
  *
432
441
  * Logged at debug + null return on failure: every call site already
433
442
  * has a sensible fallback path (cache hit, conservative default, …),
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BH2tZ9ZN.js");
6
+ const require_dist = require("../../dist-BspfS8Q7.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { _t as BaseAddon, dt as storageProviderCapability, z as filesystemBrowseCapability } from "../../dist-DXDvrBi0.mjs";
1
+ import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-COd223t-.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BH2tZ9ZN.js");
6
+ const require_dist = require("../../dist-BspfS8Q7.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -1,4 +1,4 @@
1
- import { A as decodeVectorBase64, Dt as asJsonObject, Rt as parseJsonUnknown, _t as BaseAddon, f as RUNTIME_DEFAULTS, gt as errMsg, ht as vectorStoreCapability, k as dataStoreProviderCapability, mt as vectorDimFromBase64, w as bareAddonId } from "../../dist-DXDvrBi0.mjs";
1
+ import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-COd223t-.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { statSync } from "node:fs";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BH2tZ9ZN.js");
6
+ const require_dist = require("../../dist-BspfS8Q7.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { Lt as parseJsonObject, _t as BaseAddon, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as storageCapability, st as settingsStoreCapability, ut as storageMigrationCapability } from "../../dist-DXDvrBi0.mjs";
1
+ import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-COd223t-.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BH2tZ9ZN.js");
6
+ const require_dist = require("../../dist-BspfS8Q7.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { Pt as hydrateSchema, _t as BaseAddon, gt as errMsg } from "../../dist-DXDvrBi0.mjs";
1
+ import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-COd223t-.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BH2tZ9ZN.js");
6
+ const require_dist = require("../../dist-BspfS8Q7.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { J as logDestinationCapability, _t as BaseAddon } from "../../dist-DXDvrBi0.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-COd223t-.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -20825,6 +20825,25 @@ var BatteryStatusSchema = zod.z.object({
20825
20825
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
20826
20826
  lastUpdated: zod.z.number(),
20827
20827
  /**
20828
+ * Ms epoch of the last time the device PROVED it was reachable — a
20829
+ * completed firmware round-trip, an observed wake, or an inbound push
20830
+ * (firmware event, email). `0`/absent = never since this slice was born.
20831
+ *
20832
+ * This is the ONLY input that separates "asleep" from "gone", and it is
20833
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
20834
+ * for the radio, because a poll that confirms reachability is the same
20835
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
20836
+ * single derivation every consumer must use; no surface computes its own.
20837
+ *
20838
+ * It is deliberately NOT a clock in the
20839
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
20840
+ * observation itself, and it is the only thing a 30-hour silence is
20841
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
20842
+ * Reolink provider) so a value that means "recently" cannot cost a
20843
+ * SQLite commit per round-trip.
20844
+ */
20845
+ lastContactAt: zod.z.number().optional(),
20846
+ /**
20828
20847
  * True when the source is a BINARY low-battery indicator (HA
20829
20848
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
20830
20849
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -27507,13 +27526,63 @@ var CamStreamDescriptorSchema = zod.z.object({
27507
27526
  * set of stream descriptors it can offer for the device, synchronously, so the
27508
27527
  * broker can reconcile its registry against the authoritative provider state.
27509
27528
  */
27529
+ /**
27530
+ * The catalog as a DURABLE fact rather than a live answer.
27531
+ *
27532
+ * A battery camera's descriptors are profile-stable — they change when the
27533
+ * operator rewrites an encoder profile, not minute to minute — but building
27534
+ * them costs a Baichuan login, which on a sleeping Argus IS a wake. So the
27535
+ * provider is allowed to build them exactly once per profile and must serve
27536
+ * every later pull from a cache.
27537
+ *
27538
+ * Holding that cache only in RAM is what turned a restart into an outage. The
27539
+ * runner comes back with the camera asleep, `buildStreamCatalogUncached`
27540
+ * correctly refuses to wake it, the pull answers `[]`, the broker has no
27541
+ * cam-stream entry to build a broker from, and `webrtcSession.handleOffer`
27542
+ * fails with a flat "No broker for stream" — for as long as the camera sleeps,
27543
+ * which on a battery cam is most of the day. The camera was fine. The stream
27544
+ * was unreachable because the process had forgotten what the camera offers.
27545
+ *
27546
+ * Declaring it here puts it in `device-runtime-state`, the kernel's canonical
27547
+ * declared collection, with the same `restored` durability `battery` uses for
27548
+ * the same reason: the last known value is the only value there is while the
27549
+ * device is asleep. The broker's brokers are therefore always DEFINABLE — it
27550
+ * is the DIAL that wakes a camera, never the catalog (D173).
27551
+ */
27552
+ var StreamCatalogStateSchema = zod.z.object({
27553
+ /** The descriptors as last built from a real camera response. Never a guess:
27554
+ * a failed or refused build writes NOTHING, so a restored catalog is always
27555
+ * one the camera itself once produced. */
27556
+ descriptors: zod.z.array(CamStreamDescriptorSchema),
27557
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
27558
+ * path decide whether the camera's own awake window is worth spending on a
27559
+ * re-read. */
27560
+ lastFetchedAt: zod.z.number()
27561
+ });
27510
27562
  var streamCatalogCapability = {
27511
27563
  name: "stream-catalog",
27512
27564
  scope: "device",
27513
27565
  deviceNative: true,
27514
27566
  mode: "singleton",
27515
27567
  deviceTypes: [DeviceType.Camera],
27516
- methods: { getCatalog: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.array(CamStreamDescriptorSchema).readonly()) }
27568
+ methods: { getCatalog: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.array(CamStreamDescriptorSchema).readonly()) },
27569
+ runtimeState: StreamCatalogStateSchema,
27570
+ /**
27571
+ * Runtime-state durability: **restored** — see the schema doc. A cold
27572
+ * catalog on a sleeping battery camera is not a slow first frame, it is a
27573
+ * camera that cannot be watched at all until it happens to wake.
27574
+ *
27575
+ * Churn is nil by construction: the slice is written only by a SUCCESSFUL
27576
+ * build, and a build only runs when there is no cached copy (or the copy is
27577
+ * a day old and the camera is awake anyway).
27578
+ *
27579
+ * See `RuntimeStateDurability`. Enforced by
27580
+ * `scripts/check-runtime-state-durability.ts`.
27581
+ */
27582
+ durability: "restored",
27583
+ /** Clock field: written, but excluded from the compare that decides whether
27584
+ * persisting is worth a SQLite commit — the descriptors are the value. */
27585
+ volatileStateFields: ["lastFetchedAt"]
27517
27586
  };
27518
27587
  /** One of the camera's stream profiles. */
27519
27588
  var StreamProfileSchema = zod.z.enum([
@@ -29510,6 +29579,24 @@ var DeclaredDevices = class {
29510
29579
  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;
29511
29580
  new Set(Object.values(DeviceType));
29512
29581
  DeviceFeature.BatteryOperated;
29582
+ /**
29583
+ * THE derivation. One crop rectangle, one presence verdict — see D52 for why
29584
+ * this repo insists a derived value has exactly one implementation.
29585
+ *
29586
+ * `undefined` status ⇒ `sleeping`. A device with no slice has made no claim,
29587
+ * and the caller decides separately whether it is even battery-operated
29588
+ * (`DeviceFeature.BatteryOperated`); this function never answers that
29589
+ * question, only what a battery device is doing.
29590
+ */
29591
+ function deriveBatteryPresence(input) {
29592
+ const status = input.status;
29593
+ if (!status) return "sleeping";
29594
+ if (status.sleeping !== true) return "awake";
29595
+ const lastContactAt = status.lastContactAt;
29596
+ if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
29597
+ const budget = input.unreachableAfterMs ?? 216e5;
29598
+ return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
29599
+ }
29513
29600
  /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
29514
29601
  * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
29515
29602
  function unwrap(schema) {
@@ -37123,7 +37210,7 @@ Object.freeze({
37123
37210
  });
37124
37211
  /**
37125
37212
  * Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
37126
- * `runtimeState`. 63 caps: 33 `restored`, 30 `session`.
37213
+ * `runtimeState`. 64 caps: 34 `restored`, 30 `session`.
37127
37214
  *
37128
37215
  * A cap absent from this map has no runtime-state slice at all. A cap PRESENT
37129
37216
  * with `durability: 'session'` has one and has decided not to keep it.
@@ -37329,6 +37416,10 @@ var RUNTIME_STATE_POLICY = {
37329
37416
  durability: "restored",
37330
37417
  volatileFields: ["lastChangedAt"]
37331
37418
  },
37419
+ "stream-catalog": {
37420
+ durability: "restored",
37421
+ volatileFields: ["lastFetchedAt"]
37422
+ },
37332
37423
  "stream-params": {
37333
37424
  durability: "restored",
37334
37425
  volatileFields: ["lastFetchedAt"]
@@ -38162,6 +38253,12 @@ Object.defineProperty(exports, "bareAddonId", {
38162
38253
  return bareAddonId;
38163
38254
  }
38164
38255
  });
38256
+ Object.defineProperty(exports, "batteryCapability", {
38257
+ enumerable: true,
38258
+ get: function() {
38259
+ return batteryCapability;
38260
+ }
38261
+ });
38165
38262
  Object.defineProperty(exports, "buildStreamParamsConfigSchema", {
38166
38263
  enumerable: true,
38167
38264
  get: function() {
@@ -38204,6 +38301,12 @@ Object.defineProperty(exports, "decodeVectorBase64", {
38204
38301
  return decodeVectorBase64;
38205
38302
  }
38206
38303
  });
38304
+ Object.defineProperty(exports, "deriveBatteryPresence", {
38305
+ enumerable: true,
38306
+ get: function() {
38307
+ return deriveBatteryPresence;
38308
+ }
38309
+ });
38207
38310
  Object.defineProperty(exports, "deviceManagerCapability", {
38208
38311
  enumerable: true,
38209
38312
  get: function() {
@@ -20825,6 +20825,25 @@ var BatteryStatusSchema = z.object({
20825
20825
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
20826
20826
  lastUpdated: z.number(),
20827
20827
  /**
20828
+ * Ms epoch of the last time the device PROVED it was reachable — a
20829
+ * completed firmware round-trip, an observed wake, or an inbound push
20830
+ * (firmware event, email). `0`/absent = never since this slice was born.
20831
+ *
20832
+ * This is the ONLY input that separates "asleep" from "gone", and it is
20833
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
20834
+ * for the radio, because a poll that confirms reachability is the same
20835
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
20836
+ * single derivation every consumer must use; no surface computes its own.
20837
+ *
20838
+ * It is deliberately NOT a clock in the
20839
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
20840
+ * observation itself, and it is the only thing a 30-hour silence is
20841
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
20842
+ * Reolink provider) so a value that means "recently" cannot cost a
20843
+ * SQLite commit per round-trip.
20844
+ */
20845
+ lastContactAt: z.number().optional(),
20846
+ /**
20828
20847
  * True when the source is a BINARY low-battery indicator (HA
20829
20848
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
20830
20849
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -27507,13 +27526,63 @@ var CamStreamDescriptorSchema = z.object({
27507
27526
  * set of stream descriptors it can offer for the device, synchronously, so the
27508
27527
  * broker can reconcile its registry against the authoritative provider state.
27509
27528
  */
27529
+ /**
27530
+ * The catalog as a DURABLE fact rather than a live answer.
27531
+ *
27532
+ * A battery camera's descriptors are profile-stable — they change when the
27533
+ * operator rewrites an encoder profile, not minute to minute — but building
27534
+ * them costs a Baichuan login, which on a sleeping Argus IS a wake. So the
27535
+ * provider is allowed to build them exactly once per profile and must serve
27536
+ * every later pull from a cache.
27537
+ *
27538
+ * Holding that cache only in RAM is what turned a restart into an outage. The
27539
+ * runner comes back with the camera asleep, `buildStreamCatalogUncached`
27540
+ * correctly refuses to wake it, the pull answers `[]`, the broker has no
27541
+ * cam-stream entry to build a broker from, and `webrtcSession.handleOffer`
27542
+ * fails with a flat "No broker for stream" — for as long as the camera sleeps,
27543
+ * which on a battery cam is most of the day. The camera was fine. The stream
27544
+ * was unreachable because the process had forgotten what the camera offers.
27545
+ *
27546
+ * Declaring it here puts it in `device-runtime-state`, the kernel's canonical
27547
+ * declared collection, with the same `restored` durability `battery` uses for
27548
+ * the same reason: the last known value is the only value there is while the
27549
+ * device is asleep. The broker's brokers are therefore always DEFINABLE — it
27550
+ * is the DIAL that wakes a camera, never the catalog (D173).
27551
+ */
27552
+ var StreamCatalogStateSchema = z.object({
27553
+ /** The descriptors as last built from a real camera response. Never a guess:
27554
+ * a failed or refused build writes NOTHING, so a restored catalog is always
27555
+ * one the camera itself once produced. */
27556
+ descriptors: z.array(CamStreamDescriptorSchema),
27557
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
27558
+ * path decide whether the camera's own awake window is worth spending on a
27559
+ * re-read. */
27560
+ lastFetchedAt: z.number()
27561
+ });
27510
27562
  var streamCatalogCapability = {
27511
27563
  name: "stream-catalog",
27512
27564
  scope: "device",
27513
27565
  deviceNative: true,
27514
27566
  mode: "singleton",
27515
27567
  deviceTypes: [DeviceType.Camera],
27516
- methods: { getCatalog: method(z.object({ deviceId: z.number().int().nonnegative() }), z.array(CamStreamDescriptorSchema).readonly()) }
27568
+ methods: { getCatalog: method(z.object({ deviceId: z.number().int().nonnegative() }), z.array(CamStreamDescriptorSchema).readonly()) },
27569
+ runtimeState: StreamCatalogStateSchema,
27570
+ /**
27571
+ * Runtime-state durability: **restored** — see the schema doc. A cold
27572
+ * catalog on a sleeping battery camera is not a slow first frame, it is a
27573
+ * camera that cannot be watched at all until it happens to wake.
27574
+ *
27575
+ * Churn is nil by construction: the slice is written only by a SUCCESSFUL
27576
+ * build, and a build only runs when there is no cached copy (or the copy is
27577
+ * a day old and the camera is awake anyway).
27578
+ *
27579
+ * See `RuntimeStateDurability`. Enforced by
27580
+ * `scripts/check-runtime-state-durability.ts`.
27581
+ */
27582
+ durability: "restored",
27583
+ /** Clock field: written, but excluded from the compare that decides whether
27584
+ * persisting is worth a SQLite commit — the descriptors are the value. */
27585
+ volatileStateFields: ["lastFetchedAt"]
27517
27586
  };
27518
27587
  /** One of the camera's stream profiles. */
27519
27588
  var StreamProfileSchema = z.enum([
@@ -29510,6 +29579,24 @@ var DeclaredDevices = class {
29510
29579
  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;
29511
29580
  new Set(Object.values(DeviceType));
29512
29581
  DeviceFeature.BatteryOperated;
29582
+ /**
29583
+ * THE derivation. One crop rectangle, one presence verdict — see D52 for why
29584
+ * this repo insists a derived value has exactly one implementation.
29585
+ *
29586
+ * `undefined` status ⇒ `sleeping`. A device with no slice has made no claim,
29587
+ * and the caller decides separately whether it is even battery-operated
29588
+ * (`DeviceFeature.BatteryOperated`); this function never answers that
29589
+ * question, only what a battery device is doing.
29590
+ */
29591
+ function deriveBatteryPresence(input) {
29592
+ const status = input.status;
29593
+ if (!status) return "sleeping";
29594
+ if (status.sleeping !== true) return "awake";
29595
+ const lastContactAt = status.lastContactAt;
29596
+ if (typeof lastContactAt !== "number" || lastContactAt <= 0) return "sleeping";
29597
+ const budget = input.unreachableAfterMs ?? 216e5;
29598
+ return input.nowMs - lastContactAt > budget ? "unreachable" : "sleeping";
29599
+ }
29513
29600
  /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
29514
29601
  * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
29515
29602
  function unwrap(schema) {
@@ -37123,7 +37210,7 @@ Object.freeze({
37123
37210
  });
37124
37211
  /**
37125
37212
  * Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
37126
- * `runtimeState`. 63 caps: 33 `restored`, 30 `session`.
37213
+ * `runtimeState`. 64 caps: 34 `restored`, 30 `session`.
37127
37214
  *
37128
37215
  * A cap absent from this map has no runtime-state slice at all. A cap PRESENT
37129
37216
  * with `durability: 'session'` has one and has decided not to keep it.
@@ -37329,6 +37416,10 @@ var RUNTIME_STATE_POLICY = {
37329
37416
  durability: "restored",
37330
37417
  volatileFields: ["lastChangedAt"]
37331
37418
  },
37419
+ "stream-catalog": {
37420
+ durability: "restored",
37421
+ volatileFields: ["lastFetchedAt"]
37422
+ },
37332
37423
  "stream-params": {
37333
37424
  durability: "restored",
37334
37425
  volatileFields: ["lastFetchedAt"]
@@ -37934,4 +38025,4 @@ function enumerateInferenceDevices(hw) {
37934
38025
  return out;
37935
38026
  }
37936
38027
  //#endregion
37937
- export { objectInputDeclaresAddonId as $, decodeVectorBase64 as A, createEvent as At, isArrayOutputSchema as B, resolveCapMount as Bt, backupCapability as C, DeviceType as Ct, coreBlockIdFromAddonId as D, asJsonObject as Dt, coreBlockAddonId as E, WELL_KNOWN_TAB_MAP as Et, enumerateInferenceDevices as F, isDeviceConfigCap as Ft, kebabToCamel as G, isObjectInput as H, sleep as Ht, enumerateItemArrayFields as I, nodePin as It, logDestinationCapability as J, lifecycleJobSchema as K, enumerateSchemaFields as L, parseJsonObject as Lt, deviceStateCapability as M, emitReadiness as Mt, deviceStatusCapability as N, expandCapMethods as Nt, coreBlocksCapability as O, asNumber as Ot, doorbellCapability as P, hydrateSchema as Pt, normalizeUnit as Q, extractNestedAddonId as R, parseJsonUnknown as Rt, authProviderCapability as S, DeviceRole as St, buildStreamParamsConfigSchema as T, ReadinessTimeoutError as Tt, isSameAddonId as U, EventCategory as Ut, isCollectionArrayMethod as V, scopeKey as Vt, isVoidInput as W, looseSchema as X, logLevelAtMost as Y, metricsProviderCapability as Z, UserRecordSchema as _, BaseAddon as _t, CAP_NAMES_WITH_STATUS as a, scopesAllowDeviceCap as at, addonWidgetsCapability as b, DEVICE_STATUS_METHOD as bt, CoreBlockSchema as c, snapshotCapability as ct, METHOD_ACCESS_MAP as d, storageProviderCapability as dt, parseStreamParamsFormPatch as et, RUNTIME_DEFAULTS as f, streamQualityLabel as ft, StorageMigrationJobSchema as g, errMsg as gt, StorageLocationTypeSchema as h, vectorStoreCapability as ht, BatteryStatusSchema as i, scopesAllowAddon as it, deviceManagerCapability as j, emitDownForOwnedCaps as jt, dataStoreProviderCapability as k, asString as kt, DeclaredDevices as l, storageCapability as lt, ScopedTokenSchema as m, vectorDimFromBase64 as mt, AlertSchema as n, procedureAuthKey as nt, CORE_BLOCKS_ADDON_ID as o, scoreRuntimes as ot, STREAM_PROFILE_META as p, userManagementCapability as pt, localNetworkCapability as q, ApiKeyRecordSchema as r, runtimeStatePolicyFor as rt, CORE_BLOCK_ADDON_PREFIX as s, settingsStoreCapability as st, ALL_CAPABILITY_DEFINITIONS as t, platformProbeCapability as tt, DeviceStatusSchema as u, storageMigrationCapability as ut, addonPagesCapability as v, DATAPLANE_SECRET_HEADER as vt, bareAddonId as w, ReadinessRegistry as wt, alertsCapability as x, DeviceFeature as xt, addonSettingsCapability as y, DEVICE_SETTINGS_CONTRIBUTION_METHODS as yt, filesystemBrowseCapability as z, readinessKey as zt };
38028
+ export { metricsProviderCapability as $, dataStoreProviderCapability as A, asNumber as At, extractNestedAddonId as B, parseJsonUnknown as Bt, backupCapability as C, DeviceFeature as Ct, coreBlockAddonId as D, ReadinessTimeoutError as Dt, buildStreamParamsConfigSchema as E, ReadinessRegistry as Et, deviceStatusCapability as F, expandCapMethods as Ft, isSameAddonId as G, EventCategory as Gt, isArrayOutputSchema as H, resolveCapMount as Ht, doorbellCapability as I, hydrateSchema as It, lifecycleJobSchema as J, isVoidInput as K, enumerateInferenceDevices as L, isDeviceConfigCap as Lt, deriveBatteryPresence as M, createEvent as Mt, deviceManagerCapability as N, emitDownForOwnedCaps as Nt, coreBlockIdFromAddonId as O, WELL_KNOWN_TAB_MAP as Ot, deviceStateCapability as P, emitReadiness as Pt, looseSchema as Q, enumerateItemArrayFields as R, nodePin as Rt, authProviderCapability as S, DEVICE_STATUS_METHOD as St, batteryCapability as T, DeviceType as Tt, isCollectionArrayMethod as U, scopeKey as Ut, filesystemBrowseCapability as V, readinessKey as Vt, isObjectInput as W, sleep as Wt, logDestinationCapability as X, localNetworkCapability as Y, logLevelAtMost as Z, UserRecordSchema as _, vectorStoreCapability as _t, CAP_NAMES_WITH_STATUS as a, runtimeStatePolicyFor as at, addonWidgetsCapability as b, DATAPLANE_SECRET_HEADER as bt, CoreBlockSchema as c, scoreRuntimes as ct, METHOD_ACCESS_MAP as d, storageCapability as dt, normalizeUnit as et, RUNTIME_DEFAULTS as f, storageMigrationCapability as ft, StorageMigrationJobSchema as g, vectorDimFromBase64 as gt, StorageLocationTypeSchema as h, userManagementCapability as ht, BatteryStatusSchema as i, procedureAuthKey as it, decodeVectorBase64 as j, asString as jt, coreBlocksCapability as k, asJsonObject as kt, DeclaredDevices as l, settingsStoreCapability as lt, ScopedTokenSchema as m, streamQualityLabel as mt, AlertSchema as n, parseStreamParamsFormPatch as nt, CORE_BLOCKS_ADDON_ID as o, scopesAllowAddon as ot, STREAM_PROFILE_META as p, storageProviderCapability as pt, kebabToCamel as q, ApiKeyRecordSchema as r, platformProbeCapability as rt, CORE_BLOCK_ADDON_PREFIX as s, scopesAllowDeviceCap as st, ALL_CAPABILITY_DEFINITIONS as t, objectInputDeclaresAddonId as tt, DeviceStatusSchema as u, snapshotCapability as ut, addonPagesCapability as v, errMsg as vt, bareAddonId as w, DeviceRole as wt, alertsCapability as x, DEVICE_SETTINGS_CONTRIBUTION_METHODS as xt, addonSettingsCapability as y, BaseAddon as yt, enumerateSchemaFields as z, parseJsonObject as zt };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-BH2tZ9ZN.js");
3
+ const require_dist = require("./dist-BspfS8Q7.js");
4
4
  const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
5
  require("./builtins/alerts/index.js");
6
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { $ as objectInputDeclaresAddonId, At as createEvent, B as isArrayOutputSchema, Bt as resolveCapMount, Dt as asJsonObject$1, G as kebabToCamel, H as isObjectInput, K as lifecycleJobSchema, Lt as parseJsonObject, Nt as expandCapMethods, Ot as asNumber, R as extractNestedAddonId, Rt as parseJsonUnknown$1, Tt as ReadinessTimeoutError, Ut as EventCategory$1, V as isCollectionArrayMethod, Vt as scopeKey, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, at as scopesAllowDeviceCap, bt as DEVICE_STATUS_METHOD, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, gt as errMsg$1, it as scopesAllowAddon, jt as emitDownForOwnedCaps, kt as asString$1, nt as procedureAuthKey, t as ALL_CAPABILITY_DEFINITIONS, vt as DATAPLANE_SECRET_HEADER$1, wt as ReadinessRegistry, y as addonSettingsCapability, yt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, zt as readinessKey } from "./dist-DXDvrBi0.mjs";
2
+ import { At as asNumber, B as extractNestedAddonId, Bt as parseJsonUnknown$1, Dt as ReadinessTimeoutError, Et as ReadinessRegistry, Ft as expandCapMethods, Gt as EventCategory$1, H as isArrayOutputSchema, Ht as resolveCapMount, J as lifecycleJobSchema, K as isVoidInput, Mt as createEvent, Nt as emitDownForOwnedCaps, Q as looseSchema, St as DEVICE_STATUS_METHOD, U as isCollectionArrayMethod, Ut as scopeKey, Vt as readinessKey, W as isObjectInput, Z as logLevelAtMost, bt as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, it as procedureAuthKey, jt as asString$1, kt as asJsonObject$1, ot as scopesAllowAddon, q as kebabToCamel, st as scopesAllowDeviceCap, t as ALL_CAPABILITY_DEFINITIONS, tt as objectInputDeclaresAddonId, vt as errMsg$1, xt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, y as addonSettingsCapability, zt as parseJsonObject } from "./dist-COd223t-.mjs";
3
3
  import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
4
4
  import "./builtins/alerts/index.mjs";
5
5
  import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.92",
3
+ "version": "1.2.94",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",