@camstack/addon-ai 0.4.43 → 0.4.45

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/addon.js CHANGED
@@ -8221,6 +8221,20 @@ var RelocateJobSchema = object({
8221
8221
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8222
8222
  */
8223
8223
  rowsReconciled: number$1().int().nonnegative().optional(),
8224
+ /**
8225
+ * Rows this run FORGOT because the file they name is not on disk.
8226
+ *
8227
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8228
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8229
+ * and the durable row is dropped through the same channel eviction uses. It
8230
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8231
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8232
+ * the same failure as one that quietly skips them (D295).
8233
+ *
8234
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8235
+ * ledger claimed 5.65 GB of footage that no longer existed.
8236
+ */
8237
+ rowsForgotten: number$1().int().nonnegative().optional(),
8224
8238
  startedAt: number$1(),
8225
8239
  finishedAt: number$1().nullable(),
8226
8240
  error: string().nullable()
@@ -8584,6 +8598,91 @@ var RelocateResidueSchema = object({
8584
8598
  segments: number$1().int().nonnegative(),
8585
8599
  bytes: number$1().int().nonnegative()
8586
8600
  }).nullable();
8601
+ /**
8602
+ * Ask one location whether its durable hour rows describe the disk — the walk
8603
+ * (D319).
8604
+ *
8605
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8606
+ * missing tool is the question, and the dry run is how they sanity-check the
8607
+ * destructive run before authorising it.
8608
+ */
8609
+ var LedgerWalkInputSchema = object({
8610
+ locationId: string().min(1),
8611
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8612
+ apply: boolean().optional(),
8613
+ /** Narrow to one camera. */
8614
+ deviceId: number$1().int().positive().optional(),
8615
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8616
+ profiles: array(string().min(1)).optional()
8617
+ });
8618
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8619
+ var LedgerWalkRefusalSchema = _enum([
8620
+ "location-unknown",
8621
+ "source-writable",
8622
+ "no-ledger",
8623
+ "archive-unreadable",
8624
+ "anchor-absent",
8625
+ "anchor-unreadable",
8626
+ "anchor-moved"
8627
+ ]);
8628
+ _enum([
8629
+ "live-tail",
8630
+ "listing-error",
8631
+ "path-mismatch",
8632
+ "durable-refused"
8633
+ ]);
8634
+ /** Every skip reason, always present, always a number — so a reason that never
8635
+ * fired reports as zero rather than absent and the report shape is constant
8636
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8637
+ var LedgerWalkSkipCountsSchema = object({
8638
+ "live-tail": number$1().int().nonnegative(),
8639
+ "listing-error": number$1().int().nonnegative(),
8640
+ "path-mismatch": number$1().int().nonnegative(),
8641
+ "durable-refused": number$1().int().nonnegative()
8642
+ });
8643
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8644
+ var LedgerWalkDeviceReportSchema = object({
8645
+ deviceId: number$1().int(),
8646
+ hoursWalked: number$1().int().nonnegative(),
8647
+ hoursMissing: number$1().int().nonnegative(),
8648
+ ghostSegments: number$1().int().nonnegative(),
8649
+ ghostBytes: number$1().int().nonnegative(),
8650
+ forgottenSegments: number$1().int().nonnegative(),
8651
+ orphanFiles: number$1().int().nonnegative()
8652
+ });
8653
+ /**
8654
+ * What one walk claimed, listed, found and (only when armed) forgot.
8655
+ *
8656
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8657
+ * walk that saw a fraction of the location is visible in its own report rather
8658
+ * than in the absence of one.
8659
+ */
8660
+ var LedgerWalkReportSchema = object({
8661
+ locationId: string(),
8662
+ applied: boolean(),
8663
+ refused: LedgerWalkRefusalSchema.nullable(),
8664
+ archiveSegments: number$1().int().nonnegative().nullable(),
8665
+ archiveBytes: number$1().int().nonnegative().nullable(),
8666
+ hoursClaimed: number$1().int().nonnegative(),
8667
+ hoursWalked: number$1().int().nonnegative(),
8668
+ hoursMissing: number$1().int().nonnegative(),
8669
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8670
+ listings: number$1().int().nonnegative(),
8671
+ segmentsClaimed: number$1().int().nonnegative(),
8672
+ ghostSegments: number$1().int().nonnegative(),
8673
+ ghostBytes: number$1().int().nonnegative(),
8674
+ ghostHoursWhole: number$1().int().nonnegative(),
8675
+ forgottenSegments: number$1().int().nonnegative(),
8676
+ forgottenBytes: number$1().int().nonnegative(),
8677
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8678
+ orphanFiles: number$1().int().nonnegative(),
8679
+ orphanSample: array(string()).readonly(),
8680
+ hoursSkipped: number$1().int().nonnegative(),
8681
+ skippedByReason: LedgerWalkSkipCountsSchema,
8682
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8683
+ bounded: boolean(),
8684
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8685
+ });
8587
8686
  /** How many rows a media pass would still act on against a given target — the
8588
8687
  * media lane's denominator AND its residue, from ONE derivation so the two can
8589
8688
  * never disagree. `null` = the count could not be taken. */
@@ -18951,13 +19050,15 @@ var ListGroupsPageSchema = object({
18951
19050
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18952
19051
  nextCursor: string().nullable()
18953
19052
  });
19053
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19054
+ var KEY_EVENTS_MAX_LIMIT = 200;
18954
19055
  var KeyEventQueryInput = object({
18955
19056
  deviceId: number$1(),
18956
19057
  /** Window lower bound (track firstSeen ≥ since). */
18957
19058
  since: number$1(),
18958
19059
  /** Window upper bound (track firstSeen ≤ until). */
18959
19060
  until: number$1(),
18960
- limit: number$1().int().min(1).max(200).default(50),
19061
+ limit: number$1().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18961
19062
  /** Drop tracks scoring below this importance. */
18962
19063
  minImportance: number$1().min(0).max(1).optional(),
18963
19064
  /** Restrict to a single class (e.g. 'person'). */
@@ -18979,6 +19080,32 @@ var KeyEventSchema = object({
18979
19080
  ...TrackFlagFields,
18980
19081
  ...TrackRetrainFields
18981
19082
  });
19083
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19084
+ var KeyEventBatchQueryInput = object({
19085
+ deviceIds: array(number$1()).min(1).max(200),
19086
+ since: number$1(),
19087
+ until: number$1(),
19088
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19089
+ * across the set, which would let a busy camera starve a quiet one of its
19090
+ * rows and change what the merged feed contains. */
19091
+ limit: number$1().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19092
+ minImportance: number$1().min(0).max(1).optional(),
19093
+ classFilter: string().optional()
19094
+ });
19095
+ /**
19096
+ * One camera's key events in a batch answer.
19097
+ *
19098
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19099
+ * error rather than throwing, so a camera whose store read failed and one with
19100
+ * no events in the window were ALREADY indistinguishable per camera — the
19101
+ * batch does not make that worse, and the row keeps the deviceId the single
19102
+ * method's output never carried (the caller used to stamp it from the fan-out
19103
+ * key, which only worked because there was one query per camera).
19104
+ */
19105
+ var KeyEventsForDeviceSchema = object({
19106
+ deviceId: number$1(),
19107
+ events: array(KeyEventSchema).readonly()
19108
+ });
18982
19109
  object({
18983
19110
  trackId: string(),
18984
19111
  className: string(),
@@ -19050,6 +19177,50 @@ var EventStoreFootprintSchema = object({
19050
19177
  totalBytes: number$1().int(),
19051
19178
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19052
19179
  });
19180
+ /** Event-media footprint for one {@link MediaFileKind}. */
19181
+ var EventMediaKindFootprintSchema = object({
19182
+ kind: MediaFileKindEnum,
19183
+ /** Media rows of this kind. */
19184
+ rows: number$1().int(),
19185
+ /** Bytes on disk held by those rows. */
19186
+ bytes: number$1().int()
19187
+ });
19188
+ /**
19189
+ * The media footprint broken down by KIND — the axis a deletion decision
19190
+ * actually turns on.
19191
+ *
19192
+ * A byte total says how much there is; it cannot say what is safe to remove.
19193
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19194
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19195
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19196
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19197
+ * nothing else, so sizing a deletion means summing per kind.
19198
+ *
19199
+ * ## Why `unaccounted*` exists
19200
+ *
19201
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19202
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19203
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19204
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19205
+ * retired code path, or by a version that knew a kind this one does not) would
19206
+ * otherwise vanish from the total silently, and an operator would delete
19207
+ * against a denominator smaller than the disk.
19208
+ *
19209
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19210
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19211
+ */
19212
+ var EventMediaKindBreakdownSchema = object({
19213
+ /** Every media row in scope, from one unfiltered aggregate. */
19214
+ totalRows: number$1().int(),
19215
+ /** Every media byte in scope, from that same aggregate. */
19216
+ totalBytes: number$1().int(),
19217
+ /** Per-kind footprint, ordered by bytes descending. */
19218
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19219
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19220
+ unaccountedRows: number$1().int(),
19221
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19222
+ unaccountedBytes: number$1().int()
19223
+ });
19053
19224
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19054
19225
  var EventPruneCountsSchema = object({
19055
19226
  motion: number$1().int(),
@@ -19204,7 +19375,7 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19204
19375
  until: number$1().optional(),
19205
19376
  kinds: array(string()).optional(),
19206
19377
  limit: number$1().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19207
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19378
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19208
19379
  deviceId: number$1(),
19209
19380
  since: number$1(),
19210
19381
  until: number$1(),
@@ -19253,6 +19424,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19253
19424
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19254
19425
  kind: "query",
19255
19426
  auth: "admin"
19427
+ }), method(object({ deviceId: number$1().int().optional() }), EventMediaKindBreakdownSchema, {
19428
+ kind: "query",
19429
+ auth: "admin"
19256
19430
  }), method(object({
19257
19431
  olderThanMs: number$1(),
19258
19432
  reason: OpsLogReasonSchema.optional()
@@ -21204,6 +21378,20 @@ method(object({
21204
21378
  error: string().optional()
21205
21379
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21206
21380
  providerId: string(),
21381
+ /**
21382
+ * The location this config is an UNSAVED edit of, when there is one.
21383
+ *
21384
+ * `listLocations` replaces every declared secret with the redaction
21385
+ * sentinel, so the edit modal's form state holds the sentinel for any
21386
+ * credential the operator did not retype — and posting that here
21387
+ * without a way to resolve it makes the provider try to authenticate
21388
+ * as `__camstack_redacted__` and report the operator's own working
21389
+ * password as wrong. Given this id, the orchestrator restores each
21390
+ * sentinel from the stored config (same rule as `upsertLocation`)
21391
+ * before dispatching. Omitted by the "Add location" wizard, where
21392
+ * every value was typed just now and nothing is stored yet.
21393
+ */
21394
+ locationId: string().optional(),
21207
21395
  config: record(string(), unknown())
21208
21396
  }), object({
21209
21397
  ok: boolean(),
@@ -26839,6 +27027,9 @@ method(object({
26839
27027
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26840
27028
  kind: "query",
26841
27029
  auth: "admin"
27030
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27031
+ kind: "mutation",
27032
+ auth: "admin"
26842
27033
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26843
27034
  kind: "mutation",
26844
27035
  auth: "admin"
@@ -27230,7 +27421,26 @@ var SceneMonitorStatusSchema = object({
27230
27421
  monitors: array(SceneMonitorSchema),
27231
27422
  lastFetchedAt: number$1()
27232
27423
  });
27233
- DeviceType.Camera, method(object({ deviceId: number$1() }), SceneMonitorStatusSchema), method(object({
27424
+ /**
27425
+ * One camera's row in a `listScenesBatch` answer.
27426
+ *
27427
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27428
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27429
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27430
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27431
+ * configured. Fanned out per camera the difference was visible — one query
27432
+ * errored while the others resolved — and a batch that returned only the rows
27433
+ * it managed would have destroyed it, silently, by making an unreachable camera
27434
+ * indistinguishable from one that answered `monitors: []`.
27435
+ *
27436
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27437
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27438
+ */
27439
+ var SceneMonitorStatusForDeviceSchema = object({
27440
+ deviceId: number$1(),
27441
+ status: SceneMonitorStatusSchema.nullable()
27442
+ });
27443
+ DeviceType.Camera, method(object({ deviceId: number$1() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27234
27444
  deviceId: number$1(),
27235
27445
  label: string(),
27236
27446
  roi: MaskRectShapeSchema,
@@ -28554,6 +28764,27 @@ var CameraOccupancySnapshotSchema = object({
28554
28764
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28555
28765
  });
28556
28766
  /**
28767
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28768
+ *
28769
+ * THREE outcomes, and the single-camera method could only express two of them
28770
+ * because `snapshot: null` was already spoken for:
28771
+ *
28772
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28773
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28774
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28775
+ * - `read: 'unreadable'` — the owner could not answer for this
28776
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28777
+ *
28778
+ * Collapsing the last two is the failure this field exists to prevent: a
28779
+ * hydration that threw would otherwise render as an empty Stationary section,
28780
+ * which is a definite claim about a camera nobody could read.
28781
+ */
28782
+ var CameraOccupancySnapshotForDeviceSchema = object({
28783
+ deviceId: number$1(),
28784
+ read: _enum(["read", "unreadable"]),
28785
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28786
+ });
28787
+ /**
28557
28788
  * Time-series resolution. The history methods return one bucket per
28558
28789
  * step over the requested range. Smaller resolutions cost more
28559
28790
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28577,7 +28808,7 @@ var HistoryPointSchema = object({
28577
28808
  /** Object count averaged over the bucket (rounded to nearest integer). */
28578
28809
  count: number$1().int().nonnegative()
28579
28810
  });
28580
- DeviceType.Camera, method(object({ deviceId: number$1() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28811
+ DeviceType.Camera, method(object({ deviceId: number$1() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28581
28812
  deviceId: number$1(),
28582
28813
  zoneId: string(),
28583
28814
  className: string().optional()
@@ -31858,6 +32089,12 @@ Object.freeze({
31858
32089
  addonId: null,
31859
32090
  access: "view"
31860
32091
  },
32092
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32093
+ capName: "pipeline-analytics",
32094
+ capScope: "device",
32095
+ addonId: null,
32096
+ access: "view"
32097
+ },
31861
32098
  "pipelineAnalytics.getEventStoreFootprint": {
31862
32099
  capName: "pipeline-analytics",
31863
32100
  capScope: "device",
@@ -31876,6 +32113,12 @@ Object.freeze({
31876
32113
  addonId: null,
31877
32114
  access: "view"
31878
32115
  },
32116
+ "pipelineAnalytics.getKeyEventsBatch": {
32117
+ capName: "pipeline-analytics",
32118
+ capScope: "device",
32119
+ addonId: null,
32120
+ access: "view"
32121
+ },
31879
32122
  "pipelineAnalytics.getMotionEvents": {
31880
32123
  capName: "pipeline-analytics",
31881
32124
  capScope: "device",
@@ -33052,6 +33295,12 @@ Object.freeze({
33052
33295
  addonId: null,
33053
33296
  access: "view"
33054
33297
  },
33298
+ "recording.reconcileLedgerAgainstDisk": {
33299
+ capName: "recording",
33300
+ capScope: "system",
33301
+ addonId: null,
33302
+ access: "create"
33303
+ },
33055
33304
  "recording.refreshStorageLocationsForMigration": {
33056
33305
  capName: "recording",
33057
33306
  capScope: "system",
@@ -33178,6 +33427,12 @@ Object.freeze({
33178
33427
  addonId: null,
33179
33428
  access: "view"
33180
33429
  },
33430
+ "sceneMonitor.listScenesBatch": {
33431
+ capName: "scene-monitor",
33432
+ capScope: "device",
33433
+ addonId: null,
33434
+ access: "view"
33435
+ },
33181
33436
  "sceneMonitor.recheckNow": {
33182
33437
  capName: "scene-monitor",
33183
33438
  capScope: "device",
@@ -34534,6 +34789,12 @@ Object.freeze({
34534
34789
  addonId: null,
34535
34790
  access: "view"
34536
34791
  },
34792
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34793
+ capName: "zone-analytics",
34794
+ capScope: "device",
34795
+ addonId: null,
34796
+ access: "view"
34797
+ },
34537
34798
  "zoneAnalytics.getUnzonedHistory": {
34538
34799
  capName: "zone-analytics",
34539
34800
  capScope: "device",
@@ -35523,6 +35784,11 @@ Object.freeze({
35523
35784
  form: "single",
35524
35785
  optional: false
35525
35786
  }],
35787
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35788
+ name: "deviceId",
35789
+ form: "single",
35790
+ optional: true
35791
+ }],
35526
35792
  "pipelineAnalytics.getGroup": [{
35527
35793
  name: "deviceId",
35528
35794
  form: "single",
@@ -35533,6 +35799,11 @@ Object.freeze({
35533
35799
  form: "single",
35534
35800
  optional: false
35535
35801
  }],
35802
+ "pipelineAnalytics.getKeyEventsBatch": [{
35803
+ name: "deviceIds",
35804
+ form: "array",
35805
+ optional: false
35806
+ }],
35536
35807
  "pipelineAnalytics.getMotionEvents": [{
35537
35808
  name: "deviceId",
35538
35809
  form: "single",
@@ -35973,6 +36244,11 @@ Object.freeze({
35973
36244
  form: "single",
35974
36245
  optional: false
35975
36246
  }],
36247
+ "recording.reconcileLedgerAgainstDisk": [{
36248
+ name: "deviceId",
36249
+ form: "single",
36250
+ optional: true
36251
+ }],
35976
36252
  "recording.relocateFootage": [{
35977
36253
  name: "deviceId",
35978
36254
  form: "single",
@@ -36038,6 +36314,11 @@ Object.freeze({
36038
36314
  form: "single",
36039
36315
  optional: false
36040
36316
  }],
36317
+ "sceneMonitor.listScenesBatch": [{
36318
+ name: "deviceIds",
36319
+ form: "array",
36320
+ optional: false
36321
+ }],
36041
36322
  "sceneMonitor.recheckNow": [{
36042
36323
  name: "deviceId",
36043
36324
  form: "single",
@@ -36299,6 +36580,11 @@ Object.freeze({
36299
36580
  form: "single",
36300
36581
  optional: false
36301
36582
  }],
36583
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36584
+ name: "deviceIds",
36585
+ form: "array",
36586
+ optional: false
36587
+ }],
36302
36588
  "zoneAnalytics.getUnzonedHistory": [{
36303
36589
  name: "deviceId",
36304
36590
  form: "single",
package/dist/addon.mjs CHANGED
@@ -8248,6 +8248,20 @@ var RelocateJobSchema = object({
8248
8248
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8249
8249
  */
8250
8250
  rowsReconciled: number$1().int().nonnegative().optional(),
8251
+ /**
8252
+ * Rows this run FORGOT because the file they name is not on disk.
8253
+ *
8254
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8255
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8256
+ * and the durable row is dropped through the same channel eviction uses. It
8257
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8258
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8259
+ * the same failure as one that quietly skips them (D295).
8260
+ *
8261
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8262
+ * ledger claimed 5.65 GB of footage that no longer existed.
8263
+ */
8264
+ rowsForgotten: number$1().int().nonnegative().optional(),
8251
8265
  startedAt: number$1(),
8252
8266
  finishedAt: number$1().nullable(),
8253
8267
  error: string().nullable()
@@ -8611,6 +8625,91 @@ var RelocateResidueSchema = object({
8611
8625
  segments: number$1().int().nonnegative(),
8612
8626
  bytes: number$1().int().nonnegative()
8613
8627
  }).nullable();
8628
+ /**
8629
+ * Ask one location whether its durable hour rows describe the disk — the walk
8630
+ * (D319).
8631
+ *
8632
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8633
+ * missing tool is the question, and the dry run is how they sanity-check the
8634
+ * destructive run before authorising it.
8635
+ */
8636
+ var LedgerWalkInputSchema = object({
8637
+ locationId: string().min(1),
8638
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8639
+ apply: boolean().optional(),
8640
+ /** Narrow to one camera. */
8641
+ deviceId: number$1().int().positive().optional(),
8642
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8643
+ profiles: array(string().min(1)).optional()
8644
+ });
8645
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8646
+ var LedgerWalkRefusalSchema = _enum([
8647
+ "location-unknown",
8648
+ "source-writable",
8649
+ "no-ledger",
8650
+ "archive-unreadable",
8651
+ "anchor-absent",
8652
+ "anchor-unreadable",
8653
+ "anchor-moved"
8654
+ ]);
8655
+ _enum([
8656
+ "live-tail",
8657
+ "listing-error",
8658
+ "path-mismatch",
8659
+ "durable-refused"
8660
+ ]);
8661
+ /** Every skip reason, always present, always a number — so a reason that never
8662
+ * fired reports as zero rather than absent and the report shape is constant
8663
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8664
+ var LedgerWalkSkipCountsSchema = object({
8665
+ "live-tail": number$1().int().nonnegative(),
8666
+ "listing-error": number$1().int().nonnegative(),
8667
+ "path-mismatch": number$1().int().nonnegative(),
8668
+ "durable-refused": number$1().int().nonnegative()
8669
+ });
8670
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8671
+ var LedgerWalkDeviceReportSchema = object({
8672
+ deviceId: number$1().int(),
8673
+ hoursWalked: number$1().int().nonnegative(),
8674
+ hoursMissing: number$1().int().nonnegative(),
8675
+ ghostSegments: number$1().int().nonnegative(),
8676
+ ghostBytes: number$1().int().nonnegative(),
8677
+ forgottenSegments: number$1().int().nonnegative(),
8678
+ orphanFiles: number$1().int().nonnegative()
8679
+ });
8680
+ /**
8681
+ * What one walk claimed, listed, found and (only when armed) forgot.
8682
+ *
8683
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8684
+ * walk that saw a fraction of the location is visible in its own report rather
8685
+ * than in the absence of one.
8686
+ */
8687
+ var LedgerWalkReportSchema = object({
8688
+ locationId: string(),
8689
+ applied: boolean(),
8690
+ refused: LedgerWalkRefusalSchema.nullable(),
8691
+ archiveSegments: number$1().int().nonnegative().nullable(),
8692
+ archiveBytes: number$1().int().nonnegative().nullable(),
8693
+ hoursClaimed: number$1().int().nonnegative(),
8694
+ hoursWalked: number$1().int().nonnegative(),
8695
+ hoursMissing: number$1().int().nonnegative(),
8696
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8697
+ listings: number$1().int().nonnegative(),
8698
+ segmentsClaimed: number$1().int().nonnegative(),
8699
+ ghostSegments: number$1().int().nonnegative(),
8700
+ ghostBytes: number$1().int().nonnegative(),
8701
+ ghostHoursWhole: number$1().int().nonnegative(),
8702
+ forgottenSegments: number$1().int().nonnegative(),
8703
+ forgottenBytes: number$1().int().nonnegative(),
8704
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8705
+ orphanFiles: number$1().int().nonnegative(),
8706
+ orphanSample: array(string()).readonly(),
8707
+ hoursSkipped: number$1().int().nonnegative(),
8708
+ skippedByReason: LedgerWalkSkipCountsSchema,
8709
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8710
+ bounded: boolean(),
8711
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8712
+ });
8614
8713
  /** How many rows a media pass would still act on against a given target — the
8615
8714
  * media lane's denominator AND its residue, from ONE derivation so the two can
8616
8715
  * never disagree. `null` = the count could not be taken. */
@@ -18978,13 +19077,15 @@ var ListGroupsPageSchema = object({
18978
19077
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18979
19078
  nextCursor: string().nullable()
18980
19079
  });
19080
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19081
+ var KEY_EVENTS_MAX_LIMIT = 200;
18981
19082
  var KeyEventQueryInput = object({
18982
19083
  deviceId: number$1(),
18983
19084
  /** Window lower bound (track firstSeen ≥ since). */
18984
19085
  since: number$1(),
18985
19086
  /** Window upper bound (track firstSeen ≤ until). */
18986
19087
  until: number$1(),
18987
- limit: number$1().int().min(1).max(200).default(50),
19088
+ limit: number$1().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18988
19089
  /** Drop tracks scoring below this importance. */
18989
19090
  minImportance: number$1().min(0).max(1).optional(),
18990
19091
  /** Restrict to a single class (e.g. 'person'). */
@@ -19006,6 +19107,32 @@ var KeyEventSchema = object({
19006
19107
  ...TrackFlagFields,
19007
19108
  ...TrackRetrainFields
19008
19109
  });
19110
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19111
+ var KeyEventBatchQueryInput = object({
19112
+ deviceIds: array(number$1()).min(1).max(200),
19113
+ since: number$1(),
19114
+ until: number$1(),
19115
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19116
+ * across the set, which would let a busy camera starve a quiet one of its
19117
+ * rows and change what the merged feed contains. */
19118
+ limit: number$1().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19119
+ minImportance: number$1().min(0).max(1).optional(),
19120
+ classFilter: string().optional()
19121
+ });
19122
+ /**
19123
+ * One camera's key events in a batch answer.
19124
+ *
19125
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19126
+ * error rather than throwing, so a camera whose store read failed and one with
19127
+ * no events in the window were ALREADY indistinguishable per camera — the
19128
+ * batch does not make that worse, and the row keeps the deviceId the single
19129
+ * method's output never carried (the caller used to stamp it from the fan-out
19130
+ * key, which only worked because there was one query per camera).
19131
+ */
19132
+ var KeyEventsForDeviceSchema = object({
19133
+ deviceId: number$1(),
19134
+ events: array(KeyEventSchema).readonly()
19135
+ });
19009
19136
  object({
19010
19137
  trackId: string(),
19011
19138
  className: string(),
@@ -19077,6 +19204,50 @@ var EventStoreFootprintSchema = object({
19077
19204
  totalBytes: number$1().int(),
19078
19205
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19079
19206
  });
19207
+ /** Event-media footprint for one {@link MediaFileKind}. */
19208
+ var EventMediaKindFootprintSchema = object({
19209
+ kind: MediaFileKindEnum,
19210
+ /** Media rows of this kind. */
19211
+ rows: number$1().int(),
19212
+ /** Bytes on disk held by those rows. */
19213
+ bytes: number$1().int()
19214
+ });
19215
+ /**
19216
+ * The media footprint broken down by KIND — the axis a deletion decision
19217
+ * actually turns on.
19218
+ *
19219
+ * A byte total says how much there is; it cannot say what is safe to remove.
19220
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19221
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19222
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19223
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19224
+ * nothing else, so sizing a deletion means summing per kind.
19225
+ *
19226
+ * ## Why `unaccounted*` exists
19227
+ *
19228
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19229
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19230
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19231
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19232
+ * retired code path, or by a version that knew a kind this one does not) would
19233
+ * otherwise vanish from the total silently, and an operator would delete
19234
+ * against a denominator smaller than the disk.
19235
+ *
19236
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19237
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19238
+ */
19239
+ var EventMediaKindBreakdownSchema = object({
19240
+ /** Every media row in scope, from one unfiltered aggregate. */
19241
+ totalRows: number$1().int(),
19242
+ /** Every media byte in scope, from that same aggregate. */
19243
+ totalBytes: number$1().int(),
19244
+ /** Per-kind footprint, ordered by bytes descending. */
19245
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19246
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19247
+ unaccountedRows: number$1().int(),
19248
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19249
+ unaccountedBytes: number$1().int()
19250
+ });
19080
19251
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19081
19252
  var EventPruneCountsSchema = object({
19082
19253
  motion: number$1().int(),
@@ -19231,7 +19402,7 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19231
19402
  until: number$1().optional(),
19232
19403
  kinds: array(string()).optional(),
19233
19404
  limit: number$1().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19234
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19405
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19235
19406
  deviceId: number$1(),
19236
19407
  since: number$1(),
19237
19408
  until: number$1(),
@@ -19280,6 +19451,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19280
19451
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19281
19452
  kind: "query",
19282
19453
  auth: "admin"
19454
+ }), method(object({ deviceId: number$1().int().optional() }), EventMediaKindBreakdownSchema, {
19455
+ kind: "query",
19456
+ auth: "admin"
19283
19457
  }), method(object({
19284
19458
  olderThanMs: number$1(),
19285
19459
  reason: OpsLogReasonSchema.optional()
@@ -21231,6 +21405,20 @@ method(object({
21231
21405
  error: string().optional()
21232
21406
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21233
21407
  providerId: string(),
21408
+ /**
21409
+ * The location this config is an UNSAVED edit of, when there is one.
21410
+ *
21411
+ * `listLocations` replaces every declared secret with the redaction
21412
+ * sentinel, so the edit modal's form state holds the sentinel for any
21413
+ * credential the operator did not retype — and posting that here
21414
+ * without a way to resolve it makes the provider try to authenticate
21415
+ * as `__camstack_redacted__` and report the operator's own working
21416
+ * password as wrong. Given this id, the orchestrator restores each
21417
+ * sentinel from the stored config (same rule as `upsertLocation`)
21418
+ * before dispatching. Omitted by the "Add location" wizard, where
21419
+ * every value was typed just now and nothing is stored yet.
21420
+ */
21421
+ locationId: string().optional(),
21234
21422
  config: record(string(), unknown())
21235
21423
  }), object({
21236
21424
  ok: boolean(),
@@ -26866,6 +27054,9 @@ method(object({
26866
27054
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26867
27055
  kind: "query",
26868
27056
  auth: "admin"
27057
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27058
+ kind: "mutation",
27059
+ auth: "admin"
26869
27060
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26870
27061
  kind: "mutation",
26871
27062
  auth: "admin"
@@ -27257,7 +27448,26 @@ var SceneMonitorStatusSchema = object({
27257
27448
  monitors: array(SceneMonitorSchema),
27258
27449
  lastFetchedAt: number$1()
27259
27450
  });
27260
- DeviceType.Camera, method(object({ deviceId: number$1() }), SceneMonitorStatusSchema), method(object({
27451
+ /**
27452
+ * One camera's row in a `listScenesBatch` answer.
27453
+ *
27454
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27455
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27456
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27457
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27458
+ * configured. Fanned out per camera the difference was visible — one query
27459
+ * errored while the others resolved — and a batch that returned only the rows
27460
+ * it managed would have destroyed it, silently, by making an unreachable camera
27461
+ * indistinguishable from one that answered `monitors: []`.
27462
+ *
27463
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27464
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27465
+ */
27466
+ var SceneMonitorStatusForDeviceSchema = object({
27467
+ deviceId: number$1(),
27468
+ status: SceneMonitorStatusSchema.nullable()
27469
+ });
27470
+ DeviceType.Camera, method(object({ deviceId: number$1() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27261
27471
  deviceId: number$1(),
27262
27472
  label: string(),
27263
27473
  roi: MaskRectShapeSchema,
@@ -28581,6 +28791,27 @@ var CameraOccupancySnapshotSchema = object({
28581
28791
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28582
28792
  });
28583
28793
  /**
28794
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28795
+ *
28796
+ * THREE outcomes, and the single-camera method could only express two of them
28797
+ * because `snapshot: null` was already spoken for:
28798
+ *
28799
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28800
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28801
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28802
+ * - `read: 'unreadable'` — the owner could not answer for this
28803
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28804
+ *
28805
+ * Collapsing the last two is the failure this field exists to prevent: a
28806
+ * hydration that threw would otherwise render as an empty Stationary section,
28807
+ * which is a definite claim about a camera nobody could read.
28808
+ */
28809
+ var CameraOccupancySnapshotForDeviceSchema = object({
28810
+ deviceId: number$1(),
28811
+ read: _enum(["read", "unreadable"]),
28812
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28813
+ });
28814
+ /**
28584
28815
  * Time-series resolution. The history methods return one bucket per
28585
28816
  * step over the requested range. Smaller resolutions cost more
28586
28817
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28604,7 +28835,7 @@ var HistoryPointSchema = object({
28604
28835
  /** Object count averaged over the bucket (rounded to nearest integer). */
28605
28836
  count: number$1().int().nonnegative()
28606
28837
  });
28607
- DeviceType.Camera, method(object({ deviceId: number$1() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28838
+ DeviceType.Camera, method(object({ deviceId: number$1() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number$1()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28608
28839
  deviceId: number$1(),
28609
28840
  zoneId: string(),
28610
28841
  className: string().optional()
@@ -31885,6 +32116,12 @@ Object.freeze({
31885
32116
  addonId: null,
31886
32117
  access: "view"
31887
32118
  },
32119
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32120
+ capName: "pipeline-analytics",
32121
+ capScope: "device",
32122
+ addonId: null,
32123
+ access: "view"
32124
+ },
31888
32125
  "pipelineAnalytics.getEventStoreFootprint": {
31889
32126
  capName: "pipeline-analytics",
31890
32127
  capScope: "device",
@@ -31903,6 +32140,12 @@ Object.freeze({
31903
32140
  addonId: null,
31904
32141
  access: "view"
31905
32142
  },
32143
+ "pipelineAnalytics.getKeyEventsBatch": {
32144
+ capName: "pipeline-analytics",
32145
+ capScope: "device",
32146
+ addonId: null,
32147
+ access: "view"
32148
+ },
31906
32149
  "pipelineAnalytics.getMotionEvents": {
31907
32150
  capName: "pipeline-analytics",
31908
32151
  capScope: "device",
@@ -33079,6 +33322,12 @@ Object.freeze({
33079
33322
  addonId: null,
33080
33323
  access: "view"
33081
33324
  },
33325
+ "recording.reconcileLedgerAgainstDisk": {
33326
+ capName: "recording",
33327
+ capScope: "system",
33328
+ addonId: null,
33329
+ access: "create"
33330
+ },
33082
33331
  "recording.refreshStorageLocationsForMigration": {
33083
33332
  capName: "recording",
33084
33333
  capScope: "system",
@@ -33205,6 +33454,12 @@ Object.freeze({
33205
33454
  addonId: null,
33206
33455
  access: "view"
33207
33456
  },
33457
+ "sceneMonitor.listScenesBatch": {
33458
+ capName: "scene-monitor",
33459
+ capScope: "device",
33460
+ addonId: null,
33461
+ access: "view"
33462
+ },
33208
33463
  "sceneMonitor.recheckNow": {
33209
33464
  capName: "scene-monitor",
33210
33465
  capScope: "device",
@@ -34561,6 +34816,12 @@ Object.freeze({
34561
34816
  addonId: null,
34562
34817
  access: "view"
34563
34818
  },
34819
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34820
+ capName: "zone-analytics",
34821
+ capScope: "device",
34822
+ addonId: null,
34823
+ access: "view"
34824
+ },
34564
34825
  "zoneAnalytics.getUnzonedHistory": {
34565
34826
  capName: "zone-analytics",
34566
34827
  capScope: "device",
@@ -35550,6 +35811,11 @@ Object.freeze({
35550
35811
  form: "single",
35551
35812
  optional: false
35552
35813
  }],
35814
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35815
+ name: "deviceId",
35816
+ form: "single",
35817
+ optional: true
35818
+ }],
35553
35819
  "pipelineAnalytics.getGroup": [{
35554
35820
  name: "deviceId",
35555
35821
  form: "single",
@@ -35560,6 +35826,11 @@ Object.freeze({
35560
35826
  form: "single",
35561
35827
  optional: false
35562
35828
  }],
35829
+ "pipelineAnalytics.getKeyEventsBatch": [{
35830
+ name: "deviceIds",
35831
+ form: "array",
35832
+ optional: false
35833
+ }],
35563
35834
  "pipelineAnalytics.getMotionEvents": [{
35564
35835
  name: "deviceId",
35565
35836
  form: "single",
@@ -36000,6 +36271,11 @@ Object.freeze({
36000
36271
  form: "single",
36001
36272
  optional: false
36002
36273
  }],
36274
+ "recording.reconcileLedgerAgainstDisk": [{
36275
+ name: "deviceId",
36276
+ form: "single",
36277
+ optional: true
36278
+ }],
36003
36279
  "recording.relocateFootage": [{
36004
36280
  name: "deviceId",
36005
36281
  form: "single",
@@ -36065,6 +36341,11 @@ Object.freeze({
36065
36341
  form: "single",
36066
36342
  optional: false
36067
36343
  }],
36344
+ "sceneMonitor.listScenesBatch": [{
36345
+ name: "deviceIds",
36346
+ form: "array",
36347
+ optional: false
36348
+ }],
36068
36349
  "sceneMonitor.recheckNow": [{
36069
36350
  name: "deviceId",
36070
36351
  form: "single",
@@ -36326,6 +36607,11 @@ Object.freeze({
36326
36607
  form: "single",
36327
36608
  optional: false
36328
36609
  }],
36610
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36611
+ name: "deviceIds",
36612
+ form: "array",
36613
+ optional: false
36614
+ }],
36329
36615
  "zoneAnalytics.getUnzonedHistory": [{
36330
36616
  name: "deviceId",
36331
36617
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",