@camstack/addon-export-hap 1.2.30 → 1.2.32

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.
@@ -84,7 +84,7 @@ function carryForward(base, existing, keys) {
84
84
  return out;
85
85
  }
86
86
  //#endregion
87
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
87
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
88
88
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemBoot"] = "system.boot";
90
90
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -101,9 +101,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
101
101
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
102
102
  /**
103
103
  * A newer addon or server-root package version was found by the
104
- * authoritative registry check. Emitted once per
105
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
106
- * polling of the same result is deduplicated by the checker.
104
+ * authoritative registry check. Emitted once when any observed
105
+ * `latestVersion` changes (or a package/node first appears behind);
106
+ * the payload carries the full currently-available list. Repeated
107
+ * polling of the same latests is silent.
107
108
  */
108
109
  EventCategory["UpdateAvailable"] = "update.available";
109
110
  /**
@@ -122,6 +123,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
122
123
  EventCategory["AddonInstalled"] = "addon.installed";
123
124
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
124
125
  EventCategory["AddonCrashed"] = "addon.crashed";
126
+ /**
127
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
128
+ *
129
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
130
+ * respawned"); this is the one that never resolves itself. Emitted exactly
131
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
132
+ * addons it hosted and the crash count that tripped the breaker.
133
+ *
134
+ * It exists because a runner marked terminally `failed` used to be SILENT:
135
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
136
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
137
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
138
+ * operator's phone told him. The Notification Center's system-event intake
139
+ * consumes this category and maps it to the `addon-crash-loop` kind.
140
+ */
141
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
125
142
  EventCategory["AddonError"] = "addon.error";
126
143
  EventCategory["AddonPageReady"] = "addon.page-ready";
127
144
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -8214,6 +8231,10 @@ var RecordingBandSchema = object({
8214
8231
  preBufferSec: number().min(0).optional(),
8215
8232
  postBufferSec: number().min(0).optional()
8216
8233
  });
8234
+ ({
8235
+ preBufferSec: 10,
8236
+ postBufferSec: 30
8237
+ }).postBufferSec * 1e3;
8217
8238
  /**
8218
8239
  * Per-device retention overrides. Every field is optional; an unset or `0`
8219
8240
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8261,6 +8282,12 @@ var RecordingConfigSchema = object({
8261
8282
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8262
8283
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8263
8284
  mode: RecordingStorageModeSchema.optional(),
8285
+ /**
8286
+ * Which assigned broker slots to record. Absent / empty = {@link
8287
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8288
+ * camera's currently assigned slots — never `mid` unless the operator
8289
+ * picks it, and never a slot the broker has not assigned.
8290
+ */
8264
8291
  profiles: array(CamProfileSchema).optional(),
8265
8292
  segmentSeconds: number().int().positive().optional(),
8266
8293
  /**
@@ -8341,7 +8368,11 @@ var RelocateFootageInputSchema = object({
8341
8368
  profiles: array(string()).optional(),
8342
8369
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8343
8370
  * never allowed to starve live writers. */
8344
- throttleMbps: number().min(1).max(1e3).optional()
8371
+ throttleMbps: number().min(1).max(1e3).optional(),
8372
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8373
+ * pile. Used when a full drain is too expensive and the operator only
8374
+ * wants the recent window on the new disk. */
8375
+ sinceMs: number().int().optional()
8345
8376
  });
8346
8377
  /** Internal, lease-scoped participant operation. It is intentionally separate
8347
8378
  * from persistent recording settings: a migration never changes
@@ -15039,6 +15070,7 @@ var NcSystemEventKindSchema = _enum([
15039
15070
  "node-offline",
15040
15071
  "node-inference-unavailable",
15041
15072
  "detection-blind",
15073
+ "addon-crash-loop",
15042
15074
  "addon-update-available",
15043
15075
  "server-update-available",
15044
15076
  "alarm-triggered",
@@ -15046,6 +15078,9 @@ var NcSystemEventKindSchema = _enum([
15046
15078
  "alarm-disarmed",
15047
15079
  "alarm-arming",
15048
15080
  "alarm-arm-refused",
15081
+ "addon-updated",
15082
+ "server-updated",
15083
+ "export-completed",
15049
15084
  "camera-online",
15050
15085
  "camera-offline",
15051
15086
  "camera-disabled",
@@ -16939,13 +16974,19 @@ var RetrainStatusSchema = _enum([
16939
16974
  * "never marked" from "already trained" must read `retrainStatus`.
16940
16975
  *
16941
16976
  * `debug` does NOT pin; it is attention, not durability.
16977
+ *
16978
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16979
+ * A favourited track is skipped by retention the same way `staging` is, but
16980
+ * it does not enter `none|staging|trained` and has no staging budget.
16942
16981
  */
