@camstack/addon-export-alexa 1.2.48 → 1.2.51

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.
@@ -8207,18 +8207,61 @@ var RelocateFootageInputSchema = object({
8207
8207
  * `RecordingConfig.enabled` or camera wrapper bindings. */
8208
8208
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
8209
8209
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
8210
+ /**
8211
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
8212
+ * mover (the engine already walks both collections with a timestamp cursor and
8213
+ * already has a stamp-without-copy path).
8214
+ *
8215
+ * - `move` — the default and the historical behaviour: event-media and
8216
+ * retrain blobs move to `toLocationId` and their rows are
8217
+ * stamped. The enrolled gallery is skipped (D197).
8218
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
8219
+ * stamped with `toLocationId`. `toLocationId` here is the id the
8220
+ * bytes ALREADY sit on — today's `eventMedia` default — because
8221
+ * a NULL row means "wherever `eventMedia` points *now*", and the
8222
+ * instant a repoint moves that pointer the row reads from the
8223
+ * new disk while its bytes are on the old one.
8224
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
8225
+ * (enrolled-gallery) rows, which `move` deliberately skips.
8226
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
8227
+ * never run beside a live second location: it is stop-the-world
8228
+ * by construction, which is acceptable only because the gallery
8229
+ * is a few KB per enrolled sample.
8230
+ */
8231
+ var MediaRelocateModeSchema = _enum([
8232
+ "move",
8233
+ "seal",
8234
+ "gallery"
8235
+ ]);
8210
8236
  var RelocateMediaInputSchema = object({
8211
8237
  toLocationId: string(),
8212
- throttleMbps: number().min(1).max(1e3).optional()
8238
+ throttleMbps: number().min(1).max(1e3).optional(),
8239
+ /** Omitted = `move`, the pre-existing behaviour. */
8240
+ mode: MediaRelocateModeSchema.optional()
8241
+ });
8242
+ /** How many rows still carry NO `locationId` — the population a repoint would
8243
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
8244
+ * value that permits a non-blocking `eventMedia` cutover. */
8245
+ var UnstampedEventMediaCountSchema = object({
8246
+ media: number().int().nonnegative(),
8247
+ retrainFrames: number().int().nonnegative(),
8248
+ total: number().int().nonnegative()
8213
8249
  });
8214
8250
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8215
- /** The independently selectable logical storage classes. `recordings`
8216
- * encompasses the high and mid segment profiles; `recordingsLow` is low
8217
- * segments; `eventMedia` is post-analysis blobs. */
8251
+ /** The independently selectable logical storage classes — every class
8252
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
8253
+ * Zod enum error where they should meet an explanation.
8254
+ *
8255
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
8256
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
8257
+ * enrolled gallery; `backups` is the system backup archive. The last two have
8258
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
8218
8259
  var StorageMigrationClassSchema = _enum([
8219
8260
  "recordings",
8220
8261
  "recordingsLow",
8221
- "eventMedia"
8262
+ "eventMedia",
8263
+ "backups",
8264
+ "galleryMedia"
8222
8265
  ]);
8223
8266
  /** A destination is always an existing, fully-qualified location id. The
8224
8267
  * migration API intentionally never changes a source location's `basePath`:
@@ -8226,20 +8269,56 @@ var StorageMigrationClassSchema = _enum([
8226
8269
  var StorageMigrationDestinationsSchema = object({
8227
8270
  recordings: string().min(1).optional(),
8228
8271
  recordingsLow: string().min(1).optional(),
8229
- eventMedia: string().min(1).optional()
8272
+ eventMedia: string().min(1).optional(),
8273
+ backups: string().min(1).optional(),
8274
+ galleryMedia: string().min(1).optional()
8230
8275
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
8276
+ /**
8277
+ * How a migration sequences the cutover against the byte move.
8278
+ *
8279
+ * - `blocking` — the historical order: pause, move every byte, repoint,
8280
+ * resume. Recording is stopped for the whole move. Right
8281
+ * for a small or a cold class, and the only legal mode for
8282
+ * a `cardinality: 'single'` class.
8283
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
8284
+ * refresh, resume, then move the past with everything
8285
+ * running. The pause is three bounded instants (a detach +
8286
+ * attach round, a write-gate drain, a lease) instead of one
8287
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
8288
+ * stopped recording under `blocking`; the same move is
8289
+ * seconds of stopped recording under `nonBlocking`.
8290
+ *
8291
+ * The mode is on the JOB, not only on the input, because `status` is where an
8292
+ * operator finds out which one is running.
8293
+ */
8294
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
8231
8295
  /** Shared input for planning and starting an orchestrated storage migration. */
8232
8296
  var StorageMigrationInputSchema = object({
8233
8297
  destinations: StorageMigrationDestinationsSchema,
8234
- throttleMbps: number().min(1).max(1e3).optional()
8298
+ throttleMbps: number().min(1).max(1e3).optional(),
8299
+ /** Omitted = `blocking`, which stays the default. */
8300
+ mode: StorageMigrationModeSchema.optional()
8235
8301
  });
8236
- /** The durable coordinator state machine. The only phase that changes default
8237
- * locations is `repointing`, after every selected mover has completed and been
8238
- * verified. */
8302
+ /**
8303
+ * The durable coordinator state machine.
8304
+ *
8305
+ * `blocking`:
8306
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
8307
+ *
8308
+ * `nonBlocking`:
8309
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
8310
+ *
8311
+ * Same phases, different order plus two new ones — not a second mover.
8312
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
8313
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
8314
+ * `repointing` is still the only phase that changes a default location.
8315
+ */
8239
8316
  var StorageMigrationPhaseSchema = _enum([
8240
8317
  "planning",
8318
+ "sealing",
8241
8319
  "pausing",
8242
8320
  "moving",
8321
+ "draining",
8243
8322
  "verifying",
8244
8323
  "repointing",
8245
8324
  "refreshing",
@@ -8253,17 +8332,56 @@ var StorageMigrationParticipantSchema = _enum([
8253
8332
  "recorder",
8254
8333
  "analytics"
8255
8334
  ]);
8335
+ /**
8336
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8337
+ *
8338
+ * The long half of a non-blocking migration is `draining`, and it is measured
8339
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8340
+ * existed the only place those numbers appeared was a Loki line, so an operator
8341
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8342
+ * afternoon.
8343
+ *
8344
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8345
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8346
+ * mover — which is the exact failure this is meant to end. The coordinator's
8347
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8348
+ * read `state`; folding the counters costs no extra read and makes the durable
8349
+ * record say afterwards how far a move actually got.
8350
+ *
8351
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8352
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8353
+ * cannot say M, and a 0 there would render as "100 % done".
8354
+ */
8355
+ var StorageMigrationMoveProgressSchema = object({
8356
+ filesMoved: number().int().nonnegative(),
8357
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8358
+ filesTotal: number().int().nonnegative().nullable(),
8359
+ bytesMoved: number().int().nonnegative(),
8360
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8361
+ * crash gets a new mover, and a rate computed from the migration's start
8362
+ * would silently average in the time nothing was running. */
8363
+ startedAt: number(),
8364
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8365
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8366
+ * subtract its own. */
8367
+ observedAt: number()
8368
+ });
8256
8369
  var StorageMigrationMoveSchema = object({
8257
8370
  storageClass: StorageMigrationClassSchema,
8258
8371
  fromLocationId: string(),
8259
8372
  toLocationId: string(),
8260
8373
  moverJobId: string().nullable(),
8261
8374
  state: RelocateJobStateSchema.nullable(),
8262
- error: string().nullable()
8375
+ error: string().nullable(),
8376
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8377
+ progress: StorageMigrationMoveProgressSchema.nullable()
8263
8378
  });
8264
8379
  var StorageMigrationJobSchema = object({
8265
8380
  jobId: string(),
8266
8381
  phase: StorageMigrationPhaseSchema,
8382
+ /** Which order this job is running. `status` is the only place an operator
8383
+ * can tell a seconds-long cutover from a thirty-hour one. */
8384
+ mode: StorageMigrationModeSchema,
8267
8385
  destinations: StorageMigrationDestinationsSchema,
8268
8386
  throttleMbps: number(),
8269
8387
  moves: array(StorageMigrationMoveSchema),
@@ -8276,13 +8394,122 @@ var StorageMigrationJobSchema = object({
8276
8394
  finishedAt: number().nullable(),
8277
8395
  error: string().nullable()
8278
8396
  });
8397
+ var StorageMigrationFindingSchema = object({
8398
+ code: _enum([
8399
+ "sharesDeviceWithSource",
8400
+ "deviceIdentityUnknown",
8401
+ "unstampedEventMediaRows",
8402
+ "blockingOnly",
8403
+ "noMover"
8404
+ ]),
8405
+ storageClass: StorageMigrationClassSchema,
8406
+ /** Human-readable, already carrying the ids and counts. */
8407
+ message: string()
8408
+ });
8279
8409
  var StorageMigrationPlanSchema = object({
8280
8410
  destinations: StorageMigrationDestinationsSchema,
8411
+ /** The mode this plan was built for. A plan is only valid for its mode: the
8412
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
8413
+ * it. */
8414
+ mode: StorageMigrationModeSchema,
8281
8415
  moves: array(object({
8282
8416
  storageClass: StorageMigrationClassSchema,
8283
8417
  fromLocationId: string(),
8284
8418
  toLocationId: string()
8285
- }))
8419
+ })),
8420
+ findings: array(StorageMigrationFindingSchema)
8421
+ });
8422
+ /**
8423
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8424
+ *
8425
+ * The coordinator's job record is the state of record for a migration, and its
8426
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8427
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8428
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8429
+ * way because no supported UI path existed. A mover armed like that has no job
8430
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8431
+ *
8432
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8433
+ * orchestrated it.
8434
+ */
8435
+ var StorageMigrationMoverSchema = object({
8436
+ lane: _enum(["footage", "media"]),
8437
+ job: RelocateJobSchema,
8438
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8439
+ * directly against the owning addon. */
8440
+ migrationJobId: string().nullable(),
8441
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8442
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8443
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8444
+ * rate made of two different clocks. */
8445
+ observedAt: number()
8446
+ });
8447
+ /**
8448
+ * What a SOURCE still holds for one storage class — the number that makes a
8449
+ * "drain remaining" action honest rather than hopeful.
8450
+ *
8451
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8452
+ * engine's own selection count for media), never from the resident index: a
8453
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8454
+ * never been told about (D295).
8455
+ *
8456
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8457
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8458
+ * because refusing on an unanswerable read would hide exactly the case an
8459
+ * operator needs to act on.
8460
+ */
8461
+ var StorageMigrationResidueSchema = object({
8462
+ storageClass: StorageMigrationClassSchema,
8463
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8464
+ * move from wherever they are rather than from one named source. */
8465
+ fromLocationId: string(),
8466
+ /** Where a drain would move it — the class's CURRENT default. */
8467
+ toLocationId: string(),
8468
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8469
+ items: number().int().nonnegative().nullable(),
8470
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8471
+ bytes: number().int().nonnegative().nullable()
8472
+ });
8473
+ /**
8474
+ * Run the DRAIN half and nothing else.
8475
+ *
8476
+ * A migration that reached `done` has already repointed, so `start` correctly
8477
+ * refuses its destination ("already the default") — there is nothing left to
8478
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8479
+ * or finish against a work list that was a tenth of the archive (D295), and
8480
+ * before this there was no supported way to run only that half: the only way
8481
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8482
+ *
8483
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8484
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8485
+ * re-repoint a class that is already migrated.
8486
+ */
8487
+ var StorageMigrationDrainInputSchema = object({
8488
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8489
+ * a class whose source is already empty is refused rather than started. */
8490
+ classes: array(StorageMigrationClassSchema).min(1),
8491
+ throttleMbps: number().min(1).max(1e3).optional()
8492
+ });
8493
+ /** What a footage source still holds, asked of the durable hour ledger. */
8494
+ var RelocateResidueInputSchema = object({
8495
+ fromLocationId: string().min(1),
8496
+ /** Narrow to one logical class; omit for every profile on the location. */
8497
+ footageClass: RelocateFootageClassSchema.optional()
8498
+ });
8499
+ /** `null` = the archive could not answer (no ledger on this node, or the
8500
+ * aggregate failed). Never conflated with an empty source. */
8501
+ var RelocateResidueSchema = object({
8502
+ segments: number().int().nonnegative(),
8503
+ bytes: number().int().nonnegative()
8504
+ }).nullable();
8505
+ /** How many rows a media pass would still act on against a given target — the
8506
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8507
+ * never disagree. `null` = the count could not be taken. */
8508
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8509
+ var RelocatableMediaCountInputSchema = object({
8510
+ toLocationId: string().min(1),
8511
+ /** Omitted = `move`. */
8512
+ mode: MediaRelocateModeSchema.optional()
8286
8513
  });
8287
8514
  /**
8288
8515
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -8388,6 +8615,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8388
8615
  * two addons declaring the same `id` must agree on `cardinality` (validated
8389
8616
  * at kernel aggregation time, not here).
8390
8617
  */
8618
+ /**
8619
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8620
+ * actually reaches the bytes. It is the constraint that decides which
8621
+ * `storage-provider`s may back a location of that kind.
8622
+ *
8623
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8624
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8625
+ * post-analysis media roots). Only a provider that serves a genuine local
8626
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8627
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8628
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8629
+ * against a same-named local directory that is something else entirely.
8630
+ *
8631
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8632
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8633
+ * service never sees a path, so any provider can back it. `backups` is the
8634
+ * one kind that qualifies today.
8635
+ *
8636
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8637
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8638
+ * refused the configuration; the first write simply went somewhere wrong, and
8639
+ * a recording write that goes wrong surfaces as a silent black window rather
8640
+ * than an error (the read path does not `stat`). This turns that accident into
8641
+ * a declared, enforced, testable refusal.
8642
+ */
8643
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8391
8644
  var StorageLocationDeclarationSchema = object({
8392
8645
  /**
8393
8646
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8407,6 +8660,19 @@ var StorageLocationDeclarationSchema = object({
8407
8660
  */
8408
8661
  cardinality: _enum(["single", "multi"]),
8409
8662
  /**
8663
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8664
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8665
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8666
+ *
8667
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8668
+ * can only over-restrict (refuse a remote provider for a kind that might
8669
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8670
+ * permissive direction and is therefore never inferred — a repo guard
8671
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8672
+ * reached by omission.
8673
+ */
8674
+ access: StorageAccessSchema.optional(),
8675
+ /**
8410
8676
  * When set, the default instance for this location inherits its resolved
8411
8677
  * root from the named location's default instance. Useful for derivative
8412
8678
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18075,8 +18341,10 @@ var TrackSchema = object({
18075
18341
  lastSeen: number(),
18076
18342
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18077
18343
  positions: array(TrackPositionSchema).readonly(),
18078
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18079
- * saveThumbnails policy). */
18344
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18345
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18346
+ * the retired `saveThumbnails` used to gate this and the rolling
18347
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18080
18348
  snapshots: array(TrackSnapshotSchema).readonly(),
18081
18349
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18082
18350
  zonesVisited: array(string()).readonly(),
@@ -18936,6 +19204,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18936
19204
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
18937
19205
  kind: "mutation",
18938
19206
  auth: "admin"
19207
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19208
+ kind: "query",
19209
+ auth: "admin"
18939
19210
  }), method(object({}), array(RelocateJobSchema).readonly(), {
18940
19211
  kind: "query",
18941
19212
  auth: "admin"
@@ -20843,7 +21114,10 @@ method(object({
20843
21114
  }), StorageLocationSchema, {
20844
21115
  kind: "mutation",
20845
21116
  auth: "admin"
20846
- }), method(object({ id: string() }), _void(), {
21117
+ }), method(object({
21118
+ id: string(),
21119
+ force: boolean().optional()
21120
+ }), _void(), {
20847
21121
  kind: "mutation",
20848
21122
  auth: "admin"
20849
21123
  }), method(object({ id: string() }), object({
@@ -20892,6 +21166,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20892
21166
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20893
21167
  kind: "mutation",
20894
21168
  auth: "admin"
21169
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21170
+ kind: "mutation",
21171
+ auth: "admin"
20895
21172
  });
20896
21173
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20897
21174
  providerId: string().min(1),
@@ -21290,12 +21567,38 @@ response: record(string(), unknown()) }), object({
21290
21567
  *
21291
21568
  * ## Why this is a capability and not a helper
21292
21569
  *
21293
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21294
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21295
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21296
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21297
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21298
- * load 5,000 rows before ranking anything.
21570
+ * This capability was introduced with the claim that SIX stores in
21571
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21572
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21573
+ * claim was never true, and leaving it here made five stores look like pending
21574
+ * work when three of them have no vector at all. Counted column by column on
21575
+ * 2026-08-30, exactly THREE ever held one:
21576
+ *
21577
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21578
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21579
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21580
+ * face, migrated 2026-08-30 into its OWN index (see below).
21581
+ *
21582
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21583
+ * and `identities` store a name; the event store stores no derivative vector.
21584
+ * They are not migration candidates and never were.
21585
+ *
21586
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21587
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21588
+ * rows before ranking anything.
21589
+ *
21590
+ * ## One index per COMPARISON, never per encoder
21591
+ *
21592
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21593
+ * model, and they still get two indexes. An index is a set of things that are
21594
+ * ranked against each other and that live and die together, and these two are
21595
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21596
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21597
+ * forever and is the gallery every recognition ranks against. One index would
21598
+ * mean every gallery load and every reconcile carried a filter whose failure
21599
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21600
+ * person's only sample. The dimension they share is not a reason to share an
21601
+ * index; the question they answer is, and it differs.
21299
21602
  *
21300
21603
  * The fix is not a faster loop, it is a different backend — and the backend
21301
21604
  * should be replaceable without touching six callers. So: a singleton
@@ -21400,7 +21703,20 @@ var VectorQueryResultSchema = object({
21400
21703
  */
21401
21704
  scanned: number(),
21402
21705
  /** True when the backend could not consider every row that passed the filter. */
21403
- truncated: boolean()
21706
+ truncated: boolean(),
21707
+ /**
21708
+ * The `topK` the backend actually ran with.
21709
+ *
21710
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21711
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21712
+ * own log rather than in its answer. That is how an audit asking for 20,000
21713
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21714
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21715
+ * MUCH, in the return value, where the caller cannot fail to see it.
21716
+ *
21717
+ * Equals the requested `topK` whenever nothing was lowered.
21718
+ */
21719
+ effectiveTopK: number().int().positive()
21404
21720
  });
21405
21721
  var VectorDeleteInputSchema = object({
21406
21722
  index: string(),
@@ -21429,6 +21745,68 @@ var VectorGetResultSchema = object({ items: array(object({
21429
21745
  id: string(),
21430
21746
  metadata: VectorMetadataSchema
21431
21747
  })) });
21748
+ /**
21749
+ * Ids to read back WITH their vectors.
21750
+ *
21751
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21752
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21753
+ * caller depends on that promise. This one promises the opposite.
21754
+ *
21755
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21756
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21757
+ * a per-face cross-process KNN would be a network round trip inside the
21758
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21759
+ * it requires the index to hand the floats back. Without this method the only
21760
+ * way to keep a readable vector is a JSON column, which is the thing this
21761
+ * capability exists to delete.
21762
+ *
21763
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21764
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21765
+ */
21766
+ var VectorFetchInputSchema = object({
21767
+ index: string(),
21768
+ ids: array(string())
21769
+ });
21770
+ var VectorFetchResultSchema = object({ items: array(object({
21771
+ id: string(),
21772
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21773
+ vector: string(),
21774
+ metadata: VectorMetadataSchema
21775
+ })) });
21776
+ /**
21777
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21778
+ *
21779
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21780
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21781
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21782
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21783
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21784
+ * looked" for as long as anyone cared to read it.
21785
+ *
21786
+ * This is the primitive that question actually needs: a bounded page, ordered
21787
+ * by the backend's own row order, costing no distance computation at all.
21788
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21789
+ * the full-table read this capability was built to stop.
21790
+ */
21791
+ var VectorScanInputSchema = object({
21792
+ index: string(),
21793
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21794
+ cursor: number().int().nonnegative().default(0),
21795
+ limit: number().int().positive()
21796
+ });
21797
+ var VectorScanResultSchema = object({
21798
+ items: array(object({
21799
+ id: string(),
21800
+ metadata: VectorMetadataSchema
21801
+ })),
21802
+ /**
21803
+ * Where the next page starts, or `null` when the walk reached the end.
21804
+ *
21805
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21806
+ * from a short page: a backend is free to return fewer rows than asked.
21807
+ */
21808
+ nextCursor: number().int().nonnegative().nullable()
21809
+ });
21432
21810
  var VectorStatsInputSchema = object({ index: string() });
21433
21811
  var VectorStatsResultSchema = object({
21434
21812
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21447,7 +21825,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21447
21825
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21448
21826
  kind: "mutation",
21449
21827
  auth: "admin"
21450
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21828
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21451
21829
  kind: "mutation",
21452
21830
  auth: "admin"
21453
21831
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26351,6 +26729,9 @@ method(object({
26351
26729
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26352
26730
  kind: "query",
26353
26731
  auth: "admin"
26732
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26733
+ kind: "query",
26734
+ auth: "admin"
26354
26735
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26355
26736
  kind: "mutation",
26356
26737
  auth: "admin"
@@ -31316,6 +31697,18 @@ Object.freeze({
31316
31697
  addonId: null,
31317
31698
  access: "create"
31318
31699
  },
31700
+ "pipelineAnalytics.countRelocatableMedia": {
31701
+ capName: "pipeline-analytics",
31702
+ capScope: "device",
31703
+ addonId: null,
31704
+ access: "view"
31705
+ },
31706
+ "pipelineAnalytics.countUnstampedEventMedia": {
31707
+ capName: "pipeline-analytics",
31708
+ capScope: "device",
31709
+ addonId: null,
31710
+ access: "view"
31711
+ },
31319
31712
  "pipelineAnalytics.deleteDeviceEvents": {
31320
31713
  capName: "pipeline-analytics",
31321
31714
  capScope: "device",
@@ -32474,6 +32867,12 @@ Object.freeze({
32474
32867
  addonId: null,
32475
32868
  access: "view"
32476
32869
  },
32870
+ "recording.getRelocateResidue": {
32871
+ capName: "recording",
32872
+ capScope: "system",
32873
+ addonId: null,
32874
+ access: "view"
32875
+ },
32477
32876
  "recording.getStorageMigrationMoveStatus": {
32478
32877
  capName: "recording",
32479
32878
  capScope: "system",
@@ -33020,12 +33419,30 @@ Object.freeze({
33020
33419
  addonId: null,
33021
33420
  access: "create"
33022
33421
  },
33422
+ "storageMigration.drain": {
33423
+ capName: "storage-migration",
33424
+ capScope: "system",
33425
+ addonId: null,
33426
+ access: "create"
33427
+ },
33428
+ "storageMigration.movers": {
33429
+ capName: "storage-migration",
33430
+ capScope: "system",
33431
+ addonId: null,
33432
+ access: "view"
33433
+ },
33023
33434
  "storageMigration.plan": {
33024
33435
  capName: "storage-migration",
33025
33436
  capScope: "system",
33026
33437
  addonId: null,
33027
33438
  access: "view"
33028
33439
  },
33440
+ "storageMigration.residue": {
33441
+ capName: "storage-migration",
33442
+ capScope: "system",
33443
+ addonId: null,
33444
+ access: "view"
33445
+ },
33029
33446
  "storageMigration.start": {
33030
33447
  capName: "storage-migration",
33031
33448
  capScope: "system",
@@ -33860,6 +34277,12 @@ Object.freeze({
33860
34277
  addonId: null,
33861
34278
  access: "delete"
33862
34279
  },
34280
+ "vectorStore.fetchByIds": {
34281
+ capName: "vector-store",
34282
+ capScope: "system",
34283
+ addonId: null,
34284
+ access: "view"
34285
+ },
33863
34286
  "vectorStore.getByIds": {
33864
34287
  capName: "vector-store",
33865
34288
  capScope: "system",
@@ -33872,6 +34295,12 @@ Object.freeze({
33872
34295
  addonId: null,
33873
34296
  access: "view"
33874
34297
  },
34298
+ "vectorStore.scan": {
34299
+ capName: "vector-store",
34300
+ capScope: "system",
34301
+ addonId: null,
34302
+ access: "view"
34303
+ },
33875
34304
  "vectorStore.stats": {
33876
34305
  capName: "vector-store",
33877
34306
  capScope: "system",
@@ -8180,18 +8180,61 @@ var RelocateFootageInputSchema = object({
8180
8180
  * `RecordingConfig.enabled` or camera wrapper bindings. */
8181
8181
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
8182
8182
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
8183
+ /**
8184
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
8185
+ * mover (the engine already walks both collections with a timestamp cursor and
8186
+ * already has a stamp-without-copy path).
8187
+ *
8188
+ * - `move` — the default and the historical behaviour: event-media and
8189
+ * retrain blobs move to `toLocationId` and their rows are
8190
+ * stamped. The enrolled gallery is skipped (D197).
8191
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
8192
+ * stamped with `toLocationId`. `toLocationId` here is the id the
8193
+ * bytes ALREADY sit on — today's `eventMedia` default — because
8194
+ * a NULL row means "wherever `eventMedia` points *now*", and the
8195
+ * instant a repoint moves that pointer the row reads from the
8196
+ * new disk while its bytes are on the old one.
8197
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
8198
+ * (enrolled-gallery) rows, which `move` deliberately skips.
8199
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
8200
+ * never run beside a live second location: it is stop-the-world
8201
+ * by construction, which is acceptable only because the gallery
8202
+ * is a few KB per enrolled sample.
8203
+ */
8204
+ var MediaRelocateModeSchema = _enum([
8205
+ "move",
8206
+ "seal",
8207
+ "gallery"
8208
+ ]);
8183
8209
  var RelocateMediaInputSchema = object({
8184
8210
  toLocationId: string(),
8185
- throttleMbps: number().min(1).max(1e3).optional()
8211
+ throttleMbps: number().min(1).max(1e3).optional(),
8212
+ /** Omitted = `move`, the pre-existing behaviour. */
8213
+ mode: MediaRelocateModeSchema.optional()
8214
+ });
8215
+ /** How many rows still carry NO `locationId` — the population a repoint would
8216
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
8217
+ * value that permits a non-blocking `eventMedia` cutover. */
8218
+ var UnstampedEventMediaCountSchema = object({
8219
+ media: number().int().nonnegative(),
8220
+ retrainFrames: number().int().nonnegative(),
8221
+ total: number().int().nonnegative()
8186
8222
  });
8187
8223
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8188
- /** The independently selectable logical storage classes. `recordings`
8189
- * encompasses the high and mid segment profiles; `recordingsLow` is low
8190
- * segments; `eventMedia` is post-analysis blobs. */
8224
+ /** The independently selectable logical storage classes — every class
8225
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
8226
+ * Zod enum error where they should meet an explanation.
8227
+ *
8228
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
8229
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
8230
+ * enrolled gallery; `backups` is the system backup archive. The last two have
8231
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
8191
8232
  var StorageMigrationClassSchema = _enum([
8192
8233
  "recordings",
8193
8234
  "recordingsLow",
8194
- "eventMedia"
8235
+ "eventMedia",
8236
+ "backups",
8237
+ "galleryMedia"
8195
8238
  ]);
8196
8239
  /** A destination is always an existing, fully-qualified location id. The
8197
8240
  * migration API intentionally never changes a source location's `basePath`:
@@ -8199,20 +8242,56 @@ var StorageMigrationClassSchema = _enum([
8199
8242
  var StorageMigrationDestinationsSchema = object({
8200
8243
  recordings: string().min(1).optional(),
8201
8244
  recordingsLow: string().min(1).optional(),
8202
- eventMedia: string().min(1).optional()
8245
+ eventMedia: string().min(1).optional(),
8246
+ backups: string().min(1).optional(),
8247
+ galleryMedia: string().min(1).optional()
8203
8248
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
8249
+ /**
8250
+ * How a migration sequences the cutover against the byte move.
8251
+ *
8252
+ * - `blocking` — the historical order: pause, move every byte, repoint,
8253
+ * resume. Recording is stopped for the whole move. Right
8254
+ * for a small or a cold class, and the only legal mode for
8255
+ * a `cardinality: 'single'` class.
8256
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
8257
+ * refresh, resume, then move the past with everything
8258
+ * running. The pause is three bounded instants (a detach +
8259
+ * attach round, a write-gate drain, a lease) instead of one
8260
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
8261
+ * stopped recording under `blocking`; the same move is
8262
+ * seconds of stopped recording under `nonBlocking`.
8263
+ *
8264
+ * The mode is on the JOB, not only on the input, because `status` is where an
8265
+ * operator finds out which one is running.
8266
+ */
8267
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
8204
8268
  /** Shared input for planning and starting an orchestrated storage migration. */
8205
8269
  var StorageMigrationInputSchema = object({
8206
8270
  destinations: StorageMigrationDestinationsSchema,
8207
- throttleMbps: number().min(1).max(1e3).optional()
8271
+ throttleMbps: number().min(1).max(1e3).optional(),
8272
+ /** Omitted = `blocking`, which stays the default. */
8273
+ mode: StorageMigrationModeSchema.optional()
8208
8274
  });
8209
- /** The durable coordinator state machine. The only phase that changes default
8210
- * locations is `repointing`, after every selected mover has completed and been
8211
- * verified. */
8275
+ /**
8276
+ * The durable coordinator state machine.
8277
+ *
8278
+ * `blocking`:
8279
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
8280
+ *
8281
+ * `nonBlocking`:
8282
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
8283
+ *
8284
+ * Same phases, different order plus two new ones — not a second mover.
8285
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
8286
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
8287
+ * `repointing` is still the only phase that changes a default location.
8288
+ */
8212
8289
  var StorageMigrationPhaseSchema = _enum([
8213
8290
  "planning",
8291
+ "sealing",
8214
8292
  "pausing",
8215
8293
  "moving",
8294
+ "draining",
8216
8295
  "verifying",
8217
8296
  "repointing",
8218
8297
  "refreshing",
@@ -8226,17 +8305,56 @@ var StorageMigrationParticipantSchema = _enum([
8226
8305
  "recorder",
8227
8306
  "analytics"
8228
8307
  ]);
8308
+ /**
8309
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8310
+ *
8311
+ * The long half of a non-blocking migration is `draining`, and it is measured
8312
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8313
+ * existed the only place those numbers appeared was a Loki line, so an operator
8314
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8315
+ * afternoon.
8316
+ *
8317
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8318
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8319
+ * mover — which is the exact failure this is meant to end. The coordinator's
8320
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8321
+ * read `state`; folding the counters costs no extra read and makes the durable
8322
+ * record say afterwards how far a move actually got.
8323
+ *
8324
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8325
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8326
+ * cannot say M, and a 0 there would render as "100 % done".
8327
+ */
8328
+ var StorageMigrationMoveProgressSchema = object({
8329
+ filesMoved: number().int().nonnegative(),
8330
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8331
+ filesTotal: number().int().nonnegative().nullable(),
8332
+ bytesMoved: number().int().nonnegative(),
8333
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8334
+ * crash gets a new mover, and a rate computed from the migration's start
8335
+ * would silently average in the time nothing was running. */
8336
+ startedAt: number(),
8337
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8338
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8339
+ * subtract its own. */
8340
+ observedAt: number()
8341
+ });
8229
8342
  var StorageMigrationMoveSchema = object({
8230
8343
  storageClass: StorageMigrationClassSchema,
8231
8344
  fromLocationId: string(),
8232
8345
  toLocationId: string(),
8233
8346
  moverJobId: string().nullable(),
8234
8347
  state: RelocateJobStateSchema.nullable(),
8235
- error: string().nullable()
8348
+ error: string().nullable(),
8349
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8350
+ progress: StorageMigrationMoveProgressSchema.nullable()
8236
8351
  });
8237
8352
  var StorageMigrationJobSchema = object({
8238
8353
  jobId: string(),
8239
8354
  phase: StorageMigrationPhaseSchema,
8355
+ /** Which order this job is running. `status` is the only place an operator
8356
+ * can tell a seconds-long cutover from a thirty-hour one. */
8357
+ mode: StorageMigrationModeSchema,
8240
8358
  destinations: StorageMigrationDestinationsSchema,
8241
8359
  throttleMbps: number(),
8242
8360
  moves: array(StorageMigrationMoveSchema),
@@ -8249,13 +8367,122 @@ var StorageMigrationJobSchema = object({
8249
8367
  finishedAt: number().nullable(),
8250
8368
  error: string().nullable()
8251
8369
  });
8370
+ var StorageMigrationFindingSchema = object({
8371
+ code: _enum([
8372
+ "sharesDeviceWithSource",
8373
+ "deviceIdentityUnknown",
8374
+ "unstampedEventMediaRows",
8375
+ "blockingOnly",
8376
+ "noMover"
8377
+ ]),
8378
+ storageClass: StorageMigrationClassSchema,
8379
+ /** Human-readable, already carrying the ids and counts. */
8380
+ message: string()
8381
+ });
8252
8382
  var StorageMigrationPlanSchema = object({
8253
8383
  destinations: StorageMigrationDestinationsSchema,
8384
+ /** The mode this plan was built for. A plan is only valid for its mode: the
8385
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
8386
+ * it. */
8387
+ mode: StorageMigrationModeSchema,
8254
8388
  moves: array(object({
8255
8389
  storageClass: StorageMigrationClassSchema,
8256
8390
  fromLocationId: string(),
8257
8391
  toLocationId: string()
8258
- }))
8392
+ })),
8393
+ findings: array(StorageMigrationFindingSchema)
8394
+ });
8395
+ /**
8396
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8397
+ *
8398
+ * The coordinator's job record is the state of record for a migration, and its
8399
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8400
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8401
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8402
+ * way because no supported UI path existed. A mover armed like that has no job
8403
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8404
+ *
8405
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8406
+ * orchestrated it.
8407
+ */
8408
+ var StorageMigrationMoverSchema = object({
8409
+ lane: _enum(["footage", "media"]),
8410
+ job: RelocateJobSchema,
8411
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8412
+ * directly against the owning addon. */
8413
+ migrationJobId: string().nullable(),
8414
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8415
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8416
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8417
+ * rate made of two different clocks. */
8418
+ observedAt: number()
8419
+ });
8420
+ /**
8421
+ * What a SOURCE still holds for one storage class — the number that makes a
8422
+ * "drain remaining" action honest rather than hopeful.
8423
+ *
8424
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8425
+ * engine's own selection count for media), never from the resident index: a
8426
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8427
+ * never been told about (D295).
8428
+ *
8429
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8430
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8431
+ * because refusing on an unanswerable read would hide exactly the case an
8432
+ * operator needs to act on.
8433
+ */
8434
+ var StorageMigrationResidueSchema = object({
8435
+ storageClass: StorageMigrationClassSchema,
8436
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8437
+ * move from wherever they are rather than from one named source. */
8438
+ fromLocationId: string(),
8439
+ /** Where a drain would move it — the class's CURRENT default. */
8440
+ toLocationId: string(),
8441
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8442
+ items: number().int().nonnegative().nullable(),
8443
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8444
+ bytes: number().int().nonnegative().nullable()
8445
+ });
8446
+ /**
8447
+ * Run the DRAIN half and nothing else.
8448
+ *
8449
+ * A migration that reached `done` has already repointed, so `start` correctly
8450
+ * refuses its destination ("already the default") — there is nothing left to
8451
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8452
+ * or finish against a work list that was a tenth of the archive (D295), and
8453
+ * before this there was no supported way to run only that half: the only way
8454
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8455
+ *
8456
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8457
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8458
+ * re-repoint a class that is already migrated.
8459
+ */
8460
+ var StorageMigrationDrainInputSchema = object({
8461
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8462
+ * a class whose source is already empty is refused rather than started. */
8463
+ classes: array(StorageMigrationClassSchema).min(1),
8464
+ throttleMbps: number().min(1).max(1e3).optional()
8465
+ });
8466
+ /** What a footage source still holds, asked of the durable hour ledger. */
8467
+ var RelocateResidueInputSchema = object({
8468
+ fromLocationId: string().min(1),
8469
+ /** Narrow to one logical class; omit for every profile on the location. */
8470
+ footageClass: RelocateFootageClassSchema.optional()
8471
+ });
8472
+ /** `null` = the archive could not answer (no ledger on this node, or the
8473
+ * aggregate failed). Never conflated with an empty source. */
8474
+ var RelocateResidueSchema = object({
8475
+ segments: number().int().nonnegative(),
8476
+ bytes: number().int().nonnegative()
8477
+ }).nullable();
8478
+ /** How many rows a media pass would still act on against a given target — the
8479
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8480
+ * never disagree. `null` = the count could not be taken. */
8481
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8482
+ var RelocatableMediaCountInputSchema = object({
8483
+ toLocationId: string().min(1),
8484
+ /** Omitted = `move`. */
8485
+ mode: MediaRelocateModeSchema.optional()
8259
8486
  });
8260
8487
  /**
8261
8488
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -8361,6 +8588,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8361
8588
  * two addons declaring the same `id` must agree on `cardinality` (validated
8362
8589
  * at kernel aggregation time, not here).
8363
8590
  */
8591
+ /**
8592
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8593
+ * actually reaches the bytes. It is the constraint that decides which
8594
+ * `storage-provider`s may back a location of that kind.
8595
+ *
8596
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8597
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8598
+ * post-analysis media roots). Only a provider that serves a genuine local
8599
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8600
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8601
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8602
+ * against a same-named local directory that is something else entirely.
8603
+ *
8604
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8605
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8606
+ * service never sees a path, so any provider can back it. `backups` is the
8607
+ * one kind that qualifies today.
8608
+ *
8609
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8610
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8611
+ * refused the configuration; the first write simply went somewhere wrong, and
8612
+ * a recording write that goes wrong surfaces as a silent black window rather
8613
+ * than an error (the read path does not `stat`). This turns that accident into
8614
+ * a declared, enforced, testable refusal.
8615
+ */
8616
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8364
8617
  var StorageLocationDeclarationSchema = object({
8365
8618
  /**
8366
8619
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8380,6 +8633,19 @@ var StorageLocationDeclarationSchema = object({
8380
8633
  */
8381
8634
  cardinality: _enum(["single", "multi"]),
8382
8635
  /**
8636
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8637
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8638
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8639
+ *
8640
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8641
+ * can only over-restrict (refuse a remote provider for a kind that might
8642
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8643
+ * permissive direction and is therefore never inferred — a repo guard
8644
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8645
+ * reached by omission.
8646
+ */
8647
+ access: StorageAccessSchema.optional(),
8648
+ /**
8383
8649
  * When set, the default instance for this location inherits its resolved
8384
8650
  * root from the named location's default instance. Useful for derivative
8385
8651
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18048,8 +18314,10 @@ var TrackSchema = object({
18048
18314
  lastSeen: number(),
18049
18315
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18050
18316
  positions: array(TrackPositionSchema).readonly(),
18051
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18052
- * saveThumbnails policy). */
18317
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18318
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18319
+ * the retired `saveThumbnails` used to gate this and the rolling
18320
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18053
18321
  snapshots: array(TrackSnapshotSchema).readonly(),
18054
18322
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18055
18323
  zonesVisited: array(string()).readonly(),
@@ -18909,6 +19177,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18909
19177
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
18910
19178
  kind: "mutation",
18911
19179
  auth: "admin"
19180
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19181
+ kind: "query",
19182
+ auth: "admin"
18912
19183
  }), method(object({}), array(RelocateJobSchema).readonly(), {
18913
19184
  kind: "query",
18914
19185
  auth: "admin"
@@ -20816,7 +21087,10 @@ method(object({
20816
21087
  }), StorageLocationSchema, {
20817
21088
  kind: "mutation",
20818
21089
  auth: "admin"
20819
- }), method(object({ id: string() }), _void(), {
21090
+ }), method(object({
21091
+ id: string(),
21092
+ force: boolean().optional()
21093
+ }), _void(), {
20820
21094
  kind: "mutation",
20821
21095
  auth: "admin"
20822
21096
  }), method(object({ id: string() }), object({
@@ -20865,6 +21139,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20865
21139
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20866
21140
  kind: "mutation",
20867
21141
  auth: "admin"
21142
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21143
+ kind: "mutation",
21144
+ auth: "admin"
20868
21145
  });
20869
21146
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20870
21147
  providerId: string().min(1),
@@ -21263,12 +21540,38 @@ response: record(string(), unknown()) }), object({
21263
21540
  *
21264
21541
  * ## Why this is a capability and not a helper
21265
21542
  *
21266
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21267
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21268
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21269
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21270
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21271
- * load 5,000 rows before ranking anything.
21543
+ * This capability was introduced with the claim that SIX stores in
21544
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21545
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21546
+ * claim was never true, and leaving it here made five stores look like pending
21547
+ * work when three of them have no vector at all. Counted column by column on
21548
+ * 2026-08-30, exactly THREE ever held one:
21549
+ *
21550
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21551
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21552
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21553
+ * face, migrated 2026-08-30 into its OWN index (see below).
21554
+ *
21555
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21556
+ * and `identities` store a name; the event store stores no derivative vector.
21557
+ * They are not migration candidates and never were.
21558
+ *
21559
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21560
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21561
+ * rows before ranking anything.
21562
+ *
21563
+ * ## One index per COMPARISON, never per encoder
21564
+ *
21565
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21566
+ * model, and they still get two indexes. An index is a set of things that are
21567
+ * ranked against each other and that live and die together, and these two are
21568
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21569
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21570
+ * forever and is the gallery every recognition ranks against. One index would
21571
+ * mean every gallery load and every reconcile carried a filter whose failure
21572
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21573
+ * person's only sample. The dimension they share is not a reason to share an
21574
+ * index; the question they answer is, and it differs.
21272
21575
  *
21273
21576
  * The fix is not a faster loop, it is a different backend — and the backend
21274
21577
  * should be replaceable without touching six callers. So: a singleton
@@ -21373,7 +21676,20 @@ var VectorQueryResultSchema = object({
21373
21676
  */
21374
21677
  scanned: number(),
21375
21678
  /** True when the backend could not consider every row that passed the filter. */
21376
- truncated: boolean()
21679
+ truncated: boolean(),
21680
+ /**
21681
+ * The `topK` the backend actually ran with.
21682
+ *
21683
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21684
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21685
+ * own log rather than in its answer. That is how an audit asking for 20,000
21686
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21687
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21688
+ * MUCH, in the return value, where the caller cannot fail to see it.
21689
+ *
21690
+ * Equals the requested `topK` whenever nothing was lowered.
21691
+ */
21692
+ effectiveTopK: number().int().positive()
21377
21693
  });
21378
21694
  var VectorDeleteInputSchema = object({
21379
21695
  index: string(),
@@ -21402,6 +21718,68 @@ var VectorGetResultSchema = object({ items: array(object({
21402
21718
  id: string(),
21403
21719
  metadata: VectorMetadataSchema
21404
21720
  })) });
21721
+ /**
21722
+ * Ids to read back WITH their vectors.
21723
+ *
21724
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21725
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21726
+ * caller depends on that promise. This one promises the opposite.
21727
+ *
21728
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21729
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21730
+ * a per-face cross-process KNN would be a network round trip inside the
21731
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21732
+ * it requires the index to hand the floats back. Without this method the only
21733
+ * way to keep a readable vector is a JSON column, which is the thing this
21734
+ * capability exists to delete.
21735
+ *
21736
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21737
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21738
+ */
21739
+ var VectorFetchInputSchema = object({
21740
+ index: string(),
21741
+ ids: array(string())
21742
+ });
21743
+ var VectorFetchResultSchema = object({ items: array(object({
21744
+ id: string(),
21745
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21746
+ vector: string(),
21747
+ metadata: VectorMetadataSchema
21748
+ })) });
21749
+ /**
21750
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21751
+ *
21752
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21753
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21754
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21755
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21756
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21757
+ * looked" for as long as anyone cared to read it.
21758
+ *
21759
+ * This is the primitive that question actually needs: a bounded page, ordered
21760
+ * by the backend's own row order, costing no distance computation at all.
21761
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21762
+ * the full-table read this capability was built to stop.
21763
+ */
21764
+ var VectorScanInputSchema = object({
21765
+ index: string(),
21766
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21767
+ cursor: number().int().nonnegative().default(0),
21768
+ limit: number().int().positive()
21769
+ });
21770
+ var VectorScanResultSchema = object({
21771
+ items: array(object({
21772
+ id: string(),
21773
+ metadata: VectorMetadataSchema
21774
+ })),
21775
+ /**
21776
+ * Where the next page starts, or `null` when the walk reached the end.
21777
+ *
21778
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21779
+ * from a short page: a backend is free to return fewer rows than asked.
21780
+ */
21781
+ nextCursor: number().int().nonnegative().nullable()
21782
+ });
21405
21783
  var VectorStatsInputSchema = object({ index: string() });
21406
21784
  var VectorStatsResultSchema = object({
21407
21785
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21420,7 +21798,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21420
21798
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21421
21799
  kind: "mutation",
21422
21800
  auth: "admin"
21423
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21801
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21424
21802
  kind: "mutation",
21425
21803
  auth: "admin"
21426
21804
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26324,6 +26702,9 @@ method(object({
26324
26702
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26325
26703
  kind: "query",
26326
26704
  auth: "admin"
26705
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26706
+ kind: "query",
26707
+ auth: "admin"
26327
26708
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26328
26709
  kind: "mutation",
26329
26710
  auth: "admin"
@@ -31289,6 +31670,18 @@ Object.freeze({
31289
31670
  addonId: null,
31290
31671
  access: "create"
31291
31672
  },
31673
+ "pipelineAnalytics.countRelocatableMedia": {
31674
+ capName: "pipeline-analytics",
31675
+ capScope: "device",
31676
+ addonId: null,
31677
+ access: "view"
31678
+ },
31679
+ "pipelineAnalytics.countUnstampedEventMedia": {
31680
+ capName: "pipeline-analytics",
31681
+ capScope: "device",
31682
+ addonId: null,
31683
+ access: "view"
31684
+ },
31292
31685
  "pipelineAnalytics.deleteDeviceEvents": {
31293
31686
  capName: "pipeline-analytics",
31294
31687
  capScope: "device",
@@ -32447,6 +32840,12 @@ Object.freeze({
32447
32840
  addonId: null,
32448
32841
  access: "view"
32449
32842
  },
32843
+ "recording.getRelocateResidue": {
32844
+ capName: "recording",
32845
+ capScope: "system",
32846
+ addonId: null,
32847
+ access: "view"
32848
+ },
32450
32849
  "recording.getStorageMigrationMoveStatus": {
32451
32850
  capName: "recording",
32452
32851
  capScope: "system",
@@ -32993,12 +33392,30 @@ Object.freeze({
32993
33392
  addonId: null,
32994
33393
  access: "create"
32995
33394
  },
33395
+ "storageMigration.drain": {
33396
+ capName: "storage-migration",
33397
+ capScope: "system",
33398
+ addonId: null,
33399
+ access: "create"
33400
+ },
33401
+ "storageMigration.movers": {
33402
+ capName: "storage-migration",
33403
+ capScope: "system",
33404
+ addonId: null,
33405
+ access: "view"
33406
+ },
32996
33407
  "storageMigration.plan": {
32997
33408
  capName: "storage-migration",
32998
33409
  capScope: "system",
32999
33410
  addonId: null,
33000
33411
  access: "view"
33001
33412
  },
33413
+ "storageMigration.residue": {
33414
+ capName: "storage-migration",
33415
+ capScope: "system",
33416
+ addonId: null,
33417
+ access: "view"
33418
+ },
33002
33419
  "storageMigration.start": {
33003
33420
  capName: "storage-migration",
33004
33421
  capScope: "system",
@@ -33833,6 +34250,12 @@ Object.freeze({
33833
34250
  addonId: null,
33834
34251
  access: "delete"
33835
34252
  },
34253
+ "vectorStore.fetchByIds": {
34254
+ capName: "vector-store",
34255
+ capScope: "system",
34256
+ addonId: null,
34257
+ access: "view"
34258
+ },
33836
34259
  "vectorStore.getByIds": {
33837
34260
  capName: "vector-store",
33838
34261
  capScope: "system",
@@ -33845,6 +34268,12 @@ Object.freeze({
33845
34268
  addonId: null,
33846
34269
  access: "view"
33847
34270
  },
34271
+ "vectorStore.scan": {
34272
+ capName: "vector-store",
34273
+ capScope: "system",
34274
+ addonId: null,
34275
+ access: "view"
34276
+ },
33848
34277
  "vectorStore.stats": {
33849
34278
  capName: "vector-store",
33850
34279
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.48",
3
+ "version": "1.2.51",
4
4
  "description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
5
5
  "keywords": [
6
6
  "camstack",