@camstack/addon-pipeline-orchestrator 1.2.324 → 1.2.325

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.
package/dist/index.mjs CHANGED
@@ -5390,7 +5390,7 @@ var ZodIssueCode = {
5390
5390
  var ZodFirstPartyTypeKind;
5391
5391
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5392
5392
  //#endregion
5393
- //#region ../types/dist/sleep-DAX0-2v9.mjs
5393
+ //#region ../types/dist/sleep-DRqxtc0C.mjs
5394
5394
  /**
5395
5395
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5396
5396
  * window to float samples (D455).
@@ -8730,6 +8730,10 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
8730
8730
  kind: "mutation",
8731
8731
  auth: "admin"
8732
8732
  }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
8733
+ object({
8734
+ code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
8735
+ params: record(string(), union([string(), number()])).optional()
8736
+ });
8733
8737
  /**
8734
8738
  * Promise-based timer helpers — used everywhere the codebase needs to
8735
8739
  * wait, back off, or schedule a retry. Before these helpers landed, each
@@ -8755,7 +8759,7 @@ function sleep(ms) {
8755
8759
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
8756
8760
  }
8757
8761
  //#endregion
8758
- //#region ../types/dist/err-msg-DX6i_MY4.mjs
8762
+ //#region ../types/dist/err-msg-DLerKiVO.mjs
8759
8763
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8760
8764
  DeviceType["Camera"] = "camera";
8761
8765
  DeviceType["Hub"] = "hub";
@@ -9005,6 +9009,22 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
9005
9009
  DeviceRole["Nightvision"] = "nightvision";
9006
9010
  DeviceRole["PrivacyMask"] = "privacy-mask";
9007
9011
  DeviceRole["Doorbell"] = "doorbell";
9012
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
9013
+ DeviceRole["CoverBlind"] = "cover-blind";
9014
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
9015
+ DeviceRole["CoverShutter"] = "cover-shutter";
9016
+ /** Exterior awning. HA `device_class: awning`. */
9017
+ DeviceRole["CoverAwning"] = "cover-awning";
9018
+ /** Curtain / drape. HA `device_class: curtain`. */
9019
+ DeviceRole["CoverCurtain"] = "cover-curtain";
9020
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
9021
+ DeviceRole["CoverGarage"] = "cover-garage";
9022
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
9023
+ DeviceRole["CoverGate"] = "cover-gate";
9024
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
9025
+ DeviceRole["CoverDoor"] = "cover-door";
9026
+ /** Window. HA `device_class: window`. */
9027
+ DeviceRole["CoverWindow"] = "cover-window";
9008
9028
  /** Virtual HA toggle (input_boolean.*) — distinguishable from a
9009
9029
  * real Switch device for UI rendering / export adapters. */
9010
9030
  DeviceRole["BinaryHelper"] = "binary-helper";
@@ -9085,7 +9105,7 @@ function errMsg$2(err) {
9085
9105
  return String(err);
9086
9106
  }
9087
9107
  //#endregion
9088
- //#region ../types/dist/composition-C-WpVYTX.mjs
9108
+ //#region ../types/dist/composition-EQ9Ok0Ee.mjs
9089
9109
  /**
9090
9110
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9091
9111
  *
@@ -13223,7 +13243,8 @@ var ConvertResultSchema = object({
13223
13243
  })).readonly()
13224
13244
  });
13225
13245
  _enum(["new-session", "step-up"]);
13226
- _enum(["en", "it"]);
13246
+ /** The languages the hub's app landing pages are written in (D858). */
13247
+ var AppPageLangSchema = _enum(["en", "it"]);
13227
13248
  /** How a native app starts one login method (D858), as `auth.listLoginMethods` reports it. */
13228
13249
  var AppLoginPathSchema = discriminatedUnion("kind", [object({ kind: literal("native") }), object({
13229
13250
  kind: literal("browser"),
@@ -13720,7 +13741,9 @@ var ConfigTabDeclarationSchema = object({
13720
13741
  var ConfigSectionWithValuesSchema = object({
13721
13742
  id: string(),
13722
13743
  title: string(),
13744
+ titleKey: string().optional(),
13723
13745
  description: string().optional(),
13746
+ descriptionKey: string().optional(),
13724
13747
  style: _enum(["card", "accordion"]).optional(),
13725
13748
  defaultCollapsed: boolean().optional(),
13726
13749
  columns: union([
@@ -17003,6 +17026,9 @@ method(object({
17003
17026
  }), method(object({ refs: RefListSchema }), KeychainResolveResultSchema, { auth: "admin" }), method(object({ refs: RefListSchema }), object({ deleted: number().int() }), {
17004
17027
  kind: "mutation",
17005
17028
  auth: "admin"
17029
+ }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
17030
+ kind: "mutation",
17031
+ auth: "admin"
17006
17032
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
17007
17033
  /**
17008
17034
  * device-history — the ONE mechanism for numeric history (D850).
@@ -38406,6 +38432,38 @@ var DeviceRegisterPayloadSchema = object({
38406
38432
  * omits it and the keyword backstop applies. */
38407
38433
  secretConfigPaths: array(string()).optional()
38408
38434
  });
38435
+ /** One deleted device, kept by device-manager so an addon that missed the
38436
+ * deletion can still learn of it (D865). Written by the delete path ONLY. */
38437
+ var DeviceTombstoneSchema = object({
38438
+ deviceId: number().int().positive(),
38439
+ /** Epoch ms the device was deleted. */
38440
+ deletedAt: number(),
38441
+ /** The addon that owned the device when it was deleted. */
38442
+ providerAddonId: string()
38443
+ });
38444
+ /** Data held for one device that device-manager no longer knows. */
38445
+ var OrphanDeviceDataSchema = object({
38446
+ deviceId: number().int().positive(),
38447
+ /** Rows/files per store (`<addonId>:<store>`). */
38448
+ stores: record(string(), number().int().nonnegative())
38449
+ });
38450
+ var CleanupDeletedDevicesResultSchema = object({
38451
+ /**
38452
+ * `skipped` — the absence check could not be trusted (a device-list read
38453
+ * failed or the two reads disagreed); nothing was listed or deleted.
38454
+ * `dry-run` — `devices` is what a confirmed run would delete.
38455
+ * `cleaned` — `devices` is what was deleted.
38456
+ */
38457
+ status: _enum([
38458
+ "skipped",
38459
+ "dry-run",
38460
+ "cleaned"
38461
+ ]),
38462
+ reason: string().nullable(),
38463
+ devices: array(OrphanDeviceDataSchema),
38464
+ /** Implementers that could not be asked (their stores are not in `devices`). */
38465
+ unreachable: array(string())
38466
+ });
38409
38467
  var DeviceRemovePayloadSchema = object({ deviceId: number() });
38410
38468
  var DevicePersistConfigPayloadSchema = object({
38411
38469
  deviceId: number(),
@@ -38644,6 +38702,9 @@ method(object({
38644
38702
  }), method(object({ deviceId: number() }), object({ success: literal(true) }), {
38645
38703
  kind: "mutation",
38646
38704
  auth: "admin"
38705
+ }), method(object({ sinceMs: number() }), array(DeviceTombstoneSchema).readonly(), { auth: "admin" }), method(object({ confirm: boolean() }), CleanupDeletedDevicesResultSchema, {
38706
+ kind: "mutation",
38707
+ auth: "admin"
38647
38708
  }), method(object({ integrationId: string() }), object({ removed: number() }), {
38648
38709
  kind: "mutation",
38649
38710
  auth: "admin"
@@ -39106,6 +39167,72 @@ method(object({
39106
39167
  auth: "admin"
39107
39168
  });
39108
39169
  /**
39170
+ * `device-removal-hook` — the reliable way an addon learns a device was DELETED,
39171
+ * so it can delete what it keeps for that device (D865).
39172
+ *
39173
+ * ## Why an event was not enough
39174
+ *
39175
+ * `DeviceUnregistered` is telemetry and may be dropped (D8, D11). An addon that
39176
+ * hung its per-device clean-up on it left durable garbage whenever the event was
39177
+ * lost, the addon was down, or it was restarting at that moment — footage,
39178
+ * analytics rows and settings of device 4483 outlived the device. Loss of this
39179
+ * signal corrupts durable state, so it travels by RPC with a recorded fact
39180
+ * behind it (D8: "if loss would corrupt durable state, use RPC plus a reconcile").
39181
+ *
39182
+ * ## The three pieces
39183
+ *
39184
+ * 1. device-manager records a TOMBSTONE per deleted device
39185
+ * (`deviceManager.listDeletedSince`). The tombstone is written by the delete
39186
+ * path only — a device that is unregistered, offline or whose provider is
39187
+ * restarting is NOT deleted and gets none.
39188
+ * 2. After a delete, device-manager calls `onDeviceRemoved` on EVERY provider of
39189
+ * this collection, one by one, with bounded retries, logged.
39190
+ * 3. An implementer that was down asks `deviceManager.listDeletedSince` at start
39191
+ * (and periodically) and runs its own `onDeviceRemoved` for each tombstone it
39192
+ * may have missed. Reconcile is driven by tombstones, never by "the device is
39193
+ * not in the current list": a failed or partial list read must not destroy data.
39194
+ *
39195
+ * ## Contract for an implementer
39196
+ *
39197
+ * - **Idempotent.** It is called again by a retry, by a reconcile and by the
39198
+ * admin cleanup of data that predates tombstones. A device with nothing left
39199
+ * answers zero counts.
39200
+ * - **One key per store** in `counts`, deleted rows/files per store, so the log
39201
+ * line and the admin dry run can say where the data was.
39202
+ * - **Training datasets are NOT device data** and are never deleted by a hook.
39203
+ * - **Per-device SETTINGS are not hooked.** Configuration lives in the standard
39204
+ * per-device store (`ctx.settings.*DeviceStore`), which device-manager clears
39205
+ * for every addon centrally at delete time. This hook is for device DATA
39206
+ * that is not settings: footage, event media, analytics rows, history.
39207
+ * - A provider that cannot tell is allowed to throw; the caller retries it.
39208
+ *
39209
+ * `listDeviceData` is the read side of the admin "Clean data of deleted devices"
39210
+ * action: which device ids this addon holds data for, and how much per store. It
39211
+ * never deletes. device-manager decides which of those ids are absent from the
39212
+ * fleet, from TWO consistent reads of the device list (D49).
39213
+ */
39214
+ /** What a hook is told about the deletion. */
39215
+ var DeviceRemovedInputSchema = object({
39216
+ deviceId: number().int().positive(),
39217
+ /** Epoch ms the device was deleted (the tombstone's `deletedAt`). */
39218
+ deletedAt: number(),
39219
+ /** The addon that owned the device, as device-manager last knew it. */
39220
+ providerAddonId: string()
39221
+ });
39222
+ /** Rows/files/refs deleted, per store this implementer owns. */
39223
+ var DeviceRemovedResultSchema = object({ counts: record(string(), number().int().nonnegative()) });
39224
+ /** One (store, device) pair an implementer holds data for. */
39225
+ var DeviceDataEntrySchema = object({
39226
+ store: string(),
39227
+ deviceId: number().int().positive(),
39228
+ /** Rows (or files) held for the device in the store. */
39229
+ count: number().int().nonnegative()
39230
+ });
39231
+ method(DeviceRemovedInputSchema, DeviceRemovedResultSchema, {
39232
+ kind: "mutation",
39233
+ auth: "admin"
39234
+ }), method(_void(), array(DeviceDataEntrySchema).readonly(), { auth: "admin" });
39235
+ /**
39109
39236
  * `failure-contribution` — the capability an addon reports its OWN losses
39110
39237
  * through, per camera, with the denominator attached. It stores nothing.
39111
39238
  *
@@ -64674,6 +64801,12 @@ Object.freeze({
64674
64801
  addonId: null,
64675
64802
  access: "create"
64676
64803
  },
64804
+ "deviceManager.cleanupDeletedDevicesData": {
64805
+ capName: "device-manager",
64806
+ capScope: "system",
64807
+ addonId: null,
64808
+ access: "create"
64809
+ },
64677
64810
  "deviceManager.createDevice": {
64678
64811
  capName: "device-manager",
64679
64812
  capScope: "system",
@@ -64854,6 +64987,12 @@ Object.freeze({
64854
64987
  addonId: null,
64855
64988
  access: "view"
64856
64989
  },
64990
+ "deviceManager.listDeletedSince": {
64991
+ capName: "device-manager",
64992
+ capScope: "system",
64993
+ addonId: null,
64994
+ access: "view"
64995
+ },
64857
64996
  "deviceManager.listDeviceIdsByCaps": {
64858
64997
  capName: "device-manager",
64859
64998
  capScope: "system",
@@ -65262,6 +65401,18 @@ Object.freeze({
65262
65401
  addonId: null,
65263
65402
  access: "create"
65264
65403
  },
65404
+ "deviceRemovalHook.listDeviceData": {
65405
+ capName: "device-removal-hook",
65406
+ capScope: "system",
65407
+ addonId: null,
65408
+ access: "view"
65409
+ },
65410
+ "deviceRemovalHook.onDeviceRemoved": {
65411
+ capName: "device-removal-hook",
65412
+ capScope: "system",
65413
+ addonId: null,
65414
+ access: "create"
65415
+ },
65265
65416
  "deviceState.claimFields": {
65266
65417
  capName: "device-state",
65267
65418
  capScope: "system",
@@ -65694,6 +65845,12 @@ Object.freeze({
65694
65845
  addonId: null,
65695
65846
  access: "delete"
65696
65847
  },
65848
+ "keychain.deleteDeviceScope": {
65849
+ capName: "keychain",
65850
+ capScope: "system",
65851
+ addonId: null,
65852
+ access: "delete"
65853
+ },
65697
65854
  "keychain.getState": {
65698
65855
  capName: "keychain",
65699
65856
  capScope: "system",
@@ -71734,6 +71891,11 @@ Object.freeze({
71734
71891
  form: "single",
71735
71892
  optional: false
71736
71893
  }],
71894
+ "deviceRemovalHook.onDeviceRemoved": [{
71895
+ name: "deviceId",
71896
+ form: "single",
71897
+ optional: false
71898
+ }],
71737
71899
  "deviceState.claimFields": [{
71738
71900
  name: "deviceId",
71739
71901
  form: "single",
@@ -71923,6 +72085,11 @@ Object.freeze({
71923
72085
  form: "single",
71924
72086
  optional: false
71925
72087
  }],
72088
+ "keychain.deleteDeviceScope": [{
72089
+ name: "deviceId",
72090
+ form: "single",
72091
+ optional: false
72092
+ }],
71926
72093
  "lawnMowerControl.dock": [{
71927
72094
  name: "deviceId",
71928
72095
  form: "single",
@@ -74271,6 +74438,8 @@ new Set([
74271
74438
  "audio-tools",
74272
74439
  "audio-silence"
74273
74440
  ]);
74441
+ AppPageLangSchema.options;
74442
+ record(string().min(1), string());
74274
74443
  /**
74275
74444
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
74276
74445
  *
@@ -75972,6 +76141,25 @@ var DETAIL_CROP_PADDING_FIELD = {
75972
76141
  default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
75973
76142
  };
75974
76143
  /**
76144
+ * The cadence of the automatic whole-archive face rescore (D866).
76145
+ *
76146
+ * One cluster-wide knob in `pipeline-orchestrator`'s GLOBAL settings, read by
76147
+ * `pipeline-analytics` over the hub-routed `addon-settings` cap on every tick.
76148
+ * It lives here for the same reason the detail-crop convention does: the
76149
+ * writer (orchestrator) and the reader (analytics) are different addons, they
76150
+ * never import each other, and a key copied by hand is how a setting silently
76151
+ * stops arriving.
76152
+ */
76153
+ var FACE_RESCORE_SECTION_ID = "face-rescore";
76154
+ var FACE_RESCORE_INTERVAL_KEY = "faceRescoreIntervalMinutes";
76155
+ /** Slider bounds. `0` is "disabled"; anything in 1..9 is raised to `min`. */
76156
+ var FACE_RESCORE_INTERVAL_FIELD = {
76157
+ min: 0,
76158
+ max: 1440,
76159
+ step: 5,
76160
+ default: 60
76161
+ };
76162
+ /**
75975
76163
  * THE native-frame **lease** knobs — hold depth, RAM budget, demand window and
75976
76164
  * subject-tile budget for the decode worker's native-resolution retention.
75977
76165
  *
@@ -83735,67 +83923,20 @@ function buildRunnerConfig(base, overrides) {
83735
83923
  preRollInferenceFrames: config.preRollInferenceFrames
83736
83924
  };
83737
83925
  }
83738
- var DEVICE_FEATURES_SPEC = {
83739
- collection: "pipeline-orchestrator:device-features",
83740
- columns: [
83741
- {
83742
- name: "deviceId",
83743
- type: "TEXT",
83744
- primaryKey: true,
83745
- notNull: true
83746
- },
83747
- (
83748
- /** The feature name list, verbatim. */
83749
- {
83750
- name: "features",
83751
- type: "JSON",
83752
- notNull: true
83753
- }),
83754
- {
83755
- name: "updatedAt",
83756
- type: "INTEGER",
83757
- notNull: true
83758
- }
83759
- ],
83760
- writeMode: "write-behind",
83761
- keyOf: (row) => String(row.deviceId),
83762
- toValue: (row) => ({
83763
- features: [...row.features],
83764
- updatedAt: row.updatedAt
83765
- }),
83766
- fromRecord: (key, data) => {
83767
- const deviceId = Number(key);
83768
- const raw = data["features"];
83769
- if (!Number.isFinite(deviceId) || !Array.isArray(raw)) return null;
83770
- const features = raw.filter((f) => typeof f === "string");
83771
- if (features.length === 0) return null;
83772
- const updatedAt = Number(data["updatedAt"]);
83773
- return {
83774
- deviceId,
83775
- features,
83776
- updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0,
83777
- restored: true
83778
- };
83779
- },
83780
- deviceIdOf: (row) => row.deviceId
83781
- };
83926
+ //#endregion
83927
+ //#region src/device-features-mirror.ts
83928
+ /** The per-device settings key a device's mirrored features live under. */
83929
+ var DEVICE_FEATURES_KEY = "deviceFeatures";
83782
83930
  var DeviceFeaturesMirror = class {
83783
83931
  /** Process-local fallback, used only when no store was supplied. */
83784
83932
  local = /* @__PURE__ */ new Map();
83785
- durable;
83933
+ settings;
83786
83934
  logger;
83787
83935
  now;
83788
83936
  constructor(deps) {
83789
83937
  this.logger = deps.logger;
83790
83938
  this.now = deps.now ?? (() => Date.now());
83791
- this.durable = deps.store === void 0 ? null : new DurableLedger({
83792
- spec: DEVICE_FEATURES_SPEC,
83793
- store: deps.store,
83794
- logger: deps.logger
83795
- });
83796
- }
83797
- static declare(store) {
83798
- return DurableLedger.declare(store, DEVICE_FEATURES_SPEC);
83939
+ this.settings = deps.settings ?? null;
83799
83940
  }
83800
83941
  /**
83801
83942
  * Seed the mirror from the last session. Call once at boot, after `declare`
@@ -83803,13 +83944,29 @@ var DeviceFeaturesMirror = class {
83803
83944
  * already there when the first read fails.
83804
83945
  */
83805
83946
  async hydrate() {
83806
- if (this.durable === null) return 0;
83807
- const rows = await this.durable.load();
83808
- this.logger.info("device-features mirror restored", { meta: { devices: rows.length } });
83809
- return rows.length;
83947
+ const settings = this.settings;
83948
+ if (settings === null) return 0;
83949
+ const list = settings.listDeviceStoreIds;
83950
+ if (list !== void 0) {
83951
+ const ids = await list.call(settings);
83952
+ const batch = settings.readDeviceStoreBatch;
83953
+ const blobs = batch === void 0 ? new Map(await Promise.all(ids.map(async (id) => [id, await settings.readDeviceStore(id)]))) : await batch.call(settings, ids);
83954
+ for (const [deviceId, blob] of blobs) {
83955
+ const row = rowOf(deviceId, blob[DEVICE_FEATURES_KEY]);
83956
+ if (row !== null) this.local.set(deviceId, row);
83957
+ }
83958
+ }
83959
+ this.logger.info("device-features mirror restored", { meta: { devices: this.local.size } });
83960
+ return this.local.size;
83810
83961
  }
83811
83962
  held(deviceId) {
83812
- return this.durable === null ? this.local.get(deviceId) : this.durable.get(String(deviceId));
83963
+ return this.local.get(deviceId);
83964
+ }
83965
+ async persist(deviceId, features, updatedAt) {
83966
+ await this.settings?.writeDeviceStore(deviceId, { [DEVICE_FEATURES_KEY]: {
83967
+ features: [...features],
83968
+ updatedAt
83969
+ } });
83813
83970
  }
83814
83971
  remember(deviceId, features) {
83815
83972
  const row = {
@@ -83818,20 +83975,19 @@ var DeviceFeaturesMirror = class {
83818
83975
  updatedAt: this.now(),
83819
83976
  restored: false
83820
83977
  };
83821
- if (this.durable === null) {
83822
- this.local.set(deviceId, row);
83823
- return;
83824
- }
83825
- const held = this.durable.get(String(deviceId));
83978
+ const held = this.local.get(deviceId);
83979
+ this.local.set(deviceId, row);
83826
83980
  if (held !== void 0 && !held.restored && sameFeatures(held.features, features)) return;
83827
- this.durable.put(row);
83981
+ this.persist(deviceId, features, row.updatedAt).catch((err) => {
83982
+ this.logger.warn("device-features mirror not persisted", {
83983
+ tags: { deviceId },
83984
+ meta: { error: err instanceof Error ? err.message : String(err) }
83985
+ });
83986
+ });
83828
83987
  }
83829
83988
  drop(deviceId) {
83830
- if (this.durable === null) {
83831
- this.local.delete(deviceId);
83832
- return;
83833
- }
83834
- this.durable.forget(String(deviceId));
83989
+ this.local.delete(deviceId);
83990
+ this.settings?.writeDeviceStore(deviceId, { [DEVICE_FEATURES_KEY]: void 0 });
83835
83991
  }
83836
83992
  /**
83837
83993
  * Resolve a device's features, preferring a fresh read but never letting a
@@ -83880,7 +84036,7 @@ var DeviceFeaturesMirror = class {
83880
84036
  this.drop(deviceId);
83881
84037
  return [];
83882
84038
  }
83883
- /** Drop a device's mirror — call when the device is removed. */
84039
+ /** Drop a device's mirror. (A removed device's settings are cleared centrally, D865.) */
83884
84040
  forget(deviceId) {
83885
84041
  this.drop(deviceId);
83886
84042
  }
@@ -83891,6 +84047,20 @@ function sameFeatures(a, b) {
83891
84047
  const held = new Set(a);
83892
84048
  return b.every((f) => held.has(f));
83893
84049
  }
84050
+ function rowOf(deviceId, raw) {
84051
+ if (typeof raw !== "object" || raw === null) return null;
84052
+ const features = Reflect.get(raw, "features");
84053
+ if (!Array.isArray(features)) return null;
84054
+ const kept = features.filter((f) => typeof f === "string");
84055
+ if (kept.length === 0) return null;
84056
+ const updatedAt = Number(Reflect.get(raw, "updatedAt"));
84057
+ return {
84058
+ deviceId,
84059
+ features: kept,
84060
+ updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0,
84061
+ restored: true
84062
+ };
84063
+ }
83894
84064
  //#endregion
83895
84065
  //#region src/onboard-forwarding-reconcile.ts
83896
84066
  /** The `detectionSources` member that means "this camera's own firmware". */
@@ -84049,9 +84219,10 @@ var DetectionWiringController = class {
84049
84219
  featuresMirror;
84050
84220
  constructor(deps) {
84051
84221
  this.deps = deps;
84222
+ const settings = deps.persistFeatures === true ? deps.ctx().settings : void 0;
84052
84223
  this.featuresMirror = new DeviceFeaturesMirror({
84053
84224
  logger: deps.logger,
84054
- ...deps.featuresStore !== void 0 ? { store: deps.featuresStore } : {}
84225
+ ...settings !== void 0 ? { settings } : {}
84055
84226
  });
84056
84227
  }
84057
84228
  /**
@@ -84060,8 +84231,7 @@ var DetectionWiringController = class {
84060
84231
  * that would have needed it.
84061
84232
  */
84062
84233
  async hydrateFeaturesMirror() {
84063
- if (this.deps.featuresStore === void 0) return;
84064
- await DeviceFeaturesMirror.declare(this.deps.featuresStore);
84234
+ if (this.deps.persistFeatures !== true) return;
84065
84235
  await this.featuresMirror.hydrate();
84066
84236
  }
84067
84237
  /** `activeDetections.get(deviceId)`. */
@@ -94359,7 +94529,7 @@ async function buildOrchestratorControllers(deps) {
94359
94529
  reconcile.scheduleReconcile();
94360
94530
  const detectionWiring = new DetectionWiringController({
94361
94531
  ctx: () => deps.ctx(),
94362
- featuresStore: deps.ctx().api.settingsStore,
94532
+ persistFeatures: true,
94363
94533
  ledger,
94364
94534
  placement,
94365
94535
  audio,
@@ -94579,6 +94749,24 @@ function buildGlobalSettingsSections(options) {
94579
94749
  default: DEFAULT_DETAIL_CROP_CONVENTION.square
94580
94750
  }]
94581
94751
  },
94752
+ {
94753
+ id: FACE_RESCORE_SECTION_ID,
94754
+ title: "Face suggestions",
94755
+ tab: "pipeline",
94756
+ description: "How often the people suggestions on unassigned faces are refreshed against the current gallery. The job only runs when the gallery or the set of unassigned faces changed since the last run; otherwise it does nothing.",
94757
+ fields: [{
94758
+ key: FACE_RESCORE_INTERVAL_KEY,
94759
+ type: "slider",
94760
+ label: "Suggestion refresh interval",
94761
+ description: "0 = off (use the Recompute suggestions button). Values from 1 to 9 run every 10 minutes. The first run happens one interval after the addon starts.",
94762
+ min: FACE_RESCORE_INTERVAL_FIELD.min,
94763
+ max: FACE_RESCORE_INTERVAL_FIELD.max,
94764
+ step: FACE_RESCORE_INTERVAL_FIELD.step,
94765
+ default: FACE_RESCORE_INTERVAL_FIELD.default,
94766
+ showValue: true,
94767
+ unit: "min"
94768
+ }]
94769
+ },
94582
94770
  {
94583
94771
  id: NATIVE_LEASE_SECTION_ID,
94584
94772
  title: "Native frame lease",
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C-txzh2m.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DUstnfGf.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.2.324",
3
+ "version": "1.2.325",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",