@camstack/addon-export-alexa 1.2.21 → 1.2.23

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.
@@ -1,5 +1,5 @@
1
1
  import * as crypto$1 from "node:crypto";
2
- //#region ../types/dist/event-category-Cv9dO26A.mjs
2
+ //#region ../types/dist/event-category-Bxo5yJjt.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -206,6 +206,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
206
206
  EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
207
207
  EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
208
208
  /**
209
+ * A node the orchestrator would otherwise place cameras on has NO usable
210
+ * inference device: the operator enabled one or more accelerators there and
211
+ * the live probe reports every one of them unavailable. Emitted once per
212
+ * TRANSITION into that state (never per dispatch), and the node is dropped
213
+ * from the placement candidate set for as long as it holds.
214
+ *
215
+ * This exists because the state was previously invisible: little-unraid
216
+ * absorbed 283k inference errors in a day while still being handed cameras,
217
+ * and nothing in the system said so.
218
+ *
219
+ * A node with no accelerators configured at all is NOT this — its devices
220
+ * are `disabled`, not `unavailable`, and the runner's default CPU pool
221
+ * serves it exactly as before.
222
+ */
223
+ EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
224
+ /**
225
+ * A camera has an OPEN detection session and has produced no detection at
226
+ * all for longer than the blind threshold — the camera is being decoded and
227
+ * inferred and is returning nothing. Emitted once per transition into blind,
228
+ * per camera.
229
+ *
230
+ * The failure it reports: a 1h43 detection blackout on the entrance camera
231
+ * that nobody noticed, because "a camera that detects nothing" and "a quiet
232
+ * camera" produce byte-identical silence.
233
+ */
234
+ EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
235
+ /**
209
236
  * Per-camera pipeline config was mutated by the orchestrator
210
237
  * (3-level settings change via `setAgentAddonDefaults` /
211
238
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -2997,6 +3024,9 @@ function handlePipeResult(left, next, ctx) {
2997
3024
  fallback: left.fallback
2998
3025
  }, ctx);
2999
3026
  }
3027
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3028
+ $ZodPipe.init(inst, def);
3029
+ });
3000
3030
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3001
3031
  $ZodType.init(inst, def);
3002
3032
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5182,6 +5212,10 @@ function pipe(in_, out) {
5182
5212
  out
5183
5213
  });
5184
5214
  }
5215
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5216
+ ZodPipe.init(inst, def);
5217
+ $ZodPreprocess.init(inst, def);
5218
+ });
5185
5219
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5186
5220
  $ZodReadonly.init(inst, def);
5187
5221
  ZodType.init(inst, def);
@@ -5240,6 +5274,13 @@ function _instanceof(cls, params = {}) {
5240
5274
  };
5241
5275
  return inst;
5242
5276
  }
5277
+ function preprocess(fn, schema) {
5278
+ return new ZodPreprocess({
5279
+ type: "pipe",
5280
+ in: transform(fn),
5281
+ out: schema
5282
+ });
5283
+ }
5243
5284
  //#endregion
5244
5285
  //#region ../../node_modules/zod/v4/classic/compat.js
5245
5286
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -12418,6 +12459,17 @@ var LlmImageSchema = object({
12418
12459
  bytes: _instanceof(Uint8Array),
12419
12460
  mimeType: string()
12420
12461
  });
12462
+ /**
12463
+ * Retry policy. `enabled: false` is NOT the same as `maxAttempts: 1` in intent —
12464
+ * the flag is what a consumer table flips, the count is what the operator tunes.
12465
+ * A retry doubles the wall time of a call, so the two gates that run inside a
12466
+ * notification's budget keep it off (see `CONSUMER_RETRY_POLICY` in addon-ai).
12467
+ */
12468
+ var LlmRetryPolicySchema = object({
12469
+ enabled: boolean().default(false),
12470
+ /** Total attempts INCLUDING the first. 1 = no retry. */
12471
+ maxAttempts: number().int().min(1).max(5).default(1)
12472
+ });
12421
12473
  var LlmGenerateBaseInputSchema = object({
12422
12474
  /** Collection routing (the notification-output posture). */
12423
12475
  addonId: string().optional(),
@@ -12432,7 +12484,28 @@ var LlmGenerateBaseInputSchema = object({
12432
12484
  jsonSchema: record(string(), unknown()).optional(),
12433
12485
  /** Per-call override of the profile default. */
12434
12486
  maxTokens: number().int().positive().optional(),
12435
- temperature: number().optional()
12487
+ temperature: number().optional(),
12488
+ /** Per-call override of the profile default (nucleus sampling). */
12489
+ topP: number().min(0).max(1).optional(),
12490
+ /** Per-call override of the profile default (top-k sampling). */
12491
+ topK: number().int().positive().optional(),
12492
+ /** Per-call override of `profile.timeoutMs` — the total generation bound. */
12493
+ timeoutMs: number().int().positive().optional(),
12494
+ /** Per-call override; beats both the consumer table and the profile. */
12495
+ retry: LlmRetryPolicySchema.optional(),
12496
+ /**
12497
+ * Caller-minted id that makes this generation CANCELLABLE.
12498
+ *
12499
+ * Without it a caller that stops waiting cannot stop the work: the gates race
12500
+ * the call against 8 s and free their own slot when the timer wins, while the
12501
+ * generation upstream keeps running to `profile.timeoutMs` — 60 s by default,
12502
+ * on a single-threaded local model. The per-camera bound then counts WAITS,
12503
+ * not generations, and the real load is unbounded.
12504
+ *
12505
+ * `AbortSignal` cannot cross a process boundary; an id can. Pass one here and
12506
+ * `llm.cancel({ requestId })` tears the socket down.
12507
+ */
12508
+ requestId: string().optional()
12436
12509
  });
12437
12510
  /**
12438
12511
  * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
@@ -12445,6 +12518,18 @@ var LlmGenerateBaseInputSchema = object({
12445
12518
  * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
12446
12519
  * watchdog — operator decision #3).
12447
12520
  */
12521
+ /**
12522
+ * A companion artifact that MUST land beside the main GGUF: the `mmproj`
12523
+ * projector of a vision model, or shards 2..N of a split GGUF. Carried on the
12524
+ * REF rather than looked up at install time, so what the operator approved in
12525
+ * the preview is exactly what the node downloads.
12526
+ */
12527
+ var ManagedModelExtraFileSchema = object({
12528
+ url: string(),
12529
+ filename: string(),
12530
+ sizeBytes: number(),
12531
+ sha256: string().optional()
12532
+ });
12448
12533
  var ManagedModelRefSchema = discriminatedUnion("kind", [
12449
12534
  object({
12450
12535
  kind: literal("catalog"),
@@ -12453,7 +12538,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
12453
12538
  object({
12454
12539
  kind: literal("url"),
12455
12540
  url: string(),
12456
- sha256: string().optional()
12541
+ sha256: string().optional(),
12542
+ /** Picker/status label; the file basename when absent. */
12543
+ label: string().optional(),
12544
+ sizeBytes: number().optional(),
12545
+ extraFiles: array(ManagedModelExtraFileSchema).optional()
12457
12546
  }),
12458
12547
  object({
12459
12548
  kind: literal("path"),
@@ -12471,13 +12560,82 @@ var ManagedRuntimeConfigSchema = object({
12471
12560
  gpuLayers: number().int().default(0),
12472
12561
  /** Default: cpus-2, clamped ≥1 (resolved node-side). */
12473
12562
  threads: number().int().optional(),
12474
- /** Concurrent slots. */
12563
+ /** Concurrent slots (`--parallel`). */
12475
12564
  parallel: number().int().default(1),
12565
+ /** Logical batch size (`-b`). Larger = faster prompt ingest, more RAM. */
12566
+ batchSize: number().int().positive().optional(),
12567
+ /** Physical batch / micro-batch (`-ub`). */
12568
+ ubatchSize: number().int().positive().optional(),
12569
+ /**
12570
+ * `--flash-attn`. Cuts KV-cache memory on the backends that implement it and
12571
+ * is a no-op elsewhere, so it is offered rather than assumed.
12572
+ */
12573
+ flashAttention: boolean().default(false),
12574
+ /**
12575
+ * `--mlock`. Pins the weights in RAM so the OS cannot page them out mid
12576
+ * inference. Costs the full model size in resident memory — which is exactly
12577
+ * what the RAM budget is counting.
12578
+ */
12579
+ mlock: boolean().default(false),
12580
+ /**
12581
+ * `--no-mmap`. Reads the whole GGUF up front instead of mapping it. Slower to
12582
+ * start, but avoids the page-fault stalls a network or spinning-disk model
12583
+ * store produces on every first token.
12584
+ */
12585
+ noMmap: boolean().default(false),
12586
+ /** `--cache-type-k` / `--cache-type-v` — quantising the KV cache is the
12587
+ * cheapest way to fit a longer context in the same RAM. */
12588
+ cacheTypeK: _enum([
12589
+ "f32",
12590
+ "f16",
12591
+ "q8_0",
12592
+ "q5_1",
12593
+ "q5_0",
12594
+ "q4_1",
12595
+ "q4_0"
12596
+ ]).optional(),
12597
+ cacheTypeV: _enum([
12598
+ "f32",
12599
+ "f16",
12600
+ "q8_0",
12601
+ "q5_1",
12602
+ "q5_0",
12603
+ "q4_1",
12604
+ "q4_0"
12605
+ ]).optional(),
12606
+ /**
12607
+ * Escape hatch for llama-server flags this schema does NOT model — `--jinja`
12608
+ * (which most vision chat templates need and some language-only models
12609
+ * dislike), `--cont-batching`, `--rope-scaling`, …
12610
+ *
12611
+ * It is NOT a second place to set the flags above. A token that collides
12612
+ * with a typed field is REJECTED at start, naming the field that owns it
12613
+ * (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
12614
+ * the "two switches that disagree" failure this repo has already shipped
12615
+ * twice (D62).
12616
+ */
12617
+ extraArgs: array(string()).default([]),
12476
12618
  /** Else lazy: first generate boots it. */
12477
12619
  autoStart: boolean().default(false),
12478
12620
  /** 0 = never; frees RAM after quiet periods. */
12479
12621
  idleStopMinutes: number().int().default(30)
12480
12622
  });
12623
+ /**
12624
+ * Where a multi-GB install currently is. A single 0..1 fraction cannot answer
12625
+ * "is it stuck?" for an install that is three files (shards + mmproj) followed
12626
+ * by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
12627
+ * node looked hung. Phase + file + bytes is the smallest shape that does.
12628
+ */
12629
+ var LlmDownloadProgressSchema = object({
12630
+ phase: _enum(["downloading", "verifying"]),
12631
+ /** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
12632
+ file: string(),
12633
+ fileIndex: number().int(),
12634
+ fileCount: number().int(),
12635
+ /** Across the WHOLE install, not the current file. */
12636
+ downloadedBytes: number(),
12637
+ totalBytes: number().optional()
12638
+ });
12481
12639
  var LlmRuntimeStatusSchema = object({
12482
12640
  /** Status is ALWAYS node-qualified. */
12483
12641
  nodeId: string(),
@@ -12494,6 +12652,8 @@ var LlmRuntimeStatusSchema = object({
12494
12652
  modelPath: string().optional(),
12495
12653
  modelId: string().optional(),
12496
12654
  downloadProgress: number().min(0).max(1).optional(),
12655
+ /** Detail behind `downloadProgress`; present for the same lifetime. */
12656
+ download: LlmDownloadProgressSchema.optional(),
12497
12657
  lastError: string().optional(),
12498
12658
  crashesInWindow: number(),
12499
12659
  /** Child RSS (sampled best-effort). */
@@ -12504,7 +12664,14 @@ var LlmNodeModelSchema = object({
12504
12664
  file: string(),
12505
12665
  sizeBytes: number(),
12506
12666
  catalogId: string().optional(),
12507
- installedAt: number().optional()
12667
+ installedAt: number().optional(),
12668
+ /**
12669
+ * Absolute path on the node. Present so a file that is on disk but matches
12670
+ * no catalog entry — a custom Hugging Face install, or a GGUF the operator
12671
+ * copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
12672
+ * it the picker could list such a file and do nothing with it.
12673
+ */
12674
+ path: string().optional()
12508
12675
  });
12509
12676
  var LlmRuntimeDiskUsageSchema = object({
12510
12677
  nodeId: string(),
@@ -12560,10 +12727,47 @@ var LlmProfileSchema = object({
12560
12727
  baseUrl: string().optional(),
12561
12728
  /** ConfigUISchema type:'password' — never round-trips (spec §5). */
12562
12729
  apiKey: string().optional(),
12730
+ /** Vision on/off. A vision call against a `false` profile is REFUSED, never
12731
+ * degraded to text — that shipped once and produced a confident answer to a
12732
+ * question about a picture nobody sent. */
12563
12733
  supportsVision: boolean(),
12564
12734
  temperature: number().min(0).max(2).optional(),
12735
+ /** Nucleus sampling. Every wire we speak has it. */
12736
+ topP: number().min(0).max(1).optional(),
12737
+ /** Top-k sampling. Carried only by the wires that have it — NEITHER OpenAI
12738
+ * wire does, and the client drops it there (measured: the request body gets
12739
+ * `top_p` and no `top_k`). The profile editor hides the field wherever it
12740
+ * would change nothing; `KINDS_WITH_TOP_K` is the single owner of that list. */
12741
+ topK: number().int().positive().optional(),
12565
12742
  maxTokens: number().int().positive().optional(),
12743
+ /** Prompt context window. Advisory for cloud kinds (they enforce their own);
12744
+ * for `managed-local` it is the llama.cpp `--ctx-size` the runtime starts
12745
+ * the model with, so it is the one field that changes a PROCESS. */
12746
+ contextLength: number().int().positive().optional(),
12747
+ /** Default system prompt. A caller's `system` REPLACES it (never appends —
12748
+ * two system prompts fighting is worse than either alone). */
12749
+ systemPrompt: string().optional(),
12750
+ /** Total generation bound — the only one a unary call has. */
12566
12751
  timeoutMs: number().int().positive().default(6e4),
12752
+ /** The TCP handshake only — "is the port even open". NOT the wait for
12753
+ * response headers: on the LM Studio / llama-server wire those are written
12754
+ * once the model has finished loading, so they belong to the bound below. */
12755
+ connectTimeoutMs: number().int().positive().default(1e4),
12756
+ /** Accepted, but no output yet — response headers included, because a cold
12757
+ * GPU load is exactly what happens before them. */
12758
+ firstTokenTimeoutMs: number().int().positive().default(12e4),
12759
+ /** Output started then stopped. */
12760
+ idleTimeoutMs: number().int().positive().default(6e4),
12761
+ /** Profile-level default. The per-consumer table and a per-call override
12762
+ * both beat it — see `resolveRetryPolicy`. */
12763
+ retry: LlmRetryPolicySchema.default({
12764
+ enabled: false,
12765
+ maxAttempts: 1
12766
+ }),
12767
+ /** Whether this profile may use tools. The tool-call plumbing rides the
12768
+ * library; the REGISTRY of callable tools is ours and is empty in v1, so a
12769
+ * `true` here buys the wiring, not behaviour, until tools are registered. */
12770
+ toolsEnabled: boolean().default(false),
12567
12771
  extraHeaders: record(string(), string()).optional(),
12568
12772
  /** kind === 'managed-local' only (spec §4). */
12569
12773
  runtime: ManagedRuntimeConfigSchema.optional()
@@ -12613,6 +12817,36 @@ var ManagedModelCatalogEntrySchema = object({
12613
12817
  /** Vision models: companion projector file. */
12614
12818
  mmprojUrl: string().optional()
12615
12819
  });
12820
+ /**
12821
+ * The outcome of turning one operator-typed Hugging Face reference into a
12822
+ * download plan. A RESULT, never a throw: "this repo has 24 quantizations and
12823
+ * I will not pick for you" is a normal answer the UI has to render, not an
12824
+ * exception.
12825
+ *
12826
+ * `candidates` is the whole reason the refusal is usable — every string in it
12827
+ * is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
12828
+ */
12829
+ var HfModelResolutionSchema = discriminatedUnion("ok", [object({
12830
+ ok: literal(true),
12831
+ /** Ready to hand to `installModel` unchanged. */
12832
+ model: ManagedModelRefSchema,
12833
+ label: string(),
12834
+ repo: string(),
12835
+ quantization: string(),
12836
+ purpose: _enum(["text", "vision"]),
12837
+ totalBytes: number(),
12838
+ /** mmproj + shards, for the preview: an operator approving 23 GB should
12839
+ * see that 0.9 GB of it is a projector they did not name. */
12840
+ extraFilenames: array(string())
12841
+ }), object({
12842
+ ok: literal(false),
12843
+ code: string(),
12844
+ message: string(),
12845
+ candidates: array(string()).optional(),
12846
+ /** Set when the refusal was only the ceiling: re-calling with
12847
+ * `maxBytes: requiredBytes` is the operator's explicit override. */
12848
+ requiredBytes: number().optional()
12849
+ })]);
12616
12850
  var LlmRuntimeNodeSchema = object({
12617
12851
  nodeId: string(),
12618
12852
  reachable: boolean(),
@@ -12625,7 +12859,10 @@ var ProfileRefInputSchema = object({
12625
12859
  addonId: string(),
12626
12860
  profileId: string()
12627
12861
  });
12628
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
12862
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
12863
+ addonId: string().optional(),
12864
+ requestId: string()
12865
+ }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
12629
12866
  kind: "mutation",
12630
12867
  auth: "admin"
12631
12868
  }), method(ProfileRefInputSchema, _void(), {
@@ -12646,6 +12883,15 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
12646
12883
  consumer: string().optional(),
12647
12884
  profileId: string().optional()
12648
12885
  }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
12886
+ /** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
12887
+ * `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
12888
+ ref: string(),
12889
+ /** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
12890
+ maxBytes: number().positive().optional()
12891
+ }), HfModelResolutionSchema, {
12892
+ kind: "mutation",
12893
+ auth: "admin"
12894
+ }), method(object({
12649
12895
  nodeId: string(),
12650
12896
  model: ManagedModelRefSchema
12651
12897
  }), _void(), {
@@ -14255,6 +14501,8 @@ var NcSystemEventKindSchema = _enum([
14255
14501
  "stream-offline",
14256
14502
  "node-online",
14257
14503
  "node-offline",
14504
+ "node-inference-unavailable",
14505
+ "detection-blind",
14258
14506
  "addon-update-available",
14259
14507
  "server-update-available",
14260
14508
  "alarm-triggered",
@@ -14316,7 +14564,16 @@ var NcScheduleSchema = object({
14316
14564
  });
14317
14565
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
14318
14566
  var NcPlateMatcherSchema = object({
14319
- values: array(string().min(1)).min(1),
14567
+ /**
14568
+ * Plate texts (or gallery vehicle names) to match. EMPTY = **any plate the
14569
+ * pipeline could read** — the plate half of "no selection = no narrowing",
14570
+ * and the switch that says this rule is about vehicles that were IDENTIFIED
14571
+ * rather than merely seen. A subject carrying no plate still fails.
14572
+ *
14573
+ * The `.min(1)` this used to carry made that state unauthorable; nothing has
14574
+ * ever persisted an empty list, so widening it cannot change an existing rule.
14575
+ */
14576
+ values: array(string().min(1)),
14320
14577
  /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
14321
14578
  maxDistance: number().int().min(0).max(3).default(1)
14322
14579
  });
@@ -14350,28 +14607,36 @@ var NcOccupancyConditionSchema = object({
14350
14607
  /**
14351
14608
  * Audio condition (IMMEDIATE trigger) — a rule on SOUND, not on a picture.
14352
14609
  *
14353
- * Operator-approved vocabulary (2026-08-12, option A — the same one the
14354
- * reference notifier uses, so an operator moving between them re-uses what
14355
- * they already know): a rule matches when, over a sampling window of
14356
- * `samplingSeconds`, at least `hitPercent`% of the audio samples in that
14357
- * window are HITS. A sample is a hit when it satisfies BOTH present filters:
14358
- *
14359
- * - `dbThreshold` — its level is at or above this many dBFS (see
14360
- * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale);
14361
- * - `labels` — the classifier put at least one of these labels on it.
14362
- *
14363
- * Both are OPTIONAL and independent, which is the point of the shape: a
14364
- * loudness rule ("something loud at 3am") needs no model to be right, and a
14365
- * label rule ("a dog barked") needs no threshold. **Fail-closed when NEITHER
14366
- * is given** — a window in which every sample is trivially a hit would fire on
14367
- * silence, so the engine refuses such a condition rather than notifying on
14368
- * nothing (the schema cannot express "at least one of" without becoming a
14369
- * ZodEffects the cap path would have to special-case).
14370
- *
14371
- * `hitPercent` is over the samples the window actually HOLDS, and the window
14372
- * must be FULL before it can match — a window that has been open for two
14373
- * seconds of its ten is 100% of nothing, and firing on it would make
14374
- * `samplingSeconds` decorative.
14610
+ * **TWO EXCLUSIVE MODES** (operator decision 2026-08-14, D157). Which one a
14611
+ * rule is in is not a stored field — it is WHICH FILTER the rule carries, so
14612
+ * there is no second switch that can disagree with the first and every rule
14613
+ * authored before the decision migrates for free (`audioModeOf`):
14614
+ *
14615
+ * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14616
+ * classifier labels with one of them. No window, no percentage:
14617
+ * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14618
+ * `throttle` cooldown is the only brake. The per-label confidence floor is
14619
+ * the analyzer's (`classificationMinScore`, per device) — a label only
14620
+ * reaches this condition if the classifier was already confident enough.
14621
+ * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14622
+ * the condition: at least `hitPercent`% of the samples over
14623
+ * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
14624
+ * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale). The window
14625
+ * must be FULL before it can match — a window open for two of its ten
14626
+ * seconds is 100% of nothing.
14627
+ *
14628
+ * **Why label mode has no window.** It had one, and it never fired: the
14629
+ * analyzer emits ~1 audio frame per second but YAMNet only LABELS one to three
14630
+ * of them per episode, even through continuous crying. The measured maximum
14631
+ * `hitPercent` over the whole live history was 40 — under the shipped default
14632
+ * of 60, so a label rule could not fire at all, ever. A percentage of frames is
14633
+ * the wrong question to ask of a sparse classifier.
14634
+ *
14635
+ * **Fail-closed when NEITHER is given** — every sample would be a trivial hit
14636
+ * and the rule would fire on silence. The schema cannot express "exactly one
14637
+ * of" without becoming a ZodEffects the cap path would have to special-case, so
14638
+ * the exclusivity is enforced where every editor writes (`patchAudio`) and a
14639
+ * legacy rule carrying both resolves to LABEL (the mode that fires).
14375
14640
  *
14376
14641
  * Labels are the audio macro classes (`AUDIO_MACRO_LABELS` / the NC taxonomy's
14377
14642
  * `audio-*` ids). Both spellings are accepted — the matcher normalizes the
@@ -14379,13 +14644,13 @@ var NcOccupancyConditionSchema = object({
14379
14644
  * an operator who typed `dog` mean the same thing.
14380
14645
  */
14381
14646
  var NcAudioConditionSchema = object({
14382
- /** Audio macro labels; absent = any sound (level-only rule). */
14647
+ /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14383
14648
  labels: array(string().min(1)).min(1).optional(),
14384
- /** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
14649
+ /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14385
14650
  dbThreshold: number().min(-96).max(0).optional(),
14386
- /** Percentage of the window's samples that must be hits (1–100). */
14651
+ /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14387
14652
  hitPercent: number().int().min(1).max(100).default(60),
14388
- /** Length of the sampling window in seconds. */
14653
+ /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14389
14654
  samplingSeconds: number().int().min(1).max(300).default(10)
14390
14655
  });
14391
14656
  /**
@@ -14523,13 +14788,81 @@ var NcRuleActionsSchema = object({
14523
14788
  */
14524
14789
  buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
14525
14790
  });
14791
+ /**
14792
+ * "This rule applies only while `deviceId` is in one of `states`."
14793
+ *
14794
+ * The states are the DEVICE's own vocabulary — `AlarmState` for a panel,
14795
+ * `on`/`off` for a switch — not a normalised set, because normalising would
14796
+ * make the condition lie about devices whose states have no equivalent.
14797
+ *
14798
+ * An unreadable state does NOT match: see the engine's fail-closed gate. A
14799
+ * condition that fired on "I could not read it" would be worse than no gate.
14800
+ */
14801
+ var NcDeviceStateConditionSchema = object({
14802
+ deviceId: number().int(),
14803
+ /** Any of these matches. */
14804
+ states: array(string().min(1)).min(1)
14805
+ });
14806
+ /**
14807
+ * "This rule applies only while scene `sceneId` is `matched` / `diverged`."
14808
+ *
14809
+ * A GATE, not a trigger. `occupancy` and `audio` each DISCRIMINATE their rule —
14810
+ * carrying one makes the rule fire on that subject and nothing else. Scene is
14811
+ * the other shape entirely, the `deviceState` shape: it narrows a rule that
14812
+ * already has a trigger ("tell me about a person at the front door, but only
14813
+ * while the bin is still out"). That is why it composes with every delivery
14814
+ * instead of owning one, and why no new `NcDelivery` member and no new subject
14815
+ * kind exist for it — see D159.
14816
+ *
14817
+ * ── Identity ───────────────────────────────────────────────────────────────
14818
+ * `sceneId` is `SceneMonitor.id`, a `randomUUID()` minted by `createScene` —
14819
+ * globally unique, so it needs no device to disambiguate it. `deviceId` is
14820
+ * carried as a HINT for the editor and for the log line, never as part of the
14821
+ * lookup key: a rule whose hint drifted must still gate correctly.
14822
+ *
14823
+ * ── Which boolean ──────────────────────────────────────────────────────────
14824
+ * `latched` ABSENT means "whatever the scene itself says" — `SceneMonitor.emit`
14825
+ * already declares which boolean drives notification rules, and a second knob
14826
+ * that could disagree with it is exactly the D62 failure. Set it only to
14827
+ * override one rule against the scene's own default.
14828
+ *
14829
+ * - LIVE reading (`emit`/`latched` resolve to live): passes iff
14830
+ * `verdict === requiredState`. `unknown` — no reference for this light, view
14831
+ * shifted, no snapshot — passes NEITHER. A scene that cannot judge is not
14832
+ * evidence, in either direction.
14833
+ * - LATCHED reading: passes iff `latched === (requiredState === 'diverged')`.
14834
+ * The latch is a durable fact about the past ("it has diverged since I armed
14835
+ * it"), so a camera that has gone dark does not clear it — that is the whole
14836
+ * reason the operator asked for a latch.
14837
+ *
14838
+ * The gate reads an in-memory mirror (`NcSceneStateCache`) refreshed OFF the
14839
+ * event path, never the cap: D49. A mirror that has never loaded, or a scene it
14840
+ * does not carry, reads absent and the rule does NOT fire — fail closed, and
14841
+ * said out loud in the log rather than dropped in silence.
14842
+ */
14843
+ var NcSceneConditionSchema = object({
14844
+ /** `SceneMonitor.id` — the uuid the cap mints. The whole lookup key. */
14845
+ sceneId: string().min(1),
14846
+ /** The camera the scene lives on. A hint for the editor and the log line. */
14847
+ deviceId: number().int().optional(),
14848
+ /** The state the scene must be in for the rule to fire. */
14849
+ requiredState: _enum(["matched", "diverged"]),
14850
+ /**
14851
+ * Read the LATCH (`true`) or the LIVE verdict (`false`). Absent = follow the
14852
+ * scene's own `emit` field, which is the only place that decision belongs.
14853
+ */
14854
+ latched: boolean().optional()
14855
+ });
14526
14856
  var NcConditionsSchema = object({
14527
14857
  /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
14528
- deviceState: object({
14529
- deviceId: number().int(),
14530
- /** Any of these matches. */
14531
- states: array(string().min(1)).min(1)
14532
- }).optional(),
14858
+ deviceState: NcDeviceStateConditionSchema.optional(),
14859
+ /**
14860
+ * Gate on a SCENE's state — "only while the bin is still out". Composes with
14861
+ * every trigger (detection, occupancy, audio, sensor, package, track-end);
14862
+ * unlike `occupancy`/`audio` it discriminates nothing. See
14863
+ * {@link NcSceneCondition} and D159.
14864
+ */
14865
+ scene: NcSceneConditionSchema.optional(),
14533
14866
  /** Device scope — absent = all devices. */
14534
14867
  devices: array(number()).optional(),
14535
14868
  /** Detector class names (any overlap with the record's class set). */
@@ -14555,18 +14888,47 @@ var NcConditionsSchema = object({
14555
14888
  */
14556
14889
  labelEquals: array(string().min(1)).optional(),
14557
14890
  /**
14558
- * Identity matcher. P1 boundary: matched against the record's collapsed
14559
- * `label` (the identity display name propagated by the face pipeline) —
14560
- * identity-ID matching rides in P2 when identity ids reach the record.
14891
+ * KNOWN FACES — the rule's identity scope, and the switch that says the rule
14892
+ * is about recognised people at all.
14893
+ *
14894
+ * Three states, and the empty one is the point:
14895
+ *
14896
+ * | value | meaning |
14897
+ * | --- | --- |
14898
+ * | absent | the rule does not care who it is; an unrecognised person matches |
14899
+ * | `[]` | **only known faces** — any identity in the gallery, nobody in particular |
14900
+ * | a list | only these identities |
14901
+ *
14902
+ * `[]` is the repo-wide "no selection = no narrowing" reading (an absent
14903
+ * `devices` list is every device), applied one level down: the operator has
14904
+ * turned the face scope ON and narrowed it to nothing, which is every known
14905
+ * face. No second field states the same thing — a switch that can disagree
14906
+ * with the list under it is worse than no switch (D62).
14907
+ *
14908
+ * MEMBERS ARE FACE-GALLERY `Identity.id`s (uuid), not display names. A name is
14909
+ * renameable, and a rule authored on "Gianluca" went silently dark the moment
14910
+ * the operator fixed the spelling. The id reaches the record on
14911
+ * `LabelAttribution.identityId`; the name is what the editor shows and what
14912
+ * `{{label}}` renders.
14913
+ *
14914
+ * Rules written before this carry NAMES, and are resolved to ids lazily at
14915
+ * load (`NcRuleStore.load`) against the live gallery — a name nothing answers
14916
+ * for is left as it stands and reported, never dropped. The engine also
14917
+ * accepts a display-name hit as a compatibility leg, so a rule whose
14918
+ * migration could not resolve keeps matching exactly what it matched before.
14561
14919
  */
14562
14920
  identities: array(string().min(1)).optional(),
14563
- /** Fuzzy plate matcher against the record's `label` (plate text). */
14921
+ /**
14922
+ * KNOWN PLATES / VEHICLES — the plate mirror of {@link identities}, including
14923
+ * the empty-list reading: `values: []` is "any plate the OCR could read",
14924
+ * a non-empty list is those plates (fuzzily). See {@link NcPlateMatcherSchema}.
14925
+ */
14564
14926
  plates: NcPlateMatcherSchema.optional(),
14565
14927
  /**
14566
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
14567
- * Same P1 boundary: matched against the record's collapsed `label` (the
14568
- * identity display name). A record with NO label passes (nothing to
14569
- * exclude), unlike the include variant which fails on an absent label.
14928
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics, and
14929
+ * the same id members and the same lazy name→id migration. A record with NO
14930
+ * identity passes (nothing to exclude), unlike the include variant which
14931
+ * fails on an unrecognised subject. An EMPTY list excludes nobody.
14570
14932
  */
14571
14933
  identitiesExclude: array(string().min(1)).optional(),
14572
14934
  /**
@@ -14958,7 +15320,80 @@ var NcRuleInputSchema = object({
14958
15320
  * a rule that predates the gate must keep delivering byte-for-byte as it
14959
15321
  * did, and absent is the only way to say that without a migration.
14960
15322
  */
14961
- confirm: NcConfirmSchema.optional()
15323
+ confirm: NcConfirmSchema.optional(),
15324
+ /**
15325
+ * WAIT for face/plate recognition before saying anything.
15326
+ *
15327
+ * A notification's TEXT is frozen at enqueue and its media is re-resolved at
15328
+ * send; the identity is neither. A face is confirmed after `confirmFrames`
15329
+ * agreeing observations — p50 **11.4 s** after the track was first seen,
15330
+ * measured on this hub — and an `immediate` rule enqueues on the first object
15331
+ * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
15332
+ * immediate path, and no amount of media re-resolution fixes a sentence.
15333
+ *
15334
+ * Only two honest answers exist, and this flag picks between them. It has
15335
+ * effect ONLY on a rule that declares a recognition scope
15336
+ * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
15337
+ * other rule there is nothing to wait for and the flag is inert.
15338
+ *
15339
+ * | value | what happens |
15340
+ * | --- | --- |
15341
+ * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
15342
+ * | absent / `false` | it fires at once WITHOUT the name, and if recognition lands before the track closes a SECOND, "…is Gianluca" notification follows (one per track, per rule, per target) |
15343
+ *
15344
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15345
+ * on the addon cap path, and absent has to keep meaning exactly what every
15346
+ * rule authored before this field meant.
15347
+ *
15348
+ * The cost of `true` is stated here because the editor states it too: a rule
15349
+ * that waits also inherits track-close SEMANTICS — its `zones` condition
15350
+ * tests every zone the track visited and a `crossing` condition can no longer
15351
+ * be satisfied, because a closed track carries no crossing.
15352
+ */
15353
+ waitForEnhancement: boolean().optional(),
15354
+ /**
15355
+ * GROUP a burst of subjects into ONE notification that grows.
15356
+ *
15357
+ * Seconds of quiet after the last matching subject before the burst is
15358
+ * considered over. While it is open, the first subject enqueues immediately —
15359
+ * **exactly as today, with no added latency** — and every real growth (a new
15360
+ * subject, or a name confirmed on one already in it) REPLACES that
15361
+ * notification with an updated one naming everybody. The push carries the
15362
+ * group's own coalescing tag, so the phone replaces rather than stacks.
15363
+ *
15364
+ * `0` / absent = off, and off is today's behaviour byte for byte.
15365
+ *
15366
+ * ### Why an idle cutoff and not a window
15367
+ *
15368
+ * The measured seven-person arrival on device 590 spans 110 s with every
15369
+ * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
15370
+ * idle cutoff holds it as one and ends it when the arrival actually ends.
15371
+ * 30 is Frigate's shipped value for the same decision.
15372
+ *
15373
+ * ### What it replaces
15374
+ *
15375
+ * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
15376
+ * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
15377
+ * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
15378
+ * track that did fire and 7 carrying a confirmed identity nobody heard about.
15379
+ * A group collapses the same volume by MERGING, so the cooldown becomes a
15380
+ * budget over GROUPS — which is what it always meant — and a growth is never
15381
+ * throttled by the window its own first member spent.
15382
+ *
15383
+ * ### Interaction with {@link waitForEnhancement}
15384
+ *
15385
+ * They compose, and the order matters. `waitForEnhancement` defers the rule to
15386
+ * TRACK CLOSE, so with both set the group is opened by the first member to
15387
+ * CLOSE — already carrying its name — and grows as later members close. That
15388
+ * is later, and complete. With grouping alone the group opens on the first
15389
+ * object event and picks up names as they are confirmed, through the growth
15390
+ * path. Neither combination fires twice for one subject.
15391
+ *
15392
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15393
+ * on the addon cap path, so absent must keep meaning what it meant before this
15394
+ * field existed.
15395
+ */
15396
+ groupIdleSec: number().int().min(0).max(600).optional()
14962
15397
  });
14963
15398
  /**
14964
15399
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -15065,6 +15500,7 @@ var NcConditionDescriptorSchema = object({
15065
15500
  "occupancy",
15066
15501
  "audio",
15067
15502
  "deviceState",
15503
+ "scene",
15068
15504
  "systemEvent"
15069
15505
  ]),
15070
15506
  operator: _enum([
@@ -15470,7 +15906,87 @@ var MethodAccessSchema = _enum([
15470
15906
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15471
15907
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15472
15908
  var CapScopeSchema = _enum(["device", "system"]);
15473
- var TokenScopeSchema = discriminatedUnion("type", [
15909
+ /**
15910
+ * DeviceSelector (scope model v3 — 2026-08-12).
15911
+ *
15912
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
15913
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
15914
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
15915
+ * AFTER the grant was minted — no re-grant, no re-login.
15916
+ *
15917
+ * - `all` — every device in the deployment. The broad viewer/operator
15918
+ * lever without a `category` grant (a `category` grant also covers device
15919
+ * caps that carry no deviceId; `all` is specifically the device set).
15920
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
15921
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
15922
+ * is NOT covered until the grant is edited.
15923
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
15924
+ * DYNAMIC. A device that changes type, or a new device of the type,
15925
+ * re-resolves on the next request.
15926
+ * - `locations` — every device whose operator-assigned `location` label is
15927
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
15928
+ * null/unset location matches NO `locations` selector.
15929
+ */
15930
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
15931
+ object({ kind: literal("all") }),
15932
+ object({
15933
+ kind: literal("ids"),
15934
+ ids: array(number().int()).min(1)
15935
+ }),
15936
+ object({
15937
+ kind: literal("types"),
15938
+ types: array(_enum(DeviceType)).min(1)
15939
+ }),
15940
+ object({
15941
+ kind: literal("locations"),
15942
+ locations: array(string().min(1)).min(1)
15943
+ })
15944
+ ]);
15945
+ var DeviceTokenScopeSchema = object({
15946
+ type: literal("device"),
15947
+ /** The device SET this grant covers — resolved against the live fleet. */
15948
+ selector: DeviceSelectorSchema,
15949
+ access: array(MethodAccessSchema).min(1),
15950
+ /**
15951
+ * Whether a grant on a PARENT device transparently covers its accessory
15952
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
15953
+ * Direction is parent → children ONLY.
15954
+ *
15955
+ * Absent → the matcher DERIVES it from the access flavour: `view`
15956
+ * inherits (a camera viewer sees the camera's accessories), `create` /
15957
+ * `delete` do NOT (actuating/removing a child is an explicit act the
15958
+ * operator must grant on the child, not inherit from the parent). Set it
15959
+ * explicitly to override that default per grant.
15960
+ */
15961
+ includeLinked: boolean().optional()
15962
+ });
15963
+ /**
15964
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
15965
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
15966
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
15967
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
15968
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
15969
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
15970
+ * migration cannot reach a JWT already in a client's hands; parse-time
15971
+ * migration covers both without a flag day. No cast — the raw object is read
15972
+ * through `Reflect.get` (its static type is `unknown`).
15973
+ */
15974
+ function migrateLegacyTokenScope(raw) {
15975
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
15976
+ if (Reflect.get(raw, "type") !== "device") return raw;
15977
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
15978
+ const targets = Reflect.get(raw, "targets");
15979
+ if (!Array.isArray(targets)) return raw;
15980
+ return {
15981
+ type: "device",
15982
+ selector: {
15983
+ kind: "ids",
15984
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
15985
+ },
15986
+ access: Reflect.get(raw, "access")
15987
+ };
15988
+ }
15989
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
15474
15990
  object({
15475
15991
  type: literal("category"),
15476
15992
  target: CapScopeSchema,
@@ -15486,18 +16002,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
15486
16002
  target: string(),
15487
16003
  access: array(MethodAccessSchema).min(1)
15488
16004
  }),
15489
- object({
15490
- type: literal("device"),
15491
- /**
15492
- * One or more deviceIds (serialised as strings for wire-format
15493
- * consistency with the rest of the union). Matcher accepts if
15494
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
15495
- * of one scope-per-device when granting access to a set of cameras.
15496
- */
15497
- targets: array(string()).min(1),
15498
- access: array(MethodAccessSchema).min(1)
15499
- })
15500
- ]);
16005
+ DeviceTokenScopeSchema
16006
+ ]));
15501
16007
  object({
15502
16008
  id: string(),
15503
16009
  username: string(),
@@ -15820,7 +16326,7 @@ var TrackEnvelopeSchema = object({
15820
16326
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
15821
16327
  * keeps every scalar the list surfaces actually render (ids, class(es),
15822
16328
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
15823
- * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
16329
+ * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
15824
16330
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
15825
16331
  * `getTrack`. Mirrors the event-store `projection` convention
15826
16332
  * (`getObjectEvents` et al.).
@@ -15956,7 +16462,21 @@ union([literal(1), literal(2)]);
15956
16462
  var LabelAttributionSchema = object({
15957
16463
  stepId: string(),
15958
16464
  modelId: string().optional(),
15959
- decidedAt: number()
16465
+ decidedAt: number(),
16466
+ /**
16467
+ * The GALLERY id behind a recognised tier-2 label — a face-gallery
16468
+ * `Identity.id` or a plate-gallery `Vehicle.id` (both `randomUUID`).
16469
+ *
16470
+ * The text alone is a DISPLAY NAME, and a display name is renameable: a
16471
+ * notification rule authored on "Gianluca" stopped matching the moment the
16472
+ * operator fixed the spelling in the gallery, and nothing said so. The id is
16473
+ * the thing that does not move, so it is what a rule matches on
16474
+ * (`NcConditions.identities`) and the text is what a human is shown.
16475
+ *
16476
+ * Absent when the label names no gallery row — a plate the OCR read but no
16477
+ * vehicle claims, a sub-class, a species, any tier-1 value.
16478
+ */
16479
+ identityId: string().optional()
15960
16480
  });
15961
16481
  /**
15962
16482
  * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
@@ -16093,6 +16613,28 @@ var TrackSchema = object({
16093
16613
  * `=== true` and render nothing otherwise, never infer "no face".
16094
16614
  */
16095
16615
  hasFace: boolean().optional(),
16616
+ /**
16617
+ * This subject CONTAINS a folded rider — a person the rider-pairing step
16618
+ * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
16619
+ * so the passage is tracked once and as a VEHICLE.
16620
+ *
16621
+ * It exists because the fold's record was dishonest. D34 and the code both
16622
+ * said "the person is not lost — it is reported so both entities stay on the
16623
+ * record"; in fact the pair went into a per-processor RAM field behind an
16624
+ * accessor nobody called, and every durable surface said `vehicle`, full
16625
+ * stop. This is the composition note that makes the row true.
16626
+ *
16627
+ * A COMPOSITION, never a class and never a label. "This vehicle contains a
16628
+ * person" is not an answer to "what is this" — both label tiers would refuse
16629
+ * a macro token anyway (D89), and correctly. Nothing here changes what the
16630
+ * subject IS: a cyclist stays one vehicle track, occupancy still counts one,
16631
+ * and a `person` rule still does not fire for someone cycling past.
16632
+ *
16633
+ * **Absent ≠ false**, exactly like {@link hasFace}: every row written before
16634
+ * the column, and every hub that predates the field, omits it. Test
16635
+ * `=== true` and render nothing otherwise — never infer "no rider".
16636
+ */
16637
+ hasRider: boolean().optional(),
16096
16638
  ...TrackFlagFields,
16097
16639
  ...TrackRetrainFields
16098
16640
  });
@@ -17488,6 +18030,17 @@ var maxSessionHoldMsField = {
17488
18030
  default: 12e4,
17489
18031
  step: 5e3
17490
18032
  };
18033
+ /**
18034
+ * Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
18035
+ * 5s so a rearm can never degenerate into per-event stream churn; default 90s
18036
+ * comfortably outlives the gap between two PIR wakes on a battery camera.
18037
+ */
18038
+ var audioMotionWindowMsField = {
18039
+ min: 5e3,
18040
+ max: 6e5,
18041
+ default: 9e4,
18042
+ step: 5e3
18043
+ };
17491
18044
  var motionFpsField = {
17492
18045
  min: 1,
17493
18046
  max: 30,
@@ -17519,7 +18072,7 @@ var detectionFpsField = {
17519
18072
  var occupancyRecheckSecField = {
17520
18073
  min: 0,
17521
18074
  max: 300,
17522
- default: 30,
18075
+ default: 300,
17523
18076
  step: 5
17524
18077
  };
17525
18078
  var occupancyRecheckFramesField = {
@@ -17664,6 +18217,27 @@ var RunnerCameraConfigSchema = object({
17664
18217
  * resolved `CameraDetectionConfig`.
17665
18218
  */
17666
18219
  maxSessionHoldMs: number().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
18220
+ /**
18221
+ * Orchestrator-side quiet period (ms) that closes an `audioMode:
18222
+ * 'on-motion'` audio window, measured from the LAST motion event.
18223
+ *
18224
+ * This exists because the falling edge cannot be relied on. Camera-native
18225
+ * providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
18226
+ * its email-push SMTP path both emit `detected: true` and never the
18227
+ * counterpart); only the frame-diff analyzer emits falls. So on an
18228
+ * onboard-only camera a window that closed only on `detected: false` never
18229
+ * closed at all, and `on-motion` silently behaved as `always-on` — on a
18230
+ * battery camera, the one failure mode the mode exists to prevent.
18231
+ *
18232
+ * Every motion event rearms this timer WITHOUT restarting the stream, so a
18233
+ * burst of re-fires costs nothing. A falling edge, when one does arrive,
18234
+ * still closes earlier via `motionCooldownMs` — whichever comes first wins.
18235
+ *
18236
+ * Not consumed by the runner: carried here so it shares the per-camera
18237
+ * device-settings surface with `motionCooldownMs`, exactly like
18238
+ * `maxSessionHoldMs`.
18239
+ */
18240
+ audioMotionWindowMs: number().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
17667
18241
  motionFps: number().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
17668
18242
  detectionFps: number().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
17669
18243
  motionStreamId: string(),
@@ -17759,7 +18333,7 @@ var RunnerCameraConfigSchema = object({
17759
18333
  */
17760
18334
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
17761
18335
  });
17762
- 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;
18336
+ 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;
17763
18337
  /**
17764
18338
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
17765
18339
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -18775,7 +19349,16 @@ targets: array(object({
18775
19349
  /** A sleeping battery camera: the frame is deliberately stale and will
18776
19350
  * NOT refresh in the background. A surface should say so rather than
18777
19351
  * present it as current. */
18778
- sleeping: boolean()
19352
+ sleeping: boolean(),
19353
+ /** Current device state rendered over the cached frame. State images
19354
+ * remain authoritative even when their photographic background is
19355
+ * old; null means the link must carry a current camera frame. */
19356
+ stateReason: _enum([
19357
+ "disabled",
19358
+ "sleeping",
19359
+ "unreachable",
19360
+ "waking"
19361
+ ]).nullable()
18779
19362
  })));
18780
19363
  /**
18781
19364
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20297,6 +20880,25 @@ var BatteryStatusSchema = object({
20297
20880
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
20298
20881
  lastUpdated: number(),
20299
20882
  /**
20883
+ * Ms epoch of the last time the device PROVED it was reachable — a
20884
+ * completed firmware round-trip, an observed wake, or an inbound push
20885
+ * (firmware event, email). `0`/absent = never since this slice was born.
20886
+ *
20887
+ * This is the ONLY input that separates "asleep" from "gone", and it is
20888
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
20889
+ * for the radio, because a poll that confirms reachability is the same
20890
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
20891
+ * single derivation every consumer must use; no surface computes its own.
20892
+ *
20893
+ * It is deliberately NOT a clock in the
20894
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
20895
+ * observation itself, and it is the only thing a 30-hour silence is
20896
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
20897
+ * Reolink provider) so a value that means "recently" cannot cost a
20898
+ * SQLite commit per round-trip.
20899
+ */
20900
+ lastContactAt: number().optional(),
20901
+ /**
20300
20902
  * True when the source is a BINARY low-battery indicator (HA
20301
20903
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
20302
20904
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -23834,7 +24436,7 @@ method(object({
23834
24436
  toMs: number()
23835
24437
  }), RecordingAvailabilitySchema, {
23836
24438
  kind: "query",
23837
- auth: "admin"
24439
+ auth: "protected"
23838
24440
  }), method(object({
23839
24441
  deviceId: number(),
23840
24442
  fromMs: number(),
@@ -23842,14 +24444,14 @@ method(object({
23842
24444
  tzOffsetMinutes: number()
23843
24445
  }), RecordingDaysSchema, {
23844
24446
  kind: "query",
23845
- auth: "admin"
24447
+ auth: "protected"
23846
24448
  }), method(object({
23847
24449
  deviceId: number(),
23848
24450
  fromMs: number(),
23849
24451
  toMs: number()
23850
24452
  }), RecordingManifestSchema, {
23851
24453
  kind: "query",
23852
- auth: "admin"
24454
+ auth: "protected"
23853
24455
  }), method(object({}), RecordingStorageUsageSchema, {
23854
24456
  kind: "query",
23855
24457
  auth: "admin"
@@ -24139,14 +24741,77 @@ method(object({
24139
24741
  * thing except the comparator: `similarity` (CLIP cosine at the same ROI coords
24140
24742
  * vs condition-tagged references) and `llm` (vision-LLM judgment over the crop).
24141
24743
  *
24142
- * D14 device-config archetype (`deviceConfig.ui.kind:'widget'`) — the framework
24143
- * derives the device-detail contribution; the provider carries NO hand-written
24144
- * settings-contribution methods. `status.kind:'push'` — the engine pushes on
24145
- * every hysteresis flip / availability change; consumers never poll.
24146
- */
24147
- /** Extensible condition tag. Seeded 'day' | 'night'; open by design so more can
24148
- * be added without a wire break (matching falls back to any-condition refs). */
24744
+ * **No `deviceConfig`, deliberately.** This shipped as the D14 widget archetype,
24745
+ * which put a "Scenes" tab on one camera's detail page. That is the wrong shape
24746
+ * for the thing: a scene is a standing question about the property ("is the bin
24747
+ * still out"), and the operator's question is "which of my scenes have tripped",
24748
+ * across every camera at once — not "what does camera 617 think". Buried one
24749
+ * camera deep it also could not be found. The surface is now a top-level admin
24750
+ * page (`/scenes`, `pages/Scenes.tsx`) that lists every scene on every camera and
24751
+ * picks the camera inside the create flow, the same shape Events and Faces have.
24752
+ *
24753
+ * The consequence to keep in mind: `host/scene-monitor-editor` is gone from
24754
+ * `HOST_WIDGETS` too. `scripts/check-host-widget-resolves.ts` asserts BOTH
24755
+ * directions, so a registration nobody declares fails exactly as loudly as a
24756
+ * declaration nobody registers. The editor is imported directly by the page.
24757
+ *
24758
+ * `status.kind:'push'` — the engine pushes on every hysteresis flip /
24759
+ * availability change; consumers never poll.
24760
+ */
24761
+ /** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
24762
+ * captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
24763
+ * Open by design so more can be added without a wire break.
24764
+ *
24765
+ * Matching does NOT fall back across conditions: cross-condition cosines are
24766
+ * not comparable, so "I have never seen this scene in this light" is reported
24767
+ * as `unknown`, never guessed. A day reference scored against an IR frame
24768
+ * collapses the cosine and would latch a false alarm every single night. */
24149
24769
  var SceneConditionSchema = string();
24770
+ /**
24771
+ * What a scene does when the CURRENT light has no reference of its own.
24772
+ *
24773
+ * The lighting variants are not equally likely to exist. Almost every operator
24774
+ * captures daylight and then never stands outside at 22:00 to capture IR, and a
24775
+ * scene that is only ever going to be asked about a daytime question ("is the
24776
+ * bin still on the kerb at 08:00") does not need a night reference at all. The
24777
+ * night half must therefore be OPTIONAL, and optional means the scene keeps
24778
+ * working without it rather than degrading into a permanent complaint.
24779
+ *
24780
+ * - `skip` (default) — the check in that light is not made. Not a verdict, not
24781
+ * an alarm, not even an `unknown`: the live state simply stays whatever the
24782
+ * last covered light left it at, the latch is untouched, and the hysteresis
24783
+ * run is neither spent nor cleared. The scene resumes by itself at first
24784
+ * light. This is the only behaviour under which "I never captured IR" is a
24785
+ * configuration choice instead of a nightly fault.
24786
+ * - `judge-anyway` — score against the OTHER conditions' references. Available
24787
+ * for cameras whose IR frame is close enough to daylight (a floodlit
24788
+ * driveway, an always-white-light doorbell), and wrong for everything else:
24789
+ * cross-condition cosines are not comparable, so a day reference against a
24790
+ * true IR frame collapses and the scene reports a theft at 21:40.
24791
+ *
24792
+ * Never applies when the scene has NO comparable reference at all — that is
24793
+ * "not armed yet", it is reported as `no-reference-for-condition`, and silence
24794
+ * there would hide a scene the operator never finished setting up.
24795
+ */
24796
+ var SceneUncoveredPolicySchema = _enum(["skip", "judge-anyway"]);
24797
+ /** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
24798
+ * `unknown` = we cannot judge (no reference for this condition, encoder model
24799
+ * changed, view shifted, no snapshot). `unknown` is a real value, not a null,
24800
+ * and never counts toward hysteresis in either direction. */
24801
+ var SceneVerdictSchema = _enum([
24802
+ "matched",
24803
+ "diverged",
24804
+ "unknown"
24805
+ ]);
24806
+ /** Why a scene cannot judge. Named, because this feature's failure mode is
24807
+ * silence that reads as "nothing has happened". */
24808
+ var SceneUnavailableSchema = _enum([
24809
+ "no-reference-for-condition",
24810
+ "view-shifted",
24811
+ "no-vision-profile",
24812
+ "encoder-model-changed",
24813
+ "no-snapshot"
24814
+ ]);
24150
24815
  /** One captured reference — condition-tagged, model-version-gated. `embedding`
24151
24816
  * is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
24152
24817
  var SceneReferenceSchema = object({
@@ -24154,7 +24819,14 @@ var SceneReferenceSchema = object({
24154
24819
  modelId: string(),
24155
24820
  condition: SceneConditionSchema,
24156
24821
  capturedAt: number(),
24157
- thumbnailMediaId: string().optional()
24822
+ thumbnailMediaId: string().optional(),
24823
+ /** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
24824
+ * anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
24825
+ * normalized rect frame a different piece of world, and the scene would
24826
+ * diverge forever with a perfectly plausible cosine. Checked LAZILY, only
24827
+ * when hysteresis is about to flip — one extra encode per candidate
24828
+ * transition, not per poll. */
24829
+ anchorEmbedding: array(number()).optional()
24158
24830
  });
24159
24831
  var SceneMonitorStateSchema = object({
24160
24832
  id: string(),
@@ -24176,6 +24848,28 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
24176
24848
  profileId: string().optional(),
24177
24849
  hysteresisCount: number().int().positive()
24178
24850
  })]);
24851
+ var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
24852
+ /** Night is OPTIONAL. A scene with only a daylight reference sits the IR hours
24853
+ * out in silence rather than reporting a fault every night. */
24854
+ var SCENE_DEFAULT_UNCOVERED_POLICY = "skip";
24855
+ /**
24856
+ * Vision-model adjudication of a candidate flip. Field names deliberately
24857
+ * mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
24858
+ *
24859
+ * `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
24860
+ * fail-open: a notification suppressed is the worse error there, but a vision
24861
+ * model that timed out has not told us the bin is gone, and a latch is a
24862
+ * stateful claim that costs the operator a trip to reset.
24863
+ */
24864
+ var SceneConfirmSchema = object({
24865
+ enabled: boolean().default(false),
24866
+ prompt: string().min(1).max(1e3),
24867
+ profileId: string().optional(),
24868
+ timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
24869
+ maxImagePx: number().int().min(64).max(2048).default(448),
24870
+ /** What a timeout / unavailable model means for the PENDING flip. */
24871
+ onTimeout: _enum(["flip", "hold"]).default("hold")
24872
+ });
24179
24873
  var SceneMonitorSchema = object({
24180
24874
  id: string(),
24181
24875
  label: string(),
@@ -24194,7 +24888,56 @@ var SceneMonitorSchema = object({
24194
24888
  lastConfidence: number().nullable(),
24195
24889
  currentCondition: SceneConditionSchema.nullable(),
24196
24890
  availability: _enum(["ok", "unavailable"]),
24197
- unavailableReason: string().nullable()
24891
+ unavailableReason: string().nullable(),
24892
+ /** Which state is "the initial screen". `null` until the first capture. */
24893
+ baselineStateId: string().nullable(),
24894
+ /** Which boolean drives notification rules and any export. */
24895
+ emit: _enum(["latched", "live"]).default("latched"),
24896
+ /** Live: does the region match the baseline RIGHT NOW. */
24897
+ verdict: SceneVerdictSchema,
24898
+ /** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
24899
+ latched: boolean(),
24900
+ /** Last reset (or creation). */
24901
+ armedAt: number(),
24902
+ divergedAt: number().nullable(),
24903
+ restoredAt: number().nullable(),
24904
+ /** A check is only COUNTED when the device has been quiet this long. Motion
24905
+ * during the window DISCARDS the observation — a car pulling up in front of
24906
+ * the bin must not be able to spend hysteresis credit. */
24907
+ quietSeconds: number().int().min(0).max(3600).default(60),
24908
+ /** An observation only advances the pending count when it is at least this
24909
+ * far from the previously counted one, so N agreeing checks span real time
24910
+ * rather than N adjacent polls inside one occlusion. */
24911
+ minObservationSpacingSec: number().int().min(0).max(3600).default(120),
24912
+ /** Vision-model adjudication of a candidate flip. Similarity primary only. */
24913
+ confirm: SceneConfirmSchema.optional(),
24914
+ /** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
24915
+ anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
24916
+ /** Clear the latch on its own when the scene matches again? Default false —
24917
+ * `restoredAt` and the `scene-restored` edge are recorded regardless, so an
24918
+ * automation can react to the bin coming back without the operator's own
24919
+ * alarm silently clearing itself. */
24920
+ autoRestore: boolean().default(false),
24921
+ /** What to do when the current light has no reference of its own. See
24922
+ * {@link SceneUncoveredPolicySchema} — the default makes night OPTIONAL. */
24923
+ onUncoveredCondition: SceneUncoveredPolicySchema.default(SCENE_DEFAULT_UNCOVERED_POLICY),
24924
+ /**
24925
+ * The light whose checks are currently being SAT OUT under
24926
+ * `onUncoveredCondition: 'skip'` — `null` when the scene is checking normally.
24927
+ *
24928
+ * Engine-reported and advisory only: it moves no verdict, no latch and no
24929
+ * hysteresis. It exists so the card can say *"night (IR) — checks paused,
24930
+ * nothing captured in this light"* in the same calm voice as the coverage
24931
+ * line, because the alternative is a scene that silently stops answering
24932
+ * after sunset with nothing anywhere saying why. A skipped check must never
24933
+ * read as a broken one.
24934
+ */
24935
+ suspendedCondition: SceneConditionSchema.nullable().default(null),
24936
+ /** Named cause when `verdict === 'unknown'`. */
24937
+ unavailable: SceneUnavailableSchema.nullable(),
24938
+ /** Conditions that have at least one comparable reference — the coverage line
24939
+ * ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
24940
+ coveredConditions: array(SceneConditionSchema)
24198
24941
  });
24199
24942
  var SceneMonitorStatusSchema = object({
24200
24943
  monitors: array(SceneMonitorSchema),
@@ -24227,7 +24970,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24227
24970
  "both"
24228
24971
  ]).optional(),
24229
24972
  checkIntervalSec: number().optional(),
24230
- check: SceneCheckSchema.optional()
24973
+ check: SceneCheckSchema.optional(),
24974
+ emit: _enum(["latched", "live"]).optional(),
24975
+ quietSeconds: number().int().min(0).max(3600).optional(),
24976
+ minObservationSpacingSec: number().int().min(0).max(3600).optional(),
24977
+ anchorThreshold: number().min(0).max(1).optional(),
24978
+ autoRestore: boolean().optional(),
24979
+ onUncoveredCondition: SceneUncoveredPolicySchema.optional(),
24980
+ /** `null` clears the vision-model adjudicator. */
24981
+ confirm: SceneConfirmSchema.nullable().optional()
24231
24982
  })
24232
24983
  }), _void(), {
24233
24984
  kind: "mutation",
@@ -24264,6 +25015,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24264
25015
  }), _void(), {
24265
25016
  kind: "mutation",
24266
25017
  auth: "admin"
25018
+ }), method(object({
25019
+ deviceId: number(),
25020
+ monitorId: string(),
25021
+ /** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
25022
+ recapture: boolean().optional()
25023
+ }), _void(), {
25024
+ kind: "mutation",
25025
+ auth: "admin"
24267
25026
  });
24268
25027
  /**
24269
25028
  * Per-stage gating mode applied to the zones a rule references.
@@ -24417,6 +25176,16 @@ var CamStreamDescriptorSchema = object({
24417
25176
  /** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
24418
25177
  metadata: record(string(), unknown()).optional()
24419
25178
  });
25179
+ object({
25180
+ /** The descriptors as last built from a real camera response. Never a guess:
25181
+ * a failed or refused build writes NOTHING, so a restored catalog is always
25182
+ * one the camera itself once produced. */
25183
+ descriptors: array(CamStreamDescriptorSchema),
25184
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
25185
+ * path decide whether the camera's own awake window is worth spending on a
25186
+ * re-read. */
25187
+ lastFetchedAt: number()
25188
+ });
24420
25189
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly());
24421
25190
  /** One of the camera's stream profiles. */
24422
25191
  var StreamProfileSchema = _enum([
@@ -24572,12 +25341,64 @@ var NetworkAddressSchema = object({
24572
25341
  family: string(),
24573
25342
  internal: boolean()
24574
25343
  });
25344
+ /**
25345
+ * Provenance of the site coordinates, and the whole reason this is not just two
25346
+ * numbers.
25347
+ *
25348
+ * - `operator-set` — a human typed it, or accepted a detection. Authoritative;
25349
+ * nothing overwrites it.
25350
+ * - `derived-from-ip` — the hub geolocated its own public IP once, because a
25351
+ * default that is right to a few kilometres beats the coarse UTC clock split
25352
+ * the sun-times consumers otherwise fall back to.
25353
+ *
25354
+ * The UI shows which one it is. An operator who cannot tell a guess from their
25355
+ * own input will eventually trust the guess.
25356
+ */
25357
+ var SiteLocationSourceSchema = _enum(["operator-set", "derived-from-ip"]);
25358
+ /**
25359
+ * The read shape: the location plus the honest state of the one-shot derivation.
25360
+ *
25361
+ * `derivationAttemptedAt` is what makes the "one call, ever" contract
25362
+ * inspectable. When it is set and `location` is null, the geo-IP lookup ran and
25363
+ * failed; the hub will NOT try again on its own — the fallback is declared
25364
+ * (consumers degrade to their own last resort) and the operator either types the
25365
+ * coordinates or presses detect.
25366
+ */
25367
+ var SiteLocationStatusSchema = object({
25368
+ location: object({
25369
+ /** WGS84 decimal degrees. */
25370
+ latitude: number().min(-90).max(90),
25371
+ longitude: number().min(-180).max(180),
25372
+ source: SiteLocationSourceSchema,
25373
+ /** Epoch ms the value was last written. */
25374
+ updatedAt: number(),
25375
+ /**
25376
+ * Human-readable place the geo-IP service reported ("Napoli, IT"). Display
25377
+ * only — never parsed, never matched on. Absent for an operator-typed value.
25378
+ */
25379
+ label: string().optional()
25380
+ }).nullable(),
25381
+ derivationAttemptedAt: number().nullable(),
25382
+ /** Why the last derivation failed, for the UI to show instead of a shrug. */
25383
+ derivationError: string().nullable()
25384
+ });
25385
+ /** `null` clears the location and re-arms nothing — the derivation stays spent. */
25386
+ var SetSiteLocationInputSchema = object({
25387
+ latitude: number().min(-90).max(90),
25388
+ longitude: number().min(-180).max(180)
25389
+ }).nullable();
24575
25390
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
24576
25391
  kind: "mutation",
24577
25392
  auth: "admin"
24578
25393
  }), method(_void(), _void(), {
24579
25394
  kind: "mutation",
24580
25395
  auth: "admin"
25396
+ }), method(_void(), SiteLocationStatusSchema), method(SetSiteLocationInputSchema, SiteLocationStatusSchema, {
25397
+ kind: "mutation",
25398
+ auth: "admin"
25399
+ }), method(_void(), SiteLocationStatusSchema, {
25400
+ kind: "mutation",
25401
+ auth: "admin"
24581
25402
  });
24582
25403
  object({
24583
25404
  /** True when the device's tamper switch / case-open contact is
@@ -27307,6 +28128,12 @@ Object.freeze({
27307
28128
  addonId: null,
27308
28129
  access: "create"
27309
28130
  },
28131
+ "llm.cancel": {
28132
+ capName: "llm",
28133
+ capScope: "system",
28134
+ addonId: null,
28135
+ access: "create"
28136
+ },
27310
28137
  "llm.deleteModel": {
27311
28138
  capName: "llm",
27312
28139
  capScope: "system",
@@ -27391,6 +28218,12 @@ Object.freeze({
27391
28218
  addonId: null,
27392
28219
  access: "view"
27393
28220
  },
28221
+ "llm.resolveModelRef": {
28222
+ capName: "llm",
28223
+ capScope: "system",
28224
+ addonId: null,
28225
+ access: "create"
28226
+ },
27394
28227
  "llm.setDefault": {
27395
28228
  capName: "llm",
27396
28229
  capScope: "system",
@@ -29557,6 +30390,12 @@ Object.freeze({
29557
30390
  addonId: null,
29558
30391
  access: "create"
29559
30392
  },
30393
+ "sceneMonitor.resetScene": {
30394
+ capName: "scene-monitor",
30395
+ capScope: "device",
30396
+ addonId: null,
30397
+ access: "delete"
30398
+ },
29560
30399
  "sceneMonitor.updateScene": {
29561
30400
  capName: "scene-monitor",
29562
30401
  capScope: "device",
@@ -30235,6 +31074,12 @@ Object.freeze({
30235
31074
  addonId: null,
30236
31075
  access: "create"
30237
31076
  },
31077
+ "system.detectSiteLocation": {
31078
+ capName: "system",
31079
+ capScope: "system",
31080
+ addonId: null,
31081
+ access: "create"
31082
+ },
30238
31083
  "system.featureFlags": {
30239
31084
  capName: "system",
30240
31085
  capScope: "system",
@@ -30253,6 +31098,12 @@ Object.freeze({
30253
31098
  addonId: null,
30254
31099
  access: "view"
30255
31100
  },
31101
+ "system.getSiteLocation": {
31102
+ capName: "system",
31103
+ capScope: "system",
31104
+ addonId: null,
31105
+ access: "view"
31106
+ },
30256
31107
  "system.health": {
30257
31108
  capName: "system",
30258
31109
  capScope: "system",
@@ -30277,6 +31128,12 @@ Object.freeze({
30277
31128
  addonId: null,
30278
31129
  access: "create"
30279
31130
  },
31131
+ "system.setSiteLocation": {
31132
+ capName: "system",
31133
+ capScope: "system",
31134
+ addonId: null,
31135
+ access: "create"
31136
+ },
30280
31137
  "terminalSession.adoptLegacyMonitor": {
30281
31138
  capName: "terminal-session",
30282
31139
  capScope: "system",
@@ -30848,6 +31705,1683 @@ Object.freeze({
30848
31705
  access: "create"
30849
31706
  }
30850
31707
  });
31708
+ Object.freeze({
31709
+ "accessories.setChildHidden": [{
31710
+ name: "childDeviceId",
31711
+ form: "single",
31712
+ optional: false
31713
+ }, {
31714
+ name: "deviceId",
31715
+ form: "single",
31716
+ optional: false
31717
+ }],
31718
+ "addonSettings.getDeviceSettings": [{
31719
+ name: "deviceId",
31720
+ form: "single",
31721
+ optional: false
31722
+ }],
31723
+ "addonSettings.updateDeviceSettings": [{
31724
+ name: "deviceId",
31725
+ form: "single",
31726
+ optional: false
31727
+ }],
31728
+ "alarmPanel.arm": [{
31729
+ name: "deviceId",
31730
+ form: "single",
31731
+ optional: false
31732
+ }],
31733
+ "alarmPanel.disarm": [{
31734
+ name: "deviceId",
31735
+ form: "single",
31736
+ optional: false
31737
+ }],
31738
+ "alarmPanel.trigger": [{
31739
+ name: "deviceId",
31740
+ form: "single",
31741
+ optional: false
31742
+ }],
31743
+ "audioAnalysis.resolveDeviceSettings": [{
31744
+ name: "deviceId",
31745
+ form: "single",
31746
+ optional: false
31747
+ }],
31748
+ "audioAnalyzer.classify": [{
31749
+ name: "deviceId",
31750
+ form: "single",
31751
+ optional: true
31752
+ }],
31753
+ "audioMetrics.getCurrentSnapshot": [{
31754
+ name: "deviceId",
31755
+ form: "single",
31756
+ optional: false
31757
+ }],
31758
+ "audioMetrics.getHistory": [{
31759
+ name: "deviceId",
31760
+ form: "single",
31761
+ optional: false
31762
+ }],
31763
+ "automationControl.disable": [{
31764
+ name: "deviceId",
31765
+ form: "single",
31766
+ optional: false
31767
+ }],
31768
+ "automationControl.enable": [{
31769
+ name: "deviceId",
31770
+ form: "single",
31771
+ optional: false
31772
+ }],
31773
+ "automationControl.trigger": [{
31774
+ name: "deviceId",
31775
+ form: "single",
31776
+ optional: false
31777
+ }],
31778
+ "battery.wakeForStream": [{
31779
+ name: "deviceId",
31780
+ form: "single",
31781
+ optional: false
31782
+ }],
31783
+ "brightness.setBrightness": [{
31784
+ name: "deviceId",
31785
+ form: "single",
31786
+ optional: false
31787
+ }],
31788
+ "button.press": [{
31789
+ name: "deviceId",
31790
+ form: "single",
31791
+ optional: false
31792
+ }],
31793
+ "cameraCredentials.getCredentials": [{
31794
+ name: "deviceId",
31795
+ form: "single",
31796
+ optional: false
31797
+ }],
31798
+ "cameraStreams.getBrokerStreams": [{
31799
+ name: "deviceId",
31800
+ form: "single",
31801
+ optional: false
31802
+ }],
31803
+ "cameraStreams.getCameraStreams": [{
31804
+ name: "deviceId",
31805
+ form: "single",
31806
+ optional: false
31807
+ }],
31808
+ "cameraStreams.getProfileRtspEntries": [{
31809
+ name: "deviceId",
31810
+ form: "single",
31811
+ optional: false
31812
+ }],
31813
+ "cameraStreams.getRtspEntries": [{
31814
+ name: "deviceId",
31815
+ form: "single",
31816
+ optional: false
31817
+ }],
31818
+ "cameraStreams.pickStream": [{
31819
+ name: "deviceId",
31820
+ form: "single",
31821
+ optional: false
31822
+ }],
31823
+ "climateControl.setFanMode": [{
31824
+ name: "deviceId",
31825
+ form: "single",
31826
+ optional: false
31827
+ }],
31828
+ "climateControl.setMode": [{
31829
+ name: "deviceId",
31830
+ form: "single",
31831
+ optional: false
31832
+ }],
31833
+ "climateControl.setPreset": [{
31834
+ name: "deviceId",
31835
+ form: "single",
31836
+ optional: false
31837
+ }],
31838
+ "climateControl.setSwingHorizontal": [{
31839
+ name: "deviceId",
31840
+ form: "single",
31841
+ optional: false
31842
+ }],
31843
+ "climateControl.setSwingVertical": [{
31844
+ name: "deviceId",
31845
+ form: "single",
31846
+ optional: false
31847
+ }],
31848
+ "climateControl.setTarget": [{
31849
+ name: "deviceId",
31850
+ form: "single",
31851
+ optional: false
31852
+ }],
31853
+ "climateControl.setTargetHumidity": [{
31854
+ name: "deviceId",
31855
+ form: "single",
31856
+ optional: false
31857
+ }],
31858
+ "climateControl.setTargetRange": [{
31859
+ name: "deviceId",
31860
+ form: "single",
31861
+ optional: false
31862
+ }],
31863
+ "color.setColor": [{
31864
+ name: "deviceId",
31865
+ form: "single",
31866
+ optional: false
31867
+ }],
31868
+ "consumables.reset": [{
31869
+ name: "deviceId",
31870
+ form: "single",
31871
+ optional: false
31872
+ }],
31873
+ "control.setValue": [{
31874
+ name: "deviceId",
31875
+ form: "single",
31876
+ optional: false
31877
+ }],
31878
+ "cover.close": [{
31879
+ name: "deviceId",
31880
+ form: "single",
31881
+ optional: false
31882
+ }],
31883
+ "cover.open": [{
31884
+ name: "deviceId",
31885
+ form: "single",
31886
+ optional: false
31887
+ }],
31888
+ "cover.setPosition": [{
31889
+ name: "deviceId",
31890
+ form: "single",
31891
+ optional: false
31892
+ }],
31893
+ "cover.setTiltPosition": [{
31894
+ name: "deviceId",
31895
+ form: "single",
31896
+ optional: false
31897
+ }],
31898
+ "cover.stop": [{
31899
+ name: "deviceId",
31900
+ form: "single",
31901
+ optional: false
31902
+ }],
31903
+ "dayNight.getOptions": [{
31904
+ name: "deviceId",
31905
+ form: "single",
31906
+ optional: false
31907
+ }],
31908
+ "dayNight.setSettings": [{
31909
+ name: "deviceId",
31910
+ form: "single",
31911
+ optional: false
31912
+ }],
31913
+ "decoder.createSession": [{
31914
+ name: "deviceId",
31915
+ form: "single",
31916
+ optional: true
31917
+ }],
31918
+ "deviceAdoption.release": [{
31919
+ name: "camDeviceId",
31920
+ form: "single",
31921
+ optional: false
31922
+ }],
31923
+ "deviceAdoption.resync": [{
31924
+ name: "camDeviceId",
31925
+ form: "single",
31926
+ optional: false
31927
+ }],
31928
+ "deviceDiscovery.adoptDevice": [{
31929
+ name: "deviceId",
31930
+ form: "single",
31931
+ optional: false
31932
+ }],
31933
+ "deviceDiscovery.listDiscovered": [{
31934
+ name: "deviceId",
31935
+ form: "single",
31936
+ optional: false
31937
+ }],
31938
+ "deviceDiscovery.refreshDiscovery": [{
31939
+ name: "deviceId",
31940
+ form: "single",
31941
+ optional: false
31942
+ }],
31943
+ "deviceDiscovery.releaseDevice": [{
31944
+ name: "childDeviceId",
31945
+ form: "single",
31946
+ optional: false
31947
+ }, {
31948
+ name: "deviceId",
31949
+ form: "single",
31950
+ optional: false
31951
+ }],
31952
+ "deviceManager.adoptionRelease": [{
31953
+ name: "camDeviceId",
31954
+ form: "single",
31955
+ optional: false
31956
+ }],
31957
+ "deviceManager.adoptionResync": [{
31958
+ name: "camDeviceId",
31959
+ form: "single",
31960
+ optional: false
31961
+ }],
31962
+ "deviceManager.applyInitialMeta": [{
31963
+ name: "deviceId",
31964
+ form: "single",
31965
+ optional: false
31966
+ }, {
31967
+ name: "linkDeviceId",
31968
+ form: "single",
31969
+ optional: true
31970
+ }],
31971
+ "deviceManager.disable": [{
31972
+ name: "deviceId",
31973
+ form: "single",
31974
+ optional: false
31975
+ }],
31976
+ "deviceManager.enable": [{
31977
+ name: "deviceId",
31978
+ form: "single",
31979
+ optional: false
31980
+ }],
31981
+ "deviceManager.getBindings": [{
31982
+ name: "deviceId",
31983
+ form: "single",
31984
+ optional: false
31985
+ }],
31986
+ "deviceManager.getChildren": [{
31987
+ name: "parentDeviceId",
31988
+ form: "single",
31989
+ optional: false
31990
+ }],
31991
+ "deviceManager.getConfigSchema": [{
31992
+ name: "deviceId",
31993
+ form: "single",
31994
+ optional: false
31995
+ }],
31996
+ "deviceManager.getDevice": [{
31997
+ name: "deviceId",
31998
+ form: "single",
31999
+ optional: false
32000
+ }],
32001
+ "deviceManager.getDeviceAggregate": [{
32002
+ name: "deviceId",
32003
+ form: "single",
32004
+ optional: false
32005
+ }],
32006
+ "deviceManager.getDeviceLiveInfoAggregate": [{
32007
+ name: "deviceId",
32008
+ form: "single",
32009
+ optional: false
32010
+ }],
32011
+ "deviceManager.getDeviceSettingsAggregate": [{
32012
+ name: "deviceId",
32013
+ form: "single",
32014
+ optional: false
32015
+ }],
32016
+ "deviceManager.getDeviceStatusAggregate": [{
32017
+ name: "deviceId",
32018
+ form: "single",
32019
+ optional: false
32020
+ }],
32021
+ "deviceManager.getDeviceStatusAggregateBatch": [{
32022
+ name: "deviceIds",
32023
+ form: "array",
32024
+ optional: false
32025
+ }],
32026
+ "deviceManager.getLinkedDevices": [{
32027
+ name: "deviceId",
32028
+ form: "single",
32029
+ optional: false
32030
+ }],
32031
+ "deviceManager.getSettingsSchema": [{
32032
+ name: "deviceId",
32033
+ form: "single",
32034
+ optional: false
32035
+ }],
32036
+ "deviceManager.getStreamProfileMap": [{
32037
+ name: "deviceId",
32038
+ form: "single",
32039
+ optional: false
32040
+ }],
32041
+ "deviceManager.getStreamSources": [{
32042
+ name: "deviceId",
32043
+ form: "single",
32044
+ optional: false
32045
+ }],
32046
+ "deviceManager.getWireableFields": [{
32047
+ name: "deviceId",
32048
+ form: "single",
32049
+ optional: false
32050
+ }],
32051
+ "deviceManager.loadConfig": [{
32052
+ name: "deviceId",
32053
+ form: "single",
32054
+ optional: false
32055
+ }],
32056
+ "deviceManager.loadMeta": [{
32057
+ name: "deviceId",
32058
+ form: "single",
32059
+ optional: false
32060
+ }],
32061
+ "deviceManager.loadRuntimeState": [{
32062
+ name: "deviceId",
32063
+ form: "single",
32064
+ optional: false
32065
+ }],
32066
+ "deviceManager.persistConfig": [{
32067
+ name: "deviceId",
32068
+ form: "single",
32069
+ optional: false
32070
+ }],
32071
+ "deviceManager.probeStreams": [{
32072
+ name: "deviceId",
32073
+ form: "single",
32074
+ optional: false
32075
+ }],
32076
+ "deviceManager.registerDevice": [{
32077
+ name: "parentDeviceId",
32078
+ form: "single",
32079
+ optional: true
32080
+ }],
32081
+ "deviceManager.remove": [{
32082
+ name: "deviceId",
32083
+ form: "single",
32084
+ optional: false
32085
+ }],
32086
+ "deviceManager.removeDevice": [{
32087
+ name: "deviceId",
32088
+ form: "single",
32089
+ optional: false
32090
+ }],
32091
+ "deviceManager.runDeviceAction": [{
32092
+ name: "deviceId",
32093
+ form: "single",
32094
+ optional: false
32095
+ }],
32096
+ "deviceManager.setChildLayout": [{
32097
+ name: "deviceId",
32098
+ form: "single",
32099
+ optional: false
32100
+ }],
32101
+ "deviceManager.setDisabled": [{
32102
+ name: "deviceId",
32103
+ form: "single",
32104
+ optional: false
32105
+ }],
32106
+ "deviceManager.setDisplay": [{
32107
+ name: "deviceId",
32108
+ form: "single",
32109
+ optional: false
32110
+ }],
32111
+ "deviceManager.setIntegrationId": [{
32112
+ name: "deviceId",
32113
+ form: "single",
32114
+ optional: false
32115
+ }],
32116
+ "deviceManager.setLinkDeviceId": [{
32117
+ name: "deviceId",
32118
+ form: "single",
32119
+ optional: false
32120
+ }, {
32121
+ name: "linkDeviceId",
32122
+ form: "single",
32123
+ optional: true
32124
+ }],
32125
+ "deviceManager.setLocation": [{
32126
+ name: "deviceId",
32127
+ form: "single",
32128
+ optional: false
32129
+ }],
32130
+ "deviceManager.setMetadata": [{
32131
+ name: "deviceId",
32132
+ form: "single",
32133
+ optional: false
32134
+ }],
32135
+ "deviceManager.setName": [{
32136
+ name: "deviceId",
32137
+ form: "single",
32138
+ optional: false
32139
+ }],
32140
+ "deviceManager.setPrimaryChildEntityId": [{
32141
+ name: "deviceId",
32142
+ form: "single",
32143
+ optional: false
32144
+ }],
32145
+ "deviceManager.setRole": [{
32146
+ name: "deviceId",
32147
+ form: "single",
32148
+ optional: false
32149
+ }],
32150
+ "deviceManager.setStreamProfileMap": [{
32151
+ name: "deviceId",
32152
+ form: "single",
32153
+ optional: false
32154
+ }],
32155
+ "deviceManager.setType": [{
32156
+ name: "deviceId",
32157
+ form: "single",
32158
+ optional: false
32159
+ }],
32160
+ "deviceManager.setWrapperActive": [{
32161
+ name: "deviceId",
32162
+ form: "single",
32163
+ optional: false
32164
+ }],
32165
+ "deviceManager.testField": [{
32166
+ name: "deviceId",
32167
+ form: "single",
32168
+ optional: false
32169
+ }],
32170
+ "deviceManager.updateConfig": [{
32171
+ name: "deviceId",
32172
+ form: "single",
32173
+ optional: false
32174
+ }],
32175
+ "deviceManager.updateDeviceField": [{
32176
+ name: "deviceId",
32177
+ form: "single",
32178
+ optional: false
32179
+ }],
32180
+ "deviceManager.updateDeviceFieldsBatch": [{
32181
+ name: "deviceId",
32182
+ form: "single",
32183
+ optional: false
32184
+ }],
32185
+ "deviceOps.getConfigEntries": [{
32186
+ name: "deviceId",
32187
+ form: "single",
32188
+ optional: false
32189
+ }],
32190
+ "deviceOps.getRawState": [{
32191
+ name: "deviceId",
32192
+ form: "single",
32193
+ optional: false
32194
+ }],
32195
+ "deviceOps.getSettingsSchema": [{
32196
+ name: "deviceId",
32197
+ form: "single",
32198
+ optional: false
32199
+ }],
32200
+ "deviceOps.getStreamSources": [{
32201
+ name: "deviceId",
32202
+ form: "single",
32203
+ optional: false
32204
+ }],
32205
+ "deviceOps.removeDevice": [{
32206
+ name: "deviceId",
32207
+ form: "single",
32208
+ optional: false
32209
+ }],
32210
+ "deviceOps.runAction": [{
32211
+ name: "deviceId",
32212
+ form: "single",
32213
+ optional: false
32214
+ }],
32215
+ "deviceOps.setConfig": [{
32216
+ name: "deviceId",
32217
+ form: "single",
32218
+ optional: false
32219
+ }],
32220
+ "deviceState.getCapSlice": [{
32221
+ name: "deviceId",
32222
+ form: "single",
32223
+ optional: false
32224
+ }],
32225
+ "deviceState.getSnapshot": [{
32226
+ name: "deviceId",
32227
+ form: "single",
32228
+ optional: false
32229
+ }],
32230
+ "deviceState.setCapSlice": [{
32231
+ name: "deviceId",
32232
+ form: "single",
32233
+ optional: false
32234
+ }],
32235
+ "events.getEventClipUrl": [{
32236
+ name: "deviceId",
32237
+ form: "single",
32238
+ optional: false
32239
+ }],
32240
+ "events.getEvents": [{
32241
+ name: "deviceId",
32242
+ form: "single",
32243
+ optional: false
32244
+ }],
32245
+ "events.getEventThumbnail": [{
32246
+ name: "deviceId",
32247
+ form: "single",
32248
+ optional: false
32249
+ }],
32250
+ "faceGallery.getFaceByTrack": [{
32251
+ name: "deviceId",
32252
+ form: "single",
32253
+ optional: false
32254
+ }],
32255
+ "faceGallery.listRecentFaces": [{
32256
+ name: "deviceId",
32257
+ form: "single",
32258
+ optional: true
32259
+ }],
32260
+ "fanControl.setDirection": [{
32261
+ name: "deviceId",
32262
+ form: "single",
32263
+ optional: false
32264
+ }],
32265
+ "fanControl.setOscillating": [{
32266
+ name: "deviceId",
32267
+ form: "single",
32268
+ optional: false
32269
+ }],
32270
+ "fanControl.setPercentage": [{
32271
+ name: "deviceId",
32272
+ form: "single",
32273
+ optional: false
32274
+ }],
32275
+ "fanControl.setPreset": [{
32276
+ name: "deviceId",
32277
+ form: "single",
32278
+ optional: false
32279
+ }],
32280
+ "humidifier.setMode": [{
32281
+ name: "deviceId",
32282
+ form: "single",
32283
+ optional: false
32284
+ }],
32285
+ "humidifier.setOn": [{
32286
+ name: "deviceId",
32287
+ form: "single",
32288
+ optional: false
32289
+ }],
32290
+ "humidifier.setTargetHumidity": [{
32291
+ name: "deviceId",
32292
+ form: "single",
32293
+ optional: false
32294
+ }],
32295
+ "imageSettings.getOptions": [{
32296
+ name: "deviceId",
32297
+ form: "single",
32298
+ optional: false
32299
+ }],
32300
+ "imageSettings.setSettings": [{
32301
+ name: "deviceId",
32302
+ form: "single",
32303
+ optional: false
32304
+ }],
32305
+ "intercom.endTalkSession": [{
32306
+ name: "deviceId",
32307
+ form: "single",
32308
+ optional: false
32309
+ }],
32310
+ "intercom.handleAnswer": [{
32311
+ name: "deviceId",
32312
+ form: "single",
32313
+ optional: false
32314
+ }],
32315
+ "intercom.pushTalkAudio": [{
32316
+ name: "deviceId",
32317
+ form: "single",
32318
+ optional: false
32319
+ }],
32320
+ "intercom.startSession": [{
32321
+ name: "deviceId",
32322
+ form: "single",
32323
+ optional: false
32324
+ }],
32325
+ "intercom.startTalkSession": [{
32326
+ name: "deviceId",
32327
+ form: "single",
32328
+ optional: false
32329
+ }],
32330
+ "intercom.stopSession": [{
32331
+ name: "deviceId",
32332
+ form: "single",
32333
+ optional: false
32334
+ }],
32335
+ "lawnMowerControl.dock": [{
32336
+ name: "deviceId",
32337
+ form: "single",
32338
+ optional: false
32339
+ }],
32340
+ "lawnMowerControl.pause": [{
32341
+ name: "deviceId",
32342
+ form: "single",
32343
+ optional: false
32344
+ }],
32345
+ "lawnMowerControl.startMowing": [{
32346
+ name: "deviceId",
32347
+ form: "single",
32348
+ optional: false
32349
+ }],
32350
+ "lockControl.lock": [{
32351
+ name: "deviceId",
32352
+ form: "single",
32353
+ optional: false
32354
+ }],
32355
+ "lockControl.open": [{
32356
+ name: "deviceId",
32357
+ form: "single",
32358
+ optional: false
32359
+ }],
32360
+ "lockControl.unlock": [{
32361
+ name: "deviceId",
32362
+ form: "single",
32363
+ optional: false
32364
+ }],
32365
+ "mediaPlayer.next": [{
32366
+ name: "deviceId",
32367
+ form: "single",
32368
+ optional: false
32369
+ }],
32370
+ "mediaPlayer.pause": [{
32371
+ name: "deviceId",
32372
+ form: "single",
32373
+ optional: false
32374
+ }],
32375
+ "mediaPlayer.play": [{
32376
+ name: "deviceId",
32377
+ form: "single",
32378
+ optional: false
32379
+ }],
32380
+ "mediaPlayer.playMedia": [{
32381
+ name: "deviceId",
32382
+ form: "single",
32383
+ optional: false
32384
+ }],
32385
+ "mediaPlayer.previous": [{
32386
+ name: "deviceId",
32387
+ form: "single",
32388
+ optional: false
32389
+ }],
32390
+ "mediaPlayer.seek": [{
32391
+ name: "deviceId",
32392
+ form: "single",
32393
+ optional: false
32394
+ }],
32395
+ "mediaPlayer.selectSource": [{
32396
+ name: "deviceId",
32397
+ form: "single",
32398
+ optional: false
32399
+ }],
32400
+ "mediaPlayer.setMute": [{
32401
+ name: "deviceId",
32402
+ form: "single",
32403
+ optional: false
32404
+ }],
32405
+ "mediaPlayer.setRepeat": [{
32406
+ name: "deviceId",
32407
+ form: "single",
32408
+ optional: false
32409
+ }],
32410
+ "mediaPlayer.setShuffle": [{
32411
+ name: "deviceId",
32412
+ form: "single",
32413
+ optional: false
32414
+ }],
32415
+ "mediaPlayer.setVolume": [{
32416
+ name: "deviceId",
32417
+ form: "single",
32418
+ optional: false
32419
+ }],
32420
+ "mediaPlayer.stop": [{
32421
+ name: "deviceId",
32422
+ form: "single",
32423
+ optional: false
32424
+ }],
32425
+ "motion.isDetected": [{
32426
+ name: "deviceId",
32427
+ form: "single",
32428
+ optional: false
32429
+ }],
32430
+ "motionDetection.analyze": [{
32431
+ name: "deviceId",
32432
+ form: "single",
32433
+ optional: false
32434
+ }],
32435
+ "motionDetection.removeCamera": [{
32436
+ name: "deviceId",
32437
+ form: "single",
32438
+ optional: false
32439
+ }],
32440
+ "motionTrigger.setMotionTrigger": [{
32441
+ name: "deviceId",
32442
+ form: "single",
32443
+ optional: false
32444
+ }],
32445
+ "motionZones.getOptions": [{
32446
+ name: "deviceId",
32447
+ form: "single",
32448
+ optional: false
32449
+ }],
32450
+ "motionZones.setZone": [{
32451
+ name: "deviceId",
32452
+ form: "single",
32453
+ optional: false
32454
+ }],
32455
+ "nativeObjectDetection.setEnabled": [{
32456
+ name: "deviceId",
32457
+ form: "single",
32458
+ optional: false
32459
+ }],
32460
+ "networkQuality.getDeviceStats": [{
32461
+ name: "deviceId",
32462
+ form: "single",
32463
+ optional: false
32464
+ }],
32465
+ "networkQuality.reportClientStats": [{
32466
+ name: "deviceId",
32467
+ form: "single",
32468
+ optional: false
32469
+ }],
32470
+ "notificationRules.setDeviceMuted": [{
32471
+ name: "deviceId",
32472
+ form: "single",
32473
+ optional: false
32474
+ }],
32475
+ "notifier.cancel": [{
32476
+ name: "deviceId",
32477
+ form: "single",
32478
+ optional: false
32479
+ }],
32480
+ "notifier.send": [{
32481
+ name: "deviceId",
32482
+ form: "single",
32483
+ optional: false
32484
+ }],
32485
+ "osd.setOverlay": [{
32486
+ name: "deviceId",
32487
+ form: "single",
32488
+ optional: false
32489
+ }],
32490
+ "osdManager.clearSlotBinding": [{
32491
+ name: "deviceId",
32492
+ form: "single",
32493
+ optional: false
32494
+ }],
32495
+ "osdManager.copyDeviceConfiguration": [{
32496
+ name: "sourceDeviceId",
32497
+ form: "single",
32498
+ optional: false
32499
+ }, {
32500
+ name: "targetDeviceId",
32501
+ form: "single",
32502
+ optional: false
32503
+ }],
32504
+ "osdManager.getDeviceOsd": [{
32505
+ name: "deviceId",
32506
+ form: "single",
32507
+ optional: false
32508
+ }],
32509
+ "osdManager.getSourceCatalog": [{
32510
+ name: "deviceId",
32511
+ form: "single",
32512
+ optional: false
32513
+ }],
32514
+ "osdManager.previewSlot": [{
32515
+ name: "deviceId",
32516
+ form: "single",
32517
+ optional: false
32518
+ }],
32519
+ "osdManager.renderDevice": [{
32520
+ name: "deviceId",
32521
+ form: "single",
32522
+ optional: false
32523
+ }],
32524
+ "osdManager.setSlotBinding": [{
32525
+ name: "deviceId",
32526
+ form: "single",
32527
+ optional: false
32528
+ }],
32529
+ "petFeeder.callPet": [{
32530
+ name: "deviceId",
32531
+ form: "single",
32532
+ optional: false
32533
+ }],
32534
+ "petFeeder.cancelFeed": [{
32535
+ name: "deviceId",
32536
+ form: "single",
32537
+ optional: false
32538
+ }],
32539
+ "petFeeder.feed": [{
32540
+ name: "deviceId",
32541
+ form: "single",
32542
+ optional: false
32543
+ }],
32544
+ "petFeeder.markFoodReplenished": [{
32545
+ name: "deviceId",
32546
+ form: "single",
32547
+ optional: false
32548
+ }],
32549
+ "petFeeder.playSound": [{
32550
+ name: "deviceId",
32551
+ form: "single",
32552
+ optional: false
32553
+ }],
32554
+ "petFeeder.resetDesiccant": [{
32555
+ name: "deviceId",
32556
+ form: "single",
32557
+ optional: false
32558
+ }],
32559
+ "petFeeder.setChildLock": [{
32560
+ name: "deviceId",
32561
+ form: "single",
32562
+ optional: false
32563
+ }],
32564
+ "petFeeder.setFeedSound": [{
32565
+ name: "deviceId",
32566
+ form: "single",
32567
+ optional: false
32568
+ }],
32569
+ "petFeeder.setIndicatorLight": [{
32570
+ name: "deviceId",
32571
+ form: "single",
32572
+ optional: false
32573
+ }],
32574
+ "petFeeder.setVolume": [{
32575
+ name: "deviceId",
32576
+ form: "single",
32577
+ optional: false
32578
+ }],
32579
+ "pipelineAnalytics.clearTracks": [{
32580
+ name: "deviceId",
32581
+ form: "single",
32582
+ optional: false
32583
+ }],
32584
+ "pipelineAnalytics.completeRetrainTrack": [{
32585
+ name: "deviceId",
32586
+ form: "single",
32587
+ optional: false
32588
+ }],
32589
+ "pipelineAnalytics.deleteDeviceEvents": [{
32590
+ name: "deviceId",
32591
+ form: "single",
32592
+ optional: false
32593
+ }],
32594
+ "pipelineAnalytics.deleteTracks": [{
32595
+ name: "deviceId",
32596
+ form: "single",
32597
+ optional: false
32598
+ }],
32599
+ "pipelineAnalytics.deselectRetrainFrame": [{
32600
+ name: "deviceId",
32601
+ form: "single",
32602
+ optional: false
32603
+ }],
32604
+ "pipelineAnalytics.getActiveTracks": [{
32605
+ name: "deviceId",
32606
+ form: "single",
32607
+ optional: false
32608
+ }],
32609
+ "pipelineAnalytics.getAudioEvents": [{
32610
+ name: "deviceId",
32611
+ form: "single",
32612
+ optional: false
32613
+ }],
32614
+ "pipelineAnalytics.getEventDensity": [{
32615
+ name: "deviceId",
32616
+ form: "single",
32617
+ optional: false
32618
+ }],
32619
+ "pipelineAnalytics.getKeyEvents": [{
32620
+ name: "deviceId",
32621
+ form: "single",
32622
+ optional: false
32623
+ }],
32624
+ "pipelineAnalytics.getMotionEvents": [{
32625
+ name: "deviceId",
32626
+ form: "single",
32627
+ optional: false
32628
+ }],
32629
+ "pipelineAnalytics.getObjectEvents": [{
32630
+ name: "deviceId",
32631
+ form: "single",
32632
+ optional: false
32633
+ }],
32634
+ "pipelineAnalytics.getRetrainExportUrl": [{
32635
+ name: "deviceIds",
32636
+ form: "array",
32637
+ optional: true
32638
+ }],
32639
+ "pipelineAnalytics.getSensorEvents": [{
32640
+ name: "deviceId",
32641
+ form: "single",
32642
+ optional: false
32643
+ }],
32644
+ "pipelineAnalytics.getTrack": [{
32645
+ name: "deviceId",
32646
+ form: "single",
32647
+ optional: false
32648
+ }],
32649
+ "pipelineAnalytics.getTrainingExportSummary": [{
32650
+ name: "deviceIds",
32651
+ form: "array",
32652
+ optional: true
32653
+ }],
32654
+ "pipelineAnalytics.getTrainingExportUrl": [{
32655
+ name: "deviceIds",
32656
+ form: "array",
32657
+ optional: true
32658
+ }],
32659
+ "pipelineAnalytics.listEventKinds": [{
32660
+ name: "deviceId",
32661
+ form: "single",
32662
+ optional: false
32663
+ }],
32664
+ "pipelineAnalytics.listEventKindsBatch": [{
32665
+ name: "deviceIds",
32666
+ form: "array",
32667
+ optional: false
32668
+ }],
32669
+ "pipelineAnalytics.listOpsLog": [{
32670
+ name: "deviceId",
32671
+ form: "single",
32672
+ optional: true
32673
+ }],
32674
+ "pipelineAnalytics.listRecentTracks": [{
32675
+ name: "deviceIds",
32676
+ form: "array",
32677
+ optional: false
32678
+ }],
32679
+ "pipelineAnalytics.listRetrainStaging": [{
32680
+ name: "deviceIds",
32681
+ form: "array",
32682
+ optional: true
32683
+ }],
32684
+ "pipelineAnalytics.listTracks": [{
32685
+ name: "deviceId",
32686
+ form: "single",
32687
+ optional: false
32688
+ }],
32689
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32690
+ name: "deviceId",
32691
+ form: "single",
32692
+ optional: false
32693
+ }],
32694
+ "pipelineAnalytics.pruneEventsBefore": [{
32695
+ name: "deviceId",
32696
+ form: "single",
32697
+ optional: false
32698
+ }],
32699
+ "pipelineAnalytics.pruneTracksBefore": [{
32700
+ name: "deviceId",
32701
+ form: "single",
32702
+ optional: false
32703
+ }],
32704
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32705
+ name: "deviceId",
32706
+ form: "single",
32707
+ optional: true
32708
+ }],
32709
+ "pipelineAnalytics.restageRetrainTrack": [{
32710
+ name: "deviceId",
32711
+ form: "single",
32712
+ optional: false
32713
+ }],
32714
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32715
+ name: "deviceId",
32716
+ form: "single",
32717
+ optional: false
32718
+ }],
32719
+ "pipelineAnalytics.searchObjectEvents": [{
32720
+ name: "deviceId",
32721
+ form: "single",
32722
+ optional: true
32723
+ }],
32724
+ "pipelineAnalytics.selectRetrainFrames": [{
32725
+ name: "deviceId",
32726
+ form: "single",
32727
+ optional: false
32728
+ }],
32729
+ "pipelineAnalytics.setTrackFlags": [{
32730
+ name: "deviceId",
32731
+ form: "single",
32732
+ optional: false
32733
+ }],
32734
+ "pipelineAnalytics.wipeAllAnalytics": [{
32735
+ name: "deviceId",
32736
+ form: "single",
32737
+ optional: false
32738
+ }],
32739
+ "pipelineExecutor.runPipeline": [{
32740
+ name: "deviceId",
32741
+ form: "single",
32742
+ optional: true
32743
+ }],
32744
+ "pipelineExecutor.runPipelineBatch": [{
32745
+ name: "deviceId",
32746
+ form: "single",
32747
+ optional: true
32748
+ }],
32749
+ "pipelineOrchestrator.assignAudio": [{
32750
+ name: "deviceId",
32751
+ form: "single",
32752
+ optional: false
32753
+ }],
32754
+ "pipelineOrchestrator.assignPipeline": [{
32755
+ name: "deviceId",
32756
+ form: "single",
32757
+ optional: false
32758
+ }],
32759
+ "pipelineOrchestrator.getAudioAssignment": [{
32760
+ name: "deviceId",
32761
+ form: "single",
32762
+ optional: false
32763
+ }],
32764
+ "pipelineOrchestrator.getCameraMetrics": [{
32765
+ name: "deviceId",
32766
+ form: "single",
32767
+ optional: false
32768
+ }],
32769
+ "pipelineOrchestrator.getCameraSettings": [{
32770
+ name: "deviceId",
32771
+ form: "single",
32772
+ optional: false
32773
+ }],
32774
+ "pipelineOrchestrator.getCameraStatus": [{
32775
+ name: "deviceId",
32776
+ form: "single",
32777
+ optional: false
32778
+ }],
32779
+ "pipelineOrchestrator.getCameraStatuses": [{
32780
+ name: "deviceIds",
32781
+ form: "array",
32782
+ optional: true
32783
+ }],
32784
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32785
+ name: "deviceId",
32786
+ form: "single",
32787
+ optional: false
32788
+ }],
32789
+ "pipelineOrchestrator.getCameraSwitches": [{
32790
+ name: "deviceId",
32791
+ form: "single",
32792
+ optional: false
32793
+ }],
32794
+ "pipelineOrchestrator.getPipelineAssignment": [{
32795
+ name: "deviceId",
32796
+ form: "single",
32797
+ optional: false
32798
+ }],
32799
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32800
+ name: "deviceId",
32801
+ form: "single",
32802
+ optional: false
32803
+ }],
32804
+ "pipelineOrchestrator.resolvePipeline": [{
32805
+ name: "deviceId",
32806
+ form: "single",
32807
+ optional: false
32808
+ }],
32809
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32810
+ name: "deviceId",
32811
+ form: "single",
32812
+ optional: false
32813
+ }],
32814
+ "pipelineOrchestrator.setCameraStepOverride": [{
32815
+ name: "deviceId",
32816
+ form: "single",
32817
+ optional: false
32818
+ }],
32819
+ "pipelineOrchestrator.setCameraStepToggle": [{
32820
+ name: "deviceId",
32821
+ form: "single",
32822
+ optional: false
32823
+ }],
32824
+ "pipelineOrchestrator.setCameraSwitch": [{
32825
+ name: "deviceId",
32826
+ form: "single",
32827
+ optional: false
32828
+ }],
32829
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32830
+ name: "deviceId",
32831
+ form: "single",
32832
+ optional: false
32833
+ }],
32834
+ "pipelineOrchestrator.unassignAudio": [{
32835
+ name: "deviceId",
32836
+ form: "single",
32837
+ optional: false
32838
+ }],
32839
+ "pipelineOrchestrator.unassignPipeline": [{
32840
+ name: "deviceId",
32841
+ form: "single",
32842
+ optional: false
32843
+ }],
32844
+ "pipelineRunner.attachCamera": [{
32845
+ name: "deviceId",
32846
+ form: "single",
32847
+ optional: false
32848
+ }],
32849
+ "pipelineRunner.detachCamera": [{
32850
+ name: "deviceId",
32851
+ form: "single",
32852
+ optional: false
32853
+ }],
32854
+ "pipelineRunner.getCameraMetrics": [{
32855
+ name: "deviceId",
32856
+ form: "single",
32857
+ optional: false
32858
+ }],
32859
+ "pipelineRunner.reportMotion": [{
32860
+ name: "deviceId",
32861
+ form: "single",
32862
+ optional: false
32863
+ }],
32864
+ "pipelineRunner.runDetailSubtree": [{
32865
+ name: "deviceId",
32866
+ form: "single",
32867
+ optional: false
32868
+ }],
32869
+ "pipelineRunner.runStatelessStep": [{
32870
+ name: "sourceDeviceId",
32871
+ form: "single",
32872
+ optional: false
32873
+ }],
32874
+ "plateGallery.getPlateByTrack": [{
32875
+ name: "deviceId",
32876
+ form: "single",
32877
+ optional: false
32878
+ }],
32879
+ "plateGallery.listPlates": [{
32880
+ name: "deviceId",
32881
+ form: "single",
32882
+ optional: true
32883
+ }],
32884
+ "privacyMask.getOptions": [{
32885
+ name: "deviceId",
32886
+ form: "single",
32887
+ optional: false
32888
+ }],
32889
+ "privacyMask.setAudioEnabled": [{
32890
+ name: "deviceId",
32891
+ form: "single",
32892
+ optional: false
32893
+ }],
32894
+ "privacyMask.setMask": [{
32895
+ name: "deviceId",
32896
+ form: "single",
32897
+ optional: false
32898
+ }],
32899
+ "ptz.continuousMove": [{
32900
+ name: "deviceId",
32901
+ form: "single",
32902
+ optional: false
32903
+ }],
32904
+ "ptz.deletePreset": [{
32905
+ name: "deviceId",
32906
+ form: "single",
32907
+ optional: false
32908
+ }],
32909
+ "ptz.getOptions": [{
32910
+ name: "deviceId",
32911
+ form: "single",
32912
+ optional: false
32913
+ }],
32914
+ "ptz.getPosition": [{
32915
+ name: "deviceId",
32916
+ form: "single",
32917
+ optional: false
32918
+ }],
32919
+ "ptz.getPresets": [{
32920
+ name: "deviceId",
32921
+ form: "single",
32922
+ optional: false
32923
+ }],
32924
+ "ptz.goHome": [{
32925
+ name: "deviceId",
32926
+ form: "single",
32927
+ optional: false
32928
+ }],
32929
+ "ptz.goToPreset": [{
32930
+ name: "deviceId",
32931
+ form: "single",
32932
+ optional: false
32933
+ }],
32934
+ "ptz.move": [{
32935
+ name: "deviceId",
32936
+ form: "single",
32937
+ optional: false
32938
+ }],
32939
+ "ptz.savePreset": [{
32940
+ name: "deviceId",
32941
+ form: "single",
32942
+ optional: false
32943
+ }],
32944
+ "ptz.setAutofocus": [{
32945
+ name: "deviceId",
32946
+ form: "single",
32947
+ optional: false
32948
+ }],
32949
+ "ptz.stop": [{
32950
+ name: "deviceId",
32951
+ form: "single",
32952
+ optional: false
32953
+ }],
32954
+ "ptzAutotrack.getSettings": [{
32955
+ name: "deviceId",
32956
+ form: "single",
32957
+ optional: false
32958
+ }],
32959
+ "ptzAutotrack.getStatus": [{
32960
+ name: "deviceId",
32961
+ form: "single",
32962
+ optional: false
32963
+ }],
32964
+ "ptzAutotrack.setEnabled": [{
32965
+ name: "deviceId",
32966
+ form: "single",
32967
+ optional: false
32968
+ }],
32969
+ "ptzAutotrack.setSettings": [{
32970
+ name: "deviceId",
32971
+ form: "single",
32972
+ optional: false
32973
+ }],
32974
+ "reboot.reboot": [{
32975
+ name: "deviceId",
32976
+ form: "single",
32977
+ optional: false
32978
+ }],
32979
+ "recording.deleteFootprint": [{
32980
+ name: "deviceId",
32981
+ form: "single",
32982
+ optional: false
32983
+ }],
32984
+ "recording.getAvailability": [{
32985
+ name: "deviceId",
32986
+ form: "single",
32987
+ optional: false
32988
+ }],
32989
+ "recording.getDaysWithRecordings": [{
32990
+ name: "deviceId",
32991
+ form: "single",
32992
+ optional: false
32993
+ }],
32994
+ "recording.getDeviceConfig": [{
32995
+ name: "deviceId",
32996
+ form: "single",
32997
+ optional: false
32998
+ }],
32999
+ "recording.getPlaybackManifest": [{
33000
+ name: "deviceId",
33001
+ form: "single",
33002
+ optional: false
33003
+ }],
33004
+ "recording.listOpsLog": [{
33005
+ name: "deviceId",
33006
+ form: "single",
33007
+ optional: true
33008
+ }],
33009
+ "recording.locateSegment": [{
33010
+ name: "deviceId",
33011
+ form: "single",
33012
+ optional: false
33013
+ }],
33014
+ "recording.pruneFootage": [{
33015
+ name: "deviceId",
33016
+ form: "single",
33017
+ optional: false
33018
+ }],
33019
+ "recording.readGopBytes": [{
33020
+ name: "deviceId",
33021
+ form: "single",
33022
+ optional: false
33023
+ }],
33024
+ "recording.readSegmentBytes": [{
33025
+ name: "deviceId",
33026
+ form: "single",
33027
+ optional: false
33028
+ }],
33029
+ "recording.relocateFootage": [{
33030
+ name: "deviceId",
33031
+ form: "single",
33032
+ optional: true
33033
+ }],
33034
+ "recording.renderClip": [{
33035
+ name: "deviceId",
33036
+ form: "single",
33037
+ optional: false
33038
+ }],
33039
+ "recording.renderGif": [{
33040
+ name: "deviceId",
33041
+ form: "single",
33042
+ optional: false
33043
+ }],
33044
+ "recording.rescanStorage": [{
33045
+ name: "deviceId",
33046
+ form: "single",
33047
+ optional: false
33048
+ }],
33049
+ "recording.setDeviceConfig": [{
33050
+ name: "deviceId",
33051
+ form: "single",
33052
+ optional: false
33053
+ }],
33054
+ "recording.startStorageMigrationMove": [{
33055
+ name: "deviceId",
33056
+ form: "single",
33057
+ optional: true
33058
+ }],
33059
+ "recordingExport.createExport": [{
33060
+ name: "deviceId",
33061
+ form: "single",
33062
+ optional: false
33063
+ }],
33064
+ "recordingExport.listExports": [{
33065
+ name: "deviceId",
33066
+ form: "single",
33067
+ optional: true
33068
+ }],
33069
+ "sceneMonitor.captureReference": [{
33070
+ name: "deviceId",
33071
+ form: "single",
33072
+ optional: false
33073
+ }],
33074
+ "sceneMonitor.createScene": [{
33075
+ name: "deviceId",
33076
+ form: "single",
33077
+ optional: false
33078
+ }],
33079
+ "sceneMonitor.deleteReference": [{
33080
+ name: "deviceId",
33081
+ form: "single",
33082
+ optional: false
33083
+ }],
33084
+ "sceneMonitor.deleteScene": [{
33085
+ name: "deviceId",
33086
+ form: "single",
33087
+ optional: false
33088
+ }],
33089
+ "sceneMonitor.listScenes": [{
33090
+ name: "deviceId",
33091
+ form: "single",
33092
+ optional: false
33093
+ }],
33094
+ "sceneMonitor.recheckNow": [{
33095
+ name: "deviceId",
33096
+ form: "single",
33097
+ optional: false
33098
+ }],
33099
+ "sceneMonitor.resetScene": [{
33100
+ name: "deviceId",
33101
+ form: "single",
33102
+ optional: false
33103
+ }],
33104
+ "sceneMonitor.updateScene": [{
33105
+ name: "deviceId",
33106
+ form: "single",
33107
+ optional: false
33108
+ }],
33109
+ "scriptRunner.run": [{
33110
+ name: "deviceId",
33111
+ form: "single",
33112
+ optional: false
33113
+ }],
33114
+ "scriptRunner.stop": [{
33115
+ name: "deviceId",
33116
+ form: "single",
33117
+ optional: false
33118
+ }],
33119
+ "snapshot.getSnapshot": [{
33120
+ name: "deviceId",
33121
+ form: "single",
33122
+ optional: false
33123
+ }],
33124
+ "snapshot.getSnapshotOverview": [{
33125
+ name: "deviceIds",
33126
+ form: "array",
33127
+ optional: false
33128
+ }],
33129
+ "snapshot.invalidateCache": [{
33130
+ name: "deviceId",
33131
+ form: "single",
33132
+ optional: false
33133
+ }],
33134
+ "streamBroker.acquireEgressTranscode": [{
33135
+ name: "deviceId",
33136
+ form: "single",
33137
+ optional: false
33138
+ }],
33139
+ "streamBroker.assignProfile": [{
33140
+ name: "deviceId",
33141
+ form: "single",
33142
+ optional: false
33143
+ }],
33144
+ "streamBroker.getDeviceAudioMute": [{
33145
+ name: "deviceId",
33146
+ form: "single",
33147
+ optional: false
33148
+ }],
33149
+ "streamBroker.getStreamWithCodec": [{
33150
+ name: "deviceId",
33151
+ form: "single",
33152
+ optional: false
33153
+ }],
33154
+ "streamBroker.produceEventMedia": [{
33155
+ name: "deviceId",
33156
+ form: "single",
33157
+ optional: false
33158
+ }],
33159
+ "streamBroker.publishCameraStream": [{
33160
+ name: "deviceId",
33161
+ form: "single",
33162
+ optional: false
33163
+ }],
33164
+ "streamBroker.renderPreBufferClip": [{
33165
+ name: "deviceId",
33166
+ form: "single",
33167
+ optional: false
33168
+ }],
33169
+ "streamBroker.restartProfile": [{
33170
+ name: "deviceId",
33171
+ form: "single",
33172
+ optional: false
33173
+ }],
33174
+ "streamBroker.retractCameraStream": [{
33175
+ name: "deviceId",
33176
+ form: "single",
33177
+ optional: false
33178
+ }],
33179
+ "streamBroker.setDeviceAudioMute": [{
33180
+ name: "deviceId",
33181
+ form: "single",
33182
+ optional: false
33183
+ }],
33184
+ "streamBroker.unassignProfile": [{
33185
+ name: "deviceId",
33186
+ form: "single",
33187
+ optional: false
33188
+ }],
33189
+ "streamCatalog.getCatalog": [{
33190
+ name: "deviceId",
33191
+ form: "single",
33192
+ optional: false
33193
+ }],
33194
+ "streamParams.getConfigSchema": [{
33195
+ name: "deviceId",
33196
+ form: "single",
33197
+ optional: false
33198
+ }],
33199
+ "streamParams.getOptions": [{
33200
+ name: "deviceId",
33201
+ form: "single",
33202
+ optional: false
33203
+ }],
33204
+ "streamParams.setProfile": [{
33205
+ name: "deviceId",
33206
+ form: "single",
33207
+ optional: false
33208
+ }],
33209
+ "switch.setState": [{
33210
+ name: "deviceId",
33211
+ form: "single",
33212
+ optional: false
33213
+ }],
33214
+ "vacuumControl.locate": [{
33215
+ name: "deviceId",
33216
+ form: "single",
33217
+ optional: false
33218
+ }],
33219
+ "vacuumControl.pause": [{
33220
+ name: "deviceId",
33221
+ form: "single",
33222
+ optional: false
33223
+ }],
33224
+ "vacuumControl.returnToBase": [{
33225
+ name: "deviceId",
33226
+ form: "single",
33227
+ optional: false
33228
+ }],
33229
+ "vacuumControl.setFanSpeed": [{
33230
+ name: "deviceId",
33231
+ form: "single",
33232
+ optional: false
33233
+ }],
33234
+ "vacuumControl.start": [{
33235
+ name: "deviceId",
33236
+ form: "single",
33237
+ optional: false
33238
+ }],
33239
+ "vacuumControl.stop": [{
33240
+ name: "deviceId",
33241
+ form: "single",
33242
+ optional: false
33243
+ }],
33244
+ "valve.close": [{
33245
+ name: "deviceId",
33246
+ form: "single",
33247
+ optional: false
33248
+ }],
33249
+ "valve.open": [{
33250
+ name: "deviceId",
33251
+ form: "single",
33252
+ optional: false
33253
+ }],
33254
+ "valve.setPosition": [{
33255
+ name: "deviceId",
33256
+ form: "single",
33257
+ optional: false
33258
+ }],
33259
+ "valve.stop": [{
33260
+ name: "deviceId",
33261
+ form: "single",
33262
+ optional: false
33263
+ }],
33264
+ "videoclips.getClipPlayback": [{
33265
+ name: "deviceId",
33266
+ form: "single",
33267
+ optional: false
33268
+ }],
33269
+ "videoclips.listClips": [{
33270
+ name: "deviceId",
33271
+ form: "single",
33272
+ optional: false
33273
+ }],
33274
+ "waterHeater.setAway": [{
33275
+ name: "deviceId",
33276
+ form: "single",
33277
+ optional: false
33278
+ }],
33279
+ "waterHeater.setOperationMode": [{
33280
+ name: "deviceId",
33281
+ form: "single",
33282
+ optional: false
33283
+ }],
33284
+ "waterHeater.setTargetTemp": [{
33285
+ name: "deviceId",
33286
+ form: "single",
33287
+ optional: false
33288
+ }],
33289
+ "webrtcSession.addIceCandidate": [{
33290
+ name: "deviceId",
33291
+ form: "single",
33292
+ optional: false
33293
+ }],
33294
+ "webrtcSession.closeSession": [{
33295
+ name: "deviceId",
33296
+ form: "single",
33297
+ optional: false
33298
+ }],
33299
+ "webrtcSession.createSession": [{
33300
+ name: "deviceId",
33301
+ form: "single",
33302
+ optional: false
33303
+ }],
33304
+ "webrtcSession.getIceCandidates": [{
33305
+ name: "deviceId",
33306
+ form: "single",
33307
+ optional: false
33308
+ }],
33309
+ "webrtcSession.getSessionState": [{
33310
+ name: "deviceId",
33311
+ form: "single",
33312
+ optional: false
33313
+ }],
33314
+ "webrtcSession.handleAnswer": [{
33315
+ name: "deviceId",
33316
+ form: "single",
33317
+ optional: false
33318
+ }],
33319
+ "webrtcSession.handleOffer": [{
33320
+ name: "deviceId",
33321
+ form: "single",
33322
+ optional: false
33323
+ }],
33324
+ "webrtcSession.hasAdaptiveBitrate": [{
33325
+ name: "deviceId",
33326
+ form: "single",
33327
+ optional: false
33328
+ }],
33329
+ "webrtcSession.listStreams": [{
33330
+ name: "deviceId",
33331
+ form: "single",
33332
+ optional: false
33333
+ }],
33334
+ "zoneAnalytics.getCameraHistory": [{
33335
+ name: "deviceId",
33336
+ form: "single",
33337
+ optional: false
33338
+ }],
33339
+ "zoneAnalytics.getCurrentSnapshot": [{
33340
+ name: "deviceId",
33341
+ form: "single",
33342
+ optional: false
33343
+ }],
33344
+ "zoneAnalytics.getUnzonedHistory": [{
33345
+ name: "deviceId",
33346
+ form: "single",
33347
+ optional: false
33348
+ }],
33349
+ "zoneAnalytics.getZoneHistory": [{
33350
+ name: "deviceId",
33351
+ form: "single",
33352
+ optional: false
33353
+ }],
33354
+ "zoneRules.listRules": [{
33355
+ name: "deviceId",
33356
+ form: "single",
33357
+ optional: false
33358
+ }],
33359
+ "zoneRules.setRules": [{
33360
+ name: "deviceId",
33361
+ form: "single",
33362
+ optional: false
33363
+ }],
33364
+ "zones.addZone": [{
33365
+ name: "deviceId",
33366
+ form: "single",
33367
+ optional: false
33368
+ }],
33369
+ "zones.listZones": [{
33370
+ name: "deviceId",
33371
+ form: "single",
33372
+ optional: false
33373
+ }],
33374
+ "zones.removeZone": [{
33375
+ name: "deviceId",
33376
+ form: "single",
33377
+ optional: false
33378
+ }],
33379
+ "zones.updateZone": [{
33380
+ name: "deviceId",
33381
+ form: "single",
33382
+ optional: false
33383
+ }]
33384
+ });
30851
33385
  Object.freeze({
30852
33386
  "broker": "broker",
30853
33387
  "device-export": "device-export",
@@ -31788,6 +34322,9 @@ function errorEnvelope(header, type, message) {
31788
34322
  * route to the right cap on the right device.
31789
34323
  */
31790
34324
  var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
34325
+ /** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
34326
+ * six lanes hide the round-trips without stampeding a booting hub. */
34327
+ var DISCOVERY_BUILD_CONCURRENCY = 6;
31791
34328
  var CAP_DOORBELL = "doorbell";
31792
34329
  var CAP_INTERCOM = "intercom";
31793
34330
  var CAP_MOTION = "motion";
@@ -31800,16 +34337,28 @@ var CAP_PRIVACY_MASK = "privacy-mask";
31800
34337
  async function handleDiscovery(header, payloadRaw, deps, callerIdentity) {
31801
34338
  if (!discoveryPayloadSchema.safeParse(payloadRaw).success) return errorEnvelope(header, "INVALID_DIRECTIVE", "Discovery payload shape unexpected");
31802
34339
  const exposed = await deps.listExposed();
31803
- const endpoints = [];
31804
- for (const entry of exposed) try {
31805
- const built = await buildEndpointsForExposedDevice(entry.deviceId, deps);
31806
- endpoints.push(...built);
31807
- } catch (err) {
31808
- deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
31809
- tags: { deviceId: entry.deviceId },
31810
- meta: { error: err instanceof Error ? err.message : String(err) }
31811
- });
31812
- }
34340
+ const built = new Array(exposed.length).fill(null);
34341
+ const queue = exposed.map((entry, index) => ({
34342
+ entry,
34343
+ index
34344
+ }));
34345
+ const lane = async () => {
34346
+ for (;;) {
34347
+ const item = queue.shift();
34348
+ if (item === void 0) return;
34349
+ try {
34350
+ built[item.index] = await buildEndpointsForExposedDevice(item.entry.deviceId, deps);
34351
+ } catch (err) {
34352
+ deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
34353
+ tags: { deviceId: item.entry.deviceId },
34354
+ meta: { error: err instanceof Error ? err.message : String(err) }
34355
+ });
34356
+ }
34357
+ }
34358
+ };
34359
+ const lanes = Math.min(DISCOVERY_BUILD_CONCURRENCY, Math.max(1, queue.length));
34360
+ await Promise.all(Array.from({ length: lanes }, lane));
34361
+ const endpoints = built.flatMap((b) => b ?? []);
31813
34362
  deps.logger.info("alexa discovery returned endpoints", { meta: {
31814
34363
  count: endpoints.length,
31815
34364
  userId: callerIdentity.userId
@@ -32136,28 +34685,6 @@ async function handleInitiateSessionWithOffer(header, endpointRaw, payloadRaw, d
32136
34685
  if (pref === "low" || pref === "mid" || pref === "high") return pref;
32137
34686
  })();
32138
34687
  const sourceProfile = explicitSourceProfile ?? mappedFromPreference ?? "high";
32139
- try {
32140
- if (isSleepingBatteryStatus(null)) {
32141
- const wakeStart = Date.now();
32142
- const wake = await api.battery.wakeForStream.mutate({
32143
- deviceId,
32144
- timeoutMs: 8e3
32145
- }).catch(() => ({
32146
- awoke: false,
32147
- durationMs: 0
32148
- }));
32149
- log.info("alexa rtc-session: battery pre-wake", { meta: {
32150
- awoke: wake.awoke,
32151
- waitedMs: wake.durationMs || Date.now() - wakeStart
32152
- } });
32153
- }
32154
- } catch (err) {
32155
- log.debug("alexa rtc-session: pre-wake skipped", { meta: { error: err instanceof Error ? err.message : String(err) } });
32156
- }
32157
- function isSleepingBatteryStatus(value) {
32158
- if (typeof value !== "object" || value === null) return false;
32159
- return value["sleeping"] === true;
32160
- }
32161
34688
  let derivedTarget;
32162
34689
  /**
32163
34690
  * The primitive's release handle, when this session acquired one. Recorded so
@@ -34196,8 +36723,30 @@ function endpointsSignature(endpoints) {
34196
36723
  }
34197
36724
  //#endregion
34198
36725
  //#region src/sync-engine.ts
36726
+ /**
36727
+ * Devices reconciled at once in {@link SyncEngine.reconcileAllExposed}. Each
36728
+ * lane's device build is a handful of cross-process tRPC calls — latency, not
36729
+ * CPU — so four lanes hide most of it without turning a boot-time cap
36730
+ * registration burst into a second stampede.
36731
+ */
36732
+ var RECONCILE_ALL_CONCURRENCY = 4;
34199
36733
  var SyncEngine = class {
34200
36734
  deps;
36735
+ /**
36736
+ * Serializes the `recordEndpoints` read-modify-write. The build/push half of
36737
+ * a reconcile runs parallel across devices, but the persisted maps are
36738
+ * whole-object snapshots: two concurrent `get → mutate → set` cycles would
36739
+ * silently drop one device's ids/signature (last writer wins).
36740
+ */
36741
+ recordChain = Promise.resolve();
36742
+ /**
36743
+ * One in-flight reconcile per device. At boot the 60 s full pass and the
36744
+ * debounced bindings-change reconciles overlap, and each one rebuilt the
36745
+ * same device serially — the live hub built camera 615's endpoint shape
36746
+ * four times inside one boot (2026-08-15). Joining the running pass costs
36747
+ * the duplicate nothing.
36748
+ */
36749
+ reconcileFlights = /* @__PURE__ */ new Map();
34201
36750
  constructor(deps) {
34202
36751
  this.deps = deps;
34203
36752
  }
@@ -34308,6 +36857,15 @@ var SyncEngine = class {
34308
36857
  * re-push a shape Alexa already has.
34309
36858
  */
34310
36859
  async reconcileDevice(deviceId) {
36860
+ const inFlight = this.reconcileFlights.get(deviceId);
36861
+ if (inFlight) return inFlight;
36862
+ const flight = this.reconcileDeviceNow(deviceId).finally(() => {
36863
+ if (this.reconcileFlights.get(deviceId) === flight) this.reconcileFlights.delete(deviceId);
36864
+ });
36865
+ this.reconcileFlights.set(deviceId, flight);
36866
+ return flight;
36867
+ }
36868
+ async reconcileDeviceNow(deviceId) {
34311
36869
  if (!this.deps.listExposed().some((e) => e.deviceId === deviceId)) return {
34312
36870
  deviceId,
34313
36871
  pushed: false
@@ -34348,26 +36906,42 @@ var SyncEngine = class {
34348
36906
  async reconcileAllExposed() {
34349
36907
  const checkedDeviceIds = [];
34350
36908
  const pushedDeviceIds = [];
34351
- for (const { deviceId } of this.deps.listExposed()) {
34352
- checkedDeviceIds.push(deviceId);
34353
- if ((await this.reconcileDevice(deviceId)).pushed) pushedDeviceIds.push(deviceId);
34354
- }
36909
+ const queue = [...this.deps.listExposed()];
36910
+ const lane = async () => {
36911
+ for (;;) {
36912
+ const entry = queue.shift();
36913
+ if (entry === void 0) return;
36914
+ checkedDeviceIds.push(entry.deviceId);
36915
+ if ((await this.reconcileDevice(entry.deviceId)).pushed) pushedDeviceIds.push(entry.deviceId);
36916
+ }
36917
+ };
36918
+ const lanes = Math.min(RECONCILE_ALL_CONCURRENCY, Math.max(1, queue.length));
36919
+ await Promise.all(Array.from({ length: lanes }, lane));
34355
36920
  return {
34356
36921
  checkedDeviceIds,
34357
36922
  pushedDeviceIds
34358
36923
  };
34359
36924
  }
34360
36925
  /** Persist the endpoint ids AND the shape signature for a device.
34361
- * `signature === null` forgets both (delete / empty build). */
36926
+ * `signature === null` forgets both (delete / empty build).
36927
+ *
36928
+ * Serialized through `recordChain`: the maps are whole-object snapshots
36929
+ * (`get` → spread → `set`), so parallel reconciles would otherwise race —
36930
+ * two devices' writes computed off the same stale snapshot, the second
36931
+ * `set` erasing the first's entry. */
34362
36932
  async recordEndpoints(deviceId, ids, signature) {
34363
- const next = { ...this.deps.getExportedEndpoints() };
34364
- if (ids.length > 0) next[deviceId] = ids;
34365
- else delete next[deviceId];
34366
- await this.deps.setExportedEndpoints(next);
34367
- const nextSignatures = { ...this.deps.getExportedSignatures() };
34368
- if (signature !== null) nextSignatures[deviceId] = signature;
34369
- else delete nextSignatures[deviceId];
34370
- await this.deps.setExportedSignatures(nextSignatures);
36933
+ const write = this.recordChain.then(async () => {
36934
+ const next = { ...this.deps.getExportedEndpoints() };
36935
+ if (ids.length > 0) next[deviceId] = ids;
36936
+ else delete next[deviceId];
36937
+ await this.deps.setExportedEndpoints(next);
36938
+ const nextSignatures = { ...this.deps.getExportedSignatures() };
36939
+ if (signature !== null) nextSignatures[deviceId] = signature;
36940
+ else delete nextSignatures[deviceId];
36941
+ await this.deps.setExportedSignatures(nextSignatures);
36942
+ });
36943
+ this.recordChain = write.then(() => void 0, () => void 0);
36944
+ return write;
34371
36945
  }
34372
36946
  };
34373
36947
  //#endregion