@camstack/addon-export-hap 1.2.28 → 1.2.30

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.
@@ -72,7 +72,7 @@ function carryForward(base, existing, keys) {
72
72
  return out;
73
73
  }
74
74
  //#endregion
75
- //#region ../types/dist/event-category-Cv9dO26A.mjs
75
+ //#region ../types/dist/event-category-Bxo5yJjt.mjs
76
76
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
77
77
  EventCategory["SystemBoot"] = "system.boot";
78
78
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -279,6 +279,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
279
279
  EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
280
280
  EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
281
281
  /**
282
+ * A node the orchestrator would otherwise place cameras on has NO usable
283
+ * inference device: the operator enabled one or more accelerators there and
284
+ * the live probe reports every one of them unavailable. Emitted once per
285
+ * TRANSITION into that state (never per dispatch), and the node is dropped
286
+ * from the placement candidate set for as long as it holds.
287
+ *
288
+ * This exists because the state was previously invisible: little-unraid
289
+ * absorbed 283k inference errors in a day while still being handed cameras,
290
+ * and nothing in the system said so.
291
+ *
292
+ * A node with no accelerators configured at all is NOT this — its devices
293
+ * are `disabled`, not `unavailable`, and the runner's default CPU pool
294
+ * serves it exactly as before.
295
+ */
296
+ EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
297
+ /**
298
+ * A camera has an OPEN detection session and has produced no detection at
299
+ * all for longer than the blind threshold — the camera is being decoded and
300
+ * inferred and is returning nothing. Emitted once per transition into blind,
301
+ * per camera.
302
+ *
303
+ * The failure it reports: a 1h43 detection blackout on the entrance camera
304
+ * that nobody noticed, because "a camera that detects nothing" and "a quiet
305
+ * camera" produce byte-identical silence.
306
+ */
307
+ EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
308
+ /**
282
309
  * Per-camera pipeline config was mutated by the orchestrator
283
310
  * (3-level settings change via `setAgentAddonDefaults` /
284
311
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -11496,6 +11523,8 @@ var QueryFilterSchema = object({
11496
11523
  where: record(string(), unknown()).optional(),
11497
11524
  whereIn: record(string(), array(unknown())).optional(),
11498
11525
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11526
+ /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
11527
+ whereNot: record(string(), unknown()).optional(),
11499
11528
  orderBy: object({
11500
11529
  field: string(),
11501
11530
  direction: _enum(["asc", "desc"])
@@ -11515,7 +11544,8 @@ var QueryFilterSchema = object({
11515
11544
  var MutationFilterSchema = object({
11516
11545
  where: record(string(), unknown()).optional(),
11517
11546
  whereIn: record(string(), array(unknown())).optional(),
11518
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
11547
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11548
+ whereNot: record(string(), unknown()).optional()
11519
11549
  });
11520
11550
  /** A single stored record: `{ id, data }`. */
