@camstack/addon-pipeline-orchestrator 1.2.176 → 1.2.179

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ node_fs = __toESM(node_fs);
30
30
  let node_path = require("node:path");
31
31
  node_path = __toESM(node_path);
32
32
  let node_url = require("node:url");
33
- //#region ../types/dist/event-category-CnLqLOKs.mjs
33
+ //#region ../types/dist/event-category-DAXzJeTX.mjs
34
34
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
35
35
  EventCategory["SystemBoot"] = "system.boot";
36
36
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -622,7 +622,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
622
622
  * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
623
623
  */
624
624
  EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
625
- EventCategory["FrigateLiveEvent"] = "frigate.live-event";
626
625
  EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
627
626
  /**
628
627
  * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
@@ -13478,7 +13477,7 @@ DeviceType.Camera;
13478
13477
  var SourceInfoSchema = object({
13479
13478
  /** Live dispatch key — mutable when the source system allows rename. */
13480
13479
  id: string(),
13481
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'frigate'. */
13480
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
13482
13481
  system: string(),
13483
13482
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
13484
13483
  * Used to detect rename when `id` changes. */
@@ -13965,7 +13964,7 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
13965
13964
  * - `device-management.router.ts` (deleted in Phase 2)
13966
13965
  * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13967
13966
  *
13968
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13967
+ * All device provider addons (rtsp, onvif, …) are hub-local: they may
13969
13968
  * fork into separate processes but never run on remote cluster agents. Therefore:
13970
13969
  * - No nodeId routing needed — this is a pure hub singleton.
13971
13970
  * - The hub's DeviceRegistry is the single source of truth for all live devices.
