@camstack/addon-provider-amcrest 0.2.120 → 0.2.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +877 -117
  2. package/dist/addon.mjs +877 -117
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5373,7 +5373,7 @@ var ZodIssueCode = {
5373
5373
  var ZodFirstPartyTypeKind;
5374
5374
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5375
5375
  //#endregion
5376
- //#region ../types/dist/sleep-PEo0-Fz9.mjs
5376
+ //#region ../types/dist/sleep-D_4W7xuQ.mjs
5377
5377
  /**
5378
5378
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5379
5379
  * window to float samples (D455).
@@ -7354,11 +7354,17 @@ var RawStateResultSchema = object({
7354
7354
  method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(object({ deviceId: number() }), array(ConfigEntrySchema$1)), method(object({
7355
7355
  deviceId: number(),
7356
7356
  values: record(string(), unknown())
7357
- }), _void(), { kind: "mutation" }), method(object({
7357
+ }), _void(), {
7358
+ kind: "mutation",
7359
+ auth: "admin"
7360
+ }), method(object({
7358
7361
  deviceId: number(),
7359
7362
  action: string().min(1),
7360
7363
  input: unknown()
7361
- }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7364
+ }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), {
7365
+ kind: "mutation",
7366
+ auth: "admin"
7367
+ }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7362
7368
  new Set([
7363
7369
  "track",
7364
7370
  "summary",
@@ -11582,7 +11588,7 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
11582
11588
  }), method(UnsubscribeInputSchema, _void(), {
11583
11589
  kind: "mutation",
11584
11590
  auth: "admin"
11585
- }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
11591
+ }), method(GetStateInputSchema, unknown().nullable(), { auth: "admin" }), method(_void(), RegistryStatusSchema);
11586
11592
  DeviceType.Camera;
11587
11593
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
11588
11594
  var StreamFormatSchema = _enum([
@@ -12765,17 +12771,20 @@ method(object({
12765
12771
  namespace: string().optional(),
12766
12772
  collection: string(),
12767
12773
  key: string()
12768
- }), unknown()), method(object({
12774
+ }), unknown(), { auth: "admin" }), method(object({
12769
12775
  namespace: string().optional(),
12770
12776
  collection: string(),
12771
12777
  key: string(),
12772
12778
  value: unknown()
12773
- }), _void(), { kind: "mutation" }), method(object({
12779
+ }), _void(), {
12780
+ kind: "mutation",
12781
+ auth: "admin"
12782
+ }), method(object({
12774
12783
  namespace: string().optional(),
12775
12784
  collection: string(),
12776
12785
  filter: QueryFilterSchema.optional(),
12777
12786
  columns: array(string()).readonly().optional()
12778
- }), array(SettingsRecordSchema).readonly()), method(object({
12787
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12779
12788
  namespace: string().optional(),
12780
12789
  collection: string(),
12781
12790
  /** Declared columns identifying the slot; must be covered by a UNIQUE
@@ -12785,7 +12794,10 @@ method(object({
12785
12794
  }), object({
12786
12795
  /** The id the row HAS: assigned by SQLite on an insert, or the existing
12787
12796
  * row's own id on an update. */
12788
- id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12797
+ id: union([string(), number()]) }), {
12798
+ kind: "mutation",
12799
+ auth: "admin"
12800
+ }), method(object({
12789
12801
  namespace: string().optional(),
12790
12802
  collection: string(),
12791
12803
  record: object({
@@ -12798,38 +12810,56 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12798
12810
  * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12799
12811
  * primary key — which is the only place an auto key is knowable.
12800
12812
  */
12801
- id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12813
+ id: union([string(), number()]) }), {
12814
+ kind: "mutation",
12815
+ auth: "admin"
12816
+ }), method(object({
12802
12817
  namespace: string().optional(),
12803
12818
  collection: string(),
12804
12819
  records: array(BulkRecordSchema).readonly()
12805
- }), object({ inserted: number().int() }), { kind: "mutation" }), method(object({
12820
+ }), object({ inserted: number().int() }), {
12821
+ kind: "mutation",
12822
+ auth: "admin"
12823
+ }), method(object({
12806
12824
  namespace: string().optional(),
12807
12825
  collection: string(),
12808
12826
  id: string(),
12809
12827
  data: record(string(), unknown())
12810
- }), _void(), { kind: "mutation" }), method(object({
12828
+ }), _void(), {
12829
+ kind: "mutation",
12830
+ auth: "admin"
12831
+ }), method(object({
12811
12832
  namespace: string().optional(),
12812
12833
  collection: string(),
12813
12834
  key: string()
12814
- }), _void(), { kind: "mutation" }), method(object({
12835
+ }), _void(), {
12836
+ kind: "mutation",
12837
+ auth: "admin"
12838
+ }), method(object({
12815
12839
  namespace: string().optional(),
12816
12840
  collection: string(),
12817
12841
  filter: MutationFilterSchema
12818
- }), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
12842
+ }), object({ deleted: number().int() }), {
12843
+ kind: "mutation",
12844
+ auth: "admin"
12845
+ }), method(object({
12819
12846
  namespace: string().optional(),
12820
12847
  collection: string(),
12821
12848
  filter: MutationFilterSchema,
12822
12849
  data: record(string(), unknown())
12823
- }), object({ updated: number().int() }), { kind: "mutation" }), method(object({
12850
+ }), object({ updated: number().int() }), {
12851
+ kind: "mutation",
12852
+ auth: "admin"
12853
+ }), method(object({
12824
12854
  namespace: string().optional(),
12825
12855
  collection: string(),
12826
12856
  filter: QueryFilterSchema.optional()
12827
- }), number()), method(object({
12857
+ }), number(), { auth: "admin" }), method(object({
12828
12858
  namespace: string().optional(),
12829
12859
  collection: string(),
12830
12860
  fields: array(AggregateFieldSchema).readonly(),
12831
12861
  filter: QueryFilterSchema.optional()
12832
- }), AggregateResultSchema), method(object({
12862
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
12833
12863
  namespace: string().optional(),
12834
12864
  collection: string(),
12835
12865
  field: string(),
@@ -12839,15 +12869,18 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12839
12869
  }), array(object({
12840
12870
  bucket: number().int(),
12841
12871
  count: number().int()
12842
- })).readonly()), method(object({
12872
+ })).readonly(), { auth: "admin" }), method(object({
12843
12873
  namespace: string().optional(),
12844
12874
  collection: string()
12845
- }), boolean()), method(object({
12875
+ }), boolean(), { auth: "admin" }), method(object({
12846
12876
  namespace: string().optional(),
12847
12877
  collection: string(),
12848
12878
  columns: array(CollectionColumnSchema).readonly(),
12849
12879
  indexes: array(CollectionIndexSchema).readonly().optional()
12850
- }), _void(), { kind: "mutation" });
12880
+ }), _void(), {
12881
+ kind: "mutation",
12882
+ auth: "admin"
12883
+ });
12851
12884
  /**
12852
12885
  * What one engine says about itself. The orchestrator uses `kind` to pick
12853
12886
  * a registrant for a collection; `engineId` is what a log line names when
@@ -13751,7 +13784,13 @@ var ExposeInputSchema = object({
13751
13784
  capabilities: array(string()).optional()
13752
13785
  });
13753
13786
  var UnexposeInputSchema = object({ deviceId: string() });
13754
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
13787
+ method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
13788
+ kind: "mutation",
13789
+ auth: "admin"
13790
+ }), method(UnexposeInputSchema, _void(), {
13791
+ kind: "mutation",
13792
+ auth: "admin"
13793
+ });
13755
13794
  /** Wire/storage schema for one icon id. */
13756
13795
  var LocationIconIdSchema = _enum([
13757
13796
  "map-pin",
@@ -14336,7 +14375,10 @@ method(object({
14336
14375
  kind: "mutation",
14337
14376
  auth: "admin",
14338
14377
  timeoutMs: 12 * 6e4
14339
- }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), { kind: "mutation" }), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), record(string(), unknown())), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14378
+ }), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), {
14379
+ kind: "mutation",
14380
+ auth: "admin"
14381
+ }), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
14340
14382
  deviceId: number(),
14341
14383
  name: string()