16943
16982
  var TrackFlagFields = {
16944
16983
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16945
16984
  * `'staging'`. */
16946
16985
  markForTrain: boolean().optional(),
16947
16986
  /** Operator marked this track for diagnostic attention. */
16948
- debug: boolean().optional()
16987
+ debug: boolean().optional(),
16988
+ /** Operator favourited this track. Pins it against pruning. */
16989
+ favourited: boolean().optional()
16949
16990
  };
16950
16991
  /**
16951
16992
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16970,6 +17011,7 @@ var TrackFlagsSchema = object({
16970
17011
  trackId: string(),
16971
17012
  markForTrain: boolean(),
16972
17013
  debug: boolean(),
17014
+ favourited: boolean(),
16973
17015
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16974
17016
  * a track row) because this shape is only ever produced by the write body,
16975
17017
  * which always knows it — and a surface that has just written needs to render
@@ -17602,6 +17644,12 @@ var TrackCascadeCountsSchema = object({
17602
17644
  /** Per-track CLIP search vectors removed (best-effort). */
17603
17645
  embeddings: number().int()
17604
17646
  });
17647
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17648
+ var DiskReconcileCountsSchema = object({
17649
+ mediaDropped: number().int(),
17650
+ tracks: number().int(),
17651
+ events: number().int()
17652
+ });
17605
17653
  /** Event-store footprint for one camera. */