@@ -17975,7 +17974,7 @@ var NcRuleInputSchema = object({
17975
17974
  * The measured seven-person arrival on device 590 spans 110 s with every
17976
17975
  * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
17977
17976
  * idle cutoff holds it as one and ends it when the arrival actually ends.
17978
- * 30 is Frigate's shipped value for the same decision.
17977
+ * 30 s is a widely shipped value for the same decision.
17979
17978
  *
17980
17979
  * ### What it replaces
17981
17980
  *
@@ -20241,6 +20240,13 @@ var ZoneCrossingSchema = object({
20241
20240
  /** Zone display name at crossing time (falls back to the id). */
20242
20241
  zoneName: string().optional()
20243
20242
  });
20243
+ /** One zone a box was stamped with, and how much of the BOX fell inside it. */
20244
+ var ZoneOverlapSchema = object({
20245
+ zoneId: string(),
20246
+ zoneName: string().optional(),
20247
+ /** Percentage of the BOX area inside the zone, 0–100. */
20248
+ overlapPct: number()
20249
+ });
20244
20250
  var ObjectEventSchema = object({
20245
20251
  ...BaseEventFields,
20246
20252
  kind: literal("object"),
@@ -20265,6 +20271,35 @@ var ObjectEventSchema = object({
20265
20271
  bbox: BoundingBoxSchema.optional(),
20266
20272
  /** Heavy JSON — omitted in slim projection. */
20267
20273
  zones: array(string()).readonly().optional(),
20274
+ /**
20275
+ * How far inside each stamped zone the box actually was, as a percentage of
20276
+ * the BOX area.
20277
+ *
20278
+ * Membership alone cannot express "at least a third of it inside", which is
20279
+ * the bar an operator argues with — and the number already existed: the
20280
+ * engine computed it, the pipeline logged it, and then discarded it. It
20281
+ * matters most on the events that carry no track, where a rule has nothing
20282
+ * else to judge by.
20283
+ */
20284
+ zoneOverlaps: array(ZoneOverlapSchema).readonly().optional(),
20285
+ /**
20286
+ * Present when this event describes a detection the TRACKER refused, so it
20287
+ * has no track and never will.
20288
+ *
20289
+ * A zone-scoped immediate rule asks "is there an animal in the flowerbed
20290
+ * NOW?"; tracking answers a different question ("is this the same subject as
20291
+ * before?") whose confirmation threshold presumes continuity. On 2026-09-09 a
20292
+ * dog at 0.7729, 100% inside a watched zone, was refused three times in six
20293
+ * seconds with three different candidate ids — seven analysed frames in the
20294
+ * window, the subject in a handful of them, metres apart — and produced no
20295
+ * event at all.
20296
+ *
20297
+ * It is NAMED rather than merely implied by a missing `trackId`: an event
20298
+ * with no track and no reason is indistinguishable from a track whose media
20299
+ * went missing, and a consumer that asks this one for a best shot or a
20300
+ * history is asking for something that never existed.
20301
+ */
20302
+ spawnRefused: string().optional(),
20268
20303
  /** Omitted in slim projection. */
20269
20304
  state: TrackStateSchema.optional(),
20270
20305
  /**
@@ -21338,6 +21373,60 @@ var NativeCropResultSchema = object({
21338
21373
  */
21339
21374
  tier: _enum(["native", "ram-fullframe"]).optional()
21340
21375
  });
21376
+ /**
21377
+ * What a PARKED frame is for — the `kind` half of the `(deviceId, trackId,
21378
+ * kind)` key ([D422](../../../docs/decisions/adr-0422-a-relevant-frame-is-parked-when-it-becomes-relevant.md)).
21379
+ *
21380
+ * - `keyFrame` — the track's clean best FULL frame at native width (capped by
21381
+ * the caller's `maxWidth`). What `keyFrame` + `keyFrameSmall` are written
21382
+ * from, and what the detail plane's full-frame rung cuts from.
21383
+ * - `thumbnail` — the best-shot subject slab: the caller's 16:9 central-square
21384
+ * window, cut by the runner at uncapped native resolution.
21385
+ *
21386
+ * The runner never interprets `trackId`; it is an opaque key the hub owns.
21387
+ */
21388
+ var ParkedFrameKindSchema = _enum(["keyFrame", "thumbnail"]);
21389
+ /**
21390
+ * Why a park did NOT happen. Every one is logged on the runner with
21391
+ * `tags: { deviceId }` and the handle's measured age, because a handle already
21392
+ * gone at park time IS the defect the 150–500 ms window exists to prevent —
21393
+ * never a reason to retry in a loop.
21394
+ *
21395
+ * - `handle-gone` — the native surface no longer resolves this handle (never
21396
+ * registered, past the lease bound, or the worker released it). `detail`
21397
+ * carries the runner's own miss reason.
21398
+ * - `encode-failed` — pixels resolved and the JPEG encode threw.
21399
+ */
21400
+ var ParkRefusalSchema = _enum(["handle-gone", "encode-failed"]);
21401
+ /** Answer to `parkTrackFrame` — a discriminated union, so "not parked" always names why. */
21402
+ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
21403
+ parked: literal(true),
21404
+ /** The node holding the parcel — what a later retrieval must be pinned to. */
21405
+ nodeId: string(),
21406
+ width: number().int().positive(),
21407
+ height: number().int().positive(),
21408
+ /** JPEG bytes the parcel costs on the node. */
21409
+ bytes: number().int().nonnegative(),
21410
+ /** Whether this park replaced a parcel already held for the key. */
21411
+ replaced: boolean()
21412
+ }), object({
21413
+ parked: literal(false),
21414
+ nodeId: string(),
21415
+ reason: ParkRefusalSchema,
21416
+ /** Age of the handle at the park, on the runner's clock; null if unknown. */
21417
+ handleAgeMs: number().nullable(),
21418
+ detail: string().optional()
21419
+ })]);
21420
+ /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
21421
+ var ParkedTrackFrameSchema = object({
21422
+ jpeg: string(),
21423
+ width: number().int().positive(),
21424
+ height: number().int().positive(),
21425
+ /** The frame instant the parcel shows (the caller's clock, echoed back). */
21426
+ timestamp: number(),
21427
+ /** When the node stored it (the node's clock). */
21428
+ parkedAt: number()
21429
+ });
21341
21430
  /** Parent detection context passed to `runDetailSubtree` — the crop's
21342
21431
  * originating detection, in FRAME-space coordinates. Reuses
21343
21432
  * `NativeCropBboxSchema`'s `{x,y,w,h}` shape (same numeric fields; here
@@ -21727,7 +21816,7 @@ var RunnerCameraConfigSchema = object({
21727
21816
  * - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
21728
21817
  * analyzer providers) running on decoded frames in this runner.
21729
21818
  * - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
21730
- * analytics, Frigate motion). The provider emits ProviderMotion
21819
+ * analytics). The provider emits ProviderMotion
21731
21820
  * events; the orchestrator forwards to `reportMotion`.
21732
21821
  */
21733
21822
  motionSources: MotionSourcesSchema.default(["analyzer"]),
@@ -22157,7 +22246,26 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22157
22246
  className: string(),
22158
22247
  /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
22159
22248
  sourceDeviceId: number()