11521
11551
  var SettingsRecordSchema = object({
@@ -12953,6 +12983,17 @@ var LlmImageSchema = object({
12953
12983
  bytes: _instanceof(Uint8Array),
12954
12984
  mimeType: string()
12955
12985
  });
12986
+ /**
12987
+ * Retry policy. `enabled: false` is NOT the same as `maxAttempts: 1` in intent —
12988
+ * the flag is what a consumer table flips, the count is what the operator tunes.
12989
+ * A retry doubles the wall time of a call, so the two gates that run inside a
12990
+ * notification's budget keep it off (see `CONSUMER_RETRY_POLICY` in addon-ai).
12991
+ */
12992
+ var LlmRetryPolicySchema = object({
12993
+ enabled: boolean().default(false),
12994
+ /** Total attempts INCLUDING the first. 1 = no retry. */
12995
+ maxAttempts: number().int().min(1).max(5).default(1)
12996
+ });
12956
12997
  var LlmGenerateBaseInputSchema = object({
12957
12998
  /** Collection routing (the notification-output posture). */
12958
12999
  addonId: string().optional(),
@@ -12967,7 +13008,28 @@ var LlmGenerateBaseInputSchema = object({
12967
13008
  jsonSchema: record(string(), unknown()).optional(),
12968
13009
  /** Per-call override of the profile default. */
12969
13010
  maxTokens: number().int().positive().optional(),
12970
- temperature: number().optional()
13011
+ temperature: number().optional(),
13012
+ /** Per-call override of the profile default (nucleus sampling). */
13013
+ topP: number().min(0).max(1).optional(),
13014
+ /** Per-call override of the profile default (top-k sampling). */
13015
+ topK: number().int().positive().optional(),
13016
+ /** Per-call override of `profile.timeoutMs` — the total generation bound. */
13017
+ timeoutMs: number().int().positive().optional(),
13018
+ /** Per-call override; beats both the consumer table and the profile. */
13019
+ retry: LlmRetryPolicySchema.optional(),
13020
+ /**
13021
+ * Caller-minted id that makes this generation CANCELLABLE.
13022
+ *
13023
+ * Without it a caller that stops waiting cannot stop the work: the gates race
13024
+ * the call against 8 s and free their own slot when the timer wins, while the
13025
+ * generation upstream keeps running to `profile.timeoutMs` — 60 s by default,
13026
+ * on a single-threaded local model. The per-camera bound then counts WAITS,
13027
+ * not generations, and the real load is unbounded.
13028
+ *
13029
+ * `AbortSignal` cannot cross a process boundary; an id can. Pass one here and
13030
+ * `llm.cancel({ requestId })` tears the socket down.
13031
+ */
13032
+ requestId: string().optional()
12971
13033
  });
12972
13034
  /**
12973
13035
  * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
@@ -12980,6 +13042,18 @@ var LlmGenerateBaseInputSchema = object({
12980
13042
  * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
12981
13043
  * watchdog — operator decision #3).
12982
13044
  */
13045
+ /**
13046
+ * A companion artifact that MUST land beside the main GGUF: the `mmproj`
13047
+ * projector of a vision model, or shards 2..N of a split GGUF. Carried on the
13048
+ * REF rather than looked up at install time, so what the operator approved in
13049
+ * the preview is exactly what the node downloads.
13050
+ */
13051
+ var ManagedModelExtraFileSchema = object({
13052
+ url: string(),
13053
+ filename: string(),
13054
+ sizeBytes: number(),
13055
+ sha256: string().optional()
13056
+ });
12983
13057
  var ManagedModelRefSchema = discriminatedUnion("kind", [
12984
13058
  object({
12985
13059
  kind: literal("catalog"),
@@ -12988,7 +13062,11 @@ var ManagedModelRefSchema = discriminatedUnion("kind", [
12988
13062
  object({
12989
13063
  kind: literal("url"),
12990
13064
  url: string(),
12991
- sha256: string().optional()
13065
+ sha256: string().optional(),
13066
+ /** Picker/status label; the file basename when absent. */
13067
+ label: string().optional(),
13068
+ sizeBytes: number().optional(),
13069
+ extraFiles: array(ManagedModelExtraFileSchema).optional()
12992
13070
  }),
12993
13071
  object({
12994
13072
  kind: literal("path"),
@@ -13006,13 +13084,82 @@ var ManagedRuntimeConfigSchema = object({
13006
13084
  gpuLayers: number().int().default(0),
13007
13085
  /** Default: cpus-2, clamped ≥1 (resolved node-side). */
13008
13086
  threads: number().int().optional(),
13009
- /** Concurrent slots. */
13087
+ /** Concurrent slots (`--parallel`). */
13010
13088
  parallel: number().int().default(1),
13089
+ /** Logical batch size (`-b`). Larger = faster prompt ingest, more RAM. */
13090
+ batchSize: number().int().positive().optional(),
13091
+ /** Physical batch / micro-batch (`-ub`). */
13092
+ ubatchSize: number().int().positive().optional(),
13093
+ /**
13094
+ * `--flash-attn`. Cuts KV-cache memory on the backends that implement it and
13095
+ * is a no-op elsewhere, so it is offered rather than assumed.
13096
+ */
13097
+ flashAttention: boolean().default(false),
13098
+ /**
13099
+ * `--mlock`. Pins the weights in RAM so the OS cannot page them out mid
13100
+ * inference. Costs the full model size in resident memory — which is exactly
13101
+ * what the RAM budget is counting.
13102
+ */
13103
+ mlock: boolean().default(false),
13104
+ /**
13105
+ * `--no-mmap`. Reads the whole GGUF up front instead of mapping it. Slower to
13106
+ * start, but avoids the page-fault stalls a network or spinning-disk model
13107
+ * store produces on every first token.
13108
+ */
13109
+ noMmap: boolean().default(false),
13110
+ /** `--cache-type-k` / `--cache-type-v` — quantising the KV cache is the
13111
+ * cheapest way to fit a longer context in the same RAM. */
13112
+ cacheTypeK: _enum([
13113
+ "f32",
13114
+ "f16",
13115
+ "q8_0",
13116
+ "q5_1",
13117
+ "q5_0",
13118
+ "q4_1",
13119
+ "q4_0"
13120
+ ]).optional(),
13121
+ cacheTypeV: _enum([
13122
+ "f32",
13123
+ "f16",
13124
+ "q8_0",
13125
+ "q5_1",
13126
+ "q5_0",
13127
+ "q4_1",
13128
+ "q4_0"
13129
+ ]).optional(),
13130
+ /**
13131
+ * Escape hatch for llama-server flags this schema does NOT model — `--jinja`
13132
+ * (which most vision chat templates need and some language-only models
13133
+ * dislike), `--cont-batching`, `--rope-scaling`, …
13134
+ *
13135
+ * It is NOT a second place to set the flags above. A token that collides
13136
+ * with a typed field is REJECTED at start, naming the field that owns it
13137
+ * (`assertNoOwnedFlags`), because two knobs writing the same argv is exactly
13138
+ * the "two switches that disagree" failure this repo has already shipped
13139
+ * twice (D62).
13140
+ */
13141
+ extraArgs: array(string()).default([]),
13011
13142
  /** Else lazy: first generate boots it. */
13012
13143
  autoStart: boolean().default(false),
13013
13144
  /** 0 = never; frees RAM after quiet periods. */
13014
13145
  idleStopMinutes: number().int().default(30)
13015
13146
  });
13147
+ /**
13148
+ * Where a multi-GB install currently is. A single 0..1 fraction cannot answer
13149
+ * "is it stuck?" for an install that is three files (shards + mmproj) followed
13150
+ * by a sha256 pass over 22 GB — during which the fraction sat at 1.0 and the
13151
+ * node looked hung. Phase + file + bytes is the smallest shape that does.
13152
+ */
13153
+ var LlmDownloadProgressSchema = object({
13154
+ phase: _enum(["downloading", "verifying"]),
13155
+ /** The artifact currently moving, e.g. `mmproj-F16.gguf`. */
13156
+ file: string(),
13157
+ fileIndex: number().int(),
13158
+ fileCount: number().int(),
13159
+ /** Across the WHOLE install, not the current file. */
13160
+ downloadedBytes: number(),
13161
+ totalBytes: number().optional()
13162
+ });
13016
13163
  var LlmRuntimeStatusSchema = object({
13017
13164
  /** Status is ALWAYS node-qualified. */
13018
13165
  nodeId: string(),
@@ -13029,6 +13176,8 @@ var LlmRuntimeStatusSchema = object({
13029
13176
  modelPath: string().optional(),
13030
13177
  modelId: string().optional(),
13031
13178
  downloadProgress: number().min(0).max(1).optional(),
13179
+ /** Detail behind `downloadProgress`; present for the same lifetime. */
13180
+ download: LlmDownloadProgressSchema.optional(),
13032
13181
  lastError: string().optional(),
13033
13182
  crashesInWindow: number(),
13034
13183
  /** Child RSS (sampled best-effort). */
@@ -13039,7 +13188,14 @@ var LlmNodeModelSchema = object({
13039
13188
  file: string(),
13040
13189
  sizeBytes: number(),
13041
13190
  catalogId: string().optional(),
13042
- installedAt: number().optional()
13191
+ installedAt: number().optional(),
13192
+ /**
13193
+ * Absolute path on the node. Present so a file that is on disk but matches
13194
+ * no catalog entry — a custom Hugging Face install, or a GGUF the operator
13195
+ * copied in by hand — is still SELECTABLE, as a `{kind:'path'}` ref. Without
13196
+ * it the picker could list such a file and do nothing with it.
13197
+ */
13198
+ path: string().optional()
13043
13199
  });
13044
13200
  var LlmRuntimeDiskUsageSchema = object({
13045
13201
  nodeId: string(),
@@ -13095,10 +13251,47 @@ var LlmProfileSchema = object({
13095
13251
  baseUrl: string().optional(),
13096
13252
  /** ConfigUISchema type:'password' — never round-trips (spec §5). */
13097
13253
  apiKey: string().optional(),
13254
+ /** Vision on/off. A vision call against a `false` profile is REFUSED, never
13255
+ * degraded to text — that shipped once and produced a confident answer to a
13256
+ * question about a picture nobody sent. */
13098
13257
  supportsVision: boolean(),
13099
13258
  temperature: number().min(0).max(2).optional(),
13259
+ /** Nucleus sampling. Every wire we speak has it. */
13260
+ topP: number().min(0).max(1).optional(),
13261
+ /** Top-k sampling. Carried only by the wires that have it — NEITHER OpenAI
13262
+ * wire does, and the client drops it there (measured: the request body gets
13263
+ * `top_p` and no `top_k`). The profile editor hides the field wherever it
13264
+ * would change nothing; `KINDS_WITH_TOP_K` is the single owner of that list. */
13265
+ topK: number().int().positive().optional(),
13100
13266
  maxTokens: number().int().positive().optional(),
13267
+ /** Prompt context window. Advisory for cloud kinds (they enforce their own);
13268
+ * for `managed-local` it is the llama.cpp `--ctx-size` the runtime starts
13269
+ * the model with, so it is the one field that changes a PROCESS. */
13270
+ contextLength: number().int().positive().optional(),
13271
+ /** Default system prompt. A caller's `system` REPLACES it (never appends —
13272
+ * two system prompts fighting is worse than either alone). */
13273
+ systemPrompt: string().optional(),
13274
+ /** Total generation bound — the only one a unary call has. */
13101
13275
  timeoutMs: number().int().positive().default(6e4),
13276
+ /** The TCP handshake only — "is the port even open". NOT the wait for
13277
+ * response headers: on the LM Studio / llama-server wire those are written
13278
+ * once the model has finished loading, so they belong to the bound below. */
13279
+ connectTimeoutMs: number().int().positive().default(1e4),
13280
+ /** Accepted, but no output yet — response headers included, because a cold
13281
+ * GPU load is exactly what happens before them. */
13282
+ firstTokenTimeoutMs: number().int().positive().default(12e4),
13283
+ /** Output started then stopped. */
13284
+ idleTimeoutMs: number().int().positive().default(6e4),
13285
+ /** Profile-level default. The per-consumer table and a per-call override
13286
+ * both beat it — see `resolveRetryPolicy`. */
13287
+ retry: LlmRetryPolicySchema.default({
13288
+ enabled: false,
13289
+ maxAttempts: 1
13290
+ }),
13291
+ /** Whether this profile may use tools. The tool-call plumbing rides the
13292
+ * library; the REGISTRY of callable tools is ours and is empty in v1, so a
13293
+ * `true` here buys the wiring, not behaviour, until tools are registered. */
13294
+ toolsEnabled: boolean().default(false),
13102
13295
  extraHeaders: record(string(), string()).optional(),
13103
13296
  /** kind === 'managed-local' only (spec §4). */
13104
13297
  runtime: ManagedRuntimeConfigSchema.optional()
@@ -13148,6 +13341,36 @@ var ManagedModelCatalogEntrySchema = object({
13148
13341
  /** Vision models: companion projector file. */
13149
13342
  mmprojUrl: string().optional()
13150
13343
  });
13344
+ /**
13345
+ * The outcome of turning one operator-typed Hugging Face reference into a
13346
+ * download plan. A RESULT, never a throw: "this repo has 24 quantizations and
13347
+ * I will not pick for you" is a normal answer the UI has to render, not an
13348
+ * exception.
13349
+ *
13350
+ * `candidates` is the whole reason the refusal is usable — every string in it
13351
+ * is a tag that resolves when pasted back as `<org>/<repo>:<TAG>`.
13352
+ */
13353
+ var HfModelResolutionSchema = discriminatedUnion("ok", [object({
13354
+ ok: literal(true),
13355
+ /** Ready to hand to `installModel` unchanged. */
13356
+ model: ManagedModelRefSchema,
13357
+ label: string(),
13358
+ repo: string(),
13359
+ quantization: string(),
13360
+ purpose: _enum(["text", "vision"]),
13361
+ totalBytes: number(),
13362
+ /** mmproj + shards, for the preview: an operator approving 23 GB should
13363
+ * see that 0.9 GB of it is a projector they did not name. */
13364
+ extraFilenames: array(string())
13365
+ }), object({
13366
+ ok: literal(false),
13367
+ code: string(),
13368
+ message: string(),
13369
+ candidates: array(string()).optional(),
13370
+ /** Set when the refusal was only the ceiling: re-calling with
13371
+ * `maxBytes: requiredBytes` is the operator's explicit override. */
13372
+ requiredBytes: number().optional()
13373
+ })]);
13151
13374
  var LlmRuntimeNodeSchema = object({
13152
13375
  nodeId: string(),
13153
13376
  reachable: boolean(),
@@ -13160,7 +13383,10 @@ var ProfileRefInputSchema = object({
13160
13383
  addonId: string(),
13161
13384
  profileId: string()
13162
13385
  });
13163
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
13386
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
13387
+ addonId: string().optional(),
13388
+ requestId: string()
13389
+ }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
13164
13390
  kind: "mutation",
13165
13391
  auth: "admin"
13166
13392
  }), method(ProfileRefInputSchema, _void(), {
@@ -13181,6 +13407,15 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13181
13407
  consumer: string().optional(),
13182
13408
  profileId: string().optional()
13183
13409
  }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
13410
+ /** `https://huggingface.co/<org>/<repo>/resolve/main/<f>.gguf`,
13411
+ * `<org>/<repo>/<f>.gguf`, `<org>/<repo>` or `<org>/<repo>:<QUANT>`. */
13412
+ ref: string(),
13413
+ /** Explicit ceiling override, in bytes. Absent = the built-in ceiling. */
13414
+ maxBytes: number().positive().optional()
13415
+ }), HfModelResolutionSchema, {
13416
+ kind: "mutation",
13417
+ auth: "admin"
13418
+ }), method(object({
13184
13419
  nodeId: string(),
13185
13420
  model: ManagedModelRefSchema
13186
13421
  }), _void(), {
@@ -14790,6 +15025,8 @@ var NcSystemEventKindSchema = _enum([
14790
15025
  "stream-offline",
14791
15026
  "node-online",
14792
15027
  "node-offline",
15028
+ "node-inference-unavailable",
15029
+ "detection-blind",
14793
15030
  "addon-update-available",
14794
15031
  "server-update-available",
14795
15032
  "alarm-triggered",
@@ -14851,7 +15088,16 @@ var NcScheduleSchema = object({
14851
15088
  });
14852
15089
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
14853
15090
  var NcPlateMatcherSchema = object({
14854
- values: array(string().min(1)).min(1),
15091
+ /**
15092
+ * Plate texts (or gallery vehicle names) to match. EMPTY = **any plate the
15093
+ * pipeline could read** — the plate half of "no selection = no narrowing",
15094
+ * and the switch that says this rule is about vehicles that were IDENTIFIED
15095
+ * rather than merely seen. A subject carrying no plate still fails.
15096
+ *
15097
+ * The `.min(1)` this used to carry made that state unauthorable; nothing has
15098
+ * ever persisted an empty list, so widening it cannot change an existing rule.
15099
+ */
15100
+ values: array(string().min(1)),
14855
15101
  /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
14856
15102
  maxDistance: number().int().min(0).max(3).default(1)
14857
15103
  });
@@ -14885,28 +15131,36 @@ var NcOccupancyConditionSchema = object({
14885
15131
  /**
14886
15132
  * Audio condition (IMMEDIATE trigger) — a rule on SOUND, not on a picture.
14887
15133
  *
14888
- * Operator-approved vocabulary (2026-08-12, option A — the same one the
14889
- * reference notifier uses, so an operator moving between them re-uses what
14890
- * they already know): a rule matches when, over a sampling window of
14891
- * `samplingSeconds`, at least `hitPercent`% of the audio samples in that
14892
- * window are HITS. A sample is a hit when it satisfies BOTH present filters:
14893
- *
14894
- * - `dbThreshold` its level is at or above this many dBFS (see
14895
- * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale);
14896
- * - `labels` the classifier put at least one of these labels on it.
14897
- *
14898
- * Both are OPTIONAL and independent, which is the point of the shape: a
14899
- * loudness rule ("something loud at 3am") needs no model to be right, and a
14900
- * label rule ("a dog barked") needs no threshold. **Fail-closed when NEITHER
14901
- * is given** a window in which every sample is trivially a hit would fire on
14902
- * silence, so the engine refuses such a condition rather than notifying on
14903
- * nothing (the schema cannot express "at least one of" without becoming a
14904
- * ZodEffects the cap path would have to special-case).
14905
- *
14906
- * `hitPercent` is over the samples the window actually HOLDS, and the window
14907
- * must be FULL before it can match a window that has been open for two
14908
- * seconds of its ten is 100% of nothing, and firing on it would make
14909
- * `samplingSeconds` decorative.
15134
+ * **TWO EXCLUSIVE MODES** (operator decision 2026-08-14, D157). Which one a
15135
+ * rule is in is not a stored field it is WHICH FILTER the rule carries, so
15136
+ * there is no second switch that can disagree with the first and every rule
15137
+ * authored before the decision migrates for free (`audioModeOf`):
15138
+ *
15139
+ * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15140
+ * classifier labels with one of them. No window, no percentage:
15141
+ * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15142
+ * `throttle` cooldown is the only brake. The per-label confidence floor is
15143
+ * the analyzer's (`classificationMinScore`, per device) — a label only
15144
+ * reaches this condition if the classifier was already confident enough.
15145
+ * - **LEVEL mode `dbThreshold` present, no labels.** The sampling window IS
15146
+ * the condition: at least `hitPercent`% of the samples over
15147
+ * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
15148
+ * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale). The window
15149
+ * must be FULL before it can match a window open for two of its ten
15150
+ * seconds is 100% of nothing.
15151
+ *
15152
+ * **Why label mode has no window.** It had one, and it never fired: the
15153
+ * analyzer emits ~1 audio frame per second but YAMNet only LABELS one to three
15154
+ * of them per episode, even through continuous crying. The measured maximum
15155
+ * `hitPercent` over the whole live history was 40 — under the shipped default
15156
+ * of 60, so a label rule could not fire at all, ever. A percentage of frames is
15157
+ * the wrong question to ask of a sparse classifier.
15158
+ *
15159
+ * **Fail-closed when NEITHER is given** — every sample would be a trivial hit
15160
+ * and the rule would fire on silence. The schema cannot express "exactly one
15161
+ * of" without becoming a ZodEffects the cap path would have to special-case, so
15162
+ * the exclusivity is enforced where every editor writes (`patchAudio`) and a
15163
+ * legacy rule carrying both resolves to LABEL (the mode that fires).
14910
15164
  *
14911
15165
  * Labels are the audio macro classes (`AUDIO_MACRO_LABELS` / the NC taxonomy's
14912
15166
  * `audio-*` ids). Both spellings are accepted — the matcher normalizes the
@@ -14914,13 +15168,13 @@ var NcOccupancyConditionSchema = object({
14914
15168
  * an operator who typed `dog` mean the same thing.
14915
15169
  */
14916
15170
  var NcAudioConditionSchema = object({
14917
- /** Audio macro labels; absent = any sound (level-only rule). */
15171
+ /** LABEL MODE: audio macro labels. Present fires on the first labelled frame. */
14918
15172
  labels: array(string().min(1)).min(1).optional(),
14919
- /** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
15173
+ /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14920
15174
  dbThreshold: number().min(-96).max(0).optional(),
14921
- /** Percentage of the window's samples that must be hits (1–100). */
15175
+ /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14922
15176
  hitPercent: number().int().min(1).max(100).default(60),
14923
- /** Length of the sampling window in seconds. */
15177
+ /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14924
15178
  samplingSeconds: number().int().min(1).max(300).default(10)
14925
15179
  });
14926
15180
  /**
@@ -15058,13 +15312,81 @@ var NcRuleActionsSchema = object({
15058
15312
  */
15059
15313
  buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
15060
15314
  });
15315
+ /**
15316
+ * "This rule applies only while `deviceId` is in one of `states`."
15317
+ *
15318
+ * The states are the DEVICE's own vocabulary — `AlarmState` for a panel,
15319
+ * `on`/`off` for a switch — not a normalised set, because normalising would
15320
+ * make the condition lie about devices whose states have no equivalent.
15321
+ *
15322
+ * An unreadable state does NOT match: see the engine's fail-closed gate. A
15323
+ * condition that fired on "I could not read it" would be worse than no gate.
15324
+ */
15325
+ var NcDeviceStateConditionSchema = object({
15326
+ deviceId: number().int(),
15327
+ /** Any of these matches. */
15328
+ states: array(string().min(1)).min(1)
15329
+ });
15330
+ /**
15331
+ * "This rule applies only while scene `sceneId` is `matched` / `diverged`."
15332
+ *
15333
+ * A GATE, not a trigger. `occupancy` and `audio` each DISCRIMINATE their rule —
15334
+ * carrying one makes the rule fire on that subject and nothing else. Scene is
15335
+ * the other shape entirely, the `deviceState` shape: it narrows a rule that
15336
+ * already has a trigger ("tell me about a person at the front door, but only
15337
+ * while the bin is still out"). That is why it composes with every delivery
15338
+ * instead of owning one, and why no new `NcDelivery` member and no new subject
15339
+ * kind exist for it — see D159.
15340
+ *
15341
+ * ── Identity ───────────────────────────────────────────────────────────────
15342
+ * `sceneId` is `SceneMonitor.id`, a `randomUUID()` minted by `createScene` —
15343
+ * globally unique, so it needs no device to disambiguate it. `deviceId` is
15344
+ * carried as a HINT for the editor and for the log line, never as part of the
15345
+ * lookup key: a rule whose hint drifted must still gate correctly.
15346
+ *
15347
+ * ── Which boolean ──────────────────────────────────────────────────────────
15348
+ * `latched` ABSENT means "whatever the scene itself says" — `SceneMonitor.emit`
15349
+ * already declares which boolean drives notification rules, and a second knob
15350
+ * that could disagree with it is exactly the D62 failure. Set it only to
15351
+ * override one rule against the scene's own default.
15352
+ *
15353
+ * - LIVE reading (`emit`/`latched` resolve to live): passes iff
15354
+ * `verdict === requiredState`. `unknown` — no reference for this light, view
15355
+ * shifted, no snapshot — passes NEITHER. A scene that cannot judge is not
15356
+ * evidence, in either direction.
15357
+ * - LATCHED reading: passes iff `latched === (requiredState === 'diverged')`.
15358
+ * The latch is a durable fact about the past ("it has diverged since I armed
15359
+ * it"), so a camera that has gone dark does not clear it — that is the whole
15360
+ * reason the operator asked for a latch.
15361
+ *
15362
+ * The gate reads an in-memory mirror (`NcSceneStateCache`) refreshed OFF the
15363
+ * event path, never the cap: D49. A mirror that has never loaded, or a scene it
15364
+ * does not carry, reads absent and the rule does NOT fire — fail closed, and
15365
+ * said out loud in the log rather than dropped in silence.
15366
+ */
15367
+ var NcSceneConditionSchema = object({
15368
+ /** `SceneMonitor.id` — the uuid the cap mints. The whole lookup key. */
15369
+ sceneId: string().min(1),
15370
+ /** The camera the scene lives on. A hint for the editor and the log line. */
15371
+ deviceId: number().int().optional(),
15372
+ /** The state the scene must be in for the rule to fire. */
15373
+ requiredState: _enum(["matched", "diverged"]),
15374
+ /**
15375
+ * Read the LATCH (`true`) or the LIVE verdict (`false`). Absent = follow the
15376
+ * scene's own `emit` field, which is the only place that decision belongs.
15377
+ */
15378
+ latched: boolean().optional()
15379
+ });
15061
15380
  var NcConditionsSchema = object({
15062
15381
  /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
15063
- deviceState: object({
15064
- deviceId: number().int(),
15065
- /** Any of these matches. */
15066
- states: array(string().min(1)).min(1)
15067
- }).optional(),
15382
+ deviceState: NcDeviceStateConditionSchema.optional(),
15383
+ /**
15384
+ * Gate on a SCENE's state — "only while the bin is still out". Composes with
15385
+ * every trigger (detection, occupancy, audio, sensor, package, track-end);
15386
+ * unlike `occupancy`/`audio` it discriminates nothing. See
15387
+ * {@link NcSceneCondition} and D159.
15388
+ */
15389
+ scene: NcSceneConditionSchema.optional(),
15068
15390
  /** Device scope — absent = all devices. */
15069
15391
  devices: array(number()).optional(),
15070
15392
  /** Detector class names (any overlap with the record's class set). */
@@ -15090,18 +15412,47 @@ var NcConditionsSchema = object({
15090
15412
  */
15091
15413
  labelEquals: array(string().min(1)).optional(),
15092
15414
  /**
15093
- * Identity matcher. P1 boundary: matched against the record's collapsed
15094
- * `label` (the identity display name propagated by the face pipeline) —
15095
- * identity-ID matching rides in P2 when identity ids reach the record.
15415
+ * KNOWN FACES the rule's identity scope, and the switch that says the rule
15416
+ * is about recognised people at all.
15417
+ *
15418
+ * Three states, and the empty one is the point:
15419
+ *
15420
+ * | value | meaning |
15421
+ * | --- | --- |
15422
+ * | absent | the rule does not care who it is; an unrecognised person matches |
15423
+ * | `[]` | **only known faces** — any identity in the gallery, nobody in particular |
15424
+ * | a list | only these identities |
15425
+ *
15426
+ * `[]` is the repo-wide "no selection = no narrowing" reading (an absent
15427
+ * `devices` list is every device), applied one level down: the operator has
15428
+ * turned the face scope ON and narrowed it to nothing, which is every known
15429
+ * face. No second field states the same thing — a switch that can disagree
15430
+ * with the list under it is worse than no switch (D62).
15431
+ *
15432
+ * MEMBERS ARE FACE-GALLERY `Identity.id`s (uuid), not display names. A name is
15433
+ * renameable, and a rule authored on "Gianluca" went silently dark the moment
15434
+ * the operator fixed the spelling. The id reaches the record on
15435
+ * `LabelAttribution.identityId`; the name is what the editor shows and what
15436
+ * `{{label}}` renders.
15437
+ *
15438
+ * Rules written before this carry NAMES, and are resolved to ids lazily at
15439
+ * load (`NcRuleStore.load`) against the live gallery — a name nothing answers
15440
+ * for is left as it stands and reported, never dropped. The engine also
15441
+ * accepts a display-name hit as a compatibility leg, so a rule whose
15442
+ * migration could not resolve keeps matching exactly what it matched before.
15096
15443
  */
15097
15444
  identities: array(string().min(1)).optional(),
15098
- /** Fuzzy plate matcher against the record's `label` (plate text). */
15445
+ /**
15446
+ * KNOWN PLATES / VEHICLES — the plate mirror of {@link identities}, including
15447
+ * the empty-list reading: `values: []` is "any plate the OCR could read",
15448
+ * a non-empty list is those plates (fuzzily). See {@link NcPlateMatcherSchema}.
15449
+ */
15099
15450
  plates: NcPlateMatcherSchema.optional(),
15100
15451
  /**
15101
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
15102
- * Same P1 boundary: matched against the record's collapsed `label` (the
15103
- * identity display name). A record with NO label passes (nothing to
15104
- * exclude), unlike the include variant which fails on an absent label.
15452
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics, and
15453
+ * the same id members and the same lazy name→id migration. A record with NO
15454
+ * identity passes (nothing to exclude), unlike the include variant which
15455
+ * fails on an unrecognised subject. An EMPTY list excludes nobody.
15105
15456
  */
15106
15457
  identitiesExclude: array(string().min(1)).optional(),
15107
15458
  /**
@@ -15493,7 +15844,80 @@ var NcRuleInputSchema = object({
15493
15844
  * a rule that predates the gate must keep delivering byte-for-byte as it
15494
15845
  * did, and absent is the only way to say that without a migration.
15495
15846
  */
15496
- confirm: NcConfirmSchema.optional()
15847
+ confirm: NcConfirmSchema.optional(),
15848
+ /**
15849
+ * WAIT for face/plate recognition before saying anything.
15850
+ *
15851
+ * A notification's TEXT is frozen at enqueue and its media is re-resolved at
15852
+ * send; the identity is neither. A face is confirmed after `confirmFrames`
15853
+ * agreeing observations — p50 **11.4 s** after the track was first seen,
15854
+ * measured on this hub — and an `immediate` rule enqueues on the first object
15855
+ * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
15856
+ * immediate path, and no amount of media re-resolution fixes a sentence.
15857
+ *
15858
+ * Only two honest answers exist, and this flag picks between them. It has
15859
+ * effect ONLY on a rule that declares a recognition scope
15860
+ * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
15861
+ * other rule there is nothing to wait for and the flag is inert.
15862
+ *
15863
+ * | value | what happens |
15864
+ * | --- | --- |
15865
+ * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
15866
+ * | 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) |
15867
+ *
15868
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15869
+ * on the addon cap path, and absent has to keep meaning exactly what every
15870
+ * rule authored before this field meant.
15871
+ *
15872
+ * The cost of `true` is stated here because the editor states it too: a rule
15873
+ * that waits also inherits track-close SEMANTICS — its `zones` condition
15874
+ * tests every zone the track visited and a `crossing` condition can no longer
15875
+ * be satisfied, because a closed track carries no crossing.
15876
+ */
15877
+ waitForEnhancement: boolean().optional(),
15878
+ /**
15879
+ * GROUP a burst of subjects into ONE notification that grows.
15880
+ *
15881
+ * Seconds of quiet after the last matching subject before the burst is
15882
+ * considered over. While it is open, the first subject enqueues immediately —
15883
+ * **exactly as today, with no added latency** — and every real growth (a new
15884
+ * subject, or a name confirmed on one already in it) REPLACES that
15885
+ * notification with an updated one naming everybody. The push carries the
15886
+ * group's own coalescing tag, so the phone replaces rather than stacks.
15887
+ *
15888
+ * `0` / absent = off, and off is today's behaviour byte for byte.
15889
+ *
15890
+ * ### Why an idle cutoff and not a window
15891
+ *
15892
+ * The measured seven-person arrival on device 590 spans 110 s with every
15893
+ * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
15894
+ * idle cutoff holds it as one and ends it when the arrival actually ends.
15895
+ * 30 is Frigate's shipped value for the same decision.
15896
+ *
15897
+ * ### What it replaces
15898
+ *
15899
+ * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
15900
+ * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
15901
+ * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
15902
+ * track that did fire and 7 carrying a confirmed identity nobody heard about.
15903
+ * A group collapses the same volume by MERGING, so the cooldown becomes a
15904
+ * budget over GROUPS — which is what it always meant — and a growth is never
15905
+ * throttled by the window its own first member spent.
15906
+ *
15907
+ * ### Interaction with {@link waitForEnhancement}
15908
+ *
15909
+ * They compose, and the order matters. `waitForEnhancement` defers the rule to
15910
+ * TRACK CLOSE, so with both set the group is opened by the first member to
15911
+ * CLOSE — already carrying its name — and grows as later members close. That
15912
+ * is later, and complete. With grouping alone the group opens on the first
15913
+ * object event and picks up names as they are confirmed, through the growth
15914
+ * path. Neither combination fires twice for one subject.
15915
+ *
15916
+ * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
15917
+ * on the addon cap path, so absent must keep meaning what it meant before this
15918
+ * field existed.
15919
+ */
15920
+ groupIdleSec: number().int().min(0).max(600).optional()
15497
15921
  });
15498
15922
  /**
15499
15923
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -15600,6 +16024,7 @@ var NcConditionDescriptorSchema = object({
15600
16024
  "occupancy",
15601
16025
  "audio",
15602
16026
  "deviceState",
16027
+ "scene",
15603
16028
  "systemEvent"
15604
16029
  ]),
15605
16030
  operator: _enum([
@@ -16419,7 +16844,7 @@ var TrackEnvelopeSchema = object({
16419
16844
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
16420
16845
  * keeps every scalar the list surfaces actually render (ids, class(es),
16421
16846
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
16422
- * zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
16847
+ * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
16423
16848
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
16424
16849
  * `getTrack`. Mirrors the event-store `projection` convention
16425
16850
  * (`getObjectEvents` et al.).
@@ -16555,7 +16980,21 @@ union([literal(1), literal(2)]);
16555
16980
  var LabelAttributionSchema = object({
16556
16981
  stepId: string(),
16557
16982
  modelId: string().optional(),
16558
- decidedAt: number()
16983
+ decidedAt: number(),
16984
+ /**
16985
+ * The GALLERY id behind a recognised tier-2 label — a face-gallery
16986
+ * `Identity.id` or a plate-gallery `Vehicle.id` (both `randomUUID`).
16987
+ *
16988
+ * The text alone is a DISPLAY NAME, and a display name is renameable: a
16989
+ * notification rule authored on "Gianluca" stopped matching the moment the
16990
+ * operator fixed the spelling in the gallery, and nothing said so. The id is
16991
+ * the thing that does not move, so it is what a rule matches on
16992
+ * (`NcConditions.identities`) and the text is what a human is shown.
16993
+ *
16994
+ * Absent when the label names no gallery row — a plate the OCR read but no
16995
+ * vehicle claims, a sub-class, a species, any tier-1 value.
16996
+ */
16997
+ identityId: string().optional()
16559
16998
  });
16560
16999
  /**
16561
17000
  * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
@@ -16692,6 +17131,28 @@ var TrackSchema = object({
16692
17131
  * `=== true` and render nothing otherwise, never infer "no face".
16693
17132
  */
16694
17133
  hasFace: boolean().optional(),
17134
+ /**
17135
+ * This subject CONTAINS a folded rider — a person the rider-pairing step
17136
+ * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
17137
+ * so the passage is tracked once and as a VEHICLE.
17138
+ *
17139
+ * It exists because the fold's record was dishonest. D34 and the code both
17140
+ * said "the person is not lost — it is reported so both entities stay on the
17141
+ * record"; in fact the pair went into a per-processor RAM field behind an
17142
+ * accessor nobody called, and every durable surface said `vehicle`, full
17143
+ * stop. This is the composition note that makes the row true.
17144
+ *
17145
+ * A COMPOSITION, never a class and never a label. "This vehicle contains a
17146
+ * person" is not an answer to "what is this" — both label tiers would refuse
17147
+ * a macro token anyway (D89), and correctly. Nothing here changes what the
17148
+ * subject IS: a cyclist stays one vehicle track, occupancy still counts one,
17149
+ * and a `person` rule still does not fire for someone cycling past.
17150
+ *
17151
+ * **Absent ≠ false**, exactly like {@link hasFace}: every row written before
17152
+ * the column, and every hub that predates the field, omits it. Test
17153
+ * `=== true` and render nothing otherwise — never infer "no rider".
17154
+ */
17155
+ hasRider: boolean().optional(),
16695
17156
  ...TrackFlagFields,
16696
17157
  ...TrackRetrainFields
16697
17158
  });
@@ -17041,7 +17502,10 @@ var RecentTracksQueryInput = object({
17041
17502
  * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
17042
17503
  cursor: string().optional(),
17043
17504
  /** See {@link TrackProjectionSchema}. Default `full`. */
17044
- projection: TrackProjectionSchema.optional()
17505
+ projection: TrackProjectionSchema.optional(),
17506
+ /** Include stationary-promoted rows (parked objects). Default false: the
17507
+ * feed lists passages; parking records live on the stationary registry. */
17508
+ includeStationary: boolean().optional()
17045
17509
  });
17046
17510
  var RecentTracksPageSchema = object({
17047
17511
  /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
@@ -17259,7 +17723,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17259
17723
  zone: TrackZoneFilterSchema.optional(),
17260
17724
  /** See {@link TrackProjectionSchema}. Default `full` (backward
17261
17725
  * compatible — omitting the field keeps today's exact behaviour). */
17262
- projection: TrackProjectionSchema.optional()
17726
+ projection: TrackProjectionSchema.optional(),
17727
+ /** Include stationary-promoted rows (parked objects handed to the
17728
+ * stationary registry). Default false: the timeline lists passages,
17729
+ * not parking records (operator decision, 2026-08-15). */
17730
+ includeStationary: boolean().optional()
17263
17731
  }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17264
17732
  kind: "mutation",
17265
17733
  auth: "admin"
@@ -17423,11 +17891,16 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17423
17891
  auth: "admin"
17424
17892
  }), method(object({
17425
17893
  eventId: string(),
17426
- kind: MediaFileKindEnum.optional()
17894
+ kind: MediaFileKindEnum.optional(),
17895
+ deviceId: number()
17427
17896
  }), array(MediaFileSchema).readonly()), method(object({
17428
17897
  trackId: string(),
17429
- kinds: array(MediaFileKindEnum).optional()
17430
- }), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
17898
+ kinds: array(MediaFileKindEnum).optional(),
17899
+ deviceId: number()
17900
+ }), array(MediaFileSchema).readonly()), method(object({
17901
+ trackId: string(),
17902
+ deviceId: number()
17903
+ }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
17431
17904
  kind: "mutation",
17432
17905
  auth: "admin"
17433
17906
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -18087,6 +18560,17 @@ var maxSessionHoldMsField = {
18087
18560
  default: 12e4,
18088
18561
  step: 5e3
18089
18562
  };
18563
+ /**
18564
+ * Quiet period that closes an `audioMode: 'on-motion'` audio window. Floor of
18565
+ * 5s so a rearm can never degenerate into per-event stream churn; default 90s
18566
+ * comfortably outlives the gap between two PIR wakes on a battery camera.
18567
+ */
18568
+ var audioMotionWindowMsField = {
18569
+ min: 5e3,
18570
+ max: 6e5,
18571
+ default: 9e4,
18572
+ step: 5e3
18573
+ };
18090
18574
  var motionFpsField = {
18091
18575
  min: 1,
18092
18576
  max: 30,
@@ -18263,6 +18747,27 @@ var RunnerCameraConfigSchema = object({
18263
18747
  * resolved `CameraDetectionConfig`.
18264
18748
  */
18265
18749
  maxSessionHoldMs: number().min(maxSessionHoldMsField.min).max(maxSessionHoldMsField.max).optional(),
18750
+ /**
18751
+ * Orchestrator-side quiet period (ms) that closes an `audioMode:
18752
+ * 'on-motion'` audio window, measured from the LAST motion event.
18753
+ *
18754
+ * This exists because the falling edge cannot be relied on. Camera-native
18755
+ * providers emit motion as a RISING EDGE ONLY (Reolink's Baichuan push and
18756
+ * its email-push SMTP path both emit `detected: true` and never the
18757
+ * counterpart); only the frame-diff analyzer emits falls. So on an
18758
+ * onboard-only camera a window that closed only on `detected: false` never
18759
+ * closed at all, and `on-motion` silently behaved as `always-on` — on a
18760
+ * battery camera, the one failure mode the mode exists to prevent.
18761
+ *
18762
+ * Every motion event rearms this timer WITHOUT restarting the stream, so a
18763
+ * burst of re-fires costs nothing. A falling edge, when one does arrive,
18764
+ * still closes earlier via `motionCooldownMs` — whichever comes first wins.
18765
+ *
18766
+ * Not consumed by the runner: carried here so it shares the per-camera
18767
+ * device-settings surface with `motionCooldownMs`, exactly like
18768
+ * `maxSessionHoldMs`.
18769
+ */
18770
+ audioMotionWindowMs: number().min(audioMotionWindowMsField.min).max(audioMotionWindowMsField.max).optional(),
18266
18771
  motionFps: number().min(motionFpsField.min).max(motionFpsField.max).default(motionFpsField.default),
18267
18772
  detectionFps: number().min(detectionFpsField.min).max(detectionFpsField.max).default(detectionFpsField.default),
18268
18773
  motionStreamId: string(),
@@ -18358,7 +18863,7 @@ var RunnerCameraConfigSchema = object({
18358
18863
  */
18359
18864
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
18360
18865
  });
18361
- 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;
18866
+ 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;
18362
18867
  /**
18363
18868
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
18364
18869
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -19344,7 +19849,31 @@ DeviceType.Camera, method(object({
19344
19849
  lastCapturedAt: number().nullable(),
19345
19850
  cacheAgeMs: number().nullable(),
19346
19851
  etag: string().nullable()
19347
- }))), systemMethod(object({
19852
+ }))), systemMethod(object({ deviceId: number() }), object({
19853
+ /** The battery slice as read, or null when the device has none. */
19854
+ battery: object({
19855
+ sleeping: boolean(),
19856
+ lastUpdated: number(),
19857
+ lastContactAt: number().optional()
19858
+ }).nullable(),
19859
+ /** The resolved snapshot state (what the overlay decision used). */
19860
+ state: object({
19861
+ isBattery: boolean(),
19862
+ reason: _enum([
19863
+ "disabled",
19864
+ "sleeping",
19865
+ "unreachable",
19866
+ "waking"
19867
+ ]).nullable()
19868
+ }),
19869
+ /** The cached frame behind the next paint. */
19870
+ frame: object({
19871
+ capturedAt: number().nullable(),
19872
+ ageMs: number().nullable()
19873
+ }),
19874
+ /** A wake window is currently open (the Waking overlay's source). */
19875
+ waking: boolean()
19876
+ })), systemMethod(object({
19348
19877
  /** The tiles a surface is actually rendering. One entry per (device,
19349
19878
  * width) the caller will paint — the width is snapped to the server's
19350
19879
  * ladder and becomes part of the link's SIGNED identity. */
@@ -19374,7 +19903,16 @@ targets: array(object({
19374
19903
  /** A sleeping battery camera: the frame is deliberately stale and will
19375
19904
  * NOT refresh in the background. A surface should say so rather than
19376
19905
  * present it as current. */
19377
- sleeping: boolean()
19906
+ sleeping: boolean(),
19907
+ /** Current device state rendered over the cached frame. State images
19908
+ * remain authoritative even when their photographic background is
19909
+ * old; null means the link must carry a current camera frame. */
19910
+ stateReason: _enum([
19911
+ "disabled",
19912
+ "sleeping",
19913
+ "unreachable",
19914
+ "waking"
19915
+ ]).nullable()
19378
19916
  })));
19379
19917
  /**
19380
19918
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20900,6 +21438,25 @@ var BatteryStatusSchema = object({
20900
21438
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
20901
21439
  lastUpdated: number(),
20902
21440
  /**
21441
+ * Ms epoch of the last time the device PROVED it was reachable — a
21442
+ * completed firmware round-trip, an observed wake, or an inbound push
21443
+ * (firmware event, email). `0`/absent = never since this slice was born.
21444
+ *
21445
+ * This is the ONLY input that separates "asleep" from "gone", and it is
21446
+ * fed exclusively by PASSIVE signals: nothing may write it by reaching
21447
+ * for the radio, because a poll that confirms reachability is the same
21448
+ * poll that drains the battery. See {@link deriveBatteryPresence} — the
21449
+ * single derivation every consumer must use; no surface computes its own.
21450
+ *
21451
+ * It is deliberately NOT a clock in the
21452
+ * `scripts/check-runtime-state-durability.ts` sense: it is the
21453
+ * observation itself, and it is the only thing a 30-hour silence is
21454
+ * visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
21455
+ * Reolink provider) so a value that means "recently" cannot cost a
21456
+ * SQLite commit per round-trip.
21457
+ */
21458
+ lastContactAt: number().optional(),
21459
+ /**
20903
21460
  * True when the source is a BINARY low-battery indicator (HA
20904
21461
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
20905
21462
  * charge level — `percentage` is then a coarse stand-in (100 = normal,
@@ -24786,14 +25343,77 @@ method(object({
24786
25343
  * thing except the comparator: `similarity` (CLIP cosine at the same ROI coords
24787
25344
  * vs condition-tagged references) and `llm` (vision-LLM judgment over the crop).
24788
25345
  *
24789
- * D14 device-config archetype (`deviceConfig.ui.kind:'widget'`) the framework
24790
- * derives the device-detail contribution; the provider carries NO hand-written
24791
- * settings-contribution methods. `status.kind:'push'` the engine pushes on
24792
- * every hysteresis flip / availability change; consumers never poll.
25346
+ * **No `deviceConfig`, deliberately.** This shipped as the D14 widget archetype,
25347
+ * which put a "Scenes" tab on one camera's detail page. That is the wrong shape
25348
+ * for the thing: a scene is a standing question about the property ("is the bin
25349
+ * still out"), and the operator's question is "which of my scenes have tripped",
25350
+ * across every camera at once — not "what does camera 617 think". Buried one
25351
+ * camera deep it also could not be found. The surface is now a top-level admin
25352
+ * page (`/scenes`, `pages/Scenes.tsx`) that lists every scene on every camera and
25353
+ * picks the camera inside the create flow, the same shape Events and Faces have.
25354
+ *
25355
+ * The consequence to keep in mind: `host/scene-monitor-editor` is gone from
25356
+ * `HOST_WIDGETS` too. `scripts/check-host-widget-resolves.ts` asserts BOTH
25357
+ * directions, so a registration nobody declares fails exactly as loudly as a
25358
+ * declaration nobody registers. The editor is imported directly by the page.
25359
+ *
25360
+ * `status.kind:'push'` — the engine pushes on every hysteresis flip /
25361
+ * availability change; consumers never poll.
24793
25362
  */
24794
- /** Extensible condition tag. Seeded 'day' | 'night'; open by design so more can
24795
- * be added without a wire break (matching falls back to any-condition refs). */
25363
+ /** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
25364
+ * captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
25365
+ * Open by design so more can be added without a wire break.
25366
+ *
25367
+ * Matching does NOT fall back across conditions: cross-condition cosines are
25368
+ * not comparable, so "I have never seen this scene in this light" is reported
25369
+ * as `unknown`, never guessed. A day reference scored against an IR frame
25370
+ * collapses the cosine and would latch a false alarm every single night. */
24796
25371
  var SceneConditionSchema = string();
25372
+ /**
25373
+ * What a scene does when the CURRENT light has no reference of its own.
25374
+ *
25375
+ * The lighting variants are not equally likely to exist. Almost every operator
25376
+ * captures daylight and then never stands outside at 22:00 to capture IR, and a
25377
+ * scene that is only ever going to be asked about a daytime question ("is the
25378
+ * bin still on the kerb at 08:00") does not need a night reference at all. The
25379
+ * night half must therefore be OPTIONAL, and optional means the scene keeps
25380
+ * working without it rather than degrading into a permanent complaint.
25381
+ *
25382
+ * - `skip` (default) — the check in that light is not made. Not a verdict, not
25383
+ * an alarm, not even an `unknown`: the live state simply stays whatever the
25384
+ * last covered light left it at, the latch is untouched, and the hysteresis
25385
+ * run is neither spent nor cleared. The scene resumes by itself at first
25386
+ * light. This is the only behaviour under which "I never captured IR" is a
25387
+ * configuration choice instead of a nightly fault.
25388
+ * - `judge-anyway` — score against the OTHER conditions' references. Available
25389
+ * for cameras whose IR frame is close enough to daylight (a floodlit
25390
+ * driveway, an always-white-light doorbell), and wrong for everything else:
25391
+ * cross-condition cosines are not comparable, so a day reference against a
25392
+ * true IR frame collapses and the scene reports a theft at 21:40.
25393
+ *
25394
+ * Never applies when the scene has NO comparable reference at all — that is
25395
+ * "not armed yet", it is reported as `no-reference-for-condition`, and silence
25396
+ * there would hide a scene the operator never finished setting up.
25397
+ */
25398
+ var SceneUncoveredPolicySchema = _enum(["skip", "judge-anyway"]);
25399
+ /** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
25400
+ * `unknown` = we cannot judge (no reference for this condition, encoder model
25401
+ * changed, view shifted, no snapshot). `unknown` is a real value, not a null,
25402
+ * and never counts toward hysteresis in either direction. */
25403
+ var SceneVerdictSchema = _enum([
25404
+ "matched",
25405
+ "diverged",
25406
+ "unknown"
25407
+ ]);
25408
+ /** Why a scene cannot judge. Named, because this feature's failure mode is
25409
+ * silence that reads as "nothing has happened". */
25410
+ var SceneUnavailableSchema = _enum([
25411
+ "no-reference-for-condition",
25412
+ "view-shifted",
25413
+ "no-vision-profile",
25414
+ "encoder-model-changed",
25415
+ "no-snapshot"
25416
+ ]);
24797
25417
  /** One captured reference — condition-tagged, model-version-gated. `embedding`
24798
25418
  * is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
24799
25419
  var SceneReferenceSchema = object({
@@ -24801,7 +25421,14 @@ var SceneReferenceSchema = object({
24801
25421
  modelId: string(),
24802
25422
  condition: SceneConditionSchema,
24803
25423
  capturedAt: number(),
24804
- thumbnailMediaId: string().optional()
25424
+ thumbnailMediaId: string().optional(),
25425
+ /** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
25426
+ * anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
25427
+ * normalized rect frame a different piece of world, and the scene would
25428
+ * diverge forever with a perfectly plausible cosine. Checked LAZILY, only
25429
+ * when hysteresis is about to flip — one extra encode per candidate
25430
+ * transition, not per poll. */
25431
+ anchorEmbedding: array(number()).optional()
24805
25432
  });
24806
25433
  var SceneMonitorStateSchema = object({
24807
25434
  id: string(),
@@ -24823,6 +25450,28 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
24823
25450
  profileId: string().optional(),
24824
25451
  hysteresisCount: number().int().positive()
24825
25452
  })]);
25453
+ var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
25454
+ /** Night is OPTIONAL. A scene with only a daylight reference sits the IR hours
25455
+ * out in silence rather than reporting a fault every night. */
25456
+ var SCENE_DEFAULT_UNCOVERED_POLICY = "skip";
25457
+ /**
25458
+ * Vision-model adjudication of a candidate flip. Field names deliberately
25459
+ * mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
25460
+ *
25461
+ * `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
25462
+ * fail-open: a notification suppressed is the worse error there, but a vision
25463
+ * model that timed out has not told us the bin is gone, and a latch is a
25464
+ * stateful claim that costs the operator a trip to reset.
25465
+ */
25466
+ var SceneConfirmSchema = object({
25467
+ enabled: boolean().default(false),
25468
+ prompt: string().min(1).max(1e3),
25469
+ profileId: string().optional(),
25470
+ timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
25471
+ maxImagePx: number().int().min(64).max(2048).default(448),
25472
+ /** What a timeout / unavailable model means for the PENDING flip. */
25473
+ onTimeout: _enum(["flip", "hold"]).default("hold")
25474
+ });
24826
25475
  var SceneMonitorSchema = object({
24827
25476
  id: string(),
24828
25477
  label: string(),
@@ -24841,7 +25490,56 @@ var SceneMonitorSchema = object({
24841
25490
  lastConfidence: number().nullable(),
24842
25491
  currentCondition: SceneConditionSchema.nullable(),
24843
25492
  availability: _enum(["ok", "unavailable"]),
24844
- unavailableReason: string().nullable()
25493
+ unavailableReason: string().nullable(),
25494
+ /** Which state is "the initial screen". `null` until the first capture. */
25495
+ baselineStateId: string().nullable(),
25496
+ /** Which boolean drives notification rules and any export. */
25497
+ emit: _enum(["latched", "live"]).default("latched"),
25498
+ /** Live: does the region match the baseline RIGHT NOW. */
25499
+ verdict: SceneVerdictSchema,
25500
+ /** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
25501
+ latched: boolean(),
25502
+ /** Last reset (or creation). */
25503
+ armedAt: number(),
25504
+ divergedAt: number().nullable(),
25505
+ restoredAt: number().nullable(),
25506
+ /** A check is only COUNTED when the device has been quiet this long. Motion
25507
+ * during the window DISCARDS the observation — a car pulling up in front of
25508
+ * the bin must not be able to spend hysteresis credit. */
25509
+ quietSeconds: number().int().min(0).max(3600).default(60),
25510
+ /** An observation only advances the pending count when it is at least this
25511
+ * far from the previously counted one, so N agreeing checks span real time
25512
+ * rather than N adjacent polls inside one occlusion. */
25513
+ minObservationSpacingSec: number().int().min(0).max(3600).default(120),
25514
+ /** Vision-model adjudication of a candidate flip. Similarity primary only. */
25515
+ confirm: SceneConfirmSchema.optional(),
25516
+ /** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
25517
+ anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
25518
+ /** Clear the latch on its own when the scene matches again? Default false —
25519
+ * `restoredAt` and the `scene-restored` edge are recorded regardless, so an
25520
+ * automation can react to the bin coming back without the operator's own
25521
+ * alarm silently clearing itself. */
25522
+ autoRestore: boolean().default(false),
25523
+ /** What to do when the current light has no reference of its own. See
25524
+ * {@link SceneUncoveredPolicySchema} — the default makes night OPTIONAL. */
25525
+ onUncoveredCondition: SceneUncoveredPolicySchema.default(SCENE_DEFAULT_UNCOVERED_POLICY),
25526
+ /**
25527
+ * The light whose checks are currently being SAT OUT under
25528
+ * `onUncoveredCondition: 'skip'` — `null` when the scene is checking normally.
25529
+ *
25530
+ * Engine-reported and advisory only: it moves no verdict, no latch and no
25531
+ * hysteresis. It exists so the card can say *"night (IR) — checks paused,
25532
+ * nothing captured in this light"* in the same calm voice as the coverage
25533
+ * line, because the alternative is a scene that silently stops answering
25534
+ * after sunset with nothing anywhere saying why. A skipped check must never
25535
+ * read as a broken one.
25536
+ */
25537
+ suspendedCondition: SceneConditionSchema.nullable().default(null),
25538
+ /** Named cause when `verdict === 'unknown'`. */
25539
+ unavailable: SceneUnavailableSchema.nullable(),
25540
+ /** Conditions that have at least one comparable reference — the coverage line
25541
+ * ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
25542
+ coveredConditions: array(SceneConditionSchema)
24845
25543
  });
24846
25544
  var SceneMonitorStatusSchema = object({
24847
25545
  monitors: array(SceneMonitorSchema),
@@ -24874,7 +25572,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24874
25572
  "both"
24875
25573
  ]).optional(),
24876
25574
  checkIntervalSec: number().optional(),
24877
- check: SceneCheckSchema.optional()
25575
+ check: SceneCheckSchema.optional(),
25576
+ emit: _enum(["latched", "live"]).optional(),
25577
+ quietSeconds: number().int().min(0).max(3600).optional(),
25578
+ minObservationSpacingSec: number().int().min(0).max(3600).optional(),
25579
+ anchorThreshold: number().min(0).max(1).optional(),
25580
+ autoRestore: boolean().optional(),
25581
+ onUncoveredCondition: SceneUncoveredPolicySchema.optional(),
25582
+ /** `null` clears the vision-model adjudicator. */
25583
+ confirm: SceneConfirmSchema.nullable().optional()
24878
25584
  })
24879
25585
  }), _void(), {
24880
25586
  kind: "mutation",
@@ -24911,6 +25617,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
24911
25617
  }), _void(), {
24912
25618
  kind: "mutation",
24913
25619
  auth: "admin"
25620
+ }), method(object({
25621
+ deviceId: number(),
25622
+ monitorId: string(),
25623
+ /** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
25624
+ recapture: boolean().optional()
25625
+ }), _void(), {
25626
+ kind: "mutation",
25627
+ auth: "admin"
24914
25628
  });
24915
25629
  /**
24916
25630
  * Per-stage gating mode applied to the zones a rule references.
@@ -25073,6 +25787,16 @@ var CamStreamDescriptorSchema = object({
25073
25787
  /** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
25074
25788
  metadata: record(string(), unknown()).optional()
25075
25789
  });
25790
+ object({
25791
+ /** The descriptors as last built from a real camera response. Never a guess:
25792
+ * a failed or refused build writes NOTHING, so a restored catalog is always
25793
+ * one the camera itself once produced. */
25794
+ descriptors: array(CamStreamDescriptorSchema),
25795
+ /** Ms epoch of the build that produced {@link descriptors}. Lets the wake
25796
+ * path decide whether the camera's own awake window is worth spending on a
25797
+ * re-read. */
25798
+ lastFetchedAt: number()
25799
+ });
25076
25800
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly());
25077
25801
  /** One of the camera's stream profiles. */
25078
25802
  var StreamProfileSchema = _enum([
@@ -25228,12 +25952,64 @@ var NetworkAddressSchema = object({
25228
25952
  family: string(),
25229
25953
  internal: boolean()
25230
25954
  });
25955
+ /**
25956
+ * Provenance of the site coordinates, and the whole reason this is not just two
25957
+ * numbers.
25958
+ *
25959
+ * - `operator-set` — a human typed it, or accepted a detection. Authoritative;
25960
+ * nothing overwrites it.
25961
+ * - `derived-from-ip` — the hub geolocated its own public IP once, because a
25962
+ * default that is right to a few kilometres beats the coarse UTC clock split
25963
+ * the sun-times consumers otherwise fall back to.
25964
+ *
25965
+ * The UI shows which one it is. An operator who cannot tell a guess from their
25966
+ * own input will eventually trust the guess.
25967
+ */
25968
+ var SiteLocationSourceSchema = _enum(["operator-set", "derived-from-ip"]);
25969
+ /**
25970
+ * The read shape: the location plus the honest state of the one-shot derivation.
25971
+ *
25972
+ * `derivationAttemptedAt` is what makes the "one call, ever" contract
25973
+ * inspectable. When it is set and `location` is null, the geo-IP lookup ran and
25974
+ * failed; the hub will NOT try again on its own — the fallback is declared
25975
+ * (consumers degrade to their own last resort) and the operator either types the
25976
+ * coordinates or presses detect.
25977
+ */
25978
+ var SiteLocationStatusSchema = object({
25979
+ location: object({
25980
+ /** WGS84 decimal degrees. */
25981
+ latitude: number().min(-90).max(90),
25982
+ longitude: number().min(-180).max(180),
25983
+ source: SiteLocationSourceSchema,
25984
+ /** Epoch ms the value was last written. */
25985
+ updatedAt: number(),
25986
+ /**
25987
+ * Human-readable place the geo-IP service reported ("Napoli, IT"). Display
25988
+ * only — never parsed, never matched on. Absent for an operator-typed value.
25989
+ */
25990
+ label: string().optional()
25991
+ }).nullable(),
25992
+ derivationAttemptedAt: number().nullable(),
25993
+ /** Why the last derivation failed, for the UI to show instead of a shrug. */
25994
+ derivationError: string().nullable()
25995
+ });
25996
+ /** `null` clears the location and re-arms nothing — the derivation stays spent. */
25997
+ var SetSiteLocationInputSchema = object({
25998
+ latitude: number().min(-90).max(90),
25999
+ longitude: number().min(-180).max(180)
26000
+ }).nullable();
25231
26001
  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(), {
25232
26002
  kind: "mutation",
25233
26003
  auth: "admin"
25234
26004
  }), method(_void(), _void(), {
25235
26005
  kind: "mutation",
25236
26006
  auth: "admin"
26007
+ }), method(_void(), SiteLocationStatusSchema), method(SetSiteLocationInputSchema, SiteLocationStatusSchema, {
26008
+ kind: "mutation",
26009
+ auth: "admin"
26010
+ }), method(_void(), SiteLocationStatusSchema, {
26011
+ kind: "mutation",
26012
+ auth: "admin"
25237
26013
  });
25238
26014
  object({
25239
26015
  /** True when the device's tamper switch / case-open contact is
@@ -27979,6 +28755,12 @@ Object.freeze({
27979
28755
  addonId: null,
27980
28756
  access: "create"
27981
28757
  },
28758
+ "llm.cancel": {
28759
+ capName: "llm",
28760
+ capScope: "system",
28761
+ addonId: null,
28762
+ access: "create"
28763
+ },
27982
28764
  "llm.deleteModel": {
27983
28765
  capName: "llm",
27984
28766
  capScope: "system",
@@ -28063,6 +28845,12 @@ Object.freeze({
28063
28845
  addonId: null,
28064
28846
  access: "view"
28065
28847
  },
28848
+ "llm.resolveModelRef": {
28849
+ capName: "llm",
28850
+ capScope: "system",
28851
+ addonId: null,
28852
+ access: "create"
28853
+ },
28066
28854
  "llm.setDefault": {
28067
28855
  capName: "llm",
28068
28856
  capScope: "system",
@@ -30229,6 +31017,12 @@ Object.freeze({
30229
31017
  addonId: null,
30230
31018
  access: "create"
30231
31019
  },
31020
+ "sceneMonitor.resetScene": {
31021
+ capName: "scene-monitor",
31022
+ capScope: "device",
31023
+ addonId: null,
31024
+ access: "delete"
31025
+ },
30232
31026
  "sceneMonitor.updateScene": {
30233
31027
  capName: "scene-monitor",
30234
31028
  capScope: "device",
@@ -30367,6 +31161,12 @@ Object.freeze({
30367
31161
  addonId: null,
30368
31162
  access: "view"
30369
31163
  },
31164
+ "snapshot.getDebugState": {
31165
+ capName: "snapshot",
31166
+ capScope: "device",
31167
+ addonId: null,
31168
+ access: "view"
31169
+ },
30370
31170
  "snapshot.getSnapshot": {
30371
31171
  capName: "snapshot",
30372
31172
  capScope: "device",
@@ -30907,6 +31707,12 @@ Object.freeze({
30907
31707
  addonId: null,
30908
31708
  access: "create"
30909
31709
  },
31710
+ "system.detectSiteLocation": {
31711
+ capName: "system",
31712
+ capScope: "system",
31713
+ addonId: null,
31714
+ access: "create"
31715
+ },
30910
31716
  "system.featureFlags": {
30911
31717
  capName: "system",
30912
31718
  capScope: "system",
@@ -30925,6 +31731,12 @@ Object.freeze({
30925
31731
  addonId: null,
30926
31732
  access: "view"
30927
31733
  },
31734
+ "system.getSiteLocation": {
31735
+ capName: "system",
31736
+ capScope: "system",
31737
+ addonId: null,
31738
+ access: "view"
31739
+ },
30928
31740
  "system.health": {
30929
31741
  capName: "system",
30930
31742
  capScope: "system",
@@ -30949,6 +31761,12 @@ Object.freeze({
30949
31761
  addonId: null,
30950
31762
  access: "create"
30951
31763
  },
31764
+ "system.setSiteLocation": {
31765
+ capName: "system",
31766
+ capScope: "system",
31767
+ addonId: null,
31768
+ access: "create"
31769
+ },
30952
31770
  "terminalSession.adoptLegacyMonitor": {
30953
31771
  capName: "terminal-session",
30954
31772
  capScope: "system",
@@ -32431,6 +33249,11 @@ Object.freeze({
32431
33249
  form: "single",
32432
33250
  optional: false
32433
33251
  }],
33252
+ "pipelineAnalytics.getEventMedia": [{
33253
+ name: "deviceId",
33254
+ form: "single",
33255
+ optional: false
33256
+ }],
32434
33257
  "pipelineAnalytics.getKeyEvents": [{
32435
33258
  name: "deviceId",
32436
33259
  form: "single",
@@ -32461,6 +33284,11 @@ Object.freeze({
32461
33284
  form: "single",
32462
33285
  optional: false
32463
33286
  }],
33287
+ "pipelineAnalytics.getTrackMedia": [{
33288
+ name: "deviceId",
33289
+ form: "single",
33290
+ optional: false
33291
+ }],
32464
33292
  "pipelineAnalytics.getTrainingExportSummary": [{
32465
33293
  name: "deviceIds",
32466
33294
  form: "array",
@@ -32496,6 +33324,11 @@ Object.freeze({
32496
33324
  form: "array",
32497
33325
  optional: true
32498
33326
  }],
33327
+ "pipelineAnalytics.listTrackMedia": [{
33328
+ name: "deviceId",
33329
+ form: "single",
33330
+ optional: false
33331
+ }],
32499
33332
  "pipelineAnalytics.listTracks": [{
32500
33333
  name: "deviceId",
32501
33334
  form: "single",
@@ -32911,6 +33744,11 @@ Object.freeze({
32911
33744
  form: "single",
32912
33745
  optional: false
32913
33746
  }],
33747
+ "sceneMonitor.resetScene": [{
33748
+ name: "deviceId",
33749
+ form: "single",
33750
+ optional: false
33751
+ }],
32914
33752
  "sceneMonitor.updateScene": [{
32915
33753
  name: "deviceId",
32916
33754
  form: "single",
@@ -32926,11 +33764,22 @@ Object.freeze({
32926
33764
  form: "single",
32927
33765
  optional: false
32928
33766
  }],
33767
+ "snapshot.getDebugState": [{
33768
+ name: "deviceId",
33769
+ form: "single",
33770
+ optional: false
33771
+ }],
32929
33772
  "snapshot.getSnapshot": [{
32930
33773
  name: "deviceId",
32931
33774
  form: "single",
32932
33775
  optional: false
32933
33776
  }],
33777
+ "snapshot.getSnapshotLinks": [{
33778
+ name: "targets",
33779
+ form: "object-array",
33780
+ optional: false,
33781
+ itemField: "deviceId"
33782
+ }],
32934
33783
  "snapshot.getSnapshotOverview": [{
32935
33784
  name: "deviceIds",
32936
33785
  form: "array",