@camstack/addon-provider-petkit 0.2.42 → 0.2.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.
Files changed (3) hide show
  1. package/dist/addon.js +452 -23
  2. package/dist/addon.mjs +452 -23
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -9141,18 +9141,61 @@ var RelocateFootageInputSchema = object({
9141
9141
  * `RecordingConfig.enabled` or camera wrapper bindings. */
9142
9142
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
9143
9143
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
9144
+ /**
9145
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
9146
+ * mover (the engine already walks both collections with a timestamp cursor and
9147
+ * already has a stamp-without-copy path).
9148
+ *
9149
+ * - `move` — the default and the historical behaviour: event-media and
9150
+ * retrain blobs move to `toLocationId` and their rows are
9151
+ * stamped. The enrolled gallery is skipped (D197).
9152
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
9153
+ * stamped with `toLocationId`. `toLocationId` here is the id the
9154
+ * bytes ALREADY sit on — today's `eventMedia` default — because
9155
+ * a NULL row means "wherever `eventMedia` points *now*", and the
9156
+ * instant a repoint moves that pointer the row reads from the
9157
+ * new disk while its bytes are on the old one.
9158
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
9159
+ * (enrolled-gallery) rows, which `move` deliberately skips.
9160
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
9161
+ * never run beside a live second location: it is stop-the-world
9162
+ * by construction, which is acceptable only because the gallery
9163
+ * is a few KB per enrolled sample.
9164
+ */
9165
+ var MediaRelocateModeSchema = _enum([
9166
+ "move",
9167
+ "seal",
9168
+ "gallery"
9169
+ ]);
9144
9170
  var RelocateMediaInputSchema = object({
9145
9171
  toLocationId: string(),
9146
- throttleMbps: number().min(1).max(1e3).optional()
9172
+ throttleMbps: number().min(1).max(1e3).optional(),
9173
+ /** Omitted = `move`, the pre-existing behaviour. */
9174
+ mode: MediaRelocateModeSchema.optional()
9175
+ });
9176
+ /** How many rows still carry NO `locationId` — the population a repoint would
9177
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
9178
+ * value that permits a non-blocking `eventMedia` cutover. */
9179
+ var UnstampedEventMediaCountSchema = object({
9180
+ media: number().int().nonnegative(),
9181
+ retrainFrames: number().int().nonnegative(),
9182
+ total: number().int().nonnegative()
9147
9183
  });
9148
9184
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9149
- /** The independently selectable logical storage classes. `recordings`
9150
- * encompasses the high and mid segment profiles; `recordingsLow` is low
9151
- * segments; `eventMedia` is post-analysis blobs. */
9185
+ /** The independently selectable logical storage classes — every class
9186
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
9187
+ * Zod enum error where they should meet an explanation.
9188
+ *
9189
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
9190
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
9191
+ * enrolled gallery; `backups` is the system backup archive. The last two have
9192
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
9152
9193
  var StorageMigrationClassSchema = _enum([
9153
9194
  "recordings",
9154
9195
  "recordingsLow",
9155
- "eventMedia"
9196
+ "eventMedia",
9197
+ "backups",
9198
+ "galleryMedia"
9156
9199
  ]);
9157
9200
  /** A destination is always an existing, fully-qualified location id. The
9158
9201
  * migration API intentionally never changes a source location's `basePath`:
@@ -9160,20 +9203,56 @@ var StorageMigrationClassSchema = _enum([
9160
9203
  var StorageMigrationDestinationsSchema = object({
9161
9204
  recordings: string().min(1).optional(),
9162
9205
  recordingsLow: string().min(1).optional(),
9163
- eventMedia: string().min(1).optional()
9206
+ eventMedia: string().min(1).optional(),
9207
+ backups: string().min(1).optional(),
9208
+ galleryMedia: string().min(1).optional()
9164
9209
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
9210
+ /**
9211
+ * How a migration sequences the cutover against the byte move.
9212
+ *
9213
+ * - `blocking` — the historical order: pause, move every byte, repoint,
9214
+ * resume. Recording is stopped for the whole move. Right
9215
+ * for a small or a cold class, and the only legal mode for
9216
+ * a `cardinality: 'single'` class.
9217
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
9218
+ * refresh, resume, then move the past with everything
9219
+ * running. The pause is three bounded instants (a detach +
9220
+ * attach round, a write-gate drain, a lease) instead of one
9221
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
9222
+ * stopped recording under `blocking`; the same move is
9223
+ * seconds of stopped recording under `nonBlocking`.
9224
+ *
9225
+ * The mode is on the JOB, not only on the input, because `status` is where an
9226
+ * operator finds out which one is running.
9227
+ */
9228
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9165
9229
  /** Shared input for planning and starting an orchestrated storage migration. */
9166
9230
  var StorageMigrationInputSchema = object({
9167
9231
  destinations: StorageMigrationDestinationsSchema,
9168
- throttleMbps: number().min(1).max(1e3).optional()
9232
+ throttleMbps: number().min(1).max(1e3).optional(),
9233
+ /** Omitted = `blocking`, which stays the default. */
9234
+ mode: StorageMigrationModeSchema.optional()
9169
9235
  });
9170
- /** The durable coordinator state machine. The only phase that changes default
9171
- * locations is `repointing`, after every selected mover has completed and been
9172
- * verified. */
9236
+ /**
9237
+ * The durable coordinator state machine.
9238
+ *
9239
+ * `blocking`:
9240
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
9241
+ *
9242
+ * `nonBlocking`:
9243
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
9244
+ *
9245
+ * Same phases, different order plus two new ones — not a second mover.
9246
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
9247
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
9248
+ * `repointing` is still the only phase that changes a default location.
9249
+ */
9173
9250
  var StorageMigrationPhaseSchema = _enum([
9174
9251
  "planning",
9252
+ "sealing",
9175
9253
  "pausing",
9176
9254
  "moving",
9255
+ "draining",
9177
9256
  "verifying",
9178
9257
  "repointing",
9179
9258
  "refreshing",
@@ -9187,17 +9266,56 @@ var StorageMigrationParticipantSchema = _enum([
9187
9266
  "recorder",
9188
9267
  "analytics"
9189
9268
  ]);
9269
+ /**
9270
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9271
+ *
9272
+ * The long half of a non-blocking migration is `draining`, and it is measured
9273
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9274
+ * existed the only place those numbers appeared was a Loki line, so an operator
9275
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9276
+ * afternoon.
9277
+ *
9278
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9279
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9280
+ * mover — which is the exact failure this is meant to end. The coordinator's
9281
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9282
+ * read `state`; folding the counters costs no extra read and makes the durable
9283
+ * record say afterwards how far a move actually got.
9284
+ *
9285
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9286
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9287
+ * cannot say M, and a 0 there would render as "100 % done".
9288
+ */
9289
+ var StorageMigrationMoveProgressSchema = object({
9290
+ filesMoved: number().int().nonnegative(),
9291
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9292
+ filesTotal: number().int().nonnegative().nullable(),
9293
+ bytesMoved: number().int().nonnegative(),
9294
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9295
+ * crash gets a new mover, and a rate computed from the migration's start
9296
+ * would silently average in the time nothing was running. */
9297
+ startedAt: number(),
9298
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9299
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9300
+ * subtract its own. */
9301
+ observedAt: number()
9302
+ });
9190
9303
  var StorageMigrationMoveSchema = object({
9191
9304
  storageClass: StorageMigrationClassSchema,
9192
9305
  fromLocationId: string(),
9193
9306
  toLocationId: string(),
9194
9307
  moverJobId: string().nullable(),
9195
9308
  state: RelocateJobStateSchema.nullable(),
9196
- error: string().nullable()
9309
+ error: string().nullable(),
9310
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9311
+ progress: StorageMigrationMoveProgressSchema.nullable()
9197
9312
  });
9198
9313
  var StorageMigrationJobSchema = object({
9199
9314
  jobId: string(),
9200
9315
  phase: StorageMigrationPhaseSchema,
9316
+ /** Which order this job is running. `status` is the only place an operator
9317
+ * can tell a seconds-long cutover from a thirty-hour one. */
9318
+ mode: StorageMigrationModeSchema,
9201
9319
  destinations: StorageMigrationDestinationsSchema,
9202
9320
  throttleMbps: number(),
9203
9321
  moves: array(StorageMigrationMoveSchema),
@@ -9210,13 +9328,122 @@ var StorageMigrationJobSchema = object({
9210
9328
  finishedAt: number().nullable(),
9211
9329
  error: string().nullable()
9212
9330
  });
9331
+ var StorageMigrationFindingSchema = object({
9332
+ code: _enum([
9333
+ "sharesDeviceWithSource",
9334
+ "deviceIdentityUnknown",
9335
+ "unstampedEventMediaRows",
9336
+ "blockingOnly",
9337
+ "noMover"
9338
+ ]),
9339
+ storageClass: StorageMigrationClassSchema,
9340
+ /** Human-readable, already carrying the ids and counts. */
9341
+ message: string()
9342
+ });
9213
9343
  var StorageMigrationPlanSchema = object({
9214
9344
  destinations: StorageMigrationDestinationsSchema,
9345
+ /** The mode this plan was built for. A plan is only valid for its mode: the
9346
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
9347
+ * it. */
9348
+ mode: StorageMigrationModeSchema,
9215
9349
  moves: array(object({
9216
9350
  storageClass: StorageMigrationClassSchema,
9217
9351
  fromLocationId: string(),
9218
9352
  toLocationId: string()
9219
- }))
9353
+ })),
9354
+ findings: array(StorageMigrationFindingSchema)
9355
+ });
9356
+ /**
9357
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9358
+ *
9359
+ * The coordinator's job record is the state of record for a migration, and its
9360
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9361
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9362
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9363
+ * way because no supported UI path existed. A mover armed like that has no job
9364
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9365
+ *
9366
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9367
+ * orchestrated it.
9368
+ */
9369
+ var StorageMigrationMoverSchema = object({
9370
+ lane: _enum(["footage", "media"]),
9371
+ job: RelocateJobSchema,
9372
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9373
+ * directly against the owning addon. */
9374
+ migrationJobId: string().nullable(),
9375
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9376
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9377
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9378
+ * rate made of two different clocks. */
9379
+ observedAt: number()
9380
+ });
9381
+ /**
9382
+ * What a SOURCE still holds for one storage class — the number that makes a
9383
+ * "drain remaining" action honest rather than hopeful.
9384
+ *
9385
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9386
+ * engine's own selection count for media), never from the resident index: a
9387
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9388
+ * never been told about (D295).
9389
+ *
9390
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9391
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9392
+ * because refusing on an unanswerable read would hide exactly the case an
9393
+ * operator needs to act on.
9394
+ */
9395
+ var StorageMigrationResidueSchema = object({
9396
+ storageClass: StorageMigrationClassSchema,
9397
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9398
+ * move from wherever they are rather than from one named source. */
9399
+ fromLocationId: string(),
9400
+ /** Where a drain would move it — the class's CURRENT default. */
9401
+ toLocationId: string(),
9402
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9403
+ items: number().int().nonnegative().nullable(),
9404
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9405
+ bytes: number().int().nonnegative().nullable()
9406
+ });
9407
+ /**
9408
+ * Run the DRAIN half and nothing else.
9409
+ *
9410
+ * A migration that reached `done` has already repointed, so `start` correctly
9411
+ * refuses its destination ("already the default") — there is nothing left to
9412
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9413
+ * or finish against a work list that was a tenth of the archive (D295), and
9414
+ * before this there was no supported way to run only that half: the only way
9415
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9416
+ *
9417
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9418
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9419
+ * re-repoint a class that is already migrated.
9420
+ */
9421
+ var StorageMigrationDrainInputSchema = object({
9422
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9423
+ * a class whose source is already empty is refused rather than started. */
9424
+ classes: array(StorageMigrationClassSchema).min(1),
9425
+ throttleMbps: number().min(1).max(1e3).optional()
9426
+ });
9427
+ /** What a footage source still holds, asked of the durable hour ledger. */
9428
+ var RelocateResidueInputSchema = object({
9429
+ fromLocationId: string().min(1),
9430
+ /** Narrow to one logical class; omit for every profile on the location. */
9431
+ footageClass: RelocateFootageClassSchema.optional()
9432
+ });
9433
+ /** `null` = the archive could not answer (no ledger on this node, or the
9434
+ * aggregate failed). Never conflated with an empty source. */
9435
+ var RelocateResidueSchema = object({
9436
+ segments: number().int().nonnegative(),
9437
+ bytes: number().int().nonnegative()
9438
+ }).nullable();
9439
+ /** How many rows a media pass would still act on against a given target — the
9440
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9441
+ * never disagree. `null` = the count could not be taken. */
9442
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9443
+ var RelocatableMediaCountInputSchema = object({
9444
+ toLocationId: string().min(1),
9445
+ /** Omitted = `move`. */
9446
+ mode: MediaRelocateModeSchema.optional()
9220
9447
  });
9221
9448
  /**
9222
9449
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -9322,6 +9549,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9322
9549
  * two addons declaring the same `id` must agree on `cardinality` (validated
9323
9550
  * at kernel aggregation time, not here).
9324
9551
  */
9552
+ /**
9553
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9554
+ * actually reaches the bytes. It is the constraint that decides which
9555
+ * `storage-provider`s may back a location of that kind.
9556
+ *
9557
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9558
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9559
+ * post-analysis media roots). Only a provider that serves a genuine local
9560
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9561
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9562
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9563
+ * against a same-named local directory that is something else entirely.
9564
+ *
9565
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9566
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9567
+ * service never sees a path, so any provider can back it. `backups` is the
9568
+ * one kind that qualifies today.
9569
+ *
9570
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9571
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9572
+ * refused the configuration; the first write simply went somewhere wrong, and
9573
+ * a recording write that goes wrong surfaces as a silent black window rather
9574
+ * than an error (the read path does not `stat`). This turns that accident into
9575
+ * a declared, enforced, testable refusal.
9576
+ */
9577
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9325
9578
  var StorageLocationDeclarationSchema = object({
9326
9579
  /**
9327
9580
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9341,6 +9594,19 @@ var StorageLocationDeclarationSchema = object({
9341
9594
  */
9342
9595
  cardinality: _enum(["single", "multi"]),
9343
9596
  /**
9597
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9598
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9599
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9600
+ *
9601
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9602
+ * can only over-restrict (refuse a remote provider for a kind that might
9603
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9604
+ * permissive direction and is therefore never inferred — a repo guard
9605
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9606
+ * reached by omission.
9607
+ */
9608
+ access: StorageAccessSchema.optional(),
9609
+ /**
9344
9610
  * When set, the default instance for this location inherits its resolved
9345
9611
  * root from the named location's default instance. Useful for derivative
9346
9612
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19225,8 +19491,10 @@ var TrackSchema = object({
19225
19491
  lastSeen: number(),
19226
19492
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19227
19493
  positions: array(TrackPositionSchema).readonly(),
19228
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19229
- * saveThumbnails policy). */
19494
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19495
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19496
+ * the retired `saveThumbnails` used to gate this and the rolling
19497
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19230
19498
  snapshots: array(TrackSnapshotSchema).readonly(),
19231
19499
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19232
19500
  zonesVisited: array(string()).readonly(),
@@ -20086,6 +20354,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20086
20354
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20087
20355
  kind: "mutation",
20088
20356
  auth: "admin"
20357
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20358
+ kind: "query",
20359
+ auth: "admin"
20089
20360
  }), method(object({}), array(RelocateJobSchema).readonly(), {
20090
20361
  kind: "query",
20091
20362
  auth: "admin"
@@ -21993,7 +22264,10 @@ method(object({
21993
22264
  }), StorageLocationSchema, {
21994
22265
  kind: "mutation",
21995
22266
  auth: "admin"
21996
- }), method(object({ id: string() }), _void(), {
22267
+ }), method(object({
22268
+ id: string(),
22269
+ force: boolean().optional()
22270
+ }), _void(), {
21997
22271
  kind: "mutation",
21998
22272
  auth: "admin"
21999
22273
  }), method(object({ id: string() }), object({
@@ -22042,6 +22316,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22042
22316
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22043
22317
  kind: "mutation",
22044
22318
  auth: "admin"
22319
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22320
+ kind: "mutation",
22321
+ auth: "admin"
22045
22322
  });
22046
22323
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22047
22324
  providerId: string().min(1),
@@ -22440,12 +22717,38 @@ response: record(string(), unknown()) }), object({
22440
22717
  *
22441
22718
  * ## Why this is a capability and not a helper
22442
22719
  *
22443
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22444
- * plate, vehicle, identity, and the event store's derivativesand every one of
22445
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22446
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22447
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22448
- * load 5,000 rows before ranking anything.
22720
+ * This capability was introduced with the claim that SIX stores in
22721
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22722
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22723
+ * claim was never true, and leaving it here made five stores look like pending
22724
+ * work when three of them have no vector at all. Counted column by column on
22725
+ * 2026-08-30, exactly THREE ever held one:
22726
+ *
22727
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22728
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22729
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22730
+ * face, migrated 2026-08-30 into its OWN index (see below).
22731
+ *
22732
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22733
+ * and `identities` store a name; the event store stores no derivative vector.
22734
+ * They are not migration candidates and never were.
22735
+ *
22736
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22737
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22738
+ * rows before ranking anything.
22739
+ *
22740
+ * ## One index per COMPARISON, never per encoder
22741
+ *
22742
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22743
+ * model, and they still get two indexes. An index is a set of things that are
22744
+ * ranked against each other and that live and die together, and these two are
22745
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22746
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22747
+ * forever and is the gallery every recognition ranks against. One index would
22748
+ * mean every gallery load and every reconcile carried a filter whose failure
22749
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22750
+ * person's only sample. The dimension they share is not a reason to share an
22751
+ * index; the question they answer is, and it differs.
22449
22752
  *
22450
22753
  * The fix is not a faster loop, it is a different backend — and the backend
22451
22754
  * should be replaceable without touching six callers. So: a singleton
@@ -22550,7 +22853,20 @@ var VectorQueryResultSchema = object({
22550
22853
  */
22551
22854
  scanned: number(),
22552
22855
  /** True when the backend could not consider every row that passed the filter. */
22553
- truncated: boolean()
22856
+ truncated: boolean(),
22857
+ /**
22858
+ * The `topK` the backend actually ran with.
22859
+ *
22860
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22861
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22862
+ * own log rather than in its answer. That is how an audit asking for 20,000
22863
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22864
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22865
+ * MUCH, in the return value, where the caller cannot fail to see it.
22866
+ *
22867
+ * Equals the requested `topK` whenever nothing was lowered.
22868
+ */
22869
+ effectiveTopK: number().int().positive()
22554
22870
  });
22555
22871
  var VectorDeleteInputSchema = object({
22556
22872
  index: string(),
@@ -22579,6 +22895,68 @@ var VectorGetResultSchema = object({ items: array(object({
22579
22895
  id: string(),
22580
22896
  metadata: VectorMetadataSchema
22581
22897
  })) });
22898
+ /**
22899
+ * Ids to read back WITH their vectors.
22900
+ *
22901
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22902
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22903
+ * caller depends on that promise. This one promises the opposite.
22904
+ *
22905
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22906
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22907
+ * a per-face cross-process KNN would be a network round trip inside the
22908
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22909
+ * it requires the index to hand the floats back. Without this method the only
22910
+ * way to keep a readable vector is a JSON column, which is the thing this
22911
+ * capability exists to delete.
22912
+ *
22913
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22914
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22915
+ */
22916
+ var VectorFetchInputSchema = object({
22917
+ index: string(),
22918
+ ids: array(string())
22919
+ });
22920
+ var VectorFetchResultSchema = object({ items: array(object({
22921
+ id: string(),
22922
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22923
+ vector: string(),
22924
+ metadata: VectorMetadataSchema
22925
+ })) });
22926
+ /**
22927
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22928
+ *
22929
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22930
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22931
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22932
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22933
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22934
+ * looked" for as long as anyone cared to read it.
22935
+ *
22936
+ * This is the primitive that question actually needs: a bounded page, ordered
22937
+ * by the backend's own row order, costing no distance computation at all.
22938
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22939
+ * the full-table read this capability was built to stop.
22940
+ */
22941
+ var VectorScanInputSchema = object({
22942
+ index: string(),
22943
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22944
+ cursor: number().int().nonnegative().default(0),
22945
+ limit: number().int().positive()
22946
+ });
22947
+ var VectorScanResultSchema = object({
22948
+ items: array(object({
22949
+ id: string(),
22950
+ metadata: VectorMetadataSchema
22951
+ })),
22952
+ /**
22953
+ * Where the next page starts, or `null` when the walk reached the end.
22954
+ *
22955
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22956
+ * from a short page: a backend is free to return fewer rows than asked.
22957
+ */
22958
+ nextCursor: number().int().nonnegative().nullable()
22959
+ });
22582
22960
  var VectorStatsInputSchema = object({ index: string() });
22583
22961
  var VectorStatsResultSchema = object({
22584
22962
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22597,7 +22975,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22597
22975
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22598
22976
  kind: "mutation",
22599
22977
  auth: "admin"
22600
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22978
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22601
22979
  kind: "mutation",
22602
22980
  auth: "admin"
22603
22981
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29224,6 +29602,9 @@ method(object({
29224
29602
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29225
29603
  kind: "query",
29226
29604
  auth: "admin"
29605
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29606
+ kind: "query",
29607
+ auth: "admin"
29227
29608
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29228
29609
  kind: "mutation",
29229
29610
  auth: "admin"
@@ -35858,6 +36239,18 @@ Object.freeze({
35858
36239
  addonId: null,
35859
36240
  access: "create"
35860
36241
  },
36242
+ "pipelineAnalytics.countRelocatableMedia": {
36243
+ capName: "pipeline-analytics",
36244
+ capScope: "device",
36245
+ addonId: null,
36246
+ access: "view"
36247
+ },
36248
+ "pipelineAnalytics.countUnstampedEventMedia": {
36249
+ capName: "pipeline-analytics",
36250
+ capScope: "device",
36251
+ addonId: null,
36252
+ access: "view"
36253
+ },
35861
36254
  "pipelineAnalytics.deleteDeviceEvents": {
35862
36255
  capName: "pipeline-analytics",
35863
36256
  capScope: "device",
@@ -37016,6 +37409,12 @@ Object.freeze({
37016
37409
  addonId: null,
37017
37410
  access: "view"
37018
37411
  },
37412
+ "recording.getRelocateResidue": {
37413
+ capName: "recording",
37414
+ capScope: "system",
37415
+ addonId: null,
37416
+ access: "view"
37417
+ },
37019
37418
  "recording.getStorageMigrationMoveStatus": {
37020
37419
  capName: "recording",
37021
37420
  capScope: "system",
@@ -37562,12 +37961,30 @@ Object.freeze({
37562
37961
  addonId: null,
37563
37962
  access: "create"
37564
37963
  },
37964
+ "storageMigration.drain": {
37965
+ capName: "storage-migration",
37966
+ capScope: "system",
37967
+ addonId: null,
37968
+ access: "create"
37969
+ },
37970
+ "storageMigration.movers": {
37971
+ capName: "storage-migration",
37972
+ capScope: "system",
37973
+ addonId: null,
37974
+ access: "view"
37975
+ },
37565
37976
  "storageMigration.plan": {
37566
37977
  capName: "storage-migration",
37567
37978
  capScope: "system",
37568
37979
  addonId: null,
37569
37980
  access: "view"
37570
37981
  },
37982
+ "storageMigration.residue": {
37983
+ capName: "storage-migration",
37984
+ capScope: "system",
37985
+ addonId: null,
37986
+ access: "view"
37987
+ },
37571
37988
  "storageMigration.start": {
37572
37989
  capName: "storage-migration",
37573
37990
  capScope: "system",
@@ -38402,6 +38819,12 @@ Object.freeze({
38402
38819
  addonId: null,
38403
38820
  access: "delete"
38404
38821
  },
38822
+ "vectorStore.fetchByIds": {
38823
+ capName: "vector-store",
38824
+ capScope: "system",
38825
+ addonId: null,
38826
+ access: "view"
38827
+ },
38405
38828
  "vectorStore.getByIds": {
38406
38829
  capName: "vector-store",
38407
38830
  capScope: "system",
@@ -38414,6 +38837,12 @@ Object.freeze({
38414
38837
  addonId: null,
38415
38838
  access: "view"
38416
38839
  },
38840
+ "vectorStore.scan": {
38841
+ capName: "vector-store",
38842
+ capScope: "system",
38843
+ addonId: null,
38844
+ access: "view"
38845
+ },
38417
38846
  "vectorStore.stats": {
38418
38847
  capName: "vector-store",
38419
38848
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -9140,18 +9140,61 @@ var RelocateFootageInputSchema = object({
9140
9140
  * `RecordingConfig.enabled` or camera wrapper bindings. */
9141
9141
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
9142
9142
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
9143
+ /**
9144
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
9145
+ * mover (the engine already walks both collections with a timestamp cursor and
9146
+ * already has a stamp-without-copy path).
9147
+ *
9148
+ * - `move` — the default and the historical behaviour: event-media and
9149
+ * retrain blobs move to `toLocationId` and their rows are
9150
+ * stamped. The enrolled gallery is skipped (D197).
9151
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
9152
+ * stamped with `toLocationId`. `toLocationId` here is the id the
9153
+ * bytes ALREADY sit on — today's `eventMedia` default — because
9154
+ * a NULL row means "wherever `eventMedia` points *now*", and the
9155
+ * instant a repoint moves that pointer the row reads from the
9156
+ * new disk while its bytes are on the old one.
9157
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
9158
+ * (enrolled-gallery) rows, which `move` deliberately skips.
9159
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
9160
+ * never run beside a live second location: it is stop-the-world
9161
+ * by construction, which is acceptable only because the gallery
9162
+ * is a few KB per enrolled sample.
9163
+ */
9164
+ var MediaRelocateModeSchema = _enum([
9165
+ "move",
9166
+ "seal",
9167
+ "gallery"
9168
+ ]);
9143
9169
  var RelocateMediaInputSchema = object({
9144
9170
  toLocationId: string(),
9145
- throttleMbps: number().min(1).max(1e3).optional()
9171
+ throttleMbps: number().min(1).max(1e3).optional(),
9172
+ /** Omitted = `move`, the pre-existing behaviour. */
9173
+ mode: MediaRelocateModeSchema.optional()
9174
+ });
9175
+ /** How many rows still carry NO `locationId` — the population a repoint would
9176
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
9177
+ * value that permits a non-blocking `eventMedia` cutover. */
9178
+ var UnstampedEventMediaCountSchema = object({
9179
+ media: number().int().nonnegative(),
9180
+ retrainFrames: number().int().nonnegative(),
9181
+ total: number().int().nonnegative()
9146
9182
  });
9147
9183
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9148
- /** The independently selectable logical storage classes. `recordings`
9149
- * encompasses the high and mid segment profiles; `recordingsLow` is low
9150
- * segments; `eventMedia` is post-analysis blobs. */
9184
+ /** The independently selectable logical storage classes — every class
9185
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
9186
+ * Zod enum error where they should meet an explanation.
9187
+ *
9188
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
9189
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
9190
+ * enrolled gallery; `backups` is the system backup archive. The last two have
9191
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
9151
9192
  var StorageMigrationClassSchema = _enum([
9152
9193
  "recordings",
9153
9194
  "recordingsLow",
9154
- "eventMedia"
9195
+ "eventMedia",
9196
+ "backups",
9197
+ "galleryMedia"
9155
9198
  ]);
9156
9199
  /** A destination is always an existing, fully-qualified location id. The
9157
9200
  * migration API intentionally never changes a source location's `basePath`:
@@ -9159,20 +9202,56 @@ var StorageMigrationClassSchema = _enum([
9159
9202
  var StorageMigrationDestinationsSchema = object({
9160
9203
  recordings: string().min(1).optional(),
9161
9204
  recordingsLow: string().min(1).optional(),
9162
- eventMedia: string().min(1).optional()
9205
+ eventMedia: string().min(1).optional(),
9206
+ backups: string().min(1).optional(),
9207
+ galleryMedia: string().min(1).optional()
9163
9208
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
9209
+ /**
9210
+ * How a migration sequences the cutover against the byte move.
9211
+ *
9212
+ * - `blocking` — the historical order: pause, move every byte, repoint,
9213
+ * resume. Recording is stopped for the whole move. Right
9214
+ * for a small or a cold class, and the only legal mode for
9215
+ * a `cardinality: 'single'` class.
9216
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
9217
+ * refresh, resume, then move the past with everything
9218
+ * running. The pause is three bounded instants (a detach +
9219
+ * attach round, a write-gate drain, a lease) instead of one
9220
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
9221
+ * stopped recording under `blocking`; the same move is
9222
+ * seconds of stopped recording under `nonBlocking`.
9223
+ *
9224
+ * The mode is on the JOB, not only on the input, because `status` is where an
9225
+ * operator finds out which one is running.
9226
+ */
9227
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9164
9228
  /** Shared input for planning and starting an orchestrated storage migration. */
9165
9229
  var StorageMigrationInputSchema = object({
9166
9230
  destinations: StorageMigrationDestinationsSchema,
9167
- throttleMbps: number().min(1).max(1e3).optional()
9231
+ throttleMbps: number().min(1).max(1e3).optional(),
9232
+ /** Omitted = `blocking`, which stays the default. */
9233
+ mode: StorageMigrationModeSchema.optional()
9168
9234
  });
9169
- /** The durable coordinator state machine. The only phase that changes default
9170
- * locations is `repointing`, after every selected mover has completed and been
9171
- * verified. */
9235
+ /**
9236
+ * The durable coordinator state machine.
9237
+ *
9238
+ * `blocking`:
9239
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
9240
+ *
9241
+ * `nonBlocking`:
9242
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
9243
+ *
9244
+ * Same phases, different order plus two new ones — not a second mover.
9245
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
9246
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
9247
+ * `repointing` is still the only phase that changes a default location.
9248
+ */
9172
9249
  var StorageMigrationPhaseSchema = _enum([
9173
9250
  "planning",
9251
+ "sealing",
9174
9252
  "pausing",
9175
9253
  "moving",
9254
+ "draining",
9176
9255
  "verifying",
9177
9256
  "repointing",
9178
9257
  "refreshing",
@@ -9186,17 +9265,56 @@ var StorageMigrationParticipantSchema = _enum([
9186
9265
  "recorder",
9187
9266
  "analytics"
9188
9267
  ]);
9268
+ /**
9269
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9270
+ *
9271
+ * The long half of a non-blocking migration is `draining`, and it is measured
9272
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9273
+ * existed the only place those numbers appeared was a Loki line, so an operator
9274
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9275
+ * afternoon.
9276
+ *
9277
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9278
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9279
+ * mover — which is the exact failure this is meant to end. The coordinator's
9280
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9281
+ * read `state`; folding the counters costs no extra read and makes the durable
9282
+ * record say afterwards how far a move actually got.
9283
+ *
9284
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9285
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9286
+ * cannot say M, and a 0 there would render as "100 % done".
9287
+ */
9288
+ var StorageMigrationMoveProgressSchema = object({
9289
+ filesMoved: number().int().nonnegative(),
9290
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9291
+ filesTotal: number().int().nonnegative().nullable(),
9292
+ bytesMoved: number().int().nonnegative(),
9293
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9294
+ * crash gets a new mover, and a rate computed from the migration's start
9295
+ * would silently average in the time nothing was running. */
9296
+ startedAt: number(),
9297
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9298
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9299
+ * subtract its own. */
9300
+ observedAt: number()
9301
+ });
9189
9302
  var StorageMigrationMoveSchema = object({
9190
9303
  storageClass: StorageMigrationClassSchema,
9191
9304
  fromLocationId: string(),
9192
9305
  toLocationId: string(),
9193
9306
  moverJobId: string().nullable(),
9194
9307
  state: RelocateJobStateSchema.nullable(),
9195
- error: string().nullable()
9308
+ error: string().nullable(),
9309
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9310
+ progress: StorageMigrationMoveProgressSchema.nullable()
9196
9311
  });
9197
9312
  var StorageMigrationJobSchema = object({
9198
9313
  jobId: string(),
9199
9314
  phase: StorageMigrationPhaseSchema,
9315
+ /** Which order this job is running. `status` is the only place an operator
9316
+ * can tell a seconds-long cutover from a thirty-hour one. */
9317
+ mode: StorageMigrationModeSchema,
9200
9318
  destinations: StorageMigrationDestinationsSchema,
9201
9319
  throttleMbps: number(),
9202
9320
  moves: array(StorageMigrationMoveSchema),
@@ -9209,13 +9327,122 @@ var StorageMigrationJobSchema = object({
9209
9327
  finishedAt: number().nullable(),
9210
9328
  error: string().nullable()
9211
9329
  });
9330
+ var StorageMigrationFindingSchema = object({
9331
+ code: _enum([
9332
+ "sharesDeviceWithSource",
9333
+ "deviceIdentityUnknown",
9334
+ "unstampedEventMediaRows",
9335
+ "blockingOnly",
9336
+ "noMover"
9337
+ ]),
9338
+ storageClass: StorageMigrationClassSchema,
9339
+ /** Human-readable, already carrying the ids and counts. */
9340
+ message: string()
9341
+ });
9212
9342
  var StorageMigrationPlanSchema = object({
9213
9343
  destinations: StorageMigrationDestinationsSchema,
9344
+ /** The mode this plan was built for. A plan is only valid for its mode: the
9345
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
9346
+ * it. */
9347
+ mode: StorageMigrationModeSchema,
9214
9348
  moves: array(object({
9215
9349
  storageClass: StorageMigrationClassSchema,
9216
9350
  fromLocationId: string(),
9217
9351
  toLocationId: string()
9218
- }))
9352
+ })),
9353
+ findings: array(StorageMigrationFindingSchema)
9354
+ });
9355
+ /**
9356
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9357
+ *
9358
+ * The coordinator's job record is the state of record for a migration, and its
9359
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9360
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9361
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9362
+ * way because no supported UI path existed. A mover armed like that has no job
9363
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9364
+ *
9365
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9366
+ * orchestrated it.
9367
+ */
9368
+ var StorageMigrationMoverSchema = object({
9369
+ lane: _enum(["footage", "media"]),
9370
+ job: RelocateJobSchema,
9371
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9372
+ * directly against the owning addon. */
9373
+ migrationJobId: string().nullable(),
9374
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9375
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9376
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9377
+ * rate made of two different clocks. */
9378
+ observedAt: number()
9379
+ });
9380
+ /**
9381
+ * What a SOURCE still holds for one storage class — the number that makes a
9382
+ * "drain remaining" action honest rather than hopeful.
9383
+ *
9384
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9385
+ * engine's own selection count for media), never from the resident index: a
9386
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9387
+ * never been told about (D295).
9388
+ *
9389
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9390
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9391
+ * because refusing on an unanswerable read would hide exactly the case an
9392
+ * operator needs to act on.
9393
+ */
9394
+ var StorageMigrationResidueSchema = object({
9395
+ storageClass: StorageMigrationClassSchema,
9396
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9397
+ * move from wherever they are rather than from one named source. */
9398
+ fromLocationId: string(),
9399
+ /** Where a drain would move it — the class's CURRENT default. */
9400
+ toLocationId: string(),
9401
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9402
+ items: number().int().nonnegative().nullable(),
9403
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9404
+ bytes: number().int().nonnegative().nullable()
9405
+ });
9406
+ /**
9407
+ * Run the DRAIN half and nothing else.
9408
+ *
9409
+ * A migration that reached `done` has already repointed, so `start` correctly
9410
+ * refuses its destination ("already the default") — there is nothing left to
9411
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9412
+ * or finish against a work list that was a tenth of the archive (D295), and
9413
+ * before this there was no supported way to run only that half: the only way
9414
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9415
+ *
9416
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9417
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9418
+ * re-repoint a class that is already migrated.
9419
+ */
9420
+ var StorageMigrationDrainInputSchema = object({
9421
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9422
+ * a class whose source is already empty is refused rather than started. */
9423
+ classes: array(StorageMigrationClassSchema).min(1),
9424
+ throttleMbps: number().min(1).max(1e3).optional()
9425
+ });
9426
+ /** What a footage source still holds, asked of the durable hour ledger. */
9427
+ var RelocateResidueInputSchema = object({
9428
+ fromLocationId: string().min(1),
9429
+ /** Narrow to one logical class; omit for every profile on the location. */
9430
+ footageClass: RelocateFootageClassSchema.optional()
9431
+ });
9432
+ /** `null` = the archive could not answer (no ledger on this node, or the
9433
+ * aggregate failed). Never conflated with an empty source. */
9434
+ var RelocateResidueSchema = object({
9435
+ segments: number().int().nonnegative(),
9436
+ bytes: number().int().nonnegative()
9437
+ }).nullable();
9438
+ /** How many rows a media pass would still act on against a given target — the
9439
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9440
+ * never disagree. `null` = the count could not be taken. */
9441
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9442
+ var RelocatableMediaCountInputSchema = object({
9443
+ toLocationId: string().min(1),
9444
+ /** Omitted = `move`. */
9445
+ mode: MediaRelocateModeSchema.optional()
9219
9446
  });
9220
9447
  /**
9221
9448
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -9321,6 +9548,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9321
9548
  * two addons declaring the same `id` must agree on `cardinality` (validated
9322
9549
  * at kernel aggregation time, not here).
9323
9550
  */
9551
+ /**
9552
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9553
+ * actually reaches the bytes. It is the constraint that decides which
9554
+ * `storage-provider`s may back a location of that kind.
9555
+ *
9556
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9557
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9558
+ * post-analysis media roots). Only a provider that serves a genuine local
9559
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9560
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9561
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9562
+ * against a same-named local directory that is something else entirely.
9563
+ *
9564
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9565
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9566
+ * service never sees a path, so any provider can back it. `backups` is the
9567
+ * one kind that qualifies today.
9568
+ *
9569
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9570
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9571
+ * refused the configuration; the first write simply went somewhere wrong, and
9572
+ * a recording write that goes wrong surfaces as a silent black window rather
9573
+ * than an error (the read path does not `stat`). This turns that accident into
9574
+ * a declared, enforced, testable refusal.
9575
+ */
9576
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9324
9577
  var StorageLocationDeclarationSchema = object({
9325
9578
  /**
9326
9579
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9340,6 +9593,19 @@ var StorageLocationDeclarationSchema = object({
9340
9593
  */
9341
9594
  cardinality: _enum(["single", "multi"]),
9342
9595
  /**
9596
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9597
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9598
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9599
+ *
9600
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9601
+ * can only over-restrict (refuse a remote provider for a kind that might
9602
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9603
+ * permissive direction and is therefore never inferred — a repo guard
9604
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9605
+ * reached by omission.
9606
+ */
9607
+ access: StorageAccessSchema.optional(),
9608
+ /**
9343
9609
  * When set, the default instance for this location inherits its resolved
9344
9610
  * root from the named location's default instance. Useful for derivative
9345
9611
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19224,8 +19490,10 @@ var TrackSchema = object({
19224
19490
  lastSeen: number(),
19225
19491
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19226
19492
  positions: array(TrackPositionSchema).readonly(),
19227
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19228
- * saveThumbnails policy). */
19493
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19494
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19495
+ * the retired `saveThumbnails` used to gate this and the rolling
19496
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19229
19497
  snapshots: array(TrackSnapshotSchema).readonly(),
19230
19498
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19231
19499
  zonesVisited: array(string()).readonly(),
@@ -20085,6 +20353,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20085
20353
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20086
20354
  kind: "mutation",
20087
20355
  auth: "admin"
20356
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20357
+ kind: "query",
20358
+ auth: "admin"
20088
20359
  }), method(object({}), array(RelocateJobSchema).readonly(), {
20089
20360
  kind: "query",
20090
20361
  auth: "admin"
@@ -21992,7 +22263,10 @@ method(object({
21992
22263
  }), StorageLocationSchema, {
21993
22264
  kind: "mutation",
21994
22265
  auth: "admin"
21995
- }), method(object({ id: string() }), _void(), {
22266
+ }), method(object({
22267
+ id: string(),
22268
+ force: boolean().optional()
22269
+ }), _void(), {
21996
22270
  kind: "mutation",
21997
22271
  auth: "admin"
21998
22272
  }), method(object({ id: string() }), object({
@@ -22041,6 +22315,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22041
22315
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22042
22316
  kind: "mutation",
22043
22317
  auth: "admin"
22318
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22319
+ kind: "mutation",
22320
+ auth: "admin"
22044
22321
  });
22045
22322
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22046
22323
  providerId: string().min(1),
@@ -22439,12 +22716,38 @@ response: record(string(), unknown()) }), object({
22439
22716
  *
22440
22717
  * ## Why this is a capability and not a helper
22441
22718
  *
22442
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22443
- * plate, vehicle, identity, and the event store's derivativesand every one of
22444
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22445
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22446
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22447
- * load 5,000 rows before ranking anything.
22719
+ * This capability was introduced with the claim that SIX stores in
22720
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22721
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22722
+ * claim was never true, and leaving it here made five stores look like pending
22723
+ * work when three of them have no vector at all. Counted column by column on
22724
+ * 2026-08-30, exactly THREE ever held one:
22725
+ *
22726
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22727
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22728
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22729
+ * face, migrated 2026-08-30 into its OWN index (see below).
22730
+ *
22731
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22732
+ * and `identities` store a name; the event store stores no derivative vector.
22733
+ * They are not migration candidates and never were.
22734
+ *
22735
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22736
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22737
+ * rows before ranking anything.
22738
+ *
22739
+ * ## One index per COMPARISON, never per encoder
22740
+ *
22741
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22742
+ * model, and they still get two indexes. An index is a set of things that are
22743
+ * ranked against each other and that live and die together, and these two are
22744
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22745
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22746
+ * forever and is the gallery every recognition ranks against. One index would
22747
+ * mean every gallery load and every reconcile carried a filter whose failure
22748
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22749
+ * person's only sample. The dimension they share is not a reason to share an
22750
+ * index; the question they answer is, and it differs.
22448
22751
  *
22449
22752
  * The fix is not a faster loop, it is a different backend — and the backend
22450
22753
  * should be replaceable without touching six callers. So: a singleton
@@ -22549,7 +22852,20 @@ var VectorQueryResultSchema = object({
22549
22852
  */
22550
22853
  scanned: number(),
22551
22854
  /** True when the backend could not consider every row that passed the filter. */
22552
- truncated: boolean()
22855
+ truncated: boolean(),
22856
+ /**
22857
+ * The `topK` the backend actually ran with.
22858
+ *
22859
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22860
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22861
+ * own log rather than in its answer. That is how an audit asking for 20,000
22862
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22863
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22864
+ * MUCH, in the return value, where the caller cannot fail to see it.
22865
+ *
22866
+ * Equals the requested `topK` whenever nothing was lowered.
22867
+ */
22868
+ effectiveTopK: number().int().positive()
22553
22869
  });
22554
22870
  var VectorDeleteInputSchema = object({
22555
22871
  index: string(),
@@ -22578,6 +22894,68 @@ var VectorGetResultSchema = object({ items: array(object({
22578
22894
  id: string(),
22579
22895
  metadata: VectorMetadataSchema
22580
22896
  })) });
22897
+ /**
22898
+ * Ids to read back WITH their vectors.
22899
+ *
22900
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22901
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22902
+ * caller depends on that promise. This one promises the opposite.
22903
+ *
22904
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22905
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22906
+ * a per-face cross-process KNN would be a network round trip inside the
22907
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22908
+ * it requires the index to hand the floats back. Without this method the only
22909
+ * way to keep a readable vector is a JSON column, which is the thing this
22910
+ * capability exists to delete.
22911
+ *
22912
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22913
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22914
+ */
22915
+ var VectorFetchInputSchema = object({
22916
+ index: string(),
22917
+ ids: array(string())
22918
+ });
22919
+ var VectorFetchResultSchema = object({ items: array(object({
22920
+ id: string(),
22921
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22922
+ vector: string(),
22923
+ metadata: VectorMetadataSchema
22924
+ })) });
22925
+ /**
22926
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22927
+ *
22928
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22929
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22930
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22931
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22932
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22933
+ * looked" for as long as anyone cared to read it.
22934
+ *
22935
+ * This is the primitive that question actually needs: a bounded page, ordered
22936
+ * by the backend's own row order, costing no distance computation at all.
22937
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22938
+ * the full-table read this capability was built to stop.
22939
+ */
22940
+ var VectorScanInputSchema = object({
22941
+ index: string(),
22942
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22943
+ cursor: number().int().nonnegative().default(0),
22944
+ limit: number().int().positive()
22945
+ });
22946
+ var VectorScanResultSchema = object({
22947
+ items: array(object({
22948
+ id: string(),
22949
+ metadata: VectorMetadataSchema
22950
+ })),
22951
+ /**
22952
+ * Where the next page starts, or `null` when the walk reached the end.
22953
+ *
22954
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22955
+ * from a short page: a backend is free to return fewer rows than asked.
22956
+ */
22957
+ nextCursor: number().int().nonnegative().nullable()
22958
+ });
22581
22959
  var VectorStatsInputSchema = object({ index: string() });
22582
22960
  var VectorStatsResultSchema = object({
22583
22961
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22596,7 +22974,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22596
22974
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22597
22975
  kind: "mutation",
22598
22976
  auth: "admin"
22599
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22977
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22600
22978
  kind: "mutation",
22601
22979
  auth: "admin"
22602
22980
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29223,6 +29601,9 @@ method(object({
29223
29601
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29224
29602
  kind: "query",
29225
29603
  auth: "admin"
29604
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29605
+ kind: "query",
29606
+ auth: "admin"
29226
29607
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29227
29608
  kind: "mutation",
29228
29609
  auth: "admin"
@@ -35857,6 +36238,18 @@ Object.freeze({
35857
36238
  addonId: null,
35858
36239
  access: "create"
35859
36240
  },
36241
+ "pipelineAnalytics.countRelocatableMedia": {
36242
+ capName: "pipeline-analytics",
36243
+ capScope: "device",
36244
+ addonId: null,
36245
+ access: "view"
36246
+ },
36247
+ "pipelineAnalytics.countUnstampedEventMedia": {
36248
+ capName: "pipeline-analytics",
36249
+ capScope: "device",
36250
+ addonId: null,
36251
+ access: "view"
36252
+ },
35860
36253
  "pipelineAnalytics.deleteDeviceEvents": {
35861
36254
  capName: "pipeline-analytics",
35862
36255
  capScope: "device",
@@ -37015,6 +37408,12 @@ Object.freeze({
37015
37408
  addonId: null,
37016
37409
  access: "view"
37017
37410
  },
37411
+ "recording.getRelocateResidue": {
37412
+ capName: "recording",
37413
+ capScope: "system",
37414
+ addonId: null,
37415
+ access: "view"
37416
+ },
37018
37417
  "recording.getStorageMigrationMoveStatus": {
37019
37418
  capName: "recording",
37020
37419
  capScope: "system",
@@ -37561,12 +37960,30 @@ Object.freeze({
37561
37960
  addonId: null,
37562
37961
  access: "create"
37563
37962
  },
37963
+ "storageMigration.drain": {
37964
+ capName: "storage-migration",
37965
+ capScope: "system",
37966
+ addonId: null,
37967
+ access: "create"
37968
+ },
37969
+ "storageMigration.movers": {
37970
+ capName: "storage-migration",
37971
+ capScope: "system",
37972
+ addonId: null,
37973
+ access: "view"
37974
+ },
37564
37975
  "storageMigration.plan": {
37565
37976
  capName: "storage-migration",
37566
37977
  capScope: "system",
37567
37978
  addonId: null,
37568
37979
  access: "view"
37569
37980
  },
37981
+ "storageMigration.residue": {
37982
+ capName: "storage-migration",
37983
+ capScope: "system",
37984
+ addonId: null,
37985
+ access: "view"
37986
+ },
37570
37987
  "storageMigration.start": {
37571
37988
  capName: "storage-migration",
37572
37989
  capScope: "system",
@@ -38401,6 +38818,12 @@ Object.freeze({
38401
38818
  addonId: null,
38402
38819
  access: "delete"
38403
38820
  },
38821
+ "vectorStore.fetchByIds": {
38822
+ capName: "vector-store",
38823
+ capScope: "system",
38824
+ addonId: null,
38825
+ access: "view"
38826
+ },
38404
38827
  "vectorStore.getByIds": {
38405
38828
  capName: "vector-store",
38406
38829
  capScope: "system",
@@ -38413,6 +38836,12 @@ Object.freeze({
38413
38836
  addonId: null,
38414
38837
  access: "view"
38415
38838
  },
38839
+ "vectorStore.scan": {
38840
+ capName: "vector-store",
38841
+ capScope: "system",
38842
+ addonId: null,
38843
+ access: "view"
38844
+ },
38416
38845
  "vectorStore.stats": {
38417
38846
  capName: "vector-store",
38418
38847
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.42",
3
+ "version": "0.2.45",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",