17606
17654
  var EventStoreDeviceFootprintSchema = object({
17607
17655
  deviceId: number(),
@@ -17778,6 +17826,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17778
17826
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17779
17827
  kind: "mutation",
17780
17828
  auth: "admin"
17829
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17830
+ kind: "mutation",
17831
+ auth: "admin"
17781
17832
  }), method(object({
17782
17833
  deviceId: number(),
17783
17834
  trackIds: array(string()).min(1)
@@ -19288,6 +19339,24 @@ var CameraStatusDegradationSchema = object({
19288
19339
  *
19289
19340
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19290
19341
  */
19342
+ var DiskReconcileJobSchema = object({
19343
+ state: _enum([
19344
+ "idle",
19345
+ "running",
19346
+ "done",
19347
+ "error"
19348
+ ]),
19349
+ total: number().int().nonnegative(),
19350
+ completed: number().int().nonnegative(),
19351
+ currentDeviceId: number().int().nullable(),
19352
+ failed: array(number().int()).readonly(),
19353
+ mediaDropped: number().int().nonnegative(),
19354
+ tracks: number().int().nonnegative(),
19355
+ events: number().int().nonnegative(),
19356
+ startedAtMs: number().int().nullable(),
19357
+ finishedAtMs: number().int().nullable(),
19358
+ error: string().nullable()
19359
+ });
19291
19360
  var CameraStatusSchema = object({
19292
19361
  deviceId: number(),
19293
19362
  assignment: CameraAssignmentStatusSchema,
@@ -19519,7 +19588,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19519
19588
  }), CameraSwitchGroupSchema, {
19520
19589
  kind: "mutation",
19521
19590
  auth: "admin"
19522
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19591
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19592
+ kind: "mutation",
19593
+ auth: "admin"
19594
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19523
19595
  name: string(),
19524
19596
  description: string().optional(),
19525
19597
  config: CameraPipelineConfigSchema
@@ -19924,7 +19996,14 @@ targets: array(object({
19924
19996
  "sleeping",
19925
19997
  "unreachable",
19926
19998
  "waking"
19927
- ]).nullable()
19999
+ ]).nullable(),
20000
+ /** A battery camera (whatever its current state). An AWAKE battery
20001
+ * camera is deliberately NOT recaptured on the poll cadence — every
20002
+ * capture is a camera hit that would keep it out of sleep — so its
20003
+ * cached frame legitimately ages past the currency ceiling while
20004
+ * nothing is streaming. A surface must keep painting it (a fresh
20005
+ * frame is captured at each wake), not blank to "unavailable". */
20006
+ battery: boolean()
19928
20007
  })));
19929
20008
  /**
19930
20009
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20585,20 +20664,6 @@ var VectorStatsResultSchema = object({
20585
20664
  exact: boolean()
20586
20665
  });
20587
20666
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20588
- /**
20589
- * `videoclips` — the unified, navigable-clip surface for a camera.
20590
- *
20591
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20592
- * provider per device, substitutable. The DEFAULT provider (registered by
20593
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20594
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20595
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20596
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20597
- * wrapper provider for its device and serve its own clip catalog + URLs.
20598
- *
20599
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20600
- * temporal overlap, so the API never decides `continuous` vs `events`.
20601
- */
20602
20667
  var ClipSchema = object({
20603
20668
  /** Opaque, provider-namespaced id. The default provider encodes the time
20604
20669
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20615,8 +20680,57 @@ var ClipSchema = object({
20615
20680
  startMs: number(),
20616
20681
  endMs: number()
20617
20682
  }),
20618
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20619
- thumbnail: string().optional()
20683
+ /**
20684
+ * Lazy thumbnail URL, never inlined.
20685
+ *
20686
+ * Recording-derived clips (events-mode keep-window, and the prepared
20687
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20688
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20689
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20690
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20691
+ * mint their own stills.
20692
+ *
20693
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20694
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20695
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20696
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20697
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20698
+ * A read that FAILS drops the claim; it never invents it.
20699
+ */
20700
+ thumbnail: string().optional(),
20701
+ /**
20702
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20703
+ * can be decoded. Present whenever the visit came from recorded availability;
20704
+ * absent on a per-event padded window (there is no footage to promise).
20705
+ *
20706
+ * This is not a thumbnail and not a second byte path: it is the argument to
20707
+ * the recorder's existing still route. The surface — never the provider —
20708
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20709
+ * contiguous range, not of the visit: a visit spans its holes by
20710
+ * construction, so a naive midpoint lands in dead air.
20711
+ */
20712
+ stillAtMs: number().optional(),
20713
+ /**
20714
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20715
+ * first within kind (object → motion → audio), capped at
20716
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20717
+ *
20718
+ * Bounded because it is not a payload the surface pages through: one visit on
20719
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20720
+ * camera-day. Read {@link eventCount} for the true total.
20721
+ */
20722
+ eventIds: array(string()).optional(),
20723
+ /** How many analytics events actually overlap this visit. Differs from
20724
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20725
+ * list is never mistaken for a quiet visit. */
20726
+ eventCount: number().int().nonnegative().optional(),
20727
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20728
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20729
+ * bar keeps showing them via `recording.getAvailability`. */
20730
+ holes: array(object({
20731
+ startMs: number(),
20732
+ endMs: number()
20733
+ })).optional()
20620
20734
  });
20621
20735
  var ClipPlaybackSchema = object({
20622
20736
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -21080,7 +21194,14 @@ var SearchResultSchema = object({
21080
21194
  });
21081
21195
  var AutoUpdateSettingsSchema = object({
21082
21196
  channel: ChannelSchema,
21083
- intervalSeconds: number()
21197
+ intervalSeconds: number(),
21198
+ /**
21199
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
21200
+ * `channel`: the poller runs while auto-apply is `off`, because being told
21201
+ * about a publish and installing it are different decisions. Clamped
21202
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
21203
+ */
21204
+ updateCheckIntervalSeconds: number()
21084
21205
  });
21085
21206
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
21086
21207
  var RestartAddonResultSchema = unknown();
@@ -21221,7 +21342,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
21221
21342
  auth: "admin"
21222
21343
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
21223
21344
  channel: ChannelSchema,
21224
- intervalSeconds: number().min(300).max(86400).optional()
21345
+ intervalSeconds: number().min(300).max(86400).optional(),
21346
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21347
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
21225
21348
  }), unknown(), {
21226
21349
  kind: "mutation",
21227
21350
  auth: "admin"
@@ -25262,7 +25385,9 @@ var ExportOptionsSchema = object({
25262
25385
  includeAudio: boolean(),
25263
25386
  maxLifeMs: number().int().positive(),
25264
25387
  deleteAfterDownload: boolean(),
25265
- title: string().max(200).optional()
25388
+ title: string().max(200).optional(),
25389
+ /** Notification-output target ids to ping when this export becomes ready. */
25390
+ notifyTargetIds: array(string().min(1)).max(20).optional()
25266
25391
  }).superRefine((v, ctx) => {
25267
25392
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
25268
25393
  code: ZodIssueCode.custom,
@@ -25321,10 +25446,18 @@ var ExportBytesSchema = object({
25321
25446
  });
25322
25447
  method(object({
25323
25448
  deviceId: number(),
25324
- profile: string(),
25449
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
25450
+ profile: string().optional(),
25451
+ profiles: array(string()).min(1).optional(),
25325
25452
  fromMs: number(),
25326
25453
  toMs: number(),
25327
25454
  options: ExportOptionsSchema
25455
+ }).superRefine((v, ctx) => {
25456
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
25457
+ code: ZodIssueCode.custom,
25458
+ message: "pass profiles[] (min 1) or legacy profile",
25459
+ path: ["profiles"]
25460
+ });
25328
25461
  }), ExportRecordSchema, {
25329
25462
  kind: "mutation",
25330
25463
  auth: "protected"
@@ -29949,6 +30082,12 @@ Object.freeze({
29949
30082
  addonId: null,
29950
30083
  access: "create"
29951
30084
  },
30085
+ "pipelineAnalytics.reconcileFromDisk": {
30086
+ capName: "pipeline-analytics",
30087
+ capScope: "device",
30088
+ addonId: null,
30089
+ access: "create"
30090
+ },
29952
30091
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29953
30092
  capName: "pipeline-analytics",
29954
30093
  capScope: "device",
@@ -30351,6 +30490,12 @@ Object.freeze({
30351
30490
  addonId: null,
30352
30491
  access: "view"
30353
30492
  },
30493
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
30494
+ capName: "pipeline-orchestrator",
30495
+ capScope: "system",
30496
+ addonId: null,
30497
+ access: "view"
30498
+ },
30354
30499
  "pipelineOrchestrator.listAgentSettings": {
30355
30500
  capName: "pipeline-orchestrator",
30356
30501
  capScope: "system",
@@ -30375,6 +30520,12 @@ Object.freeze({
30375
30520
  addonId: null,
30376
30521
  access: "create"
30377
30522
  },
30523
+ "pipelineOrchestrator.reconcileFromDisk": {
30524
+ capName: "pipeline-orchestrator",
30525
+ capScope: "system",
30526
+ addonId: null,
30527
+ access: "create"
30528
+ },
30378
30529
  "pipelineOrchestrator.removeAgentSettings": {
30379
30530
  capName: "pipeline-orchestrator",
30380
30531
  capScope: "system",
@@ -33366,6 +33517,11 @@ Object.freeze({
33366
33517
  form: "single",
33367
33518
  optional: true
33368
33519
  }],
33520
+ "pipelineAnalytics.reconcileFromDisk": [{
33521
+ name: "deviceId",
33522
+ form: "single",
33523
+ optional: false
33524
+ }],
33369
33525
  "pipelineAnalytics.restageRetrainTrack": [{
33370
33526
  name: "deviceId",
33371
33527
  form: "single",
@@ -34431,6 +34587,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
34431
34587
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
34432
34588
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
34433
34589
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
34590
+ var MB = 1024 * 1024;
34591
+ 1024 * MB, 3072 * MB;
34434
34592
  /**
34435
34593
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
34436
34594
  * export-relevant shape. Two structurally-equal shapes (any feature order,
@@ -72,7 +72,7 @@ function carryForward(base, existing, keys) {
72
72
  return out;
73
73
  }
74
74
  //#endregion
75
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
75
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
76
76
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
77
77
  EventCategory["SystemBoot"] = "system.boot";
78
78
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -89,9 +89,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
90
90
  /**
91
91
  * A newer addon or server-root package version was found by the
92
- * authoritative registry check. Emitted once per
93
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
94
- * polling of the same result is deduplicated by the checker.
92
+ * authoritative registry check. Emitted once when any observed
93
+ * `latestVersion` changes (or a package/node first appears behind);
94
+ * the payload carries the full currently-available list. Repeated
95
+ * polling of the same latests is silent.
95
96
  */
96
97
  EventCategory["UpdateAvailable"] = "update.available";
97
98
  /**
@@ -110,6 +111,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
110
111
  EventCategory["AddonInstalled"] = "addon.installed";
111
112
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
112
113
  EventCategory["AddonCrashed"] = "addon.crashed";
114
+ /**
115
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
116
+ *
117
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
118
+ * respawned"); this is the one that never resolves itself. Emitted exactly
119
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
120
+ * addons it hosted and the crash count that tripped the breaker.
121
+ *
122
+ * It exists because a runner marked terminally `failed` used to be SILENT:
123
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
124
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
125
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
126
+ * operator's phone told him. The Notification Center's system-event intake
127
+ * consumes this category and maps it to the `addon-crash-loop` kind.
128
+ */
129
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
113
130
  EventCategory["AddonError"] = "addon.error";
114
131
  EventCategory["AddonPageReady"] = "addon.page-ready";
115
132
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -8202,6 +8219,10 @@ var RecordingBandSchema = object({
8202
8219
  preBufferSec: number().min(0).optional(),
8203
8220
  postBufferSec: number().min(0).optional()
8204
8221
  });
8222
+ ({
8223
+ preBufferSec: 10,
8224
+ postBufferSec: 30
8225
+ }).postBufferSec * 1e3;
8205
8226
  /**
8206
8227
  * Per-device retention overrides. Every field is optional; an unset or `0`
8207
8228
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -8249,6 +8270,12 @@ var RecordingConfigSchema = object({
8249
8270
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
8250
8271
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
8251
8272
  mode: RecordingStorageModeSchema.optional(),
8273
+ /**
8274
+ * Which assigned broker slots to record. Absent / empty = {@link
8275
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
8276
+ * camera's currently assigned slots — never `mid` unless the operator
8277
+ * picks it, and never a slot the broker has not assigned.
8278
+ */
8252
8279
  profiles: array(CamProfileSchema).optional(),
8253
8280
  segmentSeconds: number().int().positive().optional(),
8254
8281
  /**
@@ -8329,7 +8356,11 @@ var RelocateFootageInputSchema = object({
8329
8356
  profiles: array(string()).optional(),
8330
8357
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
8331
8358
  * never allowed to starve live writers. */
8332
- throttleMbps: number().min(1).max(1e3).optional()
8359
+ throttleMbps: number().min(1).max(1e3).optional(),
8360
+ /** Move only segments whose startMs is >= this. Absent = the whole source
8361
+ * pile. Used when a full drain is too expensive and the operator only
8362
+ * wants the recent window on the new disk. */
8363
+ sinceMs: number().int().optional()
8333
8364
  });
8334
8365
  /** Internal, lease-scoped participant operation. It is intentionally separate
8335
8366
  * from persistent recording settings: a migration never changes
@@ -15027,6 +15058,7 @@ var NcSystemEventKindSchema = _enum([
15027
15058
  "node-offline",
15028
15059
  "node-inference-unavailable",
15029
15060
  "detection-blind",
15061
+ "addon-crash-loop",
15030
15062
  "addon-update-available",
15031
15063
  "server-update-available",
15032
15064
  "alarm-triggered",
@@ -15034,6 +15066,9 @@ var NcSystemEventKindSchema = _enum([
15034
15066
  "alarm-disarmed",
15035
15067
  "alarm-arming",
15036
15068
  "alarm-arm-refused",
15069
+ "addon-updated",
15070
+ "server-updated",
15071
+ "export-completed",
15037
15072
  "camera-online",
15038
15073
  "camera-offline",
15039
15074
  "camera-disabled",
@@ -16927,13 +16962,19 @@ var RetrainStatusSchema = _enum([
16927
16962
  * "never marked" from "already trained" must read `retrainStatus`.
16928
16963
  *
16929
16964
  * `debug` does NOT pin; it is attention, not durability.
16965
+ *
16966
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16967
+ * A favourited track is skipped by retention the same way `staging` is, but
16968
+ * it does not enter `none|staging|trained` and has no staging budget.
16930
16969
  */
16931
16970
  var TrackFlagFields = {
16932
16971
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16933
16972
  * `'staging'`. */
16934
16973
  markForTrain: boolean().optional(),
16935
16974
  /** Operator marked this track for diagnostic attention. */
16936
- debug: boolean().optional()
16975
+ debug: boolean().optional(),
16976
+ /** Operator favourited this track. Pins it against pruning. */
16977
+ favourited: boolean().optional()
16937
16978
  };
16938
16979
  /**
16939
16980
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16958,6 +16999,7 @@ var TrackFlagsSchema = object({
16958
16999
  trackId: string(),
16959
17000
  markForTrain: boolean(),
16960
17001
  debug: boolean(),
17002
+ favourited: boolean(),
16961
17003
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16962
17004
  * a track row) because this shape is only ever produced by the write body,
16963
17005
  * which always knows it — and a surface that has just written needs to render
@@ -17590,6 +17632,12 @@ var TrackCascadeCountsSchema = object({
17590
17632
  /** Per-track CLIP search vectors removed (best-effort). */
17591
17633
  embeddings: number().int()
17592
17634
  });
17635
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17636
+ var DiskReconcileCountsSchema = object({
17637
+ mediaDropped: number().int(),
17638
+ tracks: number().int(),
17639
+ events: number().int()
17640
+ });
17593
17641
  /** Event-store footprint for one camera. */
17594
17642
  var EventStoreDeviceFootprintSchema = object({
17595
17643
  deviceId: number(),
@@ -17766,6 +17814,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17766
17814
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17767
17815
  kind: "mutation",
17768
17816
  auth: "admin"
17817
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17818
+ kind: "mutation",
17819
+ auth: "admin"
17769
17820
  }), method(object({
17770
17821
  deviceId: number(),
17771
17822
  trackIds: array(string()).min(1)
@@ -19276,6 +19327,24 @@ var CameraStatusDegradationSchema = object({
19276
19327
  *
19277
19328
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
19278
19329
  */
19330
+ var DiskReconcileJobSchema = object({
19331
+ state: _enum([
19332
+ "idle",
19333
+ "running",
19334
+ "done",
19335
+ "error"
19336
+ ]),
19337
+ total: number().int().nonnegative(),
19338
+ completed: number().int().nonnegative(),
19339
+ currentDeviceId: number().int().nullable(),
19340
+ failed: array(number().int()).readonly(),
19341
+ mediaDropped: number().int().nonnegative(),
19342
+ tracks: number().int().nonnegative(),
19343
+ events: number().int().nonnegative(),
19344
+ startedAtMs: number().int().nullable(),
19345
+ finishedAtMs: number().int().nullable(),
19346
+ error: string().nullable()
19347
+ });
19279
19348
  var CameraStatusSchema = object({
19280
19349
  deviceId: number(),
19281
19350
  assignment: CameraAssignmentStatusSchema,
@@ -19507,7 +19576,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19507
19576
  }), CameraSwitchGroupSchema, {
19508
19577
  kind: "mutation",
19509
19578
  auth: "admin"
19510
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19579
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19580
+ kind: "mutation",
19581
+ auth: "admin"
19582
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19511
19583
  name: string(),
19512
19584
  description: string().optional(),
19513
19585
  config: CameraPipelineConfigSchema
@@ -19912,7 +19984,14 @@ targets: array(object({
19912
19984
  "sleeping",
19913
19985
  "unreachable",
19914
19986
  "waking"
19915
- ]).nullable()
19987
+ ]).nullable(),
19988
+ /** A battery camera (whatever its current state). An AWAKE battery
19989
+ * camera is deliberately NOT recaptured on the poll cadence — every
19990
+ * capture is a camera hit that would keep it out of sleep — so its
19991
+ * cached frame legitimately ages past the currency ceiling while
19992
+ * nothing is streaming. A surface must keep painting it (a fresh
19993
+ * frame is captured at each wake), not blank to "unavailable". */
19994
+ battery: boolean()
19916
19995
  })));
19917
19996
  /**
19918
19997
  * `sso-bridge` — internal hub-only cap that lets SSO-style auth
@@ -20573,20 +20652,6 @@ var VectorStatsResultSchema = object({
20573
20652
  exact: boolean()
20574
20653
  });
20575
20654
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20576
- /**
20577
- * `videoclips` — the unified, navigable-clip surface for a camera.
20578
- *
20579
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20580
- * provider per device, substitutable. The DEFAULT provider (registered by
20581
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20582
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20583
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20584
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20585
- * wrapper provider for its device and serve its own clip catalog + URLs.
20586
- *
20587
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20588
- * temporal overlap, so the API never decides `continuous` vs `events`.
20589
- */
20590
20655
  var ClipSchema = object({
20591
20656
  /** Opaque, provider-namespaced id. The default provider encodes the time
20592
20657
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20603,8 +20668,57 @@ var ClipSchema = object({
20603
20668
  startMs: number(),
20604
20669
  endMs: number()
20605
20670
  }),
20606
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20607
- thumbnail: string().optional()
20671
+ /**
20672
+ * Lazy thumbnail URL, never inlined.
20673
+ *
20674
+ * Recording-derived clips (events-mode keep-window, and the prepared
20675
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20676
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20677
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20678
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20679
+ * mint their own stills.
20680
+ *
20681
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20682
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20683
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20684
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20685
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20686
+ * A read that FAILS drops the claim; it never invents it.
20687
+ */
20688
+ thumbnail: string().optional(),
20689
+ /**
20690
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20691
+ * can be decoded. Present whenever the visit came from recorded availability;
20692
+ * absent on a per-event padded window (there is no footage to promise).
20693
+ *
20694
+ * This is not a thumbnail and not a second byte path: it is the argument to
20695
+ * the recorder's existing still route. The surface — never the provider —
20696
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20697
+ * contiguous range, not of the visit: a visit spans its holes by
20698
+ * construction, so a naive midpoint lands in dead air.
20699
+ */
20700
+ stillAtMs: number().optional(),
20701
+ /**
20702
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20703
+ * first within kind (object → motion → audio), capped at
20704
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20705
+ *
20706
+ * Bounded because it is not a payload the surface pages through: one visit on
20707
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20708
+ * camera-day. Read {@link eventCount} for the true total.
20709
+ */
20710
+ eventIds: array(string()).optional(),
20711
+ /** How many analytics events actually overlap this visit. Differs from
20712
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20713
+ * list is never mistaken for a quiet visit. */
20714
+ eventCount: number().int().nonnegative().optional(),
20715
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20716
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20717
+ * bar keeps showing them via `recording.getAvailability`. */
20718
+ holes: array(object({
20719
+ startMs: number(),
20720
+ endMs: number()
20721
+ })).optional()
20608
20722
  });
20609
20723
  var ClipPlaybackSchema = object({
20610
20724
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -21068,7 +21182,14 @@ var SearchResultSchema = object({
21068
21182
  });
21069
21183
  var AutoUpdateSettingsSchema = object({
21070
21184
  channel: ChannelSchema,
21071
- intervalSeconds: number()
21185
+ intervalSeconds: number(),
21186
+ /**
21187
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
21188
+ * `channel`: the poller runs while auto-apply is `off`, because being told
21189
+ * about a publish and installing it are different decisions. Clamped
21190
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
21191
+ */
21192
+ updateCheckIntervalSeconds: number()
21072
21193
  });
21073
21194
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
21074
21195
  var RestartAddonResultSchema = unknown();
@@ -21209,7 +21330,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
21209
21330
  auth: "admin"
21210
21331
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
21211
21332
  channel: ChannelSchema,
21212
- intervalSeconds: number().min(300).max(86400).optional()
21333
+ intervalSeconds: number().min(300).max(86400).optional(),
21334
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21335
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
21213
21336
  }), unknown(), {
21214
21337
  kind: "mutation",
21215
21338
  auth: "admin"
@@ -25250,7 +25373,9 @@ var ExportOptionsSchema = object({
25250
25373
  includeAudio: boolean(),
25251
25374
  maxLifeMs: number().int().positive(),
25252
25375
  deleteAfterDownload: boolean(),
25253
- title: string().max(200).optional()
25376
+ title: string().max(200).optional(),
25377
+ /** Notification-output target ids to ping when this export becomes ready. */
25378
+ notifyTargetIds: array(string().min(1)).max(20).optional()
25254
25379
  }).superRefine((v, ctx) => {
25255
25380
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
25256
25381
  code: ZodIssueCode.custom,
@@ -25309,10 +25434,18 @@ var ExportBytesSchema = object({
25309
25434
  });
25310
25435
  method(object({
25311
25436
  deviceId: number(),
25312
- profile: string(),
25437
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
25438
+ profile: string().optional(),
25439
+ profiles: array(string()).min(1).optional(),
25313
25440
  fromMs: number(),
25314
25441
  toMs: number(),
25315
25442
  options: ExportOptionsSchema
25443
+ }).superRefine((v, ctx) => {
25444
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
25445
+ code: ZodIssueCode.custom,
25446
+ message: "pass profiles[] (min 1) or legacy profile",
25447
+ path: ["profiles"]
25448
+ });
25316
25449
  }), ExportRecordSchema, {
25317
25450
  kind: "mutation",
25318
25451
  auth: "protected"
@@ -29937,6 +30070,12 @@ Object.freeze({
29937
30070
  addonId: null,
29938
30071
  access: "create"
29939
30072
  },
30073
+ "pipelineAnalytics.reconcileFromDisk": {
30074
+ capName: "pipeline-analytics",
30075
+ capScope: "device",
30076
+ addonId: null,
30077
+ access: "create"
30078
+ },
29940
30079
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
29941
30080
  capName: "pipeline-analytics",
29942
30081
  capScope: "device",
@@ -30339,6 +30478,12 @@ Object.freeze({
30339
30478
  addonId: null,
30340
30479
  access: "view"
30341
30480
  },
30481
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
30482
+ capName: "pipeline-orchestrator",
30483
+ capScope: "system",
30484
+ addonId: null,
30485
+ access: "view"
30486
+ },
30342
30487
  "pipelineOrchestrator.listAgentSettings": {
30343
30488
  capName: "pipeline-orchestrator",
30344
30489
  capScope: "system",
@@ -30363,6 +30508,12 @@ Object.freeze({
30363
30508
  addonId: null,
30364
30509
  access: "create"
30365
30510
  },
30511
+ "pipelineOrchestrator.reconcileFromDisk": {
30512
+ capName: "pipeline-orchestrator",
30513
+ capScope: "system",
30514
+ addonId: null,
30515
+ access: "create"
30516
+ },
30366
30517
  "pipelineOrchestrator.removeAgentSettings": {
30367
30518
  capName: "pipeline-orchestrator",
30368
30519
  capScope: "system",
@@ -33354,6 +33505,11 @@ Object.freeze({
33354
33505
  form: "single",
33355
33506
  optional: true
33356
33507
  }],
33508
+ "pipelineAnalytics.reconcileFromDisk": [{
33509
+ name: "deviceId",
33510
+ form: "single",
33511
+ optional: false
33512
+ }],
33357
33513
  "pipelineAnalytics.restageRetrainTrack": [{
33358
33514
  name: "deviceId",
33359
33515
  form: "single",
@@ -34419,6 +34575,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
34419
34575
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
34420
34576
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
34421
34577
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
34578
+ var MB = 1024 * 1024;
34579
+ 1024 * MB, 3072 * MB;
34422
34580
  /**
34423
34581
  * Compute the stable 64-char lowercase-hex fingerprint of a device's
34424
34582
  * export-relevant shape. Two structurally-equal shapes (any feature order,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.30",
3
+ "version": "1.2.32",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",