14342
14384
  }), _void(), {
@@ -14541,7 +14583,7 @@ method(object({
14541
14583
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
14542
14584
  kind: "mutation",
14543
14585
  auth: "admin"
14544
- }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
14586
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({ capNames: array(string().min(1)).min(1).max(16) }), array(number())), method(object({
14545
14587
  deviceId: number(),
14546
14588
  capName: string(),
14547
14589
  wrapperAddonId: string(),
@@ -14896,18 +14938,30 @@ var LlmErrorCodeSchema = _enum([
14896
14938
  "budget-exceeded",
14897
14939
  "adapter-error"
14898
14940
  ]);
14899
- var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
14941
+ var LlmGenerateOkSchema = object({
14900
14942
  ok: literal(true),
14901
14943
  text: string(),
14902
14944
  model: string(),
14903
14945
  usage: LlmUsageSchema,
14904
14946
  truncated: boolean(),
14905
14947
  latencyMs: number()
14906
- }), object({
14948
+ });
14949
+ /**
14950
+ * WHO refused, on a `refusal`. `guardrail` = a safety filter in front of the
14951
+ * model rejected the request before it answered (Apple Foundation Models
14952
+ * refuses surveillance-framed prompts in 0.1 s); `model` = the model itself
14953
+ * declined. They are different faults: a guardrail refusal is a property of
14954
+ * the BACKEND, so the provider falls back to the next profile; a model refusal
14955
+ * is an answer. Absent on every other code, and on a refusal a wire cannot
14956
+ * attribute.
14957
+ */
14958
+ var LlmRefusalSourceSchema = _enum(["guardrail", "model"]);
14959
+ var LlmGenerateResultSchema = discriminatedUnion("ok", [LlmGenerateOkSchema, object({
14907
14960
  ok: literal(false),
14908
14961
  code: LlmErrorCodeSchema,
14909
14962
  message: string(),
14910
- retryAfterMs: number().optional()
14963
+ retryAfterMs: number().optional(),
14964
+ refusalSource: LlmRefusalSourceSchema.optional()
14911
14965
  })]);
14912
14966
  /**
14913
14967
  * `Uint8Array` is the sanctioned binary convention — superjson + the UDS
@@ -15137,12 +15191,78 @@ var LlmRuntimeDiskUsageSchema = object({
15137
15191
  modelsBytes: number(),
15138
15192
  freeBytes: number().optional()
15139
15193
  });
15140
- method(LlmGenerateBaseInputSchema.extend({
15194
+ var LlmRuntimeCompleteInputSchema = LlmGenerateBaseInputSchema.extend({
15141
15195
  images: array(LlmImageSchema).optional(),
15142
15196
  runtime: ManagedRuntimeConfigSchema,
15143
15197
  /** The managed profile's timeout, threaded by the hub provider. */
15144
15198
  timeoutMs: number().int().positive().optional()
15145
- }), LlmGenerateResultSchema, {
15199
+ });
15200
+ /**
15201
+ * Why Apple Foundation Models cannot run on a node that has it in principle.
15202
+ * The first three are Apple's own `UnavailableReason`s, verbatim; the rest are
15203
+ * ours: the OS predates the framework (macOS < 26), the helper binary is not
15204
+ * in the installed addon, or it crashed past the respawn bound.
15205
+ */
15206
+ var AppleFmUnavailableReasonSchema = _enum([
15207
+ "deviceNotEligible",
15208
+ "appleIntelligenceNotEnabled",
15209
+ "modelNotReady",
15210
+ "osTooOld",
15211
+ "helperMissing",
15212
+ "helperCrashed"
15213
+ ]);
15214
+ /**
15215
+ * Apple Foundation Models on ONE node — four states, never a boolean (D315).
15216
+ *
15217
+ * - `available` — the helper probed it; what it CAN do is reported
15218
+ * here (`imageInput`, `contextSize`, languages),
15219
+ * never typed by an operator.
15220
+ * - `unavailable` — a known absence, with the reason to act on.
15221
+ * - `unsupported-platform` — not a Mac; there is nothing to enable.
15222
+ * - `unknown` — the probe did not answer (not yet run, timed
15223
+ * out, helper crashed mid-probe). A failed
15224
+ * measurement is NEVER `unavailable` (D393).
15225
+ */
15226
+ var AppleFmAvailabilitySchema = discriminatedUnion("state", [
15227
+ object({
15228
+ state: literal("available"),
15229
+ osVersion: string(),
15230
+ /** Tokens, as the framework reports it. Read at runtime, never assumed. */
15231
+ contextSize: number().int().positive(),
15232
+ /** BCP-47-ish tags (`it-IT`) of the languages the model supports. */
15233
+ languages: array(string()),
15234
+ /** Whether prompts may carry images. `false` on a macOS 26 build. */
15235
+ imageInput: boolean()
15236
+ }),
15237
+ object({
15238
+ state: literal("unavailable"),
15239
+ reason: AppleFmUnavailableReasonSchema,
15240
+ osVersion: string().nullable(),
15241
+ detail: string()
15242
+ }),
15243
+ object({
15244
+ state: literal("unsupported-platform"),
15245
+ platform: string()
15246
+ }),
15247
+ object({
15248
+ state: literal("unknown"),
15249
+ error: string()
15250
+ })
15251
+ ]);
15252
+ /**
15253
+ * A generation on the node's Apple Foundation Models helper. A separate verb
15254
+ * from `complete`, whose `runtime` is a llama.cpp config: overloading it would
15255
+ * drag a GGUF model reference into a backend that has none. `images` is part
15256
+ * of the contract so vision needs no cap change later; a node whose model has
15257
+ * no image input REFUSES them (`unavailable`), never drops them.
15258
+ */
15259
+ var LlmRuntimeAppleCompleteInputSchema = LlmGenerateBaseInputSchema.extend({
15260
+ images: array(LlmImageSchema).optional(),
15261
+ /** The profile's timeout, threaded by the hub provider. */
15262
+ timeoutMs: number().int().positive().optional()
15263
+ });
15264
+ var LlmRuntimeCancelInputSchema = object({ requestId: string() });
15265
+ method(LlmRuntimeCompleteInputSchema, LlmGenerateResultSchema, {
15146
15266
  kind: "mutation",
15147
15267
  auth: "admin",
15148
15268
  timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
@@ -15160,7 +15280,14 @@ method(LlmGenerateBaseInputSchema.extend({
15160
15280
  }), method(object({ file: string() }), _void(), {
15161
15281
  kind: "mutation",
15162
15282
  auth: "admin"
15163
- }), method(object({}), array(LlmNodeModelSchema), { auth: "admin" }), method(object({}), LlmRuntimeDiskUsageSchema, { auth: "admin" });
15283
+ }), method(object({}), array(LlmNodeModelSchema), { auth: "admin" }), method(object({}), LlmRuntimeDiskUsageSchema, { auth: "admin" }), method(LlmRuntimeCancelInputSchema, _void(), {
15284
+ kind: "mutation",
15285
+ auth: "admin"
15286
+ }), method(object({}), AppleFmAvailabilitySchema, { auth: "admin" }), method(LlmRuntimeAppleCompleteInputSchema, LlmGenerateResultSchema, {
15287
+ kind: "mutation",
15288
+ auth: "admin",
15289
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
15290
+ });
15164
15291
  /**
15165
15292
  * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
15166
15293
  * methods concat-fan across providers; single-row methods route to ONE
@@ -15177,7 +15304,8 @@ var LlmProfileKindSchema = _enum([
15177
15304
  "openai",
15178
15305
  "anthropic",
15179
15306
  "google",
15180
- "managed-local"
15307
+ "managed-local",
15308
+ "apple-foundation"
15181
15309
  ]);
15182
15310
  var LlmProfileSchema = object({
15183
15311
  id: string(),
@@ -15235,7 +15363,14 @@ var LlmProfileSchema = object({
15235
15363
  toolsEnabled: boolean().default(false),
15236
15364
  extraHeaders: record(string(), string()).optional(),
15237
15365
  /** kind === 'managed-local' only (spec §4). */
15238
- runtime: ManagedRuntimeConfigSchema.optional()
15366
+ runtime: ManagedRuntimeConfigSchema.optional(),
15367
+ /**
15368
+ * kind === 'apple-foundation' only: WHERE it runs — the only thing there is
15369
+ * to configure (no key, no URL, no download; `model` is `apple-system`).
15370
+ * `supportsVision` on such a row is stamped by the provider from the node's
15371
+ * reported `imageInput`, never taken from the operator (D62).
15372
+ */
15373
+ appleFoundation: object({ nodeId: string() }).optional()
15239
15374
  });
15240
15375
  /** ConfigUISchema tree passed through untyped on the wire (the
15241
15376
  * notification-output `ConfigSchemaPassthrough` precedent at
@@ -15317,7 +15452,9 @@ var LlmRuntimeNodeSchema = object({
15317
15452
  reachable: boolean(),
15318
15453
  status: LlmRuntimeStatusSchema.optional(),
15319
15454
  disk: LlmRuntimeDiskUsageSchema.optional(),
15320
- error: string().optional()
15455
+ error: string().optional(),
15456
+ /** Absent = the node did not answer the question (render as unknown). */
15457
+ apple: AppleFmAvailabilitySchema.optional()
15321
15458
  });
15322
15459
  var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
