@camstack/addon-provider-petkit 0.2.43 → 0.2.46

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 +387 -19
  2. package/dist/addon.mjs +387 -19
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -9105,6 +9105,21 @@ var RelocateJobSchema = object({
9105
9105
  bytesMoved: number().int(),
9106
9106
  /** Total files discovered up front; null while (or when) unknown. */
9107
9107
  filesTotal: number().int().nullable(),
9108
+ /**
9109
+ * Rows this run CORRECTED while moving them — a durable mutation the move
9110
+ * made that nobody asked for, so it is reported where the operator reads the
9111
+ * job rather than only in a log line.
9112
+ *
9113
+ * A footage segment records its byte count in its own NAME, and the durable
9114
+ * hour row derives its aggregates from those names. A file that does not
9115
+ * match its name therefore makes the ledger's sums — and with them quota and
9116
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
9117
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9118
+ *
9119
+ * Absent on lanes where the question has no meaning: a media blob's size is
9120
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9121
+ */
9122
+ rowsReconciled: number().int().nonnegative().optional(),
9108
9123
  startedAt: number(),
9109
9124
  finishedAt: number().nullable(),
9110
9125
  error: string().nullable()
@@ -9173,14 +9188,42 @@ var RelocateMediaInputSchema = object({
9173
9188
  /** Omitted = `move`, the pre-existing behaviour. */
9174
9189
  mode: MediaRelocateModeSchema.optional()
9175
9190
  });
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()
9191
+ /**
9192
+ * The unstamped population of ONE collection split, because the gate and the
9193
+ * operator ask two different questions and only one of them has to be cheap.
9194
+ *
9195
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9196
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9197
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9198
+ * that matters — after a seal, when the population is empty.
9199
+ *
9200
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9201
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9202
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9203
+ * and useful answer: "there are some, and this read could not say how many"
9204
+ * still refuses the cutover, which is the whole job.
9205
+ */
9206
+ var UnstampedRowsSchema = object({
9207
+ present: boolean(),
9208
+ rows: number().int().nonnegative().nullable()
9183
9209
  });
9210
+ /**
9211
+ * How many rows still carry NO `locationId` — the population a repoint would
9212
+ * silently re-aim at a disk that does not hold their bytes.
9213
+ *
9214
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9215
+ * over. The gate opens on a measured absence and on nothing else; an unread
9216
+ * collection and an empty one are different facts, and this repo has already
9217
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9218
+ */
9219
+ var UnstampedEventMediaCountSchema = object({
9220
+ media: UnstampedRowsSchema,
9221
+ retrainFrames: UnstampedRowsSchema,
9222
+ /** True when EITHER collection holds one. The refusal reads this. */
9223
+ anyPresent: boolean(),
9224
+ /** Sum across both, or `null` when either lane could not be counted. */
9225
+ total: number().int().nonnegative().nullable()
9226
+ }).nullable();
9184
9227
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9185
9228
  /** The independently selectable logical storage classes — every class
9186
9229
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9266,13 +9309,53 @@ var StorageMigrationParticipantSchema = _enum([
9266
9309
  "recorder",
9267
9310
  "analytics"
9268
9311
  ]);
9312
+ /**
9313
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9314
+ *
9315
+ * The long half of a non-blocking migration is `draining`, and it is measured
9316
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9317
+ * existed the only place those numbers appeared was a Loki line, so an operator
9318
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9319
+ * afternoon.
9320
+ *
9321
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9322
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9323
+ * mover — which is the exact failure this is meant to end. The coordinator's
9324
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9325
+ * read `state`; folding the counters costs no extra read and makes the durable
9326
+ * record say afterwards how far a move actually got.
9327
+ *
9328
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9329
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9330
+ * cannot say M, and a 0 there would render as "100 % done".
9331
+ */
9332
+ var StorageMigrationMoveProgressSchema = object({
9333
+ filesMoved: number().int().nonnegative(),
9334
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9335
+ filesTotal: number().int().nonnegative().nullable(),
9336
+ bytesMoved: number().int().nonnegative(),
9337
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9338
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9339
+ * rows would be the same failure as one that silently skipped them. */
9340
+ rowsReconciled: number().int().nonnegative().optional(),
9341
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9342
+ * crash gets a new mover, and a rate computed from the migration's start
9343
+ * would silently average in the time nothing was running. */
9344
+ startedAt: number(),
9345
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9346
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9347
+ * subtract its own. */
9348
+ observedAt: number()
9349
+ });
9269
9350
  var StorageMigrationMoveSchema = object({
9270
9351
  storageClass: StorageMigrationClassSchema,
9271
9352
  fromLocationId: string(),
9272
9353
  toLocationId: string(),
9273
9354
  moverJobId: string().nullable(),
9274
9355
  state: RelocateJobStateSchema.nullable(),
9275
- error: string().nullable()
9356
+ error: string().nullable(),
9357
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9358
+ progress: StorageMigrationMoveProgressSchema.nullable()
9276
9359
  });
9277
9360
  var StorageMigrationJobSchema = object({
9278
9361
  jobId: string(),
@@ -9318,6 +9401,98 @@ var StorageMigrationPlanSchema = object({
9318
9401
  findings: array(StorageMigrationFindingSchema)
9319
9402
  });
9320
9403
  /**
9404
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9405
+ *
9406
+ * The coordinator's job record is the state of record for a migration, and its
9407
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9408
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9409
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9410
+ * way because no supported UI path existed. A mover armed like that has no job
9411
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9412
+ *
9413
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9414
+ * orchestrated it.
9415
+ */
9416
+ var StorageMigrationMoverSchema = object({
9417
+ lane: _enum(["footage", "media"]),
9418
+ job: RelocateJobSchema,
9419
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9420
+ * directly against the owning addon. */
9421
+ migrationJobId: string().nullable(),
9422
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9423
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9424
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9425
+ * rate made of two different clocks. */
9426
+ observedAt: number()
9427
+ });
9428
+ /**
9429
+ * What a SOURCE still holds for one storage class — the number that makes a
9430
+ * "drain remaining" action honest rather than hopeful.
9431
+ *
9432
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9433
+ * engine's own selection count for media), never from the resident index: a
9434
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9435
+ * never been told about (D295).
9436
+ *
9437
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9438
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9439
+ * because refusing on an unanswerable read would hide exactly the case an
9440
+ * operator needs to act on.
9441
+ */
9442
+ var StorageMigrationResidueSchema = object({
9443
+ storageClass: StorageMigrationClassSchema,
9444
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9445
+ * move from wherever they are rather than from one named source. */
9446
+ fromLocationId: string(),
9447
+ /** Where a drain would move it — the class's CURRENT default. */
9448
+ toLocationId: string(),
9449
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9450
+ items: number().int().nonnegative().nullable(),
9451
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9452
+ bytes: number().int().nonnegative().nullable()
9453
+ });
9454
+ /**
9455
+ * Run the DRAIN half and nothing else.
9456
+ *
9457
+ * A migration that reached `done` has already repointed, so `start` correctly
9458
+ * refuses its destination ("already the default") — there is nothing left to
9459
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9460
+ * or finish against a work list that was a tenth of the archive (D295), and
9461
+ * before this there was no supported way to run only that half: the only way
9462
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9463
+ *
9464
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9465
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9466
+ * re-repoint a class that is already migrated.
9467
+ */
9468
+ var StorageMigrationDrainInputSchema = object({
9469
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9470
+ * a class whose source is already empty is refused rather than started. */
9471
+ classes: array(StorageMigrationClassSchema).min(1),
9472
+ throttleMbps: number().min(1).max(1e3).optional()
9473
+ });
9474
+ /** What a footage source still holds, asked of the durable hour ledger. */
9475
+ var RelocateResidueInputSchema = object({
9476
+ fromLocationId: string().min(1),
9477
+ /** Narrow to one logical class; omit for every profile on the location. */
9478
+ footageClass: RelocateFootageClassSchema.optional()
9479
+ });
9480
+ /** `null` = the archive could not answer (no ledger on this node, or the
9481
+ * aggregate failed). Never conflated with an empty source. */
9482
+ var RelocateResidueSchema = object({
9483
+ segments: number().int().nonnegative(),
9484
+ bytes: number().int().nonnegative()
9485
+ }).nullable();
9486
+ /** How many rows a media pass would still act on against a given target — the
9487
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9488
+ * never disagree. `null` = the count could not be taken. */
9489
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9490
+ var RelocatableMediaCountInputSchema = object({
9491
+ toLocationId: string().min(1),
9492
+ /** Omitted = `move`. */
9493
+ mode: MediaRelocateModeSchema.optional()
9494
+ });
9495
+ /**
9321
9496
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
9322
9497
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
9323
9498
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -9421,6 +9596,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9421
9596
  * two addons declaring the same `id` must agree on `cardinality` (validated
9422
9597
  * at kernel aggregation time, not here).
9423
9598
  */
9599
+ /**
9600
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9601
+ * actually reaches the bytes. It is the constraint that decides which
9602
+ * `storage-provider`s may back a location of that kind.
9603
+ *
9604
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9605
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9606
+ * post-analysis media roots). Only a provider that serves a genuine local
9607
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9608
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9609
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9610
+ * against a same-named local directory that is something else entirely.
9611
+ *
9612
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9613
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9614
+ * service never sees a path, so any provider can back it. `backups` is the
9615
+ * one kind that qualifies today.
9616
+ *
9617
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9618
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9619
+ * refused the configuration; the first write simply went somewhere wrong, and
9620
+ * a recording write that goes wrong surfaces as a silent black window rather
9621
+ * than an error (the read path does not `stat`). This turns that accident into
9622
+ * a declared, enforced, testable refusal.
9623
+ */
9624
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9424
9625
  var StorageLocationDeclarationSchema = object({
9425
9626
  /**
9426
9627
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9440,6 +9641,19 @@ var StorageLocationDeclarationSchema = object({
9440
9641
  */
9441
9642
  cardinality: _enum(["single", "multi"]),
9442
9643
  /**
9644
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9645
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9646
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9647
+ *
9648
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9649
+ * can only over-restrict (refuse a remote provider for a kind that might
9650
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9651
+ * permissive direction and is therefore never inferred — a repo guard
9652
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9653
+ * reached by omission.
9654
+ */
9655
+ access: StorageAccessSchema.optional(),
9656
+ /**
9443
9657
  * When set, the default instance for this location inherits its resolved
9444
9658
  * root from the named location's default instance. Useful for derivative
9445
9659
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19324,8 +19538,10 @@ var TrackSchema = object({
19324
19538
  lastSeen: number(),
19325
19539
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19326
19540
  positions: array(TrackPositionSchema).readonly(),
19327
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19328
- * saveThumbnails policy). */
19541
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19542
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19543
+ * the retired `saveThumbnails` used to gate this and the rolling
19544
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19329
19545
  snapshots: array(TrackSnapshotSchema).readonly(),
19330
19546
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19331
19547
  zonesVisited: array(string()).readonly(),
@@ -20185,7 +20401,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20185
20401
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20186
20402
  kind: "mutation",
20187
20403
  auth: "admin"
20188
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
20404
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20405
+ kind: "query",
20406
+ auth: "admin"
20407
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20189
20408
  kind: "query",
20190
20409
  auth: "admin"
20191
20410
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -22144,6 +22363,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22144
22363
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22145
22364
  kind: "mutation",
22146
22365
  auth: "admin"
22366
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22367
+ kind: "mutation",
22368
+ auth: "admin"
22147
22369
  });
22148
22370
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22149
22371
  providerId: string().min(1),
@@ -22542,12 +22764,38 @@ response: record(string(), unknown()) }), object({
22542
22764
  *
22543
22765
  * ## Why this is a capability and not a helper
22544
22766
  *
22545
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22546
- * plate, vehicle, identity, and the event store's derivativesand every one of
22547
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22548
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22549
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22550
- * load 5,000 rows before ranking anything.
22767
+ * This capability was introduced with the claim that SIX stores in
22768
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22769
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22770
+ * claim was never true, and leaving it here made five stores look like pending
22771
+ * work when three of them have no vector at all. Counted column by column on
22772
+ * 2026-08-30, exactly THREE ever held one:
22773
+ *
22774
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22775
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22776
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22777
+ * face, migrated 2026-08-30 into its OWN index (see below).
22778
+ *
22779
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22780
+ * and `identities` store a name; the event store stores no derivative vector.
22781
+ * They are not migration candidates and never were.
22782
+ *
22783
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22784
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22785
+ * rows before ranking anything.
22786
+ *
22787
+ * ## One index per COMPARISON, never per encoder
22788
+ *
22789
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22790
+ * model, and they still get two indexes. An index is a set of things that are
22791
+ * ranked against each other and that live and die together, and these two are
22792
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22793
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22794
+ * forever and is the gallery every recognition ranks against. One index would
22795
+ * mean every gallery load and every reconcile carried a filter whose failure
22796
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22797
+ * person's only sample. The dimension they share is not a reason to share an
22798
+ * index; the question they answer is, and it differs.
22551
22799
  *
22552
22800
  * The fix is not a faster loop, it is a different backend — and the backend
22553
22801
  * should be replaceable without touching six callers. So: a singleton
@@ -22652,7 +22900,20 @@ var VectorQueryResultSchema = object({
22652
22900
  */
22653
22901
  scanned: number(),
22654
22902
  /** True when the backend could not consider every row that passed the filter. */
22655
- truncated: boolean()
22903
+ truncated: boolean(),
22904
+ /**
22905
+ * The `topK` the backend actually ran with.
22906
+ *
22907
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22908
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22909
+ * own log rather than in its answer. That is how an audit asking for 20,000
22910
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22911
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22912
+ * MUCH, in the return value, where the caller cannot fail to see it.
22913
+ *
22914
+ * Equals the requested `topK` whenever nothing was lowered.
22915
+ */
22916
+ effectiveTopK: number().int().positive()
22656
22917
  });
22657
22918
  var VectorDeleteInputSchema = object({
22658
22919
  index: string(),
@@ -22681,6 +22942,68 @@ var VectorGetResultSchema = object({ items: array(object({
22681
22942
  id: string(),
22682
22943
  metadata: VectorMetadataSchema
22683
22944
  })) });
22945
+ /**
22946
+ * Ids to read back WITH their vectors.
22947
+ *
22948
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22949
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22950
+ * caller depends on that promise. This one promises the opposite.
22951
+ *
22952
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22953
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22954
+ * a per-face cross-process KNN would be a network round trip inside the
22955
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22956
+ * it requires the index to hand the floats back. Without this method the only
22957
+ * way to keep a readable vector is a JSON column, which is the thing this
22958
+ * capability exists to delete.
22959
+ *
22960
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22961
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22962
+ */
22963
+ var VectorFetchInputSchema = object({
22964
+ index: string(),
22965
+ ids: array(string())
22966
+ });
22967
+ var VectorFetchResultSchema = object({ items: array(object({
22968
+ id: string(),
22969
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22970
+ vector: string(),
22971
+ metadata: VectorMetadataSchema
22972
+ })) });
22973
+ /**
22974
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22975
+ *
22976
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22977
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22978
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22979
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22980
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22981
+ * looked" for as long as anyone cared to read it.
22982
+ *
22983
+ * This is the primitive that question actually needs: a bounded page, ordered
22984
+ * by the backend's own row order, costing no distance computation at all.
22985
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22986
+ * the full-table read this capability was built to stop.
22987
+ */
22988
+ var VectorScanInputSchema = object({
22989
+ index: string(),
22990
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22991
+ cursor: number().int().nonnegative().default(0),
22992
+ limit: number().int().positive()
22993
+ });
22994
+ var VectorScanResultSchema = object({
22995
+ items: array(object({
22996
+ id: string(),
22997
+ metadata: VectorMetadataSchema
22998
+ })),
22999
+ /**
23000
+ * Where the next page starts, or `null` when the walk reached the end.
23001
+ *
23002
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
23003
+ * from a short page: a backend is free to return fewer rows than asked.
23004
+ */
23005
+ nextCursor: number().int().nonnegative().nullable()
23006
+ });
22684
23007
  var VectorStatsInputSchema = object({ index: string() });
22685
23008
  var VectorStatsResultSchema = object({
22686
23009
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22699,7 +23022,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22699
23022
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22700
23023
  kind: "mutation",
22701
23024
  auth: "admin"
22702
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
23025
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22703
23026
  kind: "mutation",
22704
23027
  auth: "admin"
22705
23028
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29326,6 +29649,9 @@ method(object({
29326
29649
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29327
29650
  kind: "query",
29328
29651
  auth: "admin"
29652
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29653
+ kind: "query",
29654
+ auth: "admin"
29329
29655
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29330
29656
  kind: "mutation",
29331
29657
  auth: "admin"
@@ -35960,6 +36286,12 @@ Object.freeze({
35960
36286
  addonId: null,
35961
36287
  access: "create"
35962
36288
  },
36289
+ "pipelineAnalytics.countRelocatableMedia": {
36290
+ capName: "pipeline-analytics",
36291
+ capScope: "device",
36292
+ addonId: null,
36293
+ access: "view"
36294
+ },
35963
36295
  "pipelineAnalytics.countUnstampedEventMedia": {
35964
36296
  capName: "pipeline-analytics",
35965
36297
  capScope: "device",
@@ -37124,6 +37456,12 @@ Object.freeze({
37124
37456
  addonId: null,
37125
37457
  access: "view"
37126
37458
  },
37459
+ "recording.getRelocateResidue": {
37460
+ capName: "recording",
37461
+ capScope: "system",
37462
+ addonId: null,
37463
+ access: "view"
37464
+ },
37127
37465
  "recording.getStorageMigrationMoveStatus": {
37128
37466
  capName: "recording",
37129
37467
  capScope: "system",
@@ -37670,12 +38008,30 @@ Object.freeze({
37670
38008
  addonId: null,
37671
38009
  access: "create"
37672
38010
  },
38011
+ "storageMigration.drain": {
38012
+ capName: "storage-migration",
38013
+ capScope: "system",
38014
+ addonId: null,
38015
+ access: "create"
38016
+ },
38017
+ "storageMigration.movers": {
38018
+ capName: "storage-migration",
38019
+ capScope: "system",
38020
+ addonId: null,
38021
+ access: "view"
38022
+ },
37673
38023
  "storageMigration.plan": {
37674
38024
  capName: "storage-migration",
37675
38025
  capScope: "system",
37676
38026
  addonId: null,
37677
38027
  access: "view"
37678
38028
  },
38029
+ "storageMigration.residue": {
38030
+ capName: "storage-migration",
38031
+ capScope: "system",
38032
+ addonId: null,
38033
+ access: "view"
38034
+ },
37679
38035
  "storageMigration.start": {
37680
38036
  capName: "storage-migration",
37681
38037
  capScope: "system",
@@ -38510,6 +38866,12 @@ Object.freeze({
38510
38866
  addonId: null,
38511
38867
  access: "delete"
38512
38868
  },
38869
+ "vectorStore.fetchByIds": {
38870
+ capName: "vector-store",
38871
+ capScope: "system",
38872
+ addonId: null,
38873
+ access: "view"
38874
+ },
38513
38875
  "vectorStore.getByIds": {
38514
38876
  capName: "vector-store",
38515
38877
  capScope: "system",
@@ -38522,6 +38884,12 @@ Object.freeze({
38522
38884
  addonId: null,
38523
38885
  access: "view"
38524
38886
  },
38887
+ "vectorStore.scan": {
38888
+ capName: "vector-store",
38889
+ capScope: "system",
38890
+ addonId: null,
38891
+ access: "view"
38892
+ },
38525
38893
  "vectorStore.stats": {
38526
38894
  capName: "vector-store",
38527
38895
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -9104,6 +9104,21 @@ var RelocateJobSchema = object({
9104
9104
  bytesMoved: number().int(),
9105
9105
  /** Total files discovered up front; null while (or when) unknown. */
9106
9106
  filesTotal: number().int().nullable(),
9107
+ /**
9108
+ * Rows this run CORRECTED while moving them — a durable mutation the move
9109
+ * made that nobody asked for, so it is reported where the operator reads the
9110
+ * job rather than only in a log line.
9111
+ *
9112
+ * A footage segment records its byte count in its own NAME, and the durable
9113
+ * hour row derives its aggregates from those names. A file that does not
9114
+ * match its name therefore makes the ledger's sums — and with them quota and
9115
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
9116
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9117
+ *
9118
+ * Absent on lanes where the question has no meaning: a media blob's size is
9119
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9120
+ */
9121
+ rowsReconciled: number().int().nonnegative().optional(),
9107
9122
  startedAt: number(),
9108
9123
  finishedAt: number().nullable(),
9109
9124
  error: string().nullable()
@@ -9172,14 +9187,42 @@ var RelocateMediaInputSchema = object({
9172
9187
  /** Omitted = `move`, the pre-existing behaviour. */
9173
9188
  mode: MediaRelocateModeSchema.optional()
9174
9189
  });
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()
9190
+ /**
9191
+ * The unstamped population of ONE collection split, because the gate and the
9192
+ * operator ask two different questions and only one of them has to be cheap.
9193
+ *
9194
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9195
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9196
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9197
+ * that matters — after a seal, when the population is empty.
9198
+ *
9199
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9200
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9201
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9202
+ * and useful answer: "there are some, and this read could not say how many"
9203
+ * still refuses the cutover, which is the whole job.
9204
+ */
9205
+ var UnstampedRowsSchema = object({
9206
+ present: boolean(),
9207
+ rows: number().int().nonnegative().nullable()
9182
9208
  });
9209
+ /**
9210
+ * How many rows still carry NO `locationId` — the population a repoint would
9211
+ * silently re-aim at a disk that does not hold their bytes.
9212
+ *
9213
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9214
+ * over. The gate opens on a measured absence and on nothing else; an unread
9215
+ * collection and an empty one are different facts, and this repo has already
9216
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9217
+ */
9218
+ var UnstampedEventMediaCountSchema = object({
9219
+ media: UnstampedRowsSchema,
9220
+ retrainFrames: UnstampedRowsSchema,
9221
+ /** True when EITHER collection holds one. The refusal reads this. */
9222
+ anyPresent: boolean(),
9223
+ /** Sum across both, or `null` when either lane could not be counted. */
9224
+ total: number().int().nonnegative().nullable()
9225
+ }).nullable();
9183
9226
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9184
9227
  /** The independently selectable logical storage classes — every class
9185
9228
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9265,13 +9308,53 @@ var StorageMigrationParticipantSchema = _enum([
9265
9308
  "recorder",
9266
9309
  "analytics"
9267
9310
  ]);
9311
+ /**
9312
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9313
+ *
9314
+ * The long half of a non-blocking migration is `draining`, and it is measured
9315
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9316
+ * existed the only place those numbers appeared was a Loki line, so an operator
9317
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9318
+ * afternoon.
9319
+ *
9320
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9321
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9322
+ * mover — which is the exact failure this is meant to end. The coordinator's
9323
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9324
+ * read `state`; folding the counters costs no extra read and makes the durable
9325
+ * record say afterwards how far a move actually got.
9326
+ *
9327
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9328
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9329
+ * cannot say M, and a 0 there would render as "100 % done".
9330
+ */
9331
+ var StorageMigrationMoveProgressSchema = object({
9332
+ filesMoved: number().int().nonnegative(),
9333
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9334
+ filesTotal: number().int().nonnegative().nullable(),
9335
+ bytesMoved: number().int().nonnegative(),
9336
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9337
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9338
+ * rows would be the same failure as one that silently skipped them. */
9339
+ rowsReconciled: number().int().nonnegative().optional(),
9340
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9341
+ * crash gets a new mover, and a rate computed from the migration's start
9342
+ * would silently average in the time nothing was running. */
9343
+ startedAt: number(),
9344
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9345
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9346
+ * subtract its own. */
9347
+ observedAt: number()
9348
+ });
9268
9349
  var StorageMigrationMoveSchema = object({
9269
9350
  storageClass: StorageMigrationClassSchema,
9270
9351
  fromLocationId: string(),
9271
9352
  toLocationId: string(),
9272
9353
  moverJobId: string().nullable(),
9273
9354
  state: RelocateJobStateSchema.nullable(),
9274
- error: string().nullable()
9355
+ error: string().nullable(),
9356
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9357
+ progress: StorageMigrationMoveProgressSchema.nullable()
9275
9358
  });
9276
9359
  var StorageMigrationJobSchema = object({
9277
9360
  jobId: string(),
@@ -9317,6 +9400,98 @@ var StorageMigrationPlanSchema = object({
9317
9400
  findings: array(StorageMigrationFindingSchema)
9318
9401
  });
9319
9402
  /**
9403
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9404
+ *
9405
+ * The coordinator's job record is the state of record for a migration, and its
9406
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9407
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9408
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9409
+ * way because no supported UI path existed. A mover armed like that has no job
9410
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9411
+ *
9412
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9413
+ * orchestrated it.
9414
+ */
9415
+ var StorageMigrationMoverSchema = object({
9416
+ lane: _enum(["footage", "media"]),
9417
+ job: RelocateJobSchema,
9418
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9419
+ * directly against the owning addon. */
9420
+ migrationJobId: string().nullable(),
9421
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9422
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9423
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9424
+ * rate made of two different clocks. */
9425
+ observedAt: number()
9426
+ });
9427
+ /**
9428
+ * What a SOURCE still holds for one storage class — the number that makes a
9429
+ * "drain remaining" action honest rather than hopeful.
9430
+ *
9431
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9432
+ * engine's own selection count for media), never from the resident index: a
9433
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9434
+ * never been told about (D295).
9435
+ *
9436
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9437
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9438
+ * because refusing on an unanswerable read would hide exactly the case an
9439
+ * operator needs to act on.
9440
+ */
9441
+ var StorageMigrationResidueSchema = object({
9442
+ storageClass: StorageMigrationClassSchema,
9443
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9444
+ * move from wherever they are rather than from one named source. */
9445
+ fromLocationId: string(),
9446
+ /** Where a drain would move it — the class's CURRENT default. */
9447
+ toLocationId: string(),
9448
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9449
+ items: number().int().nonnegative().nullable(),
9450
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9451
+ bytes: number().int().nonnegative().nullable()
9452
+ });
9453
+ /**
9454
+ * Run the DRAIN half and nothing else.
9455
+ *
9456
+ * A migration that reached `done` has already repointed, so `start` correctly
9457
+ * refuses its destination ("already the default") — there is nothing left to
9458
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9459
+ * or finish against a work list that was a tenth of the archive (D295), and
9460
+ * before this there was no supported way to run only that half: the only way
9461
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9462
+ *
9463
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9464
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9465
+ * re-repoint a class that is already migrated.
9466
+ */
9467
+ var StorageMigrationDrainInputSchema = object({
9468
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9469
+ * a class whose source is already empty is refused rather than started. */
9470
+ classes: array(StorageMigrationClassSchema).min(1),
9471
+ throttleMbps: number().min(1).max(1e3).optional()
9472
+ });
9473
+ /** What a footage source still holds, asked of the durable hour ledger. */
9474
+ var RelocateResidueInputSchema = object({
9475
+ fromLocationId: string().min(1),
9476
+ /** Narrow to one logical class; omit for every profile on the location. */
9477
+ footageClass: RelocateFootageClassSchema.optional()
9478
+ });
9479
+ /** `null` = the archive could not answer (no ledger on this node, or the
9480
+ * aggregate failed). Never conflated with an empty source. */
9481
+ var RelocateResidueSchema = object({
9482
+ segments: number().int().nonnegative(),
9483
+ bytes: number().int().nonnegative()
9484
+ }).nullable();
9485
+ /** How many rows a media pass would still act on against a given target — the
9486
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9487
+ * never disagree. `null` = the count could not be taken. */
9488
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9489
+ var RelocatableMediaCountInputSchema = object({
9490
+ toLocationId: string().min(1),
9491
+ /** Omitted = `move`. */
9492
+ mode: MediaRelocateModeSchema.optional()
9493
+ });
9494
+ /**
9320
9495
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
9321
9496
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
9322
9497
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -9420,6 +9595,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9420
9595
  * two addons declaring the same `id` must agree on `cardinality` (validated
9421
9596
  * at kernel aggregation time, not here).
9422
9597
  */
9598
+ /**
9599
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9600
+ * actually reaches the bytes. It is the constraint that decides which
9601
+ * `storage-provider`s may back a location of that kind.
9602
+ *
9603
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9604
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9605
+ * post-analysis media roots). Only a provider that serves a genuine local
9606
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9607
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9608
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9609
+ * against a same-named local directory that is something else entirely.
9610
+ *
9611
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9612
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9613
+ * service never sees a path, so any provider can back it. `backups` is the
9614
+ * one kind that qualifies today.
9615
+ *
9616
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9617
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9618
+ * refused the configuration; the first write simply went somewhere wrong, and
9619
+ * a recording write that goes wrong surfaces as a silent black window rather
9620
+ * than an error (the read path does not `stat`). This turns that accident into
9621
+ * a declared, enforced, testable refusal.
9622
+ */
9623
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9423
9624
  var StorageLocationDeclarationSchema = object({
9424
9625
  /**
9425
9626
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9439,6 +9640,19 @@ var StorageLocationDeclarationSchema = object({
9439
9640
  */
9440
9641
  cardinality: _enum(["single", "multi"]),
9441
9642
  /**
9643
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9644
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9645
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9646
+ *
9647
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9648
+ * can only over-restrict (refuse a remote provider for a kind that might
9649
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9650
+ * permissive direction and is therefore never inferred — a repo guard
9651
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9652
+ * reached by omission.
9653
+ */
9654
+ access: StorageAccessSchema.optional(),
9655
+ /**
9442
9656
  * When set, the default instance for this location inherits its resolved
9443
9657
  * root from the named location's default instance. Useful for derivative
9444
9658
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19323,8 +19537,10 @@ var TrackSchema = object({
19323
19537
  lastSeen: number(),
19324
19538
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19325
19539
  positions: array(TrackPositionSchema).readonly(),
19326
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19327
- * saveThumbnails policy). */
19540
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19541
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19542
+ * the retired `saveThumbnails` used to gate this and the rolling
19543
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19328
19544
  snapshots: array(TrackSnapshotSchema).readonly(),
19329
19545
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19330
19546
  zonesVisited: array(string()).readonly(),
@@ -20184,7 +20400,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20184
20400
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
20185
20401
  kind: "mutation",
20186
20402
  auth: "admin"
20187
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
20403
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20404
+ kind: "query",
20405
+ auth: "admin"
20406
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
20188
20407
  kind: "query",
20189
20408
  auth: "admin"
20190
20409
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -22143,6 +22362,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
22143
22362
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
22144
22363
  kind: "mutation",
22145
22364
  auth: "admin"
22365
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22366
+ kind: "mutation",
22367
+ auth: "admin"
22146
22368
  });
22147
22369
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
22148
22370
  providerId: string().min(1),
@@ -22541,12 +22763,38 @@ response: record(string(), unknown()) }), object({
22541
22763
  *
22542
22764
  * ## Why this is a capability and not a helper
22543
22765
  *
22544
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22545
- * plate, vehicle, identity, and the event store's derivativesand every one of
22546
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22547
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22548
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22549
- * load 5,000 rows before ranking anything.
22766
+ * This capability was introduced with the claim that SIX stores in
22767
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22768
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22769
+ * claim was never true, and leaving it here made five stores look like pending
22770
+ * work when three of them have no vector at all. Counted column by column on
22771
+ * 2026-08-30, exactly THREE ever held one:
22772
+ *
22773
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22774
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22775
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22776
+ * face, migrated 2026-08-30 into its OWN index (see below).
22777
+ *
22778
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22779
+ * and `identities` store a name; the event store stores no derivative vector.
22780
+ * They are not migration candidates and never were.
22781
+ *
22782
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22783
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22784
+ * rows before ranking anything.
22785
+ *
22786
+ * ## One index per COMPARISON, never per encoder
22787
+ *
22788
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22789
+ * model, and they still get two indexes. An index is a set of things that are
22790
+ * ranked against each other and that live and die together, and these two are
22791
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22792
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22793
+ * forever and is the gallery every recognition ranks against. One index would
22794
+ * mean every gallery load and every reconcile carried a filter whose failure
22795
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22796
+ * person's only sample. The dimension they share is not a reason to share an
22797
+ * index; the question they answer is, and it differs.
22550
22798
  *
22551
22799
  * The fix is not a faster loop, it is a different backend — and the backend
22552
22800
  * should be replaceable without touching six callers. So: a singleton
@@ -22651,7 +22899,20 @@ var VectorQueryResultSchema = object({
22651
22899
  */
22652
22900
  scanned: number(),
22653
22901
  /** True when the backend could not consider every row that passed the filter. */
22654
- truncated: boolean()
22902
+ truncated: boolean(),
22903
+ /**
22904
+ * The `topK` the backend actually ran with.
22905
+ *
22906
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22907
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22908
+ * own log rather than in its answer. That is how an audit asking for 20,000
22909
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22910
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22911
+ * MUCH, in the return value, where the caller cannot fail to see it.
22912
+ *
22913
+ * Equals the requested `topK` whenever nothing was lowered.
22914
+ */
22915
+ effectiveTopK: number().int().positive()
22655
22916
  });
22656
22917
  var VectorDeleteInputSchema = object({
22657
22918
  index: string(),
@@ -22680,6 +22941,68 @@ var VectorGetResultSchema = object({ items: array(object({
22680
22941
  id: string(),
22681
22942
  metadata: VectorMetadataSchema
22682
22943
  })) });
22944
+ /**
22945
+ * Ids to read back WITH their vectors.
22946
+ *
22947
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22948
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22949
+ * caller depends on that promise. This one promises the opposite.
22950
+ *
22951
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22952
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22953
+ * a per-face cross-process KNN would be a network round trip inside the
22954
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22955
+ * it requires the index to hand the floats back. Without this method the only
22956
+ * way to keep a readable vector is a JSON column, which is the thing this
22957
+ * capability exists to delete.
22958
+ *
22959
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22960
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22961
+ */
22962
+ var VectorFetchInputSchema = object({
22963
+ index: string(),
22964
+ ids: array(string())
22965
+ });
22966
+ var VectorFetchResultSchema = object({ items: array(object({
22967
+ id: string(),
22968
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22969
+ vector: string(),
22970
+ metadata: VectorMetadataSchema
22971
+ })) });
22972
+ /**
22973
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22974
+ *
22975
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22976
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22977
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22978
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22979
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22980
+ * looked" for as long as anyone cared to read it.
22981
+ *
22982
+ * This is the primitive that question actually needs: a bounded page, ordered
22983
+ * by the backend's own row order, costing no distance computation at all.
22984
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22985
+ * the full-table read this capability was built to stop.
22986
+ */
22987
+ var VectorScanInputSchema = object({
22988
+ index: string(),
22989
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22990
+ cursor: number().int().nonnegative().default(0),
22991
+ limit: number().int().positive()
22992
+ });
22993
+ var VectorScanResultSchema = object({
22994
+ items: array(object({
22995
+ id: string(),
22996
+ metadata: VectorMetadataSchema
22997
+ })),
22998
+ /**
22999
+ * Where the next page starts, or `null` when the walk reached the end.
23000
+ *
23001
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
23002
+ * from a short page: a backend is free to return fewer rows than asked.
23003
+ */
23004
+ nextCursor: number().int().nonnegative().nullable()
23005
+ });
22683
23006
  var VectorStatsInputSchema = object({ index: string() });
22684
23007
  var VectorStatsResultSchema = object({
22685
23008
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22698,7 +23021,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22698
23021
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22699
23022
  kind: "mutation",
22700
23023
  auth: "admin"
22701
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
23024
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22702
23025
  kind: "mutation",
22703
23026
  auth: "admin"
22704
23027
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29325,6 +29648,9 @@ method(object({
29325
29648
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29326
29649
  kind: "query",
29327
29650
  auth: "admin"
29651
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29652
+ kind: "query",
29653
+ auth: "admin"
29328
29654
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29329
29655
  kind: "mutation",
29330
29656
  auth: "admin"
@@ -35959,6 +36285,12 @@ Object.freeze({
35959
36285
  addonId: null,
35960
36286
  access: "create"
35961
36287
  },
36288
+ "pipelineAnalytics.countRelocatableMedia": {
36289
+ capName: "pipeline-analytics",
36290
+ capScope: "device",
36291
+ addonId: null,
36292
+ access: "view"
36293
+ },
35962
36294
  "pipelineAnalytics.countUnstampedEventMedia": {
35963
36295
  capName: "pipeline-analytics",
35964
36296
  capScope: "device",
@@ -37123,6 +37455,12 @@ Object.freeze({
37123
37455
  addonId: null,
37124
37456
  access: "view"
37125
37457
  },
37458
+ "recording.getRelocateResidue": {
37459
+ capName: "recording",
37460
+ capScope: "system",
37461
+ addonId: null,
37462
+ access: "view"
37463
+ },
37126
37464
  "recording.getStorageMigrationMoveStatus": {
37127
37465
  capName: "recording",
37128
37466
  capScope: "system",
@@ -37669,12 +38007,30 @@ Object.freeze({
37669
38007
  addonId: null,
37670
38008
  access: "create"
37671
38009
  },
38010
+ "storageMigration.drain": {
38011
+ capName: "storage-migration",
38012
+ capScope: "system",
38013
+ addonId: null,
38014
+ access: "create"
38015
+ },
38016
+ "storageMigration.movers": {
38017
+ capName: "storage-migration",
38018
+ capScope: "system",
38019
+ addonId: null,
38020
+ access: "view"
38021
+ },
37672
38022
  "storageMigration.plan": {
37673
38023
  capName: "storage-migration",
37674
38024
  capScope: "system",
37675
38025
  addonId: null,
37676
38026
  access: "view"
37677
38027
  },
38028
+ "storageMigration.residue": {
38029
+ capName: "storage-migration",
38030
+ capScope: "system",
38031
+ addonId: null,
38032
+ access: "view"
38033
+ },
37678
38034
  "storageMigration.start": {
37679
38035
  capName: "storage-migration",
37680
38036
  capScope: "system",
@@ -38509,6 +38865,12 @@ Object.freeze({
38509
38865
  addonId: null,
38510
38866
  access: "delete"
38511
38867
  },
38868
+ "vectorStore.fetchByIds": {
38869
+ capName: "vector-store",
38870
+ capScope: "system",
38871
+ addonId: null,
38872
+ access: "view"
38873
+ },
38512
38874
  "vectorStore.getByIds": {
38513
38875
  capName: "vector-store",
38514
38876
  capScope: "system",
@@ -38521,6 +38883,12 @@ Object.freeze({
38521
38883
  addonId: null,
38522
38884
  access: "view"
38523
38885
  },
38886
+ "vectorStore.scan": {
38887
+ capName: "vector-store",
38888
+ capScope: "system",
38889
+ addonId: null,
38890
+ access: "view"
38891
+ },
38524
38892
  "vectorStore.stats": {
38525
38893
  capName: "vector-store",
38526
38894
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.43",
3
+ "version": "0.2.46",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",