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