@camstack/addon-provider-reolink 1.2.66 → 1.2.69

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
@@ -8293,6 +8293,21 @@ var RelocateJobSchema = object({
8293
8293
  bytesMoved: number().int(),
8294
8294
  /** Total files discovered up front; null while (or when) unknown. */
8295
8295
  filesTotal: number().int().nullable(),
8296
+ /**
8297
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8298
+ * made that nobody asked for, so it is reported where the operator reads the
8299
+ * job rather than only in a log line.
8300
+ *
8301
+ * A footage segment records its byte count in its own NAME, and the durable
8302
+ * hour row derives its aggregates from those names. A file that does not
8303
+ * match its name therefore makes the ledger's sums — and with them quota and
8304
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8305
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8306
+ *
8307
+ * Absent on lanes where the question has no meaning: a media blob's size is
8308
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8309
+ */
8310
+ rowsReconciled: number().int().nonnegative().optional(),
8296
8311
  startedAt: number(),
8297
8312
  finishedAt: number().nullable(),
8298
8313
  error: string().nullable()
@@ -8361,14 +8376,42 @@ var RelocateMediaInputSchema = object({
8361
8376
  /** Omitted = `move`, the pre-existing behaviour. */
8362
8377
  mode: MediaRelocateModeSchema.optional()
8363
8378
  });
8364
- /** How many rows still carry NO `locationId` — the population a repoint would
8365
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8366
- * value that permits a non-blocking `eventMedia` cutover. */
8367
- var UnstampedEventMediaCountSchema = object({
8368
- media: number().int().nonnegative(),
8369
- retrainFrames: number().int().nonnegative(),
8370
- total: number().int().nonnegative()
8379
+ /**
8380
+ * The unstamped population of ONE collection split, because the gate and the
8381
+ * operator ask two different questions and only one of them has to be cheap.
8382
+ *
8383
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8384
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8385
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8386
+ * that matters — after a seal, when the population is empty.
8387
+ *
8388
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8389
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8390
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8391
+ * and useful answer: "there are some, and this read could not say how many"
8392
+ * still refuses the cutover, which is the whole job.
8393
+ */
8394
+ var UnstampedRowsSchema = object({
8395
+ present: boolean(),
8396
+ rows: number().int().nonnegative().nullable()
8371
8397
  });
8398
+ /**
8399
+ * How many rows still carry NO `locationId` — the population a repoint would
8400
+ * silently re-aim at a disk that does not hold their bytes.
8401
+ *
8402
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8403
+ * over. The gate opens on a measured absence and on nothing else; an unread
8404
+ * collection and an empty one are different facts, and this repo has already
8405
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8406
+ */
8407
+ var UnstampedEventMediaCountSchema = object({
8408
+ media: UnstampedRowsSchema,
8409
+ retrainFrames: UnstampedRowsSchema,
8410
+ /** True when EITHER collection holds one. The refusal reads this. */
8411
+ anyPresent: boolean(),
8412
+ /** Sum across both, or `null` when either lane could not be counted. */
8413
+ total: number().int().nonnegative().nullable()
8414
+ }).nullable();
8372
8415
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8373
8416
  /** The independently selectable logical storage classes — every class
8374
8417
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8454,13 +8497,53 @@ var StorageMigrationParticipantSchema = _enum([
8454
8497
  "recorder",
8455
8498
  "analytics"
8456
8499
  ]);
8500
+ /**
8501
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8502
+ *
8503
+ * The long half of a non-blocking migration is `draining`, and it is measured
8504
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8505
+ * existed the only place those numbers appeared was a Loki line, so an operator
8506
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8507
+ * afternoon.
8508
+ *
8509
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8510
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8511
+ * mover — which is the exact failure this is meant to end. The coordinator's
8512
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8513
+ * read `state`; folding the counters costs no extra read and makes the durable
8514
+ * record say afterwards how far a move actually got.
8515
+ *
8516
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8517
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8518
+ * cannot say M, and a 0 there would render as "100 % done".
8519
+ */
8520
+ var StorageMigrationMoveProgressSchema = object({
8521
+ filesMoved: number().int().nonnegative(),
8522
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8523
+ filesTotal: number().int().nonnegative().nullable(),
8524
+ bytesMoved: number().int().nonnegative(),
8525
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8526
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8527
+ * rows would be the same failure as one that silently skipped them. */
8528
+ rowsReconciled: number().int().nonnegative().optional(),
8529
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8530
+ * crash gets a new mover, and a rate computed from the migration's start
8531
+ * would silently average in the time nothing was running. */
8532
+ startedAt: number(),
8533
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8534
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8535
+ * subtract its own. */
8536
+ observedAt: number()
8537
+ });
8457
8538
  var StorageMigrationMoveSchema = object({
8458
8539
  storageClass: StorageMigrationClassSchema,
8459
8540
  fromLocationId: string(),
8460
8541
  toLocationId: string(),
8461
8542
  moverJobId: string().nullable(),
8462
8543
  state: RelocateJobStateSchema.nullable(),
8463
- error: string().nullable()
8544
+ error: string().nullable(),
8545
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8546
+ progress: StorageMigrationMoveProgressSchema.nullable()
8464
8547
  });
8465
8548
  var StorageMigrationJobSchema = object({
8466
8549
  jobId: string(),
@@ -8506,6 +8589,98 @@ var StorageMigrationPlanSchema = object({
8506
8589
  findings: array(StorageMigrationFindingSchema)
8507
8590
  });
8508
8591
  /**
8592
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8593
+ *
8594
+ * The coordinator's job record is the state of record for a migration, and its
8595
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8596
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8597
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8598
+ * way because no supported UI path existed. A mover armed like that has no job
8599
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8600
+ *
8601
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8602
+ * orchestrated it.
8603
+ */
8604
+ var StorageMigrationMoverSchema = object({
8605
+ lane: _enum(["footage", "media"]),
8606
+ job: RelocateJobSchema,
8607
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8608
+ * directly against the owning addon. */
8609
+ migrationJobId: string().nullable(),
8610
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8611
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8612
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8613
+ * rate made of two different clocks. */
8614
+ observedAt: number()
8615
+ });
8616
+ /**
8617
+ * What a SOURCE still holds for one storage class — the number that makes a
8618
+ * "drain remaining" action honest rather than hopeful.
8619
+ *
8620
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8621
+ * engine's own selection count for media), never from the resident index: a
8622
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8623
+ * never been told about (D295).
8624
+ *
8625
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8626
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8627
+ * because refusing on an unanswerable read would hide exactly the case an
8628
+ * operator needs to act on.
8629
+ */
8630
+ var StorageMigrationResidueSchema = object({
8631
+ storageClass: StorageMigrationClassSchema,
8632
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8633
+ * move from wherever they are rather than from one named source. */
8634
+ fromLocationId: string(),
8635
+ /** Where a drain would move it — the class's CURRENT default. */
8636
+ toLocationId: string(),
8637
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8638
+ items: number().int().nonnegative().nullable(),
8639
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8640
+ bytes: number().int().nonnegative().nullable()
8641
+ });
8642
+ /**
8643
+ * Run the DRAIN half and nothing else.
8644
+ *
8645
+ * A migration that reached `done` has already repointed, so `start` correctly
8646
+ * refuses its destination ("already the default") — there is nothing left to
8647
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8648
+ * or finish against a work list that was a tenth of the archive (D295), and
8649
+ * before this there was no supported way to run only that half: the only way
8650
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8651
+ *
8652
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8653
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8654
+ * re-repoint a class that is already migrated.
8655
+ */
8656
+ var StorageMigrationDrainInputSchema = object({
8657
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8658
+ * a class whose source is already empty is refused rather than started. */
8659
+ classes: array(StorageMigrationClassSchema).min(1),
8660
+ throttleMbps: number().min(1).max(1e3).optional()
8661
+ });
8662
+ /** What a footage source still holds, asked of the durable hour ledger. */
8663
+ var RelocateResidueInputSchema = object({
8664
+ fromLocationId: string().min(1),
8665
+ /** Narrow to one logical class; omit for every profile on the location. */
8666
+ footageClass: RelocateFootageClassSchema.optional()
8667
+ });
8668
+ /** `null` = the archive could not answer (no ledger on this node, or the
8669
+ * aggregate failed). Never conflated with an empty source. */
8670
+ var RelocateResidueSchema = object({
8671
+ segments: number().int().nonnegative(),
8672
+ bytes: number().int().nonnegative()
8673
+ }).nullable();
8674
+ /** How many rows a media pass would still act on against a given target — the
8675
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8676
+ * never disagree. `null` = the count could not be taken. */
8677
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8678
+ var RelocatableMediaCountInputSchema = object({
8679
+ toLocationId: string().min(1),
8680
+ /** Omitted = `move`. */
8681
+ mode: MediaRelocateModeSchema.optional()
8682
+ });
8683
+ /**
8509
8684
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8510
8685
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8511
8686
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8609,6 +8784,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8609
8784
  * two addons declaring the same `id` must agree on `cardinality` (validated
8610
8785
  * at kernel aggregation time, not here).
8611
8786
  */
8787
+ /**
8788
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8789
+ * actually reaches the bytes. It is the constraint that decides which
8790
+ * `storage-provider`s may back a location of that kind.
8791
+ *
8792
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8793
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8794
+ * post-analysis media roots). Only a provider that serves a genuine local
8795
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8796
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8797
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8798
+ * against a same-named local directory that is something else entirely.
8799
+ *
8800
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8801
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8802
+ * service never sees a path, so any provider can back it. `backups` is the
8803
+ * one kind that qualifies today.
8804
+ *
8805
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8806
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8807
+ * refused the configuration; the first write simply went somewhere wrong, and
8808
+ * a recording write that goes wrong surfaces as a silent black window rather
8809
+ * than an error (the read path does not `stat`). This turns that accident into
8810
+ * a declared, enforced, testable refusal.
8811
+ */
8812
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8612
8813
  var StorageLocationDeclarationSchema = object({
8613
8814
  /**
8614
8815
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8628,6 +8829,19 @@ var StorageLocationDeclarationSchema = object({
8628
8829
  */
8629
8830
  cardinality: _enum(["single", "multi"]),
8630
8831
  /**
8832
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8833
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8834
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8835
+ *
8836
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8837
+ * can only over-restrict (refuse a remote provider for a kind that might
8838
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8839
+ * permissive direction and is therefore never inferred — a repo guard
8840
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8841
+ * reached by omission.
8842
+ */
8843
+ access: StorageAccessSchema.optional(),
8844
+ /**
8631
8845
  * When set, the default instance for this location inherits its resolved
8632
8846
  * root from the named location's default instance. Useful for derivative
8633
8847
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18828,8 +19042,10 @@ var TrackSchema = object({
18828
19042
  lastSeen: number(),
18829
19043
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18830
19044
  positions: array(TrackPositionSchema).readonly(),
18831
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18832
- * saveThumbnails policy). */
19045
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19046
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19047
+ * the retired `saveThumbnails` used to gate this and the rolling
19048
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18833
19049
  snapshots: array(TrackSnapshotSchema).readonly(),
18834
19050
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18835
19051
  zonesVisited: array(string()).readonly(),
@@ -19689,7 +19905,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19689
19905
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19690
19906
  kind: "mutation",
19691
19907
  auth: "admin"
19692
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19908
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19909
+ kind: "query",
19910
+ auth: "admin"
19911
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19693
19912
  kind: "query",
19694
19913
  auth: "admin"
19695
19914
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21752,6 +21971,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21752
21971
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21753
21972
  kind: "mutation",
21754
21973
  auth: "admin"
21974
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21975
+ kind: "mutation",
21976
+ auth: "admin"
21755
21977
  });
21756
21978
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21757
21979
  providerId: string().min(1),
@@ -22150,12 +22372,38 @@ response: record(string(), unknown()) }), object({
22150
22372
  *
22151
22373
  * ## Why this is a capability and not a helper
22152
22374
  *
22153
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22154
- * plate, vehicle, identity, and the event store's derivativesand every one of
22155
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22156
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22157
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22158
- * load 5,000 rows before ranking anything.
22375
+ * This capability was introduced with the claim that SIX stores in
22376
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22377
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22378
+ * claim was never true, and leaving it here made five stores look like pending
22379
+ * work when three of them have no vector at all. Counted column by column on
22380
+ * 2026-08-30, exactly THREE ever held one:
22381
+ *
22382
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22383
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22384
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22385
+ * face, migrated 2026-08-30 into its OWN index (see below).
22386
+ *
22387
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22388
+ * and `identities` store a name; the event store stores no derivative vector.
22389
+ * They are not migration candidates and never were.
22390
+ *
22391
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22392
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22393
+ * rows before ranking anything.
22394
+ *
22395
+ * ## One index per COMPARISON, never per encoder
22396
+ *
22397
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22398
+ * model, and they still get two indexes. An index is a set of things that are
22399
+ * ranked against each other and that live and die together, and these two are
22400
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22401
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22402
+ * forever and is the gallery every recognition ranks against. One index would
22403
+ * mean every gallery load and every reconcile carried a filter whose failure
22404
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22405
+ * person's only sample. The dimension they share is not a reason to share an
22406
+ * index; the question they answer is, and it differs.
22159
22407
  *
22160
22408
  * The fix is not a faster loop, it is a different backend — and the backend
22161
22409
  * should be replaceable without touching six callers. So: a singleton
@@ -22260,7 +22508,20 @@ var VectorQueryResultSchema = object({
22260
22508
  */
22261
22509
  scanned: number(),
22262
22510
  /** True when the backend could not consider every row that passed the filter. */
22263
- truncated: boolean()
22511
+ truncated: boolean(),
22512
+ /**
22513
+ * The `topK` the backend actually ran with.
22514
+ *
22515
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22516
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22517
+ * own log rather than in its answer. That is how an audit asking for 20,000
22518
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22519
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22520
+ * MUCH, in the return value, where the caller cannot fail to see it.
22521
+ *
22522
+ * Equals the requested `topK` whenever nothing was lowered.
22523
+ */
22524
+ effectiveTopK: number().int().positive()
22264
22525
  });
22265
22526
  var VectorDeleteInputSchema = object({
22266
22527
  index: string(),
@@ -22289,6 +22550,68 @@ var VectorGetResultSchema = object({ items: array(object({
22289
22550
  id: string(),
22290
22551
  metadata: VectorMetadataSchema
22291
22552
  })) });
22553
+ /**
22554
+ * Ids to read back WITH their vectors.
22555
+ *
22556
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22557
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22558
+ * caller depends on that promise. This one promises the opposite.
22559
+ *
22560
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22561
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22562
+ * a per-face cross-process KNN would be a network round trip inside the
22563
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22564
+ * it requires the index to hand the floats back. Without this method the only
22565
+ * way to keep a readable vector is a JSON column, which is the thing this
22566
+ * capability exists to delete.
22567
+ *
22568
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22569
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22570
+ */
22571
+ var VectorFetchInputSchema = object({
22572
+ index: string(),
22573
+ ids: array(string())
22574
+ });
22575
+ var VectorFetchResultSchema = object({ items: array(object({
22576
+ id: string(),
22577
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22578
+ vector: string(),
22579
+ metadata: VectorMetadataSchema
22580
+ })) });
22581
+ /**
22582
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22583
+ *
22584
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22585
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22586
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22587
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22588
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22589
+ * looked" for as long as anyone cared to read it.
22590
+ *
22591
+ * This is the primitive that question actually needs: a bounded page, ordered
22592
+ * by the backend's own row order, costing no distance computation at all.
22593
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22594
+ * the full-table read this capability was built to stop.
22595
+ */
22596
+ var VectorScanInputSchema = object({
22597
+ index: string(),
22598
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22599
+ cursor: number().int().nonnegative().default(0),
22600
+ limit: number().int().positive()
22601
+ });
22602
+ var VectorScanResultSchema = object({
22603
+ items: array(object({
22604
+ id: string(),
22605
+ metadata: VectorMetadataSchema
22606
+ })),
22607
+ /**
22608
+ * Where the next page starts, or `null` when the walk reached the end.
22609
+ *
22610
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22611
+ * from a short page: a backend is free to return fewer rows than asked.
22612
+ */
22613
+ nextCursor: number().int().nonnegative().nullable()
22614
+ });
22292
22615
  var VectorStatsInputSchema = object({ index: string() });
22293
22616
  var VectorStatsResultSchema = object({
22294
22617
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22307,7 +22630,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22307
22630
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22308
22631
  kind: "mutation",
22309
22632
  auth: "admin"
22310
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22633
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22311
22634
  kind: "mutation",
22312
22635
  auth: "admin"
22313
22636
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29013,6 +29336,9 @@ method(object({
29013
29336
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29014
29337
  kind: "query",
29015
29338
  auth: "admin"
29339
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29340
+ kind: "query",
29341
+ auth: "admin"
29016
29342
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29017
29343
  kind: "mutation",
29018
29344
  auth: "admin"
@@ -36012,6 +36338,12 @@ Object.freeze({
36012
36338
  addonId: null,
36013
36339
  access: "create"
36014
36340
  },
36341
+ "pipelineAnalytics.countRelocatableMedia": {
36342
+ capName: "pipeline-analytics",
36343
+ capScope: "device",
36344
+ addonId: null,
36345
+ access: "view"
36346
+ },
36015
36347
  "pipelineAnalytics.countUnstampedEventMedia": {
36016
36348
  capName: "pipeline-analytics",
36017
36349
  capScope: "device",
@@ -37176,6 +37508,12 @@ Object.freeze({
37176
37508
  addonId: null,
37177
37509
  access: "view"
37178
37510
  },
37511
+ "recording.getRelocateResidue": {
37512
+ capName: "recording",
37513
+ capScope: "system",
37514
+ addonId: null,
37515
+ access: "view"
37516
+ },
37179
37517
  "recording.getStorageMigrationMoveStatus": {
37180
37518
  capName: "recording",
37181
37519
  capScope: "system",
@@ -37722,12 +38060,30 @@ Object.freeze({
37722
38060
  addonId: null,
37723
38061
  access: "create"
37724
38062
  },
38063
+ "storageMigration.drain": {
38064
+ capName: "storage-migration",
38065
+ capScope: "system",
38066
+ addonId: null,
38067
+ access: "create"
38068
+ },
38069
+ "storageMigration.movers": {
38070
+ capName: "storage-migration",
38071
+ capScope: "system",
38072
+ addonId: null,
38073
+ access: "view"
38074
+ },
37725
38075
  "storageMigration.plan": {
37726
38076
  capName: "storage-migration",
37727
38077
  capScope: "system",
37728
38078
  addonId: null,
37729
38079
  access: "view"
37730
38080
  },
38081
+ "storageMigration.residue": {
38082
+ capName: "storage-migration",
38083
+ capScope: "system",
38084
+ addonId: null,
38085
+ access: "view"
38086
+ },
37731
38087
  "storageMigration.start": {
37732
38088
  capName: "storage-migration",
37733
38089
  capScope: "system",
@@ -38562,6 +38918,12 @@ Object.freeze({
38562
38918
  addonId: null,
38563
38919
  access: "delete"
38564
38920
  },
38921
+ "vectorStore.fetchByIds": {
38922
+ capName: "vector-store",
38923
+ capScope: "system",
38924
+ addonId: null,
38925
+ access: "view"
38926
+ },
38565
38927
  "vectorStore.getByIds": {
38566
38928
  capName: "vector-store",
38567
38929
  capScope: "system",
@@ -38574,6 +38936,12 @@ Object.freeze({
38574
38936
  addonId: null,
38575
38937
  access: "view"
38576
38938
  },
38939
+ "vectorStore.scan": {
38940
+ capName: "vector-store",
38941
+ capScope: "system",
38942
+ addonId: null,
38943
+ access: "view"
38944
+ },
38577
38945
  "vectorStore.stats": {
38578
38946
  capName: "vector-store",
38579
38947
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -8288,6 +8288,21 @@ var RelocateJobSchema = object({
8288
8288
  bytesMoved: number().int(),
8289
8289
  /** Total files discovered up front; null while (or when) unknown. */
8290
8290
  filesTotal: number().int().nullable(),
8291
+ /**
8292
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8293
+ * made that nobody asked for, so it is reported where the operator reads the
8294
+ * job rather than only in a log line.
8295
+ *
8296
+ * A footage segment records its byte count in its own NAME, and the durable
8297
+ * hour row derives its aggregates from those names. A file that does not
8298
+ * match its name therefore makes the ledger's sums — and with them quota and
8299
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8300
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8301
+ *
8302
+ * Absent on lanes where the question has no meaning: a media blob's size is
8303
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8304
+ */
8305
+ rowsReconciled: number().int().nonnegative().optional(),
8291
8306
  startedAt: number(),
8292
8307
  finishedAt: number().nullable(),
8293
8308
  error: string().nullable()
@@ -8356,14 +8371,42 @@ var RelocateMediaInputSchema = object({
8356
8371
  /** Omitted = `move`, the pre-existing behaviour. */
8357
8372
  mode: MediaRelocateModeSchema.optional()
8358
8373
  });
8359
- /** How many rows still carry NO `locationId` — the population a repoint would
8360
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8361
- * value that permits a non-blocking `eventMedia` cutover. */
8362
- var UnstampedEventMediaCountSchema = object({
8363
- media: number().int().nonnegative(),
8364
- retrainFrames: number().int().nonnegative(),
8365
- total: number().int().nonnegative()
8374
+ /**
8375
+ * The unstamped population of ONE collection split, because the gate and the
8376
+ * operator ask two different questions and only one of them has to be cheap.
8377
+ *
8378
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8379
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8380
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8381
+ * that matters — after a seal, when the population is empty.
8382
+ *
8383
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8384
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8385
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8386
+ * and useful answer: "there are some, and this read could not say how many"
8387
+ * still refuses the cutover, which is the whole job.
8388
+ */
8389
+ var UnstampedRowsSchema = object({
8390
+ present: boolean(),
8391
+ rows: number().int().nonnegative().nullable()
8366
8392
  });
8393
+ /**
8394
+ * How many rows still carry NO `locationId` — the population a repoint would
8395
+ * silently re-aim at a disk that does not hold their bytes.
8396
+ *
8397
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8398
+ * over. The gate opens on a measured absence and on nothing else; an unread
8399
+ * collection and an empty one are different facts, and this repo has already
8400
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8401
+ */
8402
+ var UnstampedEventMediaCountSchema = object({
8403
+ media: UnstampedRowsSchema,
8404
+ retrainFrames: UnstampedRowsSchema,
8405
+ /** True when EITHER collection holds one. The refusal reads this. */
8406
+ anyPresent: boolean(),
8407
+ /** Sum across both, or `null` when either lane could not be counted. */
8408
+ total: number().int().nonnegative().nullable()
8409
+ }).nullable();
8367
8410
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8368
8411
  /** The independently selectable logical storage classes — every class
8369
8412
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8449,13 +8492,53 @@ var StorageMigrationParticipantSchema = _enum([
8449
8492
  "recorder",
8450
8493
  "analytics"
8451
8494
  ]);
8495
+ /**
8496
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8497
+ *
8498
+ * The long half of a non-blocking migration is `draining`, and it is measured
8499
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8500
+ * existed the only place those numbers appeared was a Loki line, so an operator
8501
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8502
+ * afternoon.
8503
+ *
8504
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8505
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8506
+ * mover — which is the exact failure this is meant to end. The coordinator's
8507
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8508
+ * read `state`; folding the counters costs no extra read and makes the durable
8509
+ * record say afterwards how far a move actually got.
8510
+ *
8511
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8512
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8513
+ * cannot say M, and a 0 there would render as "100 % done".
8514
+ */
8515
+ var StorageMigrationMoveProgressSchema = object({
8516
+ filesMoved: number().int().nonnegative(),
8517
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8518
+ filesTotal: number().int().nonnegative().nullable(),
8519
+ bytesMoved: number().int().nonnegative(),
8520
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8521
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8522
+ * rows would be the same failure as one that silently skipped them. */
8523
+ rowsReconciled: number().int().nonnegative().optional(),
8524
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8525
+ * crash gets a new mover, and a rate computed from the migration's start
8526
+ * would silently average in the time nothing was running. */
8527
+ startedAt: number(),
8528
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8529
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8530
+ * subtract its own. */
8531
+ observedAt: number()
8532
+ });
8452
8533
  var StorageMigrationMoveSchema = object({
8453
8534
  storageClass: StorageMigrationClassSchema,
8454
8535
  fromLocationId: string(),
8455
8536
  toLocationId: string(),
8456
8537
  moverJobId: string().nullable(),
8457
8538
  state: RelocateJobStateSchema.nullable(),
8458
- error: string().nullable()
8539
+ error: string().nullable(),
8540
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8541
+ progress: StorageMigrationMoveProgressSchema.nullable()
8459
8542
  });
8460
8543
  var StorageMigrationJobSchema = object({
8461
8544
  jobId: string(),
@@ -8501,6 +8584,98 @@ var StorageMigrationPlanSchema = object({
8501
8584
  findings: array(StorageMigrationFindingSchema)
8502
8585
  });
8503
8586
  /**
8587
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8588
+ *
8589
+ * The coordinator's job record is the state of record for a migration, and its
8590
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8591
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8592
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8593
+ * way because no supported UI path existed. A mover armed like that has no job
8594
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8595
+ *
8596
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8597
+ * orchestrated it.
8598
+ */
8599
+ var StorageMigrationMoverSchema = object({
8600
+ lane: _enum(["footage", "media"]),
8601
+ job: RelocateJobSchema,
8602
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8603
+ * directly against the owning addon. */
8604
+ migrationJobId: string().nullable(),
8605
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8606
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8607
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8608
+ * rate made of two different clocks. */
8609
+ observedAt: number()
8610
+ });
8611
+ /**
8612
+ * What a SOURCE still holds for one storage class — the number that makes a
8613
+ * "drain remaining" action honest rather than hopeful.
8614
+ *
8615
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8616
+ * engine's own selection count for media), never from the resident index: a
8617
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8618
+ * never been told about (D295).
8619
+ *
8620
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8621
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8622
+ * because refusing on an unanswerable read would hide exactly the case an
8623
+ * operator needs to act on.
8624
+ */
8625
+ var StorageMigrationResidueSchema = object({
8626
+ storageClass: StorageMigrationClassSchema,
8627
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8628
+ * move from wherever they are rather than from one named source. */
8629
+ fromLocationId: string(),
8630
+ /** Where a drain would move it — the class's CURRENT default. */
8631
+ toLocationId: string(),
8632
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8633
+ items: number().int().nonnegative().nullable(),
8634
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8635
+ bytes: number().int().nonnegative().nullable()
8636
+ });
8637
+ /**
8638
+ * Run the DRAIN half and nothing else.
8639
+ *
8640
+ * A migration that reached `done` has already repointed, so `start` correctly
8641
+ * refuses its destination ("already the default") — there is nothing left to
8642
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8643
+ * or finish against a work list that was a tenth of the archive (D295), and
8644
+ * before this there was no supported way to run only that half: the only way
8645
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8646
+ *
8647
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8648
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8649
+ * re-repoint a class that is already migrated.
8650
+ */
8651
+ var StorageMigrationDrainInputSchema = object({
8652
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8653
+ * a class whose source is already empty is refused rather than started. */
8654
+ classes: array(StorageMigrationClassSchema).min(1),
8655
+ throttleMbps: number().min(1).max(1e3).optional()
8656
+ });
8657
+ /** What a footage source still holds, asked of the durable hour ledger. */
8658
+ var RelocateResidueInputSchema = object({
8659
+ fromLocationId: string().min(1),
8660
+ /** Narrow to one logical class; omit for every profile on the location. */
8661
+ footageClass: RelocateFootageClassSchema.optional()
8662
+ });
8663
+ /** `null` = the archive could not answer (no ledger on this node, or the
8664
+ * aggregate failed). Never conflated with an empty source. */
8665
+ var RelocateResidueSchema = object({
8666
+ segments: number().int().nonnegative(),
8667
+ bytes: number().int().nonnegative()
8668
+ }).nullable();
8669
+ /** How many rows a media pass would still act on against a given target — the
8670
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8671
+ * never disagree. `null` = the count could not be taken. */
8672
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8673
+ var RelocatableMediaCountInputSchema = object({
8674
+ toLocationId: string().min(1),
8675
+ /** Omitted = `move`. */
8676
+ mode: MediaRelocateModeSchema.optional()
8677
+ });
8678
+ /**
8504
8679
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8505
8680
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8506
8681
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8604,6 +8779,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8604
8779
  * two addons declaring the same `id` must agree on `cardinality` (validated
8605
8780
  * at kernel aggregation time, not here).
8606
8781
  */
8782
+ /**
8783
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8784
+ * actually reaches the bytes. It is the constraint that decides which
8785
+ * `storage-provider`s may back a location of that kind.
8786
+ *
8787
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8788
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8789
+ * post-analysis media roots). Only a provider that serves a genuine local
8790
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8791
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8792
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8793
+ * against a same-named local directory that is something else entirely.
8794
+ *
8795
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8796
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8797
+ * service never sees a path, so any provider can back it. `backups` is the
8798
+ * one kind that qualifies today.
8799
+ *
8800
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8801
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8802
+ * refused the configuration; the first write simply went somewhere wrong, and
8803
+ * a recording write that goes wrong surfaces as a silent black window rather
8804
+ * than an error (the read path does not `stat`). This turns that accident into
8805
+ * a declared, enforced, testable refusal.
8806
+ */
8807
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8607
8808
  var StorageLocationDeclarationSchema = object({
8608
8809
  /**
8609
8810
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8623,6 +8824,19 @@ var StorageLocationDeclarationSchema = object({
8623
8824
  */
8624
8825
  cardinality: _enum(["single", "multi"]),
8625
8826
  /**
8827
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8828
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8829
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8830
+ *
8831
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8832
+ * can only over-restrict (refuse a remote provider for a kind that might
8833
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8834
+ * permissive direction and is therefore never inferred — a repo guard
8835
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8836
+ * reached by omission.
8837
+ */
8838
+ access: StorageAccessSchema.optional(),
8839
+ /**
8626
8840
  * When set, the default instance for this location inherits its resolved
8627
8841
  * root from the named location's default instance. Useful for derivative
8628
8842
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18823,8 +19037,10 @@ var TrackSchema = object({
18823
19037
  lastSeen: number(),
18824
19038
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18825
19039
  positions: array(TrackPositionSchema).readonly(),
18826
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18827
- * saveThumbnails policy). */
19040
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19041
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19042
+ * the retired `saveThumbnails` used to gate this and the rolling
19043
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18828
19044
  snapshots: array(TrackSnapshotSchema).readonly(),
18829
19045
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18830
19046
  zonesVisited: array(string()).readonly(),
@@ -19684,7 +19900,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19684
19900
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19685
19901
  kind: "mutation",
19686
19902
  auth: "admin"
19687
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19903
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19904
+ kind: "query",
19905
+ auth: "admin"
19906
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19688
19907
  kind: "query",
19689
19908
  auth: "admin"
19690
19909
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21747,6 +21966,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21747
21966
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21748
21967
  kind: "mutation",
21749
21968
  auth: "admin"
21969
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21970
+ kind: "mutation",
21971
+ auth: "admin"
21750
21972
  });
21751
21973
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21752
21974
  providerId: string().min(1),
@@ -22145,12 +22367,38 @@ response: record(string(), unknown()) }), object({
22145
22367
  *
22146
22368
  * ## Why this is a capability and not a helper
22147
22369
  *
22148
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22149
- * plate, vehicle, identity, and the event store's derivativesand every one of
22150
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22151
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22152
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22153
- * load 5,000 rows before ranking anything.
22370
+ * This capability was introduced with the claim that SIX stores in
22371
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22372
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22373
+ * claim was never true, and leaving it here made five stores look like pending
22374
+ * work when three of them have no vector at all. Counted column by column on
22375
+ * 2026-08-30, exactly THREE ever held one:
22376
+ *
22377
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22378
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22379
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22380
+ * face, migrated 2026-08-30 into its OWN index (see below).
22381
+ *
22382
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22383
+ * and `identities` store a name; the event store stores no derivative vector.
22384
+ * They are not migration candidates and never were.
22385
+ *
22386
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22387
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22388
+ * rows before ranking anything.
22389
+ *
22390
+ * ## One index per COMPARISON, never per encoder
22391
+ *
22392
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22393
+ * model, and they still get two indexes. An index is a set of things that are
22394
+ * ranked against each other and that live and die together, and these two are
22395
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22396
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22397
+ * forever and is the gallery every recognition ranks against. One index would
22398
+ * mean every gallery load and every reconcile carried a filter whose failure
22399
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22400
+ * person's only sample. The dimension they share is not a reason to share an
22401
+ * index; the question they answer is, and it differs.
22154
22402
  *
22155
22403
  * The fix is not a faster loop, it is a different backend — and the backend
22156
22404
  * should be replaceable without touching six callers. So: a singleton
@@ -22255,7 +22503,20 @@ var VectorQueryResultSchema = object({
22255
22503
  */
22256
22504
  scanned: number(),
22257
22505
  /** True when the backend could not consider every row that passed the filter. */
22258
- truncated: boolean()
22506
+ truncated: boolean(),
22507
+ /**
22508
+ * The `topK` the backend actually ran with.
22509
+ *
22510
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22511
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22512
+ * own log rather than in its answer. That is how an audit asking for 20,000
22513
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22514
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22515
+ * MUCH, in the return value, where the caller cannot fail to see it.
22516
+ *
22517
+ * Equals the requested `topK` whenever nothing was lowered.
22518
+ */
22519
+ effectiveTopK: number().int().positive()
22259
22520
  });
22260
22521
  var VectorDeleteInputSchema = object({
22261
22522
  index: string(),
@@ -22284,6 +22545,68 @@ var VectorGetResultSchema = object({ items: array(object({
22284
22545
  id: string(),
22285
22546
  metadata: VectorMetadataSchema
22286
22547
  })) });
22548
+ /**
22549
+ * Ids to read back WITH their vectors.
22550
+ *
22551
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22552
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22553
+ * caller depends on that promise. This one promises the opposite.
22554
+ *
22555
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22556
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22557
+ * a per-face cross-process KNN would be a network round trip inside the
22558
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22559
+ * it requires the index to hand the floats back. Without this method the only
22560
+ * way to keep a readable vector is a JSON column, which is the thing this
22561
+ * capability exists to delete.
22562
+ *
22563
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22564
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22565
+ */
22566
+ var VectorFetchInputSchema = object({
22567
+ index: string(),
22568
+ ids: array(string())
22569
+ });
22570
+ var VectorFetchResultSchema = object({ items: array(object({
22571
+ id: string(),
22572
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22573
+ vector: string(),
22574
+ metadata: VectorMetadataSchema
22575
+ })) });
22576
+ /**
22577
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22578
+ *
22579
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22580
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22581
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22582
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22583
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22584
+ * looked" for as long as anyone cared to read it.
22585
+ *
22586
+ * This is the primitive that question actually needs: a bounded page, ordered
22587
+ * by the backend's own row order, costing no distance computation at all.
22588
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22589
+ * the full-table read this capability was built to stop.
22590
+ */
22591
+ var VectorScanInputSchema = object({
22592
+ index: string(),
22593
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22594
+ cursor: number().int().nonnegative().default(0),
22595
+ limit: number().int().positive()
22596
+ });
22597
+ var VectorScanResultSchema = object({
22598
+ items: array(object({
22599
+ id: string(),
22600
+ metadata: VectorMetadataSchema
22601
+ })),
22602
+ /**
22603
+ * Where the next page starts, or `null` when the walk reached the end.
22604
+ *
22605
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22606
+ * from a short page: a backend is free to return fewer rows than asked.
22607
+ */
22608
+ nextCursor: number().int().nonnegative().nullable()
22609
+ });
22287
22610
  var VectorStatsInputSchema = object({ index: string() });
22288
22611
  var VectorStatsResultSchema = object({
22289
22612
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22302,7 +22625,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22302
22625
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22303
22626
  kind: "mutation",
22304
22627
  auth: "admin"
22305
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22628
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22306
22629
  kind: "mutation",
22307
22630
  auth: "admin"
22308
22631
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29008,6 +29331,9 @@ method(object({
29008
29331
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29009
29332
  kind: "query",
29010
29333
  auth: "admin"
29334
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29335
+ kind: "query",
29336
+ auth: "admin"
29011
29337
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29012
29338
  kind: "mutation",
29013
29339
  auth: "admin"
@@ -36007,6 +36333,12 @@ Object.freeze({
36007
36333
  addonId: null,
36008
36334
  access: "create"
36009
36335
  },
36336
+ "pipelineAnalytics.countRelocatableMedia": {
36337
+ capName: "pipeline-analytics",
36338
+ capScope: "device",
36339
+ addonId: null,
36340
+ access: "view"
36341
+ },
36010
36342
  "pipelineAnalytics.countUnstampedEventMedia": {
36011
36343
  capName: "pipeline-analytics",
36012
36344
  capScope: "device",
@@ -37171,6 +37503,12 @@ Object.freeze({
37171
37503
  addonId: null,
37172
37504
  access: "view"
37173
37505
  },
37506
+ "recording.getRelocateResidue": {
37507
+ capName: "recording",
37508
+ capScope: "system",
37509
+ addonId: null,
37510
+ access: "view"
37511
+ },
37174
37512
  "recording.getStorageMigrationMoveStatus": {
37175
37513
  capName: "recording",
37176
37514
  capScope: "system",
@@ -37717,12 +38055,30 @@ Object.freeze({
37717
38055
  addonId: null,
37718
38056
  access: "create"
37719
38057
  },
38058
+ "storageMigration.drain": {
38059
+ capName: "storage-migration",
38060
+ capScope: "system",
38061
+ addonId: null,
38062
+ access: "create"
38063
+ },
38064
+ "storageMigration.movers": {
38065
+ capName: "storage-migration",
38066
+ capScope: "system",
38067
+ addonId: null,
38068
+ access: "view"
38069
+ },
37720
38070
  "storageMigration.plan": {
37721
38071
  capName: "storage-migration",
37722
38072
  capScope: "system",
37723
38073
  addonId: null,
37724
38074
  access: "view"
37725
38075
  },
38076
+ "storageMigration.residue": {
38077
+ capName: "storage-migration",
38078
+ capScope: "system",
38079
+ addonId: null,
38080
+ access: "view"
38081
+ },
37726
38082
  "storageMigration.start": {
37727
38083
  capName: "storage-migration",
37728
38084
  capScope: "system",
@@ -38557,6 +38913,12 @@ Object.freeze({
38557
38913
  addonId: null,
38558
38914
  access: "delete"
38559
38915
  },
38916
+ "vectorStore.fetchByIds": {
38917
+ capName: "vector-store",
38918
+ capScope: "system",
38919
+ addonId: null,
38920
+ access: "view"
38921
+ },
38560
38922
  "vectorStore.getByIds": {
38561
38923
  capName: "vector-store",
38562
38924
  capScope: "system",
@@ -38569,6 +38931,12 @@ Object.freeze({
38569
38931
  addonId: null,
38570
38932
  access: "view"
38571
38933
  },
38934
+ "vectorStore.scan": {
38935
+ capName: "vector-store",
38936
+ capScope: "system",
38937
+ addonId: null,
38938
+ access: "view"
38939
+ },
38572
38940
  "vectorStore.stats": {
38573
38941
  capName: "vector-store",
38574
38942
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.66",
3
+ "version": "1.2.69",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",