15323
15460
  var ProfileRefInputSchema = object({
@@ -16088,7 +16225,22 @@ var StatusSchema = object({
16088
16225
  brokerCount: number(),
16089
16226
  embeddedRunning: boolean()
16090
16227
  });
16091
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16228
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
16229
+ kind: "mutation",
16230
+ auth: "admin"
16231
+ }), method(IdInputSchema, _void(), {
16232
+ kind: "mutation",
16233
+ auth: "admin"
16234
+ }), method(IdInputSchema, TestResultSchema$1, {
16235
+ kind: "mutation",
16236
+ auth: "admin"
16237
+ }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
16238
+ kind: "mutation",
16239
+ auth: "admin"
16240
+ }), method(IdInputSchema, _void(), {
16241
+ kind: "mutation",
16242
+ auth: "admin"
16243
+ }), method(_void(), StatusSchema);
16092
16244
  var NetworkEndpointSchema = object({
16093
16245
  url: string(),
16094
16246
  hostname: string(),
@@ -16383,6 +16535,18 @@ var TargetKindSchema = object({
16383
16535
  */
16384
16536
  iconMediaType: string().optional(),
16385
16537
  configSchema: ConfigSchemaPassthrough,
16538
+ /**
16539
+ * The config keys that decide WHERE this kind's secrets are sent (server,
16540
+ * base URL, instance, chat, recipient). A save that changes any of them
16541
+ * while leaving a secret redacted is refused by the provider (D634), so an
16542
+ * editor uses this to ask for the secret on exactly those edits instead of
16543
+ * on every edit.
16544
+ *
16545
+ * Optional: absent from a provider that holds no secret (the Home Assistant
16546
+ * kinds) and from a provider older than this field — an editor that finds
16547
+ * it absent must assume ANY edit may need the secret.
16548
+ */
16549
+ destinationKeys: array(string()).optional(),
16386
16550
  supportsDiscovery: boolean(),
16387
16551
  caps: TargetKindCapsSchema
16388
16552
  });
@@ -16421,19 +16585,34 @@ var SendResultSchema = object({
16421
16585
  });
16422
16586
  /** Same shape as SendResult — kept as a distinct name for the test panel. */
16423
16587
  var TestResultSchema = SendResultSchema;
16424
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16588
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({
16425
16589
  kind: string(),
16426
16590
  config: record(string(), unknown()).optional()
16427
- }), array(DiscoveredTargetSchema)), method(object({
16591
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
16428
16592
  targetId: string(),
16429
16593
  notification: NotificationSchema
16430
- }), SendResultSchema, { kind: "mutation" }), method(object({
16594
+ }), SendResultSchema, {
16595
+ kind: "mutation",
16596
+ auth: "admin"
16597
+ }), method(object({
16431
16598
  targetId: string(),
16432
16599
  sample: NotificationSchema.optional()
16433
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16600
+ }), TestResultSchema, {
16601
+ kind: "mutation",
16602
+ auth: "admin"
16603
+ }), method(object({ target: TargetSchema }), TargetSchema, {
16604
+ kind: "mutation",
16605
+ auth: "admin"
16606
+ }), method(object({ targetId: string() }), _void(), {
16607
+ kind: "mutation",
16608
+ auth: "admin"
16609
+ }), method(object({
16434
16610
  targetId: string(),
16435
16611
  enabled: boolean()
16436
- }), _void(), { kind: "mutation" });
16612
+ }), _void(), {
16613
+ kind: "mutation",
16614
+ auth: "admin"
16615
+ });
16437
16616
  new Set([
16438
16617
  {
16439
16618
  id: "person",
@@ -16954,6 +17133,7 @@ sub("presence", "sensor", "sensor", "#22c55e", "presence", "Presence");
16954
17133
  sub("enum-sensor", "sensor", "sensor", TAXONOMY_COLORS.sensor, "generic", "Sensor state");
16955
17134
  sub("device-event", "sensor", "sensor", "#10b981", "button", "Device event");
16956
17135
  sub("lock", "control", "control", "#0ea5e9", "lock", "Lock");
17136
+ sub("cover", "control", "control", "#0ea5e9", "door", "Cover");
16957
17137
  sub("switch", "control", "control", TAXONOMY_COLORS.control, "switch", "Switch");
16958
17138
  sub("siren", "control", "control", "#dc2626", "siren", "Siren");
16959
17139
  sub("button", "control", "control", "#10b981", "button", "Button");
@@ -17272,6 +17452,10 @@ var NcSystemEventKindSchema = _enum([
17272
17452
  "alarm-disarmed",
17273
17453
  "alarm-arming",
17274
17454
  "alarm-arm-refused",
17455
+ "alarm-pending",
17456
+ "alarm-rearmed",
17457
+ "alarm-sensor-bypass",
17458
+ "alarm-check-failed",
17275
17459
  "addon-updated",
17276
17460
  "server-updated",
17277
17461
  "export-completed",
@@ -17309,6 +17493,19 @@ var NcSystemEventConditionSchema = object({
17309
17493
  * does not carry) matches no `deviceTypes` list.
17310
17494
  */
17311
17495
  deviceTypes: array(string().min(1)).min(1).optional(),
17496
+ /**
17497
+ * Devices this rule NEVER speaks about, whatever else matched. Scoped to the
17498
+ * same kind family as `deviceIds` (`systemEventFilterApplies(kind,
17499
+ * 'deviceIds')`) and applied AFTER it: include first, then exclude. A
17500
+ * consumable subject (`consumable:<id>:<key>`) is excluded by its device id.
17501
+ *
17502
+ * It never fails closed: a subject that carries no device id is not in the
17503
+ * list, so it is not excluded. "Every battery except the one thermometer that
17504
+ * reads 0 % forever" is the question this answers — expressed as an include
17505
+ * list it would have to name every other battery device, and every device
17506
+ * adopted later would be silently left out (D636).
17507
+ */
17508
+ excludeDeviceIds: array(number().int()).min(1).optional(),
17312
17509
  nodeIds: array(string().min(1)).min(1).optional(),
17313
17510
  packageNames: array(string().min(1)).min(1).optional()
17314
17511
  });
@@ -17867,6 +18064,19 @@ var NcMediaFrameSchema = _enum([
17867
18064
  "full",
17868
18065
  "boxed"
17869
18066
  ]);
18067
+ /**
18068
+ * How a SENSOR notification shows the cameras that link the sensor.
18069
+ *
18070
+ * `mosaic` — one image composed of every linked camera's photograph, taken at
18071
+ * the trigger. `each` — one image per camera where the target takes several
18072
+ * attachments; a target that takes one keeps the first camera's (the degrade
18073
+ * engine drops the rest by order). Either way ONE notification per event.
18074
+ * Absent = `mosaic`, applied by the Notification Center and deliberately NOT
18075
+ * a Zod default (see NcRulePatchSchema: a default materialises on every
18076
+ * partial patch). Meaningless for a rule whose subject is not a sensor, and a
18077
+ * sensor linked to one camera ships that camera's plain snapshot either way.
18078
+ */
18079
+ var NcLinkedCameraModeSchema = _enum(["mosaic", "each"]);
17870
18080
  var NcMediaPolicySchema = object({
17871
18081
  attach: _enum([
17872
18082
  "best",
@@ -17912,7 +18122,9 @@ var NcMediaPolicySchema = object({
17912
18122
  * A profile that is not assigned falls back to the cheapest, and the render
17913
18123
  * reports which one actually ran.
17914
18124
  */
17915
- profile: CamProfileSchema.optional()
18125
+ profile: CamProfileSchema.optional(),
18126
+ /** See {@link NcLinkedCameraModeSchema}. */
18127
+ linkedCameras: NcLinkedCameraModeSchema.optional()
17916
18128
  });
17917
18129
  /**
17918
18130
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -18032,7 +18244,7 @@ var NcRuleInputSchema = object({
18032
18244
  cooldownSec: 60,
18033
18245
  scope: "rule-device"
18034
18246
  }),
18035
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
18247
+ /** `{{var}}` title/body. The variables are declared in `NC_TEMPLATE_VARS` (@camstack/types) and depend on the rule kind — see `templateVarsFor`. */
18036
18248
  template: object({
18037
18249
  title: string().max(500).optional(),
18038
18250
  body: string().max(2e3).optional()
@@ -18212,6 +18424,22 @@ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
18212
18424
  throttle: NcThrottleSchema.optional(),
18213
18425
  priority: number().int().min(1).max(5).optional()
18214
18426
  });
18427
+ var NcRuleClearableKeySchema = _enum([
18428
+ "template",
18429
+ "schedule",
18430
+ "targetUsers",
18431
+ "snoozeOptions",
18432
+ "snoozeAllowGlobal",
18433
+ "waitForEnhancement",
18434
+ "actions",
18435
+ "confirm",
18436
+ "groupIdleSec"
18437
+ ]);
18438
+ var NcRuleUpdateInputSchema = object({
18439
+ ruleId: string(),
18440
+ patch: NcRulePatchSchema,
18441
+ clear: array(NcRuleClearableKeySchema).optional()
18442
+ });
18215
18443
  /** A persisted rule. */
18216
18444
  var NcRuleSchema = NcRuleInputSchema.extend({
18217
18445
  id: string(),
@@ -18243,6 +18471,85 @@ var NcTestResultSchema = object({
18243
18471
  className: string().optional(),
18244
18472
  label: string().optional()
18245
18473
  });
18474
+ /** The five rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts`
18475
+ * is the same union; the template-vars spec pins the two together. */
18476
+ var NcRuleKindSchema = _enum([
18477
+ "detection",
18478
+ "sensor",
18479
+ "occupancy",
18480
+ "sound",
18481
+ "system"
18482
+ ]);
18483
+ /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
18484
+ var NcTemplateFamilySchema = _enum([
18485
+ "rule",
18486
+ "timelapse",
18487
+ "summary"
18488
+ ]);
18489
+ /** Which text field of that producer. `previewText` is the timelapse frame
18490
+ * caption, `captionText` the digest mosaic caption — both see fewer vars. */
18491
+ var NcTemplateFieldSchema = _enum([
18492
+ "title",
18493
+ "body",
18494
+ "previewText",
18495
+ "captionText"
18496
+ ]);
18497
+ var NcTemplateVarGroupSchema = _enum([
18498
+ "subject",
18499
+ "place",
18500
+ "time",
18501
+ "rule",
18502
+ "occupancy",
18503
+ "sound",
18504
+ "sensor",
18505
+ "system",
18506
+ "digest",
18507
+ "ai"
18508
+ ]);
18509
+ /**
18510
+ * ONE `{{var}}` a notification template may name.
18511
+ *
18512
+ * `families` / `fields` / `kinds` / `deliveries` / `systemEventKinds` say WHERE
18513
+ * it has a value; absent = no restriction on that axis. `systemEventKinds` is
18514
+ * read only for a `system` rule. `dynamic` declares a FAMILY of names
18515
+ * (`count_<class>`): the descriptor's `name` is the example member.
18516
+ */
18517
+ var NcTemplateVarDescriptorSchema = object({
18518
+ name: string().regex(/^\w+$/),
18519
+ label: string(),
18520
+ description: string().optional(),
18521
+ example: string(),
18522
+ group: NcTemplateVarGroupSchema,
18523
+ families: array(NcTemplateFamilySchema).min(1),
18524
+ fields: array(NcTemplateFieldSchema).optional(),
18525
+ kinds: array(NcRuleKindSchema).optional(),
18526
+ deliveries: array(NcDeliverySchema).optional(),
18527
+ systemEventKinds: array(NcSystemEventKindSchema).optional(),
18528
+ dynamic: object({
18529
+ prefix: literal("count_"),
18530
+ from: literal("classes")
18531
+ }).optional()
18532
+ });
18533
+ /** Input to `previewTemplate` — the editor's own context, plus the draft text. */
18534
+ var NcTemplatePreviewInputSchema = object({
18535
+ context: object({
18536
+ family: NcTemplateFamilySchema,
18537
+ field: NcTemplateFieldSchema,
18538
+ kind: NcRuleKindSchema.optional(),
18539
+ delivery: NcDeliverySchema.optional(),
18540
+ systemEventKinds: array(NcSystemEventKindSchema).optional()
18541
+ }),
18542
+ template: object({
18543
+ title: string().max(500).optional(),
18544
+ body: string().max(2e3).optional()
18545
+ })
18546
+ });
18547
+ var NcTemplatePreviewSchema = object({
18548
+ title: string().nullable(),
18549
+ body: string().nullable(),
18550
+ /** Names the template uses that this context never fills — they render empty. */
18551
+ unknown: array(string())
18552
+ });
18246
18553
  var NcConditionDescriptorSchema = object({
18247
18554
  /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
18248
18555
  id: string(),
@@ -18508,8 +18815,17 @@ object({
18508
18815
  lastAt: number()
18509
18816
  });
18510
18817
  /**
18511
- * The three durations the panel's state machine runs on, plus who hears about
18512
- * an arm.
18818
+ * The per-mode lists, at most one per mode. Two lists for one mode have no
18819
+ * meaning a reader could agree on (union? the last one?), so they are refused
18820
+ * at the boundary rather than interpreted.
18821
+ */
18822
+ var NcAlarmNonBlockingListSchema = array(object({
18823
+ mode: AlarmArmModeSchema,
18824
+ deviceIds: array(number().int()).max(200)
18825
+ })).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
18826
+ /**
18827
+ * The three durations the panel's state machine runs on, who hears about its
18828
+ * transitions, and which openings each mode tolerates.
18513
18829
  *
18514
18830
  * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
18515
18831
  * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
@@ -18528,21 +18844,39 @@ var NcAlarmSettingsSchema = object({
18528
18844
  * existed, and therefore what an untouched install keeps doing.
18529
18845
  */
18530
18846
  triggeredDurationSec: number().int().min(0).max(3600),
18531
- /** Send a notification when a mode takes effect. */
18532
- announceArm: boolean(),
18533
18847
  /**
18534
- * Where that notification goes. Target ids from `notification-output`.
18848
+ * Deprecated by D630 — every transition is announced. Parsed, never read;
18849
+ * remove after one release. Optional so an older admin's patch (which still
18850
+ * sends it) parses rather than failing the whole save.
18851
+ */
18852
+ announceArm: boolean().optional(),
18853
+ /**
18854
+ * Target ids from `notification-output`: every alarm transition goes here.
18535
18855
  *
18536
- * Explicit rather than "everyone": an arm announcement is a household
18856
+ * Explicit rather than "everyone": an alarm announcement is a household
18537
18857
  * message, and broadcasting it to every configured endpoint (including a
18538
18858
  * webhook wired to something else) is not a default anybody would choose.
18539
- * Empty with `announceArm: true` sends nothing, and the server logs that —
18540
- * silence must be attributable.
18859
+ * Empty sends nothing, and the server logs that — silence must be
18860
+ * attributable.
18861
+ */
18862
+ announceTargets: array(string().min(1)).max(16),
18863
+ /**
18864
+ * Per arm mode, the covered devices whose being OPEN does not refuse the
18865
+ * arm (a window left ajar for the cat under `home`, say). At most one entry
18866
+ * per mode is meaningful. Defaulted to `[]` so a blob stored before D630
18867
+ * parses as "every opening blocks" — the behaviour it was written under.
18541
18868
  */
18542
- announceTargets: array(string().min(1)).max(16)
18869
+ nonBlocking: NcAlarmNonBlockingListSchema.default([])
18543
18870
  });
18544
- /** Every field optional — a tab edits one control at a time. */
18545
- var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
18871
+ /**
18872
+ * Every field optional — a tab edits one control at a time.
18873
+ *
18874
+ * `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
18875
+ * remove an inner `.default()`, so a patch that never named the field would
18876
+ * parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
18877
+ * (the same trap {@link NcRulePatchSchema} documents).
18878
+ */
18879
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
18546
18880
  /**
18547
18881
  * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
18548
18882
  * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
@@ -18587,6 +18921,36 @@ var NcAlarmModeCoverageSchema = object({
18587
18921
  */
18588
18922
  skippedDevices: array(NcAlarmSkippedDeviceSchema).default([])
18589
18923
  });
18924
+ /** One covered device that reads open, and whether that refuses the arm. */
18925
+ var NcAlarmOpeningSchema = object({
18926
+ deviceId: number().int(),
18927
+ name: string(),
18928
+ /** The device's state word as read (`open`, `unlocked`, …). */
18929
+ state: string(),
18930
+ /** False when the mode's non-blocking list names this device. */
18931
+ blocking: boolean()
18932
+ });
18933
+ /** A device the panel is currently ignoring, and which side of it it is on. */
18934
+ var NcAlarmExclusionViewSchema = object({
18935
+ deviceId: number().int(),
18936
+ name: string(),
18937
+ phase: _enum(["open", "closed"])
18938
+ });
18939
+ /** One mode's openings: devices read open, plus those whose state is unknown. */
18940
+ var NcAlarmModeOpeningsSchema = object({
18941
+ mode: AlarmArmModeSchema,
18942
+ devices: array(NcAlarmOpeningSchema),
18943
+ /** Covered devices whose state could not be read (D49): neither open nor closed. */
18944
+ unknown: array(number().int())
18945
+ });
18946
+ var NcAlarmLiveSchema = object({
18947
+ state: AlarmStateSchema,
18948
+ /** The mode being armed into (exit delay) or held; null when disarmed. */
18949
+ targetMode: AlarmArmModeSchema.nullable(),
18950
+ availableModes: array(AlarmArmModeSchema),
18951
+ openings: array(NcAlarmModeOpeningsSchema),
18952
+ exclusions: array(NcAlarmExclusionViewSchema)
18953
+ });
18590
18954
  var NcAlarmConfigSchema = object({
18591
18955
  /**
18592
18956
  * The panel's device id, or null when this install has no panel (the ensure
@@ -18596,7 +18960,13 @@ var NcAlarmConfigSchema = object({
18596
18960
  */
18597
18961
  deviceId: number().int().nullable(),
18598
18962
  settings: NcAlarmSettingsSchema,
18599
- coverage: array(NcAlarmModeCoverageSchema)
18963
+ coverage: array(NcAlarmModeCoverageSchema),
18964
+ /**
18965
+ * The panel as it is NOW — state, what each mode would find open, what is
18966
+ * excluded. Optional: a panel-less hub answers without it, and a client
18967
+ * must not read its absence as "nothing is open".
18968
+ */
18969
+ live: NcAlarmLiveSchema.optional()
18600
18970
  });
18601
18971
  /**
18602
18972
  * ONE rule's demand on ONE camera's clip ring.
@@ -18644,10 +19014,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18644
19014
  kind: "mutation",
18645
19015
  auth: "admin",
18646
19016
  caller: "required"
18647
- }), method(object({
18648
- ruleId: string(),
18649
- patch: NcRulePatchSchema
18650
- }), object({ rule: NcRuleSchema }), {
19017
+ }), method(NcRuleUpdateInputSchema, object({ rule: NcRuleSchema }), {
18651
19018
  kind: "mutation",
18652
19019
  auth: "admin",
18653
19020
  caller: "required"
@@ -18675,7 +19042,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18675
19042
  }), method(object({}), object({
18676
19043
  catalog: array(NcConditionDescriptorSchema),
18677
19044
  taxonomy: NcTaxonomySchema.optional()
18678
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
19045
+ })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
19046
+ kind: "mutation",
19047
+ access: "view"
19048
+ }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
18679
19049
  kind: "mutation",
18680
19050
  caller: "required"
18681
19051
  }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
@@ -23148,6 +23518,7 @@ var CAP_TO_KIND = {
23148
23518
  "enum-sensor": "enum-sensor",
23149
23519
  "event-emitter": "device-event",
23150
23520
  "lock-control": "lock",
23521
+ cover: "cover",
23151
23522
  switch: "switch",
23152
23523
  button: "button",
23153
23524
  doorbell: "doorbell"
@@ -23812,41 +24183,65 @@ var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object(
23812
24183
  method(object({
23813
24184
  location: StorageLocationRefSchema,
23814
24185
  relativePath: string()
23815
- }), string()), method(object({
24186
+ }), string(), { auth: "admin" }), method(object({
23816
24187
  location: StorageLocationRefSchema,
23817
24188
  relativePath: string(),
23818
24189
  data: _instanceof(Uint8Array)
23819
- }), _void(), { kind: "mutation" }), method(object({
24190
+ }), _void(), {
24191
+ kind: "mutation",
24192
+ auth: "admin"
24193
+ }), method(object({
23820
24194
  location: StorageLocationRefSchema,
23821
24195
  relativePath: string()
23822
- }), _instanceof(Uint8Array)), method(object({
24196
+ }), _instanceof(Uint8Array), { auth: "admin" }), method(object({
23823
24197
  location: StorageLocationRefSchema,
23824
24198
  relativePath: string()
23825
- }), boolean()), method(object({
24199
+ }), boolean(), { auth: "admin" }), method(object({
23826
24200
  location: StorageLocationRefSchema,
23827
24201
  prefix: string().optional()
23828
- }), array(string()).readonly()), method(object({
24202
+ }), array(string()).readonly(), { auth: "admin" }), method(object({
23829
24203
  location: StorageLocationRefSchema,
23830
24204
  relativePath: string()
23831
- }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationRefSchema }), number().nullable()), method(object({
24205
+ }), _void(), {
24206
+ kind: "mutation",
24207
+ auth: "admin"
24208
+ }), method(object({ location: StorageLocationRefSchema }), number().nullable()), method(object({
23832
24209
  location: StorageLocationRefSchema,
23833
24210
  relativePath: string(),
23834
24211
  sizeBytes: number().optional()
23835
- }), object({ uploadId: string() }), { kind: "mutation" }), method(object({
24212
+ }), object({ uploadId: string() }), {
24213
+ kind: "mutation",
24214
+ auth: "admin"
24215
+ }), method(object({
23836
24216
  uploadId: string(),
23837
24217
  offset: number(),
23838
24218
  data: _instanceof(Uint8Array)
23839
- }), _void(), { kind: "mutation" }), method(object({ uploadId: string() }), _void(), { kind: "mutation" }), method(object({ uploadId: string() }), _void(), { kind: "mutation" }), method(object({
24219
+ }), _void(), {
24220
+ kind: "mutation",
24221
+ auth: "admin"
24222
+ }), method(object({ uploadId: string() }), _void(), {
24223
+ kind: "mutation",
24224
+ auth: "admin"
24225
+ }), method(object({ uploadId: string() }), _void(), {
24226
+ kind: "mutation",
24227
+ auth: "admin"
24228
+ }), method(object({
23840
24229
  location: StorageLocationRefSchema,
23841
24230
  relativePath: string()
23842
24231
  }), object({
23843
24232
  downloadId: string(),
23844
24233
  sizeBytes: number()
23845
- }), { kind: "mutation" }), method(object({
24234
+ }), {
24235
+ kind: "mutation",
24236
+ auth: "admin"
24237
+ }), method(object({
23846
24238
  downloadId: string(),
23847
24239
  offset: number(),
23848
24240
  length: number()
23849
- }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
24241
+ }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
24242
+ kind: "mutation",
24243
+ auth: "admin"
24244
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
23850
24245
  createdAt: true,
23851
24246
  updatedAt: true
23852
24247
  }), StorageLocationSchema, {
@@ -25860,7 +26255,67 @@ var WebrtcLadderDecisionSchema = object({
25860
26255
  serverSending: boolean().nullable(),
25861
26256
  /** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
25862
26257
  * must not read like one from this second. */
25863
- at: number()
26258
+ at: number(),
26259
+ /** The broker's own cause when it is finer than `cause` can carry, e.g.
26260
+ * `trial-revert` (reported as `stall`/`loss` for older consumers). Optional:
26261
+ * an older server strips it instead of rejecting the answer. */
26262
+ causeDetail: string().optional()
26263
+ });
26264
+ var WebrtcClimbHoldSchema = object({
26265
+ /** `stall-building` — the client receives ≪ what is sent; `delivery` — the
26266
+ * current rung is not landing whole; `no-evidence` — no receiver feedback;
26267
+ * `warming` — healthy, accumulating the streak; `cooldown` — too soon after
26268
+ * a change; `capacity` — a MEASURED probe below the next rung's bar;
26269
+ * `try-unclean` — no capacity evidence, and the path is not clean enough to
26270
+ * try; `try-backoff` — a try reverted, waiting out its backoff; `trial` —
26271
+ * this rung IS a try, reverting if it does not land; `suspended` —
26272
+ * recorded playback on the track. */
26273
+ code: string(),
26274
+ /** The rung a climb would go to. */
26275
+ nextTier: CamProfileSchema,
26276
+ /** Bytes received ÷ bytes sent over the delivery window (≥ one GOP). */
26277
+ deliveryRatio: number().nullable(),
26278
+ deliveryWindowMs: number().nullable(),
26279
+ /** The ratio a climb requires. */
26280
+ climbDeliveryRatio: number(),
26281
+ /** The receiver's bandwidth estimate (REMB), kbps. Capped by what is sent. */
26282
+ capacityKbps: number().nullable(),
26283
+ /** The viewer's downlink probe for this session, kbps. */
26284
+ probeKbps: number().nullable(),
26285
+ /** The next rung's measured bitrate, kbps. */
26286
+ nextTierKbps: number().nullable(),
26287
+ capacityMargin: number(),
26288
+ /** The capacity a climb to `nextTier` needs, kbps (bitrate × margin, or the
26289
+ * client ladder threshold when the rung has measured nothing). */
26290
+ neededKbps: number().nullable(),
26291
+ /** For `try-unclean`: which clean-path condition failed (a `WebrtcTryBlocker`,
26292
+ * carried as a string for the same reason as `code`). */
26293
+ tryBlockedBy: string().nullable(),
26294
+ /** The delivery ratio and the loss ratio a try requires / must stay under.
26295
+ * Optional, like every field added to this schema after it first shipped:
26296
+ * the router validates output, so a REQUIRED new field makes a newer server
26297
+ * reject an older broker's whole answer (review m1). */
26298
+ tryDeliveryRatio: number().optional(),
26299
+ tryMaxLossRatio: number().optional(),
26300
+ /** For `try-backoff`: time until the next try, ms. */
26301
+ retryInMs: number().nullable(),
26302
+ /** The last try that reverted in this viewer session. */
26303
+ lastRevert: object({
26304
+ tier: CamProfileSchema,
26305
+ deliveryRatio: number().nullable(),
26306
+ stalled: boolean(),
26307
+ at: number()
26308
+ }).nullable(),
26309
+ /** For `trial`: how long this rung has been on trial, and for how long it must hold. */
26310
+ trialForMs: number().nullable(),
26311
+ trialMs: number().nullable(),
26312
+ healthyForMs: number().nullable(),
26313
+ requiredHealthyMs: number().nullable(),
26314
+ /** Epoch ms this hold's code began. */
26315
+ since: number(),
26316
+ /** The same one-line account the broker logs, e.g.
26317
+ * `held: delivery 0.97 over 4.0s ok, REMB 480 kbps < mid 1800 kbps × 1.2`. */
26318
+ summary: string()
25864
26319
  });
25865
26320
  /** Per-tier count of MEASURED downgrades this viewer session has suffered. */
25866
26321
  var WebrtcTierFailureSchema = object({
@@ -25924,7 +26379,12 @@ var WebrtcSessionDebugSchema = object({
25924
26379
  tierFailures: array(WebrtcTierFailureSchema),
25925
26380
  /** Successful promotions in this viewer session (drives the progressive
25926
26381
  * healthy timer). Null when no ladder is running. */
25927
- upgradeCount: number().nullable()
26382
+ upgradeCount: number().nullable(),
26383
+ /** Why the ladder is not climbing right now. Null when no ladder runs, it
26384
+ * is at the top rung, or it just moved. Optional because this output is
26385
+ * validated by the router: a server carrying this schema must still accept
26386
+ * the answer of a stream-broker deployed before the field existed. */
26387
+ climbHold: WebrtcClimbHoldSchema.nullable().optional()
25928
26388
  });
25929
26389
  DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
25930
26390
  deviceId: number().int().nonnegative(),
@@ -31145,6 +31605,11 @@ var ClientNetworkStatsSchema = object({
31145
31605
  jitterMs: number(),
31146
31606
  estimatedBandwidthKbps: number(),
31147
31607
  packetLossPercent: number().min(0).max(100),
31608
+ /** The viewer's latest `/adaptive-probe` reading, Mbps — whatever it read
31609
+ * (D635 I4). Absent when it never probed or the probe failed (unknown). */
31610
+ downlinkMbps: number().optional(),
31611
+ /** Age of that reading when this report was sent, ms. */
31612
+ downlinkAgeMs: number().optional(),
31148
31613
  lastUpdated: number()
31149
31614
  });
31150
31615
  var DeviceNetworkStatsSchema = object({
@@ -31157,7 +31622,11 @@ method(object({ deviceId: number() }), DeviceNetworkStatsSchema.nullable()), met
31157
31622
  rttMs: number().min(0).max(6e4),
31158
31623
  jitterMs: number().min(0).max(1e4),
31159
31624
  estimatedBandwidthKbps: number().min(0).max(1e6),
31160
- packetLossPercent: number().min(0).max(100)
31625
+ packetLossPercent: number().min(0).max(100),
31626
+ /** Latest downlink probe, Mbps, whatever it read; absent = never / failed. */
31627
+ downlinkMbps: number().min(0).max(1e5).optional(),
31628
+ /** Age of that probe when this report is sent, ms. */
31629
+ downlinkAgeMs: number().min(0).max(864e5).optional()
31161
31630
  }), _void(), { kind: "mutation" });
31162
31631
  /**
31163
31632
  * nodes — system-scoped singleton capability for cluster topology
@@ -41971,6 +42440,12 @@ Object.freeze({
41971
42440
  addonId: null,
41972
42441
  access: "view"
41973
42442
  },
42443
+ "deviceManager.listDeviceIdsByCaps": {
42444
+ capName: "device-manager",
42445
+ capScope: "system",
42446
+ addonId: null,
42447
+ access: "view"
42448
+ },
41974
42449
  "deviceManager.listLocationDescriptions": {
41975
42450
  capName: "device-manager",
41976
42451
  capScope: "system",
@@ -42931,12 +43406,30 @@ Object.freeze({
42931
43406
  addonId: null,
42932
43407
  access: "create"
42933
43408
  },
43409
+ "llmRuntime.appleAvailability": {
43410
+ capName: "llm-runtime",
43411
+ capScope: "system",
43412
+ addonId: null,
43413
+ access: "view"
43414
+ },
43415
+ "llmRuntime.cancel": {
43416
+ capName: "llm-runtime",
43417
+ capScope: "system",
43418
+ addonId: null,
43419
+ access: "create"
43420
+ },
42934
43421
  "llmRuntime.complete": {
42935
43422
  capName: "llm-runtime",
42936
43423
  capScope: "system",
42937
43424
  addonId: null,
42938
43425
  access: "create"
42939
43426
  },
43427
+ "llmRuntime.completeApple": {
43428
+ capName: "llm-runtime",
43429
+ capScope: "system",
43430
+ addonId: null,
43431
+ access: "create"
43432
+ },
42940
43433
  "llmRuntime.deleteModel": {
42941
43434
  capName: "llm-runtime",
42942
43435
  capScope: "system",
@@ -43777,6 +44270,12 @@ Object.freeze({
43777
44270
  addonId: null,
43778
44271
  access: "view"
43779
44272
  },
44273
+ "notificationRules.getTemplateCatalog": {
44274
+ capName: "notification-rules",
44275
+ capScope: "system",
44276
+ addonId: null,
44277
+ access: "view"
44278
+ },
43780
44279
  "notificationRules.listDeviceMutes": {
43781
44280
  capName: "notification-rules",
43782
44281
  capScope: "system",
@@ -43795,6 +44294,12 @@ Object.freeze({
43795
44294
  addonId: null,
43796
44295
  access: "view"
43797
44296
  },
44297
+ "notificationRules.previewTemplate": {
44298
+ capName: "notification-rules",
44299
+ capScope: "system",
44300
+ addonId: null,
44301
+ access: "view"
44302
+ },
43798
44303
  "notificationRules.resolveArtifactUrl": {
43799
44304
  capName: "notification-rules",
43800
44305
  capScope: "system",
@@ -49362,6 +49867,41 @@ var NC_AUDIO_DEFAULTS = {
49362
49867
  };
49363
49868
  new Set(["devices", "classes"]);
49364
49869
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
49870
+ var RULE_BODY_ONLY = ["body"];
49871
+ var RULE_TEXT = ["title", "body"];
49872
+ var BATTERY = ["device-battery-low", "device-battery-normal"];
49873
+ var CONSUMABLE = ["device-consumable-low", "device-consumable-normal"];
49874
+ var UPDATES = [
49875
+ "addon-update-available",
49876
+ "server-update-available",
49877
+ "wrapper-update-available"
49878
+ ];
49879
+ var PACKAGE_UPDATES = ["addon-update-available", "server-update-available"];
49880
+ var ALARM = [
49881
+ "alarm-triggered",
49882
+ "alarm-armed",
49883
+ "alarm-disarmed",
49884
+ "alarm-arming",
49885
+ "alarm-arm-refused",
49886
+ "alarm-pending",
49887
+ "alarm-rearmed",
49888
+ "alarm-sensor-bypass",
49889
+ "alarm-check-failed"
49890
+ ];
49891
+ var COMBINED_ALARM = ["alarm-triggered"];
49892
+ var DEVICE_EVENTS = [
49893
+ "device-online",
49894
+ "device-offline",
49895
+ "device-disabled",
49896
+ "device-enabled",
49897
+ ...BATTERY,
49898
+ ...CONSUMABLE,
49899
+ "stream-online",
49900
+ "stream-offline",
49901
+ "detection-blind",
49902
+ ...ALARM
49903
+ ];
49904
+ [...DEVICE_EVENTS], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_BODY_ONLY], [...RULE_TEXT], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...BATTERY, ...CONSUMABLE], [...RULE_TEXT], [...CONSUMABLE], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT];
49365
49905
  /**
49366
49906
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
49367
49907
  *
@@ -54760,11 +55300,22 @@ function createClipThumbHandler(deps) {
54760
55300
  writeJpeg(res, cached, req.method === "HEAD");
54761
55301
  return;
54762
55302
  }
55303
+ const gone = new AbortController();
55304
+ const onClose = () => {
55305
+ if (!res.writableEnded) gone.abort();
55306
+ };
55307
+ res.on("close", onClose);
54763
55308
  let minted;
54764
55309
  try {
54765
- minted = await deps.mint(deviceId, clipKey);
55310
+ minted = await deps.mint(deviceId, clipKey, gone.signal);
54766
55311
  } catch (err) {
54767
55312
  minted = refuseStill("camera-refused", err instanceof Error ? err.message : String(err));
55313
+ } finally {
55314
+ res.off("close", onClose);
55315
+ }
55316
+ if (!minted.ok && "abandoned" in minted) {
55317
+ if (!res.writableEnded) res.end();
55318
+ return;
54768
55319
  }
54769
55320
  if (!minted.ok) {
54770
55321
  writeStillRefusal(res, minted, deviceId, deps.logger, clipKey);
@@ -55643,6 +56194,11 @@ function createClipStreamProducer(deps) {
55643
56194
  code: "replay-busy",
55644
56195
  detail: lease.detail
55645
56196
  };
56197
+ if (lease.kind === "abandoned") return {
56198
+ kind: "refused",
56199
+ code: "replay-busy",
56200
+ detail: "the playback was abandoned before this camera’s replay came free"
56201
+ };
55646
56202
  const args = buildClipStreamMuxArgs({
55647
56203
  url: request.url,
55648
56204
  audio: request.audio
@@ -55897,7 +56453,7 @@ async function cutClipStill(input, deps) {
55897
56453
  child.stderr?.on("data", (chunk) => {
55898
56454
  stderr = `${stderr}${chunk.toString("utf8")}`.slice(-4e3);
55899
56455
  });
55900
- const timeoutMs = deps.timeoutMs ?? 1e4;
56456
+ const timeoutMs = deps.timeoutMs ?? 5e3;
55901
56457
  let yielded = false;
55902
56458
  const exit = await new Promise((resolve) => {
55903
56459
  const onAbort = () => {
@@ -55952,10 +56508,73 @@ async function cutClipStill(input, deps) {
55952
56508
  detail: `the replay ran for ${String(slotMs)} ms and produced no decodable frame`
55953
56509
  };
55954
56510
  }
56511
+ function memoryKey(deviceId, clipKey) {
56512
+ return `${String(deviceId)}/${clipKey}`;
56513
+ }
56514
+ function createClipStillGuard(deps) {
56515
+ const now = deps.now ?? (() => Date.now());
56516
+ const memoryMs = deps.memoryMs ?? 6e5;
56517
+ const pauseMs = deps.pauseMs ?? 3e5;
56518
+ const failuresToPause = deps.failuresToPause ?? 2;
56519
+ const failures = /* @__PURE__ */ new Map();
56520
+ const consecutive = /* @__PURE__ */ new Map();
56521
+ const pausedUntil = /* @__PURE__ */ new Map();
56522
+ const forgetLapsed = (at) => {
56523
+ for (const [key, entry] of failures) if (entry.untilMs <= at) failures.delete(key);
56524
+ };
56525
+ return {
56526
+ remembered: (deviceId, clipKey) => {
56527
+ const key = memoryKey(deviceId, clipKey);
56528
+ const entry = failures.get(key);
56529
+ if (entry === void 0) return null;
56530
+ if (entry.untilMs <= now()) {
56531
+ failures.delete(key);
56532
+ return null;
56533
+ }
56534
+ return entry.refusal;
56535
+ },
56536
+ pausedForMs: (deviceId) => {
56537
+ const until = pausedUntil.get(deviceId);
56538
+ if (until === void 0) return null;
56539
+ const left = until - now();
56540
+ if (left > 0) return left;
56541
+ pausedUntil.delete(deviceId);
56542
+ return null;
56543
+ },
56544
+ recordFailure: (deviceId, clipKey, refusal) => {
56545
+ const at = now();
56546
+ forgetLapsed(at);
56547
+ failures.set(memoryKey(deviceId, clipKey), {
56548
+ refusal,
56549
+ untilMs: at + memoryMs
56550
+ });
56551
+ const count = (consecutive.get(deviceId) ?? 0) + 1;
56552
+ if (count < failuresToPause) {
56553
+ consecutive.set(deviceId, count);
56554
+ return;
56555
+ }
56556
+ consecutive.delete(deviceId);
56557
+ pausedUntil.set(deviceId, at + pauseMs);
56558
+ deps.logger.warn("videoclips: pausing clip stills for this camera — its replays keep failing", {
56559
+ tags: { deviceId },
56560
+ meta: {
56561
+ branch: "still-pause",
56562
+ consecutiveFailures: count,
56563
+ pauseMs,
56564
+ lastClipKey: clipKey,
56565
+ lastReason: refusal.reason
56566
+ }
56567
+ });
56568
+ },
56569
+ recordSuccess: (deviceId) => {
56570
+ consecutive.delete(deviceId);
56571
+ }
56572
+ };
56573
+ }
55955
56574
  //#endregion
55956
56575
  //#region src/clips/replay-lane.ts
55957
56576
  /**
55958
- * How long an ask may wait for a slot that is being released.
56577
+ * How long a PLAYBACK may wait for a slot that is being released.
55959
56578
  *
55960
56579
  * A holder shutting down kills its ffmpeg and the camera's socket dies with
55961
56580
  * it; there is no teardown handshake to flush. Two seconds is an order of
@@ -55974,15 +56593,29 @@ var REPLAY_LANE_WAIT_MS = 2e3;
55974
56593
  function replayPreempts(asked, holder) {
55975
56594
  return asked === "stream" && holder === "still";
55976
56595
  }
56596
+ /** Where a new waiter goes: a playback ahead of every still, else the tail. */
56597
+ function queuePosition(queue, purpose) {
56598
+ if (purpose === "still") return queue.length;
56599
+ const firstStill = queue.findIndex((w) => w.purpose === "still");
56600
+ return firstStill === -1 ? queue.length : firstStill;
56601
+ }
55977
56602
  function createReplayLane(deps) {
55978
56603
  const now = deps.now ?? (() => Date.now());
55979
56604
  const waitMs = deps.waitMs ?? 2e3;
56605
+ const maxPending = deps.maxPending ?? 8;
55980
56606
  const holding = /* @__PURE__ */ new Map();
56607
+ const queues = /* @__PURE__ */ new Map();
56608
+ const leave = (deviceId, waiter) => {
56609
+ const queue = queues.get(deviceId);
56610
+ if (queue === void 0) return;
56611
+ const index = queue.indexOf(waiter);
56612
+ if (index !== -1) queue.splice(index, 1);
56613
+ if (queue.length === 0) queues.delete(deviceId);
56614
+ };
55981
56615
  const take = (deviceId, purpose, onYield) => {
55982
56616
  const entry = {
55983
56617
  purpose,
55984
56618
  sinceMs: now(),
55985
- waiters: [],
55986
56619
  ...onYield === void 0 ? {} : { onYield }
55987
56620
  };
55988
56621
  holding.set(deviceId, entry);
@@ -55994,16 +56627,82 @@ function createReplayLane(deps) {
55994
56627
  released = true;
55995
56628
  if (holding.get(deviceId) !== entry) return;
55996
56629
  holding.delete(deviceId);
55997
- entry.waiters.shift()?.();
56630
+ const next = queues.get(deviceId)?.[0];
56631
+ if (next === void 0) return;
56632
+ leave(deviceId, next);
56633
+ next.grant();
55998
56634
  }
55999
56635
  };
56000
56636
  };
56637
+ const refuse = (deviceId, purpose, current, waitedMs, detail, branch) => {
56638
+ const heldForMs = now() - current.sinceMs;
56639
+ const extras = {
56640
+ tags: { deviceId },
56641
+ meta: {
56642
+ branch,
56643
+ asked: purpose,
56644
+ holder: current.purpose,
56645
+ heldForMs,
56646
+ waitedMs
56647
+ }
56648
+ };
56649
+ if (purpose === "stream") deps.logger.warn("videoclips: a replay was refused — this node already holds one", extras);
56650
+ else deps.logger.debug("videoclips: a still was refused a place in the replay queue", extras);
56651
+ return {
56652
+ kind: "busy",
56653
+ holder: current.purpose,
56654
+ heldForMs,
56655
+ waitedMs,
56656
+ detail
56657
+ };
56658
+ };
56001
56659
  return {
56002
56660
  held: (deviceId) => holding.has(deviceId),
56003
- acquire: async (deviceId, purpose, onYield) => {
56661
+ acquire: async (deviceId, purpose, onYield, signal) => {
56004
56662
  const askedAt = now();
56663
+ if (signal?.aborted === true) return {
56664
+ kind: "abandoned",
56665
+ waitedMs: 0
56666
+ };
56005
56667
  const current = holding.get(deviceId);
56006
56668
  if (current === void 0) return take(deviceId, purpose, onYield);
56669
+ const queue = queues.get(deviceId) ?? [];
56670
+ if (purpose === "still" && queue.length >= maxPending) return refuse(deviceId, purpose, current, 0, `queue-full: ${String(queue.length)} replays are already waiting for this camera (at most ${String(maxPending)}) — nothing about this clip; ask again once the queue drains`, "replay-queue-full");
56671
+ let settle = () => void 0;
56672
+ const settled = new Promise((resolve) => {
56673
+ settle = resolve;
56674
+ });
56675
+ let timer = null;
56676
+ let done = false;
56677
+ const finish = (value) => {
56678
+ if (done) return;
56679
+ done = true;
56680
+ if (timer !== null) clearTimeout(timer);
56681
+ timer = null;
56682
+ signal?.removeEventListener("abort", onAbort);
56683
+ settle(value);
56684
+ };
56685
+ const waiter = {
56686
+ purpose,
56687
+ grant: () => {
56688
+ finish({
56689
+ kind: "granted",
56690
+ lease: take(deviceId, purpose, onYield)
56691
+ });
56692
+ }
56693
+ };
56694
+ const onAbort = () => {
56695
+ leave(deviceId, waiter);
56696
+ finish({ kind: "abandoned" });
56697
+ };
56698
+ queue.splice(queuePosition(queue, purpose), 0, waiter);
56699
+ queues.set(deviceId, queue);
56700
+ signal?.addEventListener("abort", onAbort, { once: true });
56701
+ if (purpose === "stream") timer = setTimeout(() => {
56702
+ timer = null;
56703
+ leave(deviceId, waiter);
56704
+ finish({ kind: "timeout" });
56705
+ }, waitMs);
56007
56706
  if (replayPreempts(purpose, current.purpose) && current.onYield !== void 0) {
56008
56707
  deps.logger.debug("videoclips: a speculative replay was asked to give the slot back", {
56009
56708
  tags: { deviceId },
@@ -56015,43 +56714,15 @@ function createReplayLane(deps) {
56015
56714
  });
56016
56715
  current.onYield();
56017
56716
  }
56018
- await new Promise((resolve) => {
56019
- let timer = null;
56020
- const wake = () => {
56021
- if (timer !== null) clearTimeout(timer);
56022
- timer = null;
56023
- resolve();
56024
- };
56025
- timer = setTimeout(() => {
56026
- timer = null;
56027
- const index = current.waiters.indexOf(wake);
56028
- if (index !== -1) current.waiters.splice(index, 1);
56029
- resolve();
56030
- }, waitMs);
56031
- current.waiters.push(wake);
56032
- });
56033
- const after = holding.get(deviceId);
56034
- if (after === void 0) return take(deviceId, purpose, onYield);
56035
- const heldForMs = now() - after.sinceMs;
56717
+ const outcome = await settled;
56036
56718
  const waitedMs = now() - askedAt;
56037
- const detail = `replay-busy: this camera is already serving a clip ${after.purpose} for this node, held for ${String(heldForMs)} ms — nothing about this clip, and nothing a retry against the camera would change`;
56038
- deps.logger.warn("videoclips: a replay was refused — this node already holds one", {
56039
- tags: { deviceId },
56040
- meta: {
56041
- branch: "replay-busy",
56042
- asked: purpose,
56043
- holder: after.purpose,
56044
- heldForMs,
56045
- waitedMs
56046
- }
56047
- });
56048
- return {
56049
- kind: "busy",
56050
- holder: after.purpose,
56051
- heldForMs,
56052
- waitedMs,
56053
- detail
56719
+ if (outcome.kind === "granted") return outcome.lease;
56720
+ if (outcome.kind === "abandoned") return {
56721
+ kind: "abandoned",
56722
+ waitedMs
56054
56723
  };
56724
+ const after = holding.get(deviceId) ?? current;
56725
+ return refuse(deviceId, purpose, after, waitedMs, `replay-busy: this camera is already serving a clip ${after.purpose} for this node, held for ${String(now() - after.sinceMs)} ms — nothing about this clip, and nothing a retry against the camera would change`, "replay-busy");
56055
56726
  }
56056
56727
  };
56057
56728
  }
@@ -56063,7 +56734,7 @@ function createReplayLane(deps) {
56063
56734
  * The caller is the route, which has already answered a cached still from disk
56064
56735
  * — this is only ever reached for a clip whose picture does not exist yet.
56065
56736
  */
56066
- async function mintClipStill(deviceId, clipKey, deps) {
56737
+ async function mintClipStill(deviceId, clipKey, deps, signal) {
56067
56738
  const tags = { deviceId };
56068
56739
  const meta = { clipKey };
56069
56740
  if (!await deps.hasCamera(deviceId)) {
@@ -56100,12 +56771,41 @@ async function mintClipStill(deviceId, clipKey, deps) {
56100
56771
  });
56101
56772
  return refuseStill("no-keyframe", "this clip has no window to take a midpoint of — its catalog row spans nothing, and a midpoint of nothing is a guess rather than a picture");
56102
56773
  }
56774
+ const remembered = deps.guard.remembered(deviceId, clipKey);
56775
+ if (remembered !== null) {
56776
+ deps.logger.debug("videoclips: no still — this clip already failed recently", {
56777
+ tags,
56778
+ meta: {
56779
+ ...meta,
56780
+ branch: "remembered-failure",
56781
+ code: remembered.code
56782
+ }
56783
+ });
56784
+ return remembered;
56785
+ }
56786
+ const paused = pausedRefusal(deviceId, clipKey, deps);
56787
+ if (paused !== null) return paused;
56103
56788
  const evict = new AbortController();
56104
56789
  const lease = await deps.lane.acquire(deviceId, "still", () => {
56105
56790
  evict.abort();
56106
- });
56791
+ }, signal);
56792
+ if (lease.kind === "abandoned") {
56793
+ deps.logger.debug("videoclips: no still — its caller went away while it queued", {
56794
+ tags,
56795
+ meta: {
56796
+ ...meta,
56797
+ branch: "abandoned",
56798
+ waitedMs: lease.waitedMs
56799
+ }
56800
+ });
56801
+ return {
56802
+ ok: false,
56803
+ abandoned: true,
56804
+ waitedMs: lease.waitedMs
56805
+ };
56806
+ }
56107
56807
  if (lease.kind === "busy") {
56108
- deps.logger.debug("videoclips: no still yet — this camera’s replay is held", {
56808
+ deps.logger.debug("videoclips: no still yet — this camera’s replay queue is full", {
56109
56809
  tags,
56110
56810
  meta: {
56111
56811
  ...meta,
@@ -56116,6 +56816,26 @@ async function mintClipStill(deviceId, clipKey, deps) {
56116
56816
  });
56117
56817
  return deferStill("queue-full", lease.detail, REPLAY_LANE_WAIT_MS);
56118
56818
  }
56819
+ const pausedWhileQueued = pausedRefusal(deviceId, clipKey, deps);
56820
+ if (pausedWhileQueued !== null) {
56821
+ lease.release();
56822
+ return pausedWhileQueued;
56823
+ }
56824
+ const mintedWhileQueued = await readCachedSafely(deviceId, clipKey, deps);
56825
+ if (mintedWhileQueued !== null) {
56826
+ lease.release();
56827
+ deps.logger.debug("videoclips: still minted while this ask queued — served from disk", {
56828
+ tags,
56829
+ meta: {
56830
+ ...meta,
56831
+ branch: "cached-while-queued"
56832
+ }
56833
+ });
56834
+ return {
56835
+ ok: true,
56836
+ bytes: mintedWhileQueued
56837
+ };
56838
+ }
56119
56839
  let cut;
56120
56840
  try {
56121
56841
  const replay = await deps.replayUrlFor(deviceId, row, window);
@@ -56128,7 +56848,9 @@ async function mintClipStill(deviceId, clipKey, deps) {
56128
56848
  reason: replay.detail
56129
56849
  }
56130
56850
  });
56131
- return refuseStill("camera-refused", replay.detail);
56851
+ const refusal = refuseStill("camera-refused", replay.detail);
56852
+ deps.guard.recordFailure(deviceId, clipKey, refusal);
56853
+ return refusal;
56132
56854
  }
56133
56855
  cut = await deps.cut({
56134
56856
  url: replay.url,
@@ -56157,8 +56879,11 @@ async function mintClipStill(deviceId, clipKey, deps) {
56157
56879
  reason: cut.detail
56158
56880
  }
56159
56881
  });
56160
- return refuseStill(cut.code, cut.detail);
56882
+ const refusal = refuseStill(cut.code, cut.detail);
56883
+ deps.guard.recordFailure(deviceId, clipKey, refusal);
56884
+ return refusal;
56161
56885
  }
56886
+ deps.guard.recordSuccess(deviceId);
56162
56887
  await deps.putStill(deviceId, clipKey, cut.bytes);
56163
56888
  deps.logger.debug("videoclips: minted a clip still from the camera’s replay", {
56164
56889
  tags,
@@ -56176,6 +56901,35 @@ async function mintClipStill(deviceId, clipKey, deps) {
56176
56901
  bytes: cut.bytes
56177
56902
  };
56178
56903
  }
56904
+ /** `camera-backoff` while this camera's stills are paused, else `null`. */
56905
+ function pausedRefusal(deviceId, clipKey, deps) {
56906
+ const leftMs = deps.guard.pausedForMs(deviceId);
56907
+ if (leftMs === null) return null;
56908
+ deps.logger.debug("videoclips: no still — this camera’s stills are paused", {
56909
+ tags: { deviceId },
56910
+ meta: {
56911
+ clipKey,
56912
+ branch: "camera-backoff",
56913
+ leftMs
56914
+ }
56915
+ });
56916
+ return deferStill("camera-backoff", `camera-backoff: this camera failed its last clip replays in a row, so clip stills are paused for another ${String(Math.ceil(leftMs / 1e3))} s — nothing about this clip`, leftMs);
56917
+ }
56918
+ /** A cache read that fails is not a still: answered as "not there". */
56919
+ async function readCachedSafely(deviceId, clipKey, deps) {
56920
+ try {
56921
+ return await deps.readCached(deviceId, clipKey);
56922
+ } catch (err) {
56923
+ deps.logger.debug("videoclips: the still cache could not be read after the queue", {
56924
+ tags: { deviceId },
56925
+ meta: {
56926
+ clipKey,
56927
+ error: err instanceof Error ? err.message : String(err)
56928
+ }
56929
+ });
56930
+ return null;
56931
+ }
56932
+ }
56179
56933
  //#endregion
56180
56934
  //#region src/clips/clip-twins.ts
56181
56935
  /**
@@ -56827,9 +57581,15 @@ function createClipService(deps) {
56827
57581
  * DEFERS — lives in `clip-still-mint.ts`; this is only the wiring that gives
56828
57582
  * it a camera, a clock and a place to put the bytes.
56829
57583
  */
56830
- const mintStill = async (deviceId, clipKey) => await mintClipStill(deviceId, clipKey, {
57584
+ const stillGuard = createClipStillGuard({
57585
+ logger,
57586
+ now
57587
+ });
57588
+ const mintStill = async (deviceId, clipKey, signal) => await mintClipStill(deviceId, clipKey, {
56831
57589
  logger,
56832
57590
  lane,
57591
+ guard: stillGuard,
57592
+ readCached: async (id, key) => await thumbs.read(id, key),
56833
57593
  hasCamera: async () => await deps.resolveCamera(deviceId) !== null,
56834
57594
  rowFor: (id, key) => rowsByKey.get(rowKey(id, key)),
56835
57595
  replayUrlFor: async (id, row, window) => {
@@ -56864,7 +57624,7 @@ function createClipService(deps) {
56864
57624
  putStill: async (id, key, bytes) => {
56865
57625
  await thumbs.put(id, key, bytes);
56866
57626
  }
56867
- });
57627
+ }, signal);
56868
57628
  return {
56869
57629
  provider: createAmcrestVideoclipsProvider({
56870
57630
  now,