22160
- }), RunStatelessStepResultSchema, { kind: "mutation" });
22249
+ }), RunStatelessStepResultSchema, { kind: "mutation" }), method(object({
22250
+ deviceId: number(),
22251
+ /** Opaque to the runner — the hub's track id. */
22252
+ trackId: string(),
22253
+ kind: ParkedFrameKindSchema,
22254
+ handle: FrameHandleSchema,
22255
+ /** Normalized [0,1] window to cut. `{0,0,1,1}` for a full frame. */
22256
+ bbox: NativeCropBboxSchema,
22257
+ /** Width cap for a full-frame park (the `keyFrame` native cap). */
22258
+ maxWidth: number().int().positive().optional(),
22259
+ /** The frame instant (caller's clock) — orders parks for one key. */
22260
+ timestamp: number()
22261
+ }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
22262
+ deviceId: number(),
22263
+ trackId: string(),
22264
+ kind: ParkedFrameKindSchema
22265
+ }), ParkedTrackFrameSchema.nullable()), method(object({
22266
+ deviceId: number(),
22267
+ trackId: string()
22268
+ }), object({ released: number().int().nonnegative() }), { kind: "mutation" });
22161
22269
  var CameraPipelineConfigSchema = object({
22162
22270
  engine: PipelineEngineChoiceSchema.optional(),
22163
22271
  steps: array(PipelineStepInputSchema).readonly(),
@@ -25416,7 +25524,7 @@ DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _v
25416
25524
  *
25417
25525
  * Implementations:
25418
25526
  * - Each camera-providing addon registers this cap natively per
25419
- * device (Reolink, Hikvision, RTSP, Frigate, ONVIF self).
25527
+ * device (Reolink, Hikvision, RTSP, ONVIF self).
25420
25528
  * - Returns a snapshot from the persisted device config blob — no
25421
25529
  * extra round-trip to the camera, no probing.
25422
25530
  * - The cap is admin-only (`auth: 'admin'`) — credentials must not
@@ -26802,7 +26910,7 @@ var AvailableIntegrationTypeSchema = object({
26802
26910
  * Which integration-marker cap the addon declared, so the wizard can
26803
26911
  * branch on CAP — never on addon name. `device-adoption` integrations
26804
26912
  * (Home Assistant, …) route through the broker step (Approach A);
26805
- * `device-provider` integrations (Reolink/Frigate/ONVIF) keep the
26913
+ * `device-provider` integrations (Reolink/ONVIF) keep the
26806
26914
  * legacy config → discovery flow.
26807
26915
  */
26808
26916
  kind: _enum(["device-adoption", "device-provider"]),
@@ -29359,13 +29467,31 @@ var RecordingManifestSchema = object({
29359
29467
  */
29360
29468
  playbackEndpoints: array(string())
29361
29469
  });
29362
- /** Recording storage usage for one camera (bytes on disk across all its
29363
- * profiles/subtrees/locations on this node). */
29470
+ /**
29471
+ * Recording storage usage for one camera — what the ARCHIVE holds for it,
29472
+ * across every profile and every resolvable location on this node.
29473
+ *
29474
+ * ## Archive vs residency, which is the whole point of this shape (D418)
29475
+ *
29476
+ * `usedBytes` / `oldestMs` / `segments` are the durable hour ledger's numbers:
29477
+ * the same quantity `RecordingLocationUsage.usedBytes` reports for the volume
29478
+ * this camera writes to, so the two can be added up and compared.
29479
+ *
29480
+ * They were not always. Until D418 this row came from the recorder's in-RAM
29481
+ * `RecordingIndex`, which since D287 holds only the live tail plus the hours
29482
+ * somebody hydrated — so a hub with 1.9 TB and 3.3 days of footage reported
29483
+ * 60.5 GB and "oldest: three hours ago" on the same page as the volume row
29484
+ * that said otherwise, and two people read it as retention eating recent
29485
+ * footage. Residency is still reported, under `residentBytes` /
29486
+ * `residentOldestMs`, which answer a different question and say so.
29487
+ */
29364
29488
  var RecordingDeviceUsageSchema = object({
29365
29489
  deviceId: number(),
29490
+ /** Bytes the ARCHIVE holds for this camera on this node's resolvable
29491
+ * recordings locations. */
29366
29492
  usedBytes: number(),
29367
29493
  /**
29368
- * Start of this camera's OLDEST indexed segment, across every profile and
29494
+ * Start of this camera's OLDEST ARCHIVED segment, across every profile and
29369
29495
  * location — the "Oldest footage" column in Recordings → Storage, and the
29370
29496
  * only honest answer to "is retention actually holding?" per camera.
29371
29497
  *
@@ -29375,7 +29501,24 @@ var RecordingDeviceUsageSchema = object({
29375
29501
  * (types) and the addon ship on different trains, and the addon is usually
29376
29502
  * the later of the two.
29377
29503
  */
29378
- oldestMs: number().nullable().optional()
29504
+ oldestMs: number().nullable().optional(),
29505
+ /** Segments the archive holds for this camera — the "M" beside `usedBytes`.
29506
+ * OPTIONAL for the same train-skew reason as `oldestMs`. */
29507
+ segments: number().optional(),
29508
+ /**
29509
+ * RESIDENCY, not the archive: bytes the recorder process currently holds in
29510
+ * its RAM index — the live tail plus the hours a reader hydrated.
29511
+ *
29512
+ * It answers "what can be served without touching the disk", never "how much
29513
+ * footage this camera has". A UI that renders it must label it as such, and
29514
+ * must never total it as storage. OPTIONAL: a recorder predating D418 omits
29515
+ * it, and so does a node whose ledger could not answer.
29516
+ */
29517
+ residentBytes: number().optional(),
29518
+ /** The residency counterpart of `oldestMs` — the oldest segment currently in
29519
+ * RAM, which on a healthy recorder is minutes ago and means nothing about
29520
+ * retention. `null` = nothing resident. */
29521
+ residentOldestMs: number().nullable().optional()
29379
29522
  });
29380
29523
  /** Recording storage usage + capacity for one storage location. */
29381
29524
  var RecordingLocationUsageSchema = object({
@@ -29413,6 +29556,17 @@ var RecordingStorageUsageSchema = object({
29413
29556
  totalUsedBytes: number(),
29414
29557
  devices: array(RecordingDeviceUsageSchema),
29415
29558
  /**
29559
+ * WHERE `devices[]` came from, so an empty table is never mistaken for empty
29560
+ * disks (D418).
29561
+ *
29562
+ * `archive` = the durable hour ledger answered. `unavailable` = this node
29563
+ * declared no ledger and the recorder REFUSED rather than answering the
29564
+ * question with the resident index (D295 rule 3) — the rows are UNKNOWN, not
29565
+ * zero, and a UI must say so. OPTIONAL: a recorder predating D418 omits it,
29566
+ * and its rows are residency.
29567
+ */
29568
+ devicesRead: _enum(["archive", "unavailable"]).optional(),
29569
+ /**
29416
29570
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
29417
29571
  * of the payload without it: the hub serves this method through a BAKED
29418
29572
  * router, so a hub whose framework train predates a change to this array
@@ -35914,6 +36068,24 @@ Object.freeze({
35914
36068
  addonId: null,
35915
36069
  access: "view"
35916
36070
  },
36071
+ "pipelineRunner.getParkedTrackFrame": {
36072
+ capName: "pipeline-runner",
36073
+ capScope: "system",
36074
+ addonId: null,
36075
+ access: "view"
36076
+ },
36077
+ "pipelineRunner.parkTrackFrame": {
36078
+ capName: "pipeline-runner",
36079
+ capScope: "system",
36080
+ addonId: null,
36081
+ access: "create"
36082
+ },
36083
+ "pipelineRunner.releaseParkedTrackFrames": {
36084
+ capName: "pipeline-runner",
36085
+ capScope: "system",
36086
+ addonId: null,
36087
+ access: "create"
36088
+ },
35917
36089
  "pipelineRunner.reportMotion": {
35918
36090
  capName: "pipeline-runner",
35919
36091
  capScope: "system",
@@ -39191,6 +39363,21 @@ Object.freeze({
39191
39363
  form: "single",
39192
39364
  optional: false
39193
39365
  }],
39366
+ "pipelineRunner.getParkedTrackFrame": [{
39367
+ name: "deviceId",
39368
+ form: "single",
39369
+ optional: false
39370
+ }],
39371
+ "pipelineRunner.parkTrackFrame": [{
39372
+ name: "deviceId",
39373
+ form: "single",
39374
+ optional: false
39375
+ }],
39376
+ "pipelineRunner.releaseParkedTrackFrames": [{
39377
+ name: "deviceId",
39378
+ form: "single",
39379
+ optional: false
39380
+ }],
39194
39381
  "pipelineRunner.reportMotion": [{
39195
39382
  name: "deviceId",
39196
39383
  form: "single",
@@ -39882,7 +40069,7 @@ var PreviewTextField = string().max(200);
39882
40069
  * Whether the notification's preview is a STILL or a short animation.
39883
40070
  *
39884
40071
  * The operator's ask, verbatim: *"inviato come gif o video (come per le altre
39885
- * rule)"* — his Scrypted advanced-notifier has a `gifRule`, and a ten-hour
40072
+ * rule)"* — the other rules already offer an animated preview, and a ten-hour
39886
40073
  * night reads better as three seconds of motion than as one frame of it. Both
39887
40074
  * modes get the SAME treatment (blurred frame, large centred title); `'gif'`
39888
40075
  * simply applies it to a dozen frames sampled across the render and assembles
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- //#region ../types/dist/event-category-CnLqLOKs.mjs
5
+ //#region ../types/dist/event-category-DAXzJeTX.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -594,7 +594,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
594
594
  * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
595
595
  */
596
596
  EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
597
- EventCategory["FrigateLiveEvent"] = "frigate.live-event";
598
597
  EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
599
598
  /**
600
599
  * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
@@ -13450,7 +13449,7 @@ DeviceType.Camera;
13450
13449
  var SourceInfoSchema = object({
13451
13450
  /** Live dispatch key — mutable when the source system allows rename. */
13452
13451
  id: string(),
13453
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'frigate'. */
13452
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
13454
13453
  system: string(),
13455
13454
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
13456
13455
  * Used to detect rename when `id` changes. */
@@ -13937,7 +13936,7 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
13937
13936
  * - `device-management.router.ts` (deleted in Phase 2)
13938
13937
  * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13939
13938
  *
13940
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13939
+ * All device provider addons (rtsp, onvif, …) are hub-local: they may
13941
13940
  * fork into separate processes but never run on remote cluster agents. Therefore:
13942
13941
  * - No nodeId routing needed — this is a pure hub singleton.
13943
13942
  * - The hub's DeviceRegistry is the single source of truth for all live devices.
@@ -17947,7 +17946,7 @@ var NcRuleInputSchema = object({
17947
17946
  * The measured seven-person arrival on device 590 spans 110 s with every
17948
17947
  * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
17949
17948
  * idle cutoff holds it as one and ends it when the arrival actually ends.
17950
- * 30 is Frigate's shipped value for the same decision.
17949
+ * 30 s is a widely shipped value for the same decision.
17951
17950
  *
17952
17951
  * ### What it replaces
17953
17952
  *
@@ -20213,6 +20212,13 @@ var ZoneCrossingSchema = object({
20213
20212
  /** Zone display name at crossing time (falls back to the id). */
20214
20213
  zoneName: string().optional()
20215
20214
  });
20215
+ /** One zone a box was stamped with, and how much of the BOX fell inside it. */
20216
+ var ZoneOverlapSchema = object({
20217
+ zoneId: string(),
20218
+ zoneName: string().optional(),
20219
+ /** Percentage of the BOX area inside the zone, 0–100. */
20220
+ overlapPct: number()
20221
+ });
20216
20222
  var ObjectEventSchema = object({
20217
20223
  ...BaseEventFields,
20218
20224
  kind: literal("object"),
@@ -20237,6 +20243,35 @@ var ObjectEventSchema = object({
20237
20243
  bbox: BoundingBoxSchema.optional(),
20238
20244
  /** Heavy JSON — omitted in slim projection. */
20239
20245
  zones: array(string()).readonly().optional(),
20246
+ /**
20247
+ * How far inside each stamped zone the box actually was, as a percentage of
20248
+ * the BOX area.
20249
+ *
20250
+ * Membership alone cannot express "at least a third of it inside", which is
20251
+ * the bar an operator argues with — and the number already existed: the
20252
+ * engine computed it, the pipeline logged it, and then discarded it. It
20253
+ * matters most on the events that carry no track, where a rule has nothing
20254
+ * else to judge by.
20255
+ */
20256
+ zoneOverlaps: array(ZoneOverlapSchema).readonly().optional(),
20257
+ /**
20258
+ * Present when this event describes a detection the TRACKER refused, so it
20259
+ * has no track and never will.
20260
+ *
20261
+ * A zone-scoped immediate rule asks "is there an animal in the flowerbed
20262
+ * NOW?"; tracking answers a different question ("is this the same subject as
20263
+ * before?") whose confirmation threshold presumes continuity. On 2026-09-09 a
20264
+ * dog at 0.7729, 100% inside a watched zone, was refused three times in six
20265
+ * seconds with three different candidate ids — seven analysed frames in the
20266
+ * window, the subject in a handful of them, metres apart — and produced no
20267
+ * event at all.
20268
+ *
20269
+ * It is NAMED rather than merely implied by a missing `trackId`: an event
20270
+ * with no track and no reason is indistinguishable from a track whose media
20271
+ * went missing, and a consumer that asks this one for a best shot or a
20272
+ * history is asking for something that never existed.
20273
+ */
20274
+ spawnRefused: string().optional(),
20240
20275
  /** Omitted in slim projection. */
20241
20276
  state: TrackStateSchema.optional(),
20242
20277
  /**
@@ -21310,6 +21345,60 @@ var NativeCropResultSchema = object({
21310
21345
  */
21311
21346
  tier: _enum(["native", "ram-fullframe"]).optional()
21312
21347
  });
21348
+ /**
21349
+ * What a PARKED frame is for — the `kind` half of the `(deviceId, trackId,
21350
+ * kind)` key ([D422](../../../docs/decisions/adr-0422-a-relevant-frame-is-parked-when-it-becomes-relevant.md)).
21351
+ *
21352
+ * - `keyFrame` — the track's clean best FULL frame at native width (capped by
21353
+ * the caller's `maxWidth`). What `keyFrame` + `keyFrameSmall` are written
21354
+ * from, and what the detail plane's full-frame rung cuts from.
21355
+ * - `thumbnail` — the best-shot subject slab: the caller's 16:9 central-square
21356
+ * window, cut by the runner at uncapped native resolution.
21357
+ *
21358
+ * The runner never interprets `trackId`; it is an opaque key the hub owns.
21359
+ */
21360
+ var ParkedFrameKindSchema = _enum(["keyFrame", "thumbnail"]);
21361
+ /**
21362
+ * Why a park did NOT happen. Every one is logged on the runner with
21363
+ * `tags: { deviceId }` and the handle's measured age, because a handle already
21364
+ * gone at park time IS the defect the 150–500 ms window exists to prevent —
21365
+ * never a reason to retry in a loop.
21366
+ *
21367
+ * - `handle-gone` — the native surface no longer resolves this handle (never
21368
+ * registered, past the lease bound, or the worker released it). `detail`
21369
+ * carries the runner's own miss reason.
21370
+ * - `encode-failed` — pixels resolved and the JPEG encode threw.
21371
+ */
21372
+ var ParkRefusalSchema = _enum(["handle-gone", "encode-failed"]);
21373
+ /** Answer to `parkTrackFrame` — a discriminated union, so "not parked" always names why. */
21374
+ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
21375
+ parked: literal(true),
21376
+ /** The node holding the parcel — what a later retrieval must be pinned to. */
21377
+ nodeId: string(),
21378
+ width: number().int().positive(),
21379
+ height: number().int().positive(),
21380
+ /** JPEG bytes the parcel costs on the node. */
21381
+ bytes: number().int().nonnegative(),
21382
+ /** Whether this park replaced a parcel already held for the key. */
21383
+ replaced: boolean()
21384
+ }), object({
21385
+ parked: literal(false),
21386
+ nodeId: string(),
21387
+ reason: ParkRefusalSchema,
21388
+ /** Age of the handle at the park, on the runner's clock; null if unknown. */
21389
+ handleAgeMs: number().nullable(),
21390
+ detail: string().optional()
21391
+ })]);
21392
+ /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
21393
+ var ParkedTrackFrameSchema = object({
21394
+ jpeg: string(),
21395
+ width: number().int().positive(),
21396
+ height: number().int().positive(),
21397
+ /** The frame instant the parcel shows (the caller's clock, echoed back). */
21398
+ timestamp: number(),
21399
+ /** When the node stored it (the node's clock). */
21400
+ parkedAt: number()
21401
+ });
21313
21402
  /** Parent detection context passed to `runDetailSubtree` — the crop's
21314
21403
  * originating detection, in FRAME-space coordinates. Reuses
21315
21404
  * `NativeCropBboxSchema`'s `{x,y,w,h}` shape (same numeric fields; here
@@ -21699,7 +21788,7 @@ var RunnerCameraConfigSchema = object({
21699
21788
  * - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
21700
21789
  * analyzer providers) running on decoded frames in this runner.
21701
21790
  * - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
21702
- * analytics, Frigate motion). The provider emits ProviderMotion
21791
+ * analytics). The provider emits ProviderMotion
21703
21792
  * events; the orchestrator forwards to `reportMotion`.
21704
21793
  */
21705
21794
  motionSources: MotionSourcesSchema.default(["analyzer"]),
@@ -22129,7 +22218,26 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22129
22218
  className: string(),
22130
22219
  /** Camera the pixels came from. Diagnostics + log tags ONLY — never routing. */
22131
22220
  sourceDeviceId: number()
22132
- }), RunStatelessStepResultSchema, { kind: "mutation" });
22221
+ }), RunStatelessStepResultSchema, { kind: "mutation" }), method(object({
22222
+ deviceId: number(),
22223
+ /** Opaque to the runner — the hub's track id. */
22224
+ trackId: string(),
22225
+ kind: ParkedFrameKindSchema,
22226
+ handle: FrameHandleSchema,
22227
+ /** Normalized [0,1] window to cut. `{0,0,1,1}` for a full frame. */
22228
+ bbox: NativeCropBboxSchema,
22229
+ /** Width cap for a full-frame park (the `keyFrame` native cap). */
22230
+ maxWidth: number().int().positive().optional(),
22231
+ /** The frame instant (caller's clock) — orders parks for one key. */
22232
+ timestamp: number()
22233
+ }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
22234
+ deviceId: number(),
22235
+ trackId: string(),
22236
+ kind: ParkedFrameKindSchema
22237
+ }), ParkedTrackFrameSchema.nullable()), method(object({
22238
+ deviceId: number(),
22239
+ trackId: string()
22240
+ }), object({ released: number().int().nonnegative() }), { kind: "mutation" });
22133
22241
  var CameraPipelineConfigSchema = object({
22134
22242
  engine: PipelineEngineChoiceSchema.optional(),
22135
22243
  steps: array(PipelineStepInputSchema).readonly(),
@@ -25388,7 +25496,7 @@ DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _v
25388
25496
  *
25389
25497
  * Implementations:
25390
25498
  * - Each camera-providing addon registers this cap natively per
25391
- * device (Reolink, Hikvision, RTSP, Frigate, ONVIF self).
25499
+ * device (Reolink, Hikvision, RTSP, ONVIF self).
25392
25500
  * - Returns a snapshot from the persisted device config blob — no
25393
25501
  * extra round-trip to the camera, no probing.
25394
25502
  * - The cap is admin-only (`auth: 'admin'`) — credentials must not
@@ -26774,7 +26882,7 @@ var AvailableIntegrationTypeSchema = object({
26774
26882
  * Which integration-marker cap the addon declared, so the wizard can
26775
26883
  * branch on CAP — never on addon name. `device-adoption` integrations
26776
26884
  * (Home Assistant, …) route through the broker step (Approach A);
26777
- * `device-provider` integrations (Reolink/Frigate/ONVIF) keep the
26885
+ * `device-provider` integrations (Reolink/ONVIF) keep the
26778
26886
  * legacy config → discovery flow.
26779
26887
  */
26780
26888
  kind: _enum(["device-adoption", "device-provider"]),
@@ -29331,13 +29439,31 @@ var RecordingManifestSchema = object({
29331
29439
  */
29332
29440
  playbackEndpoints: array(string())
29333
29441
  });
29334
- /** Recording storage usage for one camera (bytes on disk across all its
29335
- * profiles/subtrees/locations on this node). */
29442
+ /**
29443
+ * Recording storage usage for one camera — what the ARCHIVE holds for it,
29444
+ * across every profile and every resolvable location on this node.
29445
+ *
29446
+ * ## Archive vs residency, which is the whole point of this shape (D418)
29447
+ *
29448
+ * `usedBytes` / `oldestMs` / `segments` are the durable hour ledger's numbers:
29449
+ * the same quantity `RecordingLocationUsage.usedBytes` reports for the volume
29450
+ * this camera writes to, so the two can be added up and compared.
29451
+ *
29452
+ * They were not always. Until D418 this row came from the recorder's in-RAM
29453
+ * `RecordingIndex`, which since D287 holds only the live tail plus the hours
29454
+ * somebody hydrated — so a hub with 1.9 TB and 3.3 days of footage reported
29455
+ * 60.5 GB and "oldest: three hours ago" on the same page as the volume row
29456
+ * that said otherwise, and two people read it as retention eating recent
29457
+ * footage. Residency is still reported, under `residentBytes` /
29458
+ * `residentOldestMs`, which answer a different question and say so.
29459
+ */
29336
29460
  var RecordingDeviceUsageSchema = object({
29337
29461
  deviceId: number(),
29462
+ /** Bytes the ARCHIVE holds for this camera on this node's resolvable
29463
+ * recordings locations. */
29338
29464
  usedBytes: number(),
29339
29465
  /**
29340
- * Start of this camera's OLDEST indexed segment, across every profile and
29466
+ * Start of this camera's OLDEST ARCHIVED segment, across every profile and
29341
29467
  * location — the "Oldest footage" column in Recordings → Storage, and the
29342
29468
  * only honest answer to "is retention actually holding?" per camera.
29343
29469
  *
@@ -29347,7 +29473,24 @@ var RecordingDeviceUsageSchema = object({
29347
29473
  * (types) and the addon ship on different trains, and the addon is usually
29348
29474
  * the later of the two.
29349
29475
  */
29350
- oldestMs: number().nullable().optional()
29476
+ oldestMs: number().nullable().optional(),
29477
+ /** Segments the archive holds for this camera — the "M" beside `usedBytes`.
29478
+ * OPTIONAL for the same train-skew reason as `oldestMs`. */
29479
+ segments: number().optional(),
29480
+ /**
29481
+ * RESIDENCY, not the archive: bytes the recorder process currently holds in
29482
+ * its RAM index — the live tail plus the hours a reader hydrated.
29483
+ *
29484
+ * It answers "what can be served without touching the disk", never "how much
29485
+ * footage this camera has". A UI that renders it must label it as such, and
29486
+ * must never total it as storage. OPTIONAL: a recorder predating D418 omits
29487
+ * it, and so does a node whose ledger could not answer.
29488
+ */
29489
+ residentBytes: number().optional(),
29490
+ /** The residency counterpart of `oldestMs` — the oldest segment currently in
29491
+ * RAM, which on a healthy recorder is minutes ago and means nothing about
29492
+ * retention. `null` = nothing resident. */
29493
+ residentOldestMs: number().nullable().optional()
29351
29494
  });
29352
29495
  /** Recording storage usage + capacity for one storage location. */
29353
29496
  var RecordingLocationUsageSchema = object({
@@ -29385,6 +29528,17 @@ var RecordingStorageUsageSchema = object({
29385
29528
  totalUsedBytes: number(),
29386
29529
  devices: array(RecordingDeviceUsageSchema),
29387
29530
  /**
29531
+ * WHERE `devices[]` came from, so an empty table is never mistaken for empty
29532
+ * disks (D418).
29533
+ *
29534
+ * `archive` = the durable hour ledger answered. `unavailable` = this node
29535
+ * declared no ledger and the recorder REFUSED rather than answering the
29536
+ * question with the resident index (D295 rule 3) — the rows are UNKNOWN, not
29537
+ * zero, and a UI must say so. OPTIONAL: a recorder predating D418 omits it,
29538
+ * and its rows are residency.
29539
+ */
29540
+ devicesRead: _enum(["archive", "unavailable"]).optional(),
29541
+ /**
29388
29542
  * Per-physical-volume capacity. OPTIONAL, and consumers must render the rest
29389
29543
  * of the payload without it: the hub serves this method through a BAKED
29390
29544
  * router, so a hub whose framework train predates a change to this array
@@ -35886,6 +36040,24 @@ Object.freeze({
35886
36040
  addonId: null,
35887
36041
  access: "view"
35888
36042
  },
36043
+ "pipelineRunner.getParkedTrackFrame": {
36044
+ capName: "pipeline-runner",
36045
+ capScope: "system",
36046
+ addonId: null,
36047
+ access: "view"
36048
+ },
36049
+ "pipelineRunner.parkTrackFrame": {
36050
+ capName: "pipeline-runner",
36051
+ capScope: "system",
36052
+ addonId: null,
36053
+ access: "create"
36054
+ },
36055
+ "pipelineRunner.releaseParkedTrackFrames": {
36056
+ capName: "pipeline-runner",
36057
+ capScope: "system",
36058
+ addonId: null,
36059
+ access: "create"
36060
+ },
35889
36061
  "pipelineRunner.reportMotion": {
35890
36062
  capName: "pipeline-runner",
35891
36063
  capScope: "system",
@@ -39163,6 +39335,21 @@ Object.freeze({
39163
39335
  form: "single",
39164
39336
  optional: false
39165
39337
  }],
39338
+ "pipelineRunner.getParkedTrackFrame": [{
39339
+ name: "deviceId",
39340
+ form: "single",
39341
+ optional: false
39342
+ }],
39343
+ "pipelineRunner.parkTrackFrame": [{
39344
+ name: "deviceId",
39345
+ form: "single",
39346
+ optional: false
39347
+ }],
39348
+ "pipelineRunner.releaseParkedTrackFrames": [{
39349
+ name: "deviceId",
39350
+ form: "single",
39351
+ optional: false
39352
+ }],
39166
39353
  "pipelineRunner.reportMotion": [{
39167
39354
  name: "deviceId",
39168
39355
  form: "single",
@@ -39854,7 +40041,7 @@ var PreviewTextField = string().max(200);
39854
40041
  * Whether the notification's preview is a STILL or a short animation.
39855
40042
  *
39856
40043
  * The operator's ask, verbatim: *"inviato come gif o video (come per le altre
39857
- * rule)"* — his Scrypted advanced-notifier has a `gifRule`, and a ten-hour
40044
+ * rule)"* — the other rules already offer an animated preview, and a ten-hour
39858
40045
  * night reads better as three seconds of motion than as one frame of it. Both
39859
40046
  * modes get the SAME treatment (blurred frame, large centred title); `'gif'`
39860
40047
  * simply applies it to a dozen frames sampled across the render and assembles