@camstack/addon-export-alexa 1.2.49 → 1.2.52

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.
@@ -8171,6 +8171,21 @@ var RelocateJobSchema = object({
8171
8171
  bytesMoved: number().int(),
8172
8172
  /** Total files discovered up front; null while (or when) unknown. */
8173
8173
  filesTotal: number().int().nullable(),
8174
+ /**
8175
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8176
+ * made that nobody asked for, so it is reported where the operator reads the
8177
+ * job rather than only in a log line.
8178
+ *
8179
+ * A footage segment records its byte count in its own NAME, and the durable
8180
+ * hour row derives its aggregates from those names. A file that does not
8181
+ * match its name therefore makes the ledger's sums — and with them quota and
8182
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8183
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8184
+ *
8185
+ * Absent on lanes where the question has no meaning: a media blob's size is
8186
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8187
+ */
8188
+ rowsReconciled: number().int().nonnegative().optional(),
8174
8189
  startedAt: number(),
8175
8190
  finishedAt: number().nullable(),
8176
8191
  error: string().nullable()
@@ -8239,14 +8254,42 @@ var RelocateMediaInputSchema = object({
8239
8254
  /** Omitted = `move`, the pre-existing behaviour. */
8240
8255
  mode: MediaRelocateModeSchema.optional()
8241
8256
  });
8242
- /** How many rows still carry NO `locationId` — the population a repoint would
8243
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8244
- * value that permits a non-blocking `eventMedia` cutover. */
8245
- var UnstampedEventMediaCountSchema = object({
8246
- media: number().int().nonnegative(),
8247
- retrainFrames: number().int().nonnegative(),
8248
- total: number().int().nonnegative()
8257
+ /**
8258
+ * The unstamped population of ONE collection — split, because the gate and the
8259
+ * operator ask two different questions and only one of them has to be cheap.
8260
+ *
8261
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8262
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8263
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8264
+ * that matters — after a seal, when the population is empty.
8265
+ *
8266
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8267
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8268
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8269
+ * and useful answer: "there are some, and this read could not say how many"
8270
+ * still refuses the cutover, which is the whole job.
8271
+ */
8272
+ var UnstampedRowsSchema = object({
8273
+ present: boolean(),
8274
+ rows: number().int().nonnegative().nullable()
8249
8275
  });
8276
+ /**
8277
+ * How many rows still carry NO `locationId` — the population a repoint would
8278
+ * silently re-aim at a disk that does not hold their bytes.
8279
+ *
8280
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8281
+ * over. The gate opens on a measured absence and on nothing else; an unread
8282
+ * collection and an empty one are different facts, and this repo has already
8283
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8284
+ */
8285
+ var UnstampedEventMediaCountSchema = object({
8286
+ media: UnstampedRowsSchema,
8287
+ retrainFrames: UnstampedRowsSchema,
8288
+ /** True when EITHER collection holds one. The refusal reads this. */
8289
+ anyPresent: boolean(),
8290
+ /** Sum across both, or `null` when either lane could not be counted. */
8291
+ total: number().int().nonnegative().nullable()
8292
+ }).nullable();
8250
8293
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8251
8294
  /** The independently selectable logical storage classes — every class
8252
8295
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8332,13 +8375,53 @@ var StorageMigrationParticipantSchema = _enum([
8332
8375
  "recorder",
8333
8376
  "analytics"
8334
8377
  ]);
8378
+ /**
8379
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8380
+ *
8381
+ * The long half of a non-blocking migration is `draining`, and it is measured
8382
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8383
+ * existed the only place those numbers appeared was a Loki line, so an operator
8384
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8385
+ * afternoon.
8386
+ *
8387
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8388
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8389
+ * mover — which is the exact failure this is meant to end. The coordinator's
8390
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8391
+ * read `state`; folding the counters costs no extra read and makes the durable
8392
+ * record say afterwards how far a move actually got.
8393
+ *
8394
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8395
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8396
+ * cannot say M, and a 0 there would render as "100 % done".
8397
+ */
8398
+ var StorageMigrationMoveProgressSchema = object({
8399
+ filesMoved: number().int().nonnegative(),
8400
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8401
+ filesTotal: number().int().nonnegative().nullable(),
8402
+ bytesMoved: number().int().nonnegative(),
8403
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8404
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8405
+ * rows would be the same failure as one that silently skipped them. */
8406
+ rowsReconciled: number().int().nonnegative().optional(),
8407
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8408
+ * crash gets a new mover, and a rate computed from the migration's start
8409
+ * would silently average in the time nothing was running. */
8410
+ startedAt: number(),
8411
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8412
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8413
+ * subtract its own. */
8414
+ observedAt: number()
8415
+ });
8335
8416
  var StorageMigrationMoveSchema = object({
8336
8417
  storageClass: StorageMigrationClassSchema,
8337
8418
  fromLocationId: string(),
8338
8419
  toLocationId: string(),
8339
8420
  moverJobId: string().nullable(),
8340
8421
  state: RelocateJobStateSchema.nullable(),
8341
- error: string().nullable()
8422
+ error: string().nullable(),
8423
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8424
+ progress: StorageMigrationMoveProgressSchema.nullable()
8342
8425
  });
8343
8426
  var StorageMigrationJobSchema = object({
8344
8427
  jobId: string(),
@@ -8384,6 +8467,98 @@ var StorageMigrationPlanSchema = object({
8384
8467
  findings: array(StorageMigrationFindingSchema)
8385
8468
  });
8386
8469
  /**
8470
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8471
+ *
8472
+ * The coordinator's job record is the state of record for a migration, and its
8473
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8474
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8475
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8476
+ * way because no supported UI path existed. A mover armed like that has no job
8477
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8478
+ *
8479
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8480
+ * orchestrated it.
8481
+ */
8482
+ var StorageMigrationMoverSchema = object({
8483
+ lane: _enum(["footage", "media"]),
8484
+ job: RelocateJobSchema,
8485
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8486
+ * directly against the owning addon. */
8487
+ migrationJobId: string().nullable(),
8488
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8489
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8490
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8491
+ * rate made of two different clocks. */
8492
+ observedAt: number()
8493
+ });
8494
+ /**
8495
+ * What a SOURCE still holds for one storage class — the number that makes a
8496
+ * "drain remaining" action honest rather than hopeful.
8497
+ *
8498
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8499
+ * engine's own selection count for media), never from the resident index: a
8500
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8501
+ * never been told about (D295).
8502
+ *
8503
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8504
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8505
+ * because refusing on an unanswerable read would hide exactly the case an
8506
+ * operator needs to act on.
8507
+ */
8508
+ var StorageMigrationResidueSchema = object({
8509
+ storageClass: StorageMigrationClassSchema,
8510
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8511
+ * move from wherever they are rather than from one named source. */
8512
+ fromLocationId: string(),
8513
+ /** Where a drain would move it — the class's CURRENT default. */
8514
+ toLocationId: string(),
8515
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8516
+ items: number().int().nonnegative().nullable(),
8517
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8518
+ bytes: number().int().nonnegative().nullable()
8519
+ });
8520
+ /**
8521
+ * Run the DRAIN half and nothing else.
8522
+ *
8523
+ * A migration that reached `done` has already repointed, so `start` correctly
8524
+ * refuses its destination ("already the default") — there is nothing left to
8525
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8526
+ * or finish against a work list that was a tenth of the archive (D295), and
8527
+ * before this there was no supported way to run only that half: the only way
8528
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8529
+ *
8530
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8531
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8532
+ * re-repoint a class that is already migrated.
8533
+ */
8534
+ var StorageMigrationDrainInputSchema = object({
8535
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8536
+ * a class whose source is already empty is refused rather than started. */
8537
+ classes: array(StorageMigrationClassSchema).min(1),
8538
+ throttleMbps: number().min(1).max(1e3).optional()
8539
+ });
8540
+ /** What a footage source still holds, asked of the durable hour ledger. */
8541
+ var RelocateResidueInputSchema = object({
8542
+ fromLocationId: string().min(1),
8543
+ /** Narrow to one logical class; omit for every profile on the location. */
8544
+ footageClass: RelocateFootageClassSchema.optional()
8545
+ });
8546
+ /** `null` = the archive could not answer (no ledger on this node, or the
8547
+ * aggregate failed). Never conflated with an empty source. */
8548
+ var RelocateResidueSchema = object({
8549
+ segments: number().int().nonnegative(),
8550
+ bytes: number().int().nonnegative()
8551
+ }).nullable();
8552
+ /** How many rows a media pass would still act on against a given target — the
8553
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8554
+ * never disagree. `null` = the count could not be taken. */
8555
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8556
+ var RelocatableMediaCountInputSchema = object({
8557
+ toLocationId: string().min(1),
8558
+ /** Omitted = `move`. */
8559
+ mode: MediaRelocateModeSchema.optional()
8560
+ });
8561
+ /**
8387
8562
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8388
8563
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8389
8564
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8487,6 +8662,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8487
8662
  * two addons declaring the same `id` must agree on `cardinality` (validated
8488
8663
  * at kernel aggregation time, not here).
8489
8664
  */
8665
+ /**
8666
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8667
+ * actually reaches the bytes. It is the constraint that decides which
8668
+ * `storage-provider`s may back a location of that kind.
8669
+ *
8670
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8671
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8672
+ * post-analysis media roots). Only a provider that serves a genuine local
8673
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8674
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8675
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8676
+ * against a same-named local directory that is something else entirely.
8677
+ *
8678
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8679
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8680
+ * service never sees a path, so any provider can back it. `backups` is the
8681
+ * one kind that qualifies today.
8682
+ *
8683
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8684
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8685
+ * refused the configuration; the first write simply went somewhere wrong, and
8686
+ * a recording write that goes wrong surfaces as a silent black window rather
8687
+ * than an error (the read path does not `stat`). This turns that accident into
8688
+ * a declared, enforced, testable refusal.
8689
+ */
8690
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8490
8691
  var StorageLocationDeclarationSchema = object({
8491
8692
  /**
8492
8693
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8506,6 +8707,19 @@ var StorageLocationDeclarationSchema = object({
8506
8707
  */
8507
8708
  cardinality: _enum(["single", "multi"]),
8508
8709
  /**
8710
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8711
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8712
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8713
+ *
8714
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8715
+ * can only over-restrict (refuse a remote provider for a kind that might
8716
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8717
+ * permissive direction and is therefore never inferred — a repo guard
8718
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8719
+ * reached by omission.
8720
+ */
8721
+ access: StorageAccessSchema.optional(),
8722
+ /**
8509
8723
  * When set, the default instance for this location inherits its resolved
8510
8724
  * root from the named location's default instance. Useful for derivative
8511
8725
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18174,8 +18388,10 @@ var TrackSchema = object({
18174
18388
  lastSeen: number(),
18175
18389
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18176
18390
  positions: array(TrackPositionSchema).readonly(),
18177
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18178
- * saveThumbnails policy). */
18391
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18392
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18393
+ * the retired `saveThumbnails` used to gate this and the rolling
18394
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18179
18395
  snapshots: array(TrackSnapshotSchema).readonly(),
18180
18396
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18181
18397
  zonesVisited: array(string()).readonly(),
@@ -19035,7 +19251,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19035
19251
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19036
19252
  kind: "mutation",
19037
19253
  auth: "admin"
19038
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19254
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19255
+ kind: "query",
19256
+ auth: "admin"
19257
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19039
19258
  kind: "query",
19040
19259
  auth: "admin"
19041
19260
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -20994,6 +21213,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20994
21213
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20995
21214
  kind: "mutation",
20996
21215
  auth: "admin"
21216
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21217
+ kind: "mutation",
21218
+ auth: "admin"
20997
21219
  });
20998
21220
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20999
21221
  providerId: string().min(1),
@@ -21392,12 +21614,38 @@ response: record(string(), unknown()) }), object({
21392
21614
  *
21393
21615
  * ## Why this is a capability and not a helper
21394
21616
  *
21395
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21396
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21397
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21398
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21399
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21400
- * load 5,000 rows before ranking anything.
21617
+ * This capability was introduced with the claim that SIX stores in
21618
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21619
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21620
+ * claim was never true, and leaving it here made five stores look like pending
21621
+ * work when three of them have no vector at all. Counted column by column on
21622
+ * 2026-08-30, exactly THREE ever held one:
21623
+ *
21624
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21625
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21626
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21627
+ * face, migrated 2026-08-30 into its OWN index (see below).
21628
+ *
21629
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21630
+ * and `identities` store a name; the event store stores no derivative vector.
21631
+ * They are not migration candidates and never were.
21632
+ *
21633
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21634
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21635
+ * rows before ranking anything.
21636
+ *
21637
+ * ## One index per COMPARISON, never per encoder
21638
+ *
21639
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21640
+ * model, and they still get two indexes. An index is a set of things that are
21641
+ * ranked against each other and that live and die together, and these two are
21642
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21643
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21644
+ * forever and is the gallery every recognition ranks against. One index would
21645
+ * mean every gallery load and every reconcile carried a filter whose failure
21646
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21647
+ * person's only sample. The dimension they share is not a reason to share an
21648
+ * index; the question they answer is, and it differs.
21401
21649
  *
21402
21650
  * The fix is not a faster loop, it is a different backend — and the backend
21403
21651
  * should be replaceable without touching six callers. So: a singleton
@@ -21502,7 +21750,20 @@ var VectorQueryResultSchema = object({
21502
21750
  */
21503
21751
  scanned: number(),
21504
21752
  /** True when the backend could not consider every row that passed the filter. */
21505
- truncated: boolean()
21753
+ truncated: boolean(),
21754
+ /**
21755
+ * The `topK` the backend actually ran with.
21756
+ *
21757
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21758
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21759
+ * own log rather than in its answer. That is how an audit asking for 20,000
21760
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21761
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21762
+ * MUCH, in the return value, where the caller cannot fail to see it.
21763
+ *
21764
+ * Equals the requested `topK` whenever nothing was lowered.
21765
+ */
21766
+ effectiveTopK: number().int().positive()
21506
21767
  });
21507
21768
  var VectorDeleteInputSchema = object({
21508
21769
  index: string(),
@@ -21531,6 +21792,68 @@ var VectorGetResultSchema = object({ items: array(object({
21531
21792
  id: string(),
21532
21793
  metadata: VectorMetadataSchema
21533
21794
  })) });
21795
+ /**
21796
+ * Ids to read back WITH their vectors.
21797
+ *
21798
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21799
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21800
+ * caller depends on that promise. This one promises the opposite.
21801
+ *
21802
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21803
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21804
+ * a per-face cross-process KNN would be a network round trip inside the
21805
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21806
+ * it requires the index to hand the floats back. Without this method the only
21807
+ * way to keep a readable vector is a JSON column, which is the thing this
21808
+ * capability exists to delete.
21809
+ *
21810
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21811
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21812
+ */
21813
+ var VectorFetchInputSchema = object({
21814
+ index: string(),
21815
+ ids: array(string())
21816
+ });
21817
+ var VectorFetchResultSchema = object({ items: array(object({
21818
+ id: string(),
21819
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21820
+ vector: string(),
21821
+ metadata: VectorMetadataSchema
21822
+ })) });
21823
+ /**
21824
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21825
+ *
21826
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21827
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21828
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21829
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21830
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21831
+ * looked" for as long as anyone cared to read it.
21832
+ *
21833
+ * This is the primitive that question actually needs: a bounded page, ordered
21834
+ * by the backend's own row order, costing no distance computation at all.
21835
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21836
+ * the full-table read this capability was built to stop.
21837
+ */
21838
+ var VectorScanInputSchema = object({
21839
+ index: string(),
21840
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21841
+ cursor: number().int().nonnegative().default(0),
21842
+ limit: number().int().positive()
21843
+ });
21844
+ var VectorScanResultSchema = object({
21845
+ items: array(object({
21846
+ id: string(),
21847
+ metadata: VectorMetadataSchema
21848
+ })),
21849
+ /**
21850
+ * Where the next page starts, or `null` when the walk reached the end.
21851
+ *
21852
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21853
+ * from a short page: a backend is free to return fewer rows than asked.
21854
+ */
21855
+ nextCursor: number().int().nonnegative().nullable()
21856
+ });
21534
21857
  var VectorStatsInputSchema = object({ index: string() });
21535
21858
  var VectorStatsResultSchema = object({
21536
21859
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21549,7 +21872,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21549
21872
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21550
21873
  kind: "mutation",
21551
21874
  auth: "admin"
21552
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21875
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21553
21876
  kind: "mutation",
21554
21877
  auth: "admin"
21555
21878
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26453,6 +26776,9 @@ method(object({
26453
26776
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26454
26777
  kind: "query",
26455
26778
  auth: "admin"
26779
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26780
+ kind: "query",
26781
+ auth: "admin"
26456
26782
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26457
26783
  kind: "mutation",
26458
26784
  auth: "admin"
@@ -31418,6 +31744,12 @@ Object.freeze({
31418
31744
  addonId: null,
31419
31745
  access: "create"
31420
31746
  },
31747
+ "pipelineAnalytics.countRelocatableMedia": {
31748
+ capName: "pipeline-analytics",
31749
+ capScope: "device",
31750
+ addonId: null,
31751
+ access: "view"
31752
+ },
31421
31753
  "pipelineAnalytics.countUnstampedEventMedia": {
31422
31754
  capName: "pipeline-analytics",
31423
31755
  capScope: "device",
@@ -32582,6 +32914,12 @@ Object.freeze({
32582
32914
  addonId: null,
32583
32915
  access: "view"
32584
32916
  },
32917
+ "recording.getRelocateResidue": {
32918
+ capName: "recording",
32919
+ capScope: "system",
32920
+ addonId: null,
32921
+ access: "view"
32922
+ },
32585
32923
  "recording.getStorageMigrationMoveStatus": {
32586
32924
  capName: "recording",
32587
32925
  capScope: "system",
@@ -33128,12 +33466,30 @@ Object.freeze({
33128
33466
  addonId: null,
33129
33467
  access: "create"
33130
33468
  },
33469
+ "storageMigration.drain": {
33470
+ capName: "storage-migration",
33471
+ capScope: "system",
33472
+ addonId: null,
33473
+ access: "create"
33474
+ },
33475
+ "storageMigration.movers": {
33476
+ capName: "storage-migration",
33477
+ capScope: "system",
33478
+ addonId: null,
33479
+ access: "view"
33480
+ },
33131
33481
  "storageMigration.plan": {
33132
33482
  capName: "storage-migration",
33133
33483
  capScope: "system",
33134
33484
  addonId: null,
33135
33485
  access: "view"
33136
33486
  },
33487
+ "storageMigration.residue": {
33488
+ capName: "storage-migration",
33489
+ capScope: "system",
33490
+ addonId: null,
33491
+ access: "view"
33492
+ },
33137
33493
  "storageMigration.start": {
33138
33494
  capName: "storage-migration",
33139
33495
  capScope: "system",
@@ -33968,6 +34324,12 @@ Object.freeze({
33968
34324
  addonId: null,
33969
34325
  access: "delete"
33970
34326
  },
34327
+ "vectorStore.fetchByIds": {
34328
+ capName: "vector-store",
34329
+ capScope: "system",
34330
+ addonId: null,
34331
+ access: "view"
34332
+ },
33971
34333
  "vectorStore.getByIds": {
33972
34334
  capName: "vector-store",
33973
34335
  capScope: "system",
@@ -33980,6 +34342,12 @@ Object.freeze({
33980
34342
  addonId: null,
33981
34343
  access: "view"
33982
34344
  },
34345
+ "vectorStore.scan": {
34346
+ capName: "vector-store",
34347
+ capScope: "system",
34348
+ addonId: null,
34349
+ access: "view"
34350
+ },
33983
34351
  "vectorStore.stats": {
33984
34352
  capName: "vector-store",
33985
34353
  capScope: "system",
@@ -8144,6 +8144,21 @@ var RelocateJobSchema = object({
8144
8144
  bytesMoved: number().int(),
8145
8145
  /** Total files discovered up front; null while (or when) unknown. */
8146
8146
  filesTotal: number().int().nullable(),
8147
+ /**
8148
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8149
+ * made that nobody asked for, so it is reported where the operator reads the
8150
+ * job rather than only in a log line.
8151
+ *
8152
+ * A footage segment records its byte count in its own NAME, and the durable
8153
+ * hour row derives its aggregates from those names. A file that does not
8154
+ * match its name therefore makes the ledger's sums — and with them quota and
8155
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8156
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8157
+ *
8158
+ * Absent on lanes where the question has no meaning: a media blob's size is
8159
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8160
+ */
8161
+ rowsReconciled: number().int().nonnegative().optional(),
8147
8162
  startedAt: number(),
8148
8163
  finishedAt: number().nullable(),
8149
8164
  error: string().nullable()
@@ -8212,14 +8227,42 @@ var RelocateMediaInputSchema = object({
8212
8227
  /** Omitted = `move`, the pre-existing behaviour. */
8213
8228
  mode: MediaRelocateModeSchema.optional()
8214
8229
  });
8215
- /** How many rows still carry NO `locationId` — the population a repoint would
8216
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8217
- * value that permits a non-blocking `eventMedia` cutover. */
8218
- var UnstampedEventMediaCountSchema = object({
8219
- media: number().int().nonnegative(),
8220
- retrainFrames: number().int().nonnegative(),
8221
- total: number().int().nonnegative()
8230
+ /**
8231
+ * The unstamped population of ONE collection — split, because the gate and the
8232
+ * operator ask two different questions and only one of them has to be cheap.
8233
+ *
8234
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8235
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8236
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8237
+ * that matters — after a seal, when the population is empty.
8238
+ *
8239
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8240
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8241
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8242
+ * and useful answer: "there are some, and this read could not say how many"
8243
+ * still refuses the cutover, which is the whole job.
8244
+ */
8245
+ var UnstampedRowsSchema = object({
8246
+ present: boolean(),
8247
+ rows: number().int().nonnegative().nullable()
8222
8248
  });
8249
+ /**
8250
+ * How many rows still carry NO `locationId` — the population a repoint would
8251
+ * silently re-aim at a disk that does not hold their bytes.
8252
+ *
8253
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8254
+ * over. The gate opens on a measured absence and on nothing else; an unread
8255
+ * collection and an empty one are different facts, and this repo has already
8256
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8257
+ */
8258
+ var UnstampedEventMediaCountSchema = object({
8259
+ media: UnstampedRowsSchema,
8260
+ retrainFrames: UnstampedRowsSchema,
8261
+ /** True when EITHER collection holds one. The refusal reads this. */
8262
+ anyPresent: boolean(),
8263
+ /** Sum across both, or `null` when either lane could not be counted. */
8264
+ total: number().int().nonnegative().nullable()
8265
+ }).nullable();
8223
8266
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8224
8267
  /** The independently selectable logical storage classes — every class
8225
8268
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8305,13 +8348,53 @@ var StorageMigrationParticipantSchema = _enum([
8305
8348
  "recorder",
8306
8349
  "analytics"
8307
8350
  ]);
8351
+ /**
8352
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8353
+ *
8354
+ * The long half of a non-blocking migration is `draining`, and it is measured
8355
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8356
+ * existed the only place those numbers appeared was a Loki line, so an operator
8357
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8358
+ * afternoon.
8359
+ *
8360
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8361
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8362
+ * mover — which is the exact failure this is meant to end. The coordinator's
8363
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8364
+ * read `state`; folding the counters costs no extra read and makes the durable
8365
+ * record say afterwards how far a move actually got.
8366
+ *
8367
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8368
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8369
+ * cannot say M, and a 0 there would render as "100 % done".
8370
+ */
8371
+ var StorageMigrationMoveProgressSchema = object({
8372
+ filesMoved: number().int().nonnegative(),
8373
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8374
+ filesTotal: number().int().nonnegative().nullable(),
8375
+ bytesMoved: number().int().nonnegative(),
8376
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8377
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8378
+ * rows would be the same failure as one that silently skipped them. */
8379
+ rowsReconciled: number().int().nonnegative().optional(),
8380
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8381
+ * crash gets a new mover, and a rate computed from the migration's start
8382
+ * would silently average in the time nothing was running. */
8383
+ startedAt: number(),
8384
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8385
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8386
+ * subtract its own. */
8387
+ observedAt: number()
8388
+ });
8308
8389
  var StorageMigrationMoveSchema = object({
8309
8390
  storageClass: StorageMigrationClassSchema,
8310
8391
  fromLocationId: string(),
8311
8392
  toLocationId: string(),
8312
8393
  moverJobId: string().nullable(),
8313
8394
  state: RelocateJobStateSchema.nullable(),
8314
- error: string().nullable()
8395
+ error: string().nullable(),
8396
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8397
+ progress: StorageMigrationMoveProgressSchema.nullable()
8315
8398
  });
8316
8399
  var StorageMigrationJobSchema = object({
8317
8400
  jobId: string(),
@@ -8357,6 +8440,98 @@ var StorageMigrationPlanSchema = object({
8357
8440
  findings: array(StorageMigrationFindingSchema)
8358
8441
  });
8359
8442
  /**
8443
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8444
+ *
8445
+ * The coordinator's job record is the state of record for a migration, and its
8446
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8447
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8448
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8449
+ * way because no supported UI path existed. A mover armed like that has no job
8450
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8451
+ *
8452
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8453
+ * orchestrated it.
8454
+ */
8455
+ var StorageMigrationMoverSchema = object({
8456
+ lane: _enum(["footage", "media"]),
8457
+ job: RelocateJobSchema,
8458
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8459
+ * directly against the owning addon. */
8460
+ migrationJobId: string().nullable(),
8461
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8462
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8463
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8464
+ * rate made of two different clocks. */
8465
+ observedAt: number()
8466
+ });
8467
+ /**
8468
+ * What a SOURCE still holds for one storage class — the number that makes a
8469
+ * "drain remaining" action honest rather than hopeful.
8470
+ *
8471
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8472
+ * engine's own selection count for media), never from the resident index: a
8473
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8474
+ * never been told about (D295).
8475
+ *
8476
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8477
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8478
+ * because refusing on an unanswerable read would hide exactly the case an
8479
+ * operator needs to act on.
8480
+ */
8481
+ var StorageMigrationResidueSchema = object({
8482
+ storageClass: StorageMigrationClassSchema,
8483
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8484
+ * move from wherever they are rather than from one named source. */
8485
+ fromLocationId: string(),
8486
+ /** Where a drain would move it — the class's CURRENT default. */
8487
+ toLocationId: string(),
8488
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8489
+ items: number().int().nonnegative().nullable(),
8490
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8491
+ bytes: number().int().nonnegative().nullable()
8492
+ });
8493
+ /**
8494
+ * Run the DRAIN half and nothing else.
8495
+ *
8496
+ * A migration that reached `done` has already repointed, so `start` correctly
8497
+ * refuses its destination ("already the default") — there is nothing left to
8498
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8499
+ * or finish against a work list that was a tenth of the archive (D295), and
8500
+ * before this there was no supported way to run only that half: the only way
8501
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8502
+ *
8503
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8504
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8505
+ * re-repoint a class that is already migrated.
8506
+ */
8507
+ var StorageMigrationDrainInputSchema = object({
8508
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8509
+ * a class whose source is already empty is refused rather than started. */
8510
+ classes: array(StorageMigrationClassSchema).min(1),
8511
+ throttleMbps: number().min(1).max(1e3).optional()
8512
+ });
8513
+ /** What a footage source still holds, asked of the durable hour ledger. */
8514
+ var RelocateResidueInputSchema = object({
8515
+ fromLocationId: string().min(1),
8516
+ /** Narrow to one logical class; omit for every profile on the location. */
8517
+ footageClass: RelocateFootageClassSchema.optional()
8518
+ });
8519
+ /** `null` = the archive could not answer (no ledger on this node, or the
8520
+ * aggregate failed). Never conflated with an empty source. */
8521
+ var RelocateResidueSchema = object({
8522
+ segments: number().int().nonnegative(),
8523
+ bytes: number().int().nonnegative()
8524
+ }).nullable();
8525
+ /** How many rows a media pass would still act on against a given target — the
8526
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8527
+ * never disagree. `null` = the count could not be taken. */
8528
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8529
+ var RelocatableMediaCountInputSchema = object({
8530
+ toLocationId: string().min(1),
8531
+ /** Omitted = `move`. */
8532
+ mode: MediaRelocateModeSchema.optional()
8533
+ });
8534
+ /**
8360
8535
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8361
8536
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8362
8537
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8460,6 +8635,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8460
8635
  * two addons declaring the same `id` must agree on `cardinality` (validated
8461
8636
  * at kernel aggregation time, not here).
8462
8637
  */
8638
+ /**
8639
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8640
+ * actually reaches the bytes. It is the constraint that decides which
8641
+ * `storage-provider`s may back a location of that kind.
8642
+ *
8643
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8644
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8645
+ * post-analysis media roots). Only a provider that serves a genuine local
8646
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8647
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8648
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8649
+ * against a same-named local directory that is something else entirely.
8650
+ *
8651
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8652
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8653
+ * service never sees a path, so any provider can back it. `backups` is the
8654
+ * one kind that qualifies today.
8655
+ *
8656
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8657
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8658
+ * refused the configuration; the first write simply went somewhere wrong, and
8659
+ * a recording write that goes wrong surfaces as a silent black window rather
8660
+ * than an error (the read path does not `stat`). This turns that accident into
8661
+ * a declared, enforced, testable refusal.
8662
+ */
8663
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8463
8664
  var StorageLocationDeclarationSchema = object({
8464
8665
  /**
8465
8666
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8479,6 +8680,19 @@ var StorageLocationDeclarationSchema = object({
8479
8680
  */
8480
8681
  cardinality: _enum(["single", "multi"]),
8481
8682
  /**
8683
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8684
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8685
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8686
+ *
8687
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8688
+ * can only over-restrict (refuse a remote provider for a kind that might
8689
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8690
+ * permissive direction and is therefore never inferred — a repo guard
8691
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8692
+ * reached by omission.
8693
+ */
8694
+ access: StorageAccessSchema.optional(),
8695
+ /**
8482
8696
  * When set, the default instance for this location inherits its resolved
8483
8697
  * root from the named location's default instance. Useful for derivative
8484
8698
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18147,8 +18361,10 @@ var TrackSchema = object({
18147
18361
  lastSeen: number(),
18148
18362
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18149
18363
  positions: array(TrackPositionSchema).readonly(),
18150
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18151
- * saveThumbnails policy). */
18364
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18365
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18366
+ * the retired `saveThumbnails` used to gate this and the rolling
18367
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18152
18368
  snapshots: array(TrackSnapshotSchema).readonly(),
18153
18369
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18154
18370
  zonesVisited: array(string()).readonly(),
@@ -19008,7 +19224,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19008
19224
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19009
19225
  kind: "mutation",
19010
19226
  auth: "admin"
19011
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19227
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19228
+ kind: "query",
19229
+ auth: "admin"
19230
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19012
19231
  kind: "query",
19013
19232
  auth: "admin"
19014
19233
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -20967,6 +21186,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20967
21186
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20968
21187
  kind: "mutation",
20969
21188
  auth: "admin"
21189
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21190
+ kind: "mutation",
21191
+ auth: "admin"
20970
21192
  });
20971
21193
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20972
21194
  providerId: string().min(1),
@@ -21365,12 +21587,38 @@ response: record(string(), unknown()) }), object({
21365
21587
  *
21366
21588
  * ## Why this is a capability and not a helper
21367
21589
  *
21368
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21369
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21370
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21371
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21372
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21373
- * load 5,000 rows before ranking anything.
21590
+ * This capability was introduced with the claim that SIX stores in
21591
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21592
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21593
+ * claim was never true, and leaving it here made five stores look like pending
21594
+ * work when three of them have no vector at all. Counted column by column on
21595
+ * 2026-08-30, exactly THREE ever held one:
21596
+ *
21597
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21598
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21599
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21600
+ * face, migrated 2026-08-30 into its OWN index (see below).
21601
+ *
21602
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21603
+ * and `identities` store a name; the event store stores no derivative vector.
21604
+ * They are not migration candidates and never were.
21605
+ *
21606
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21607
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21608
+ * rows before ranking anything.
21609
+ *
21610
+ * ## One index per COMPARISON, never per encoder
21611
+ *
21612
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21613
+ * model, and they still get two indexes. An index is a set of things that are
21614
+ * ranked against each other and that live and die together, and these two are
21615
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21616
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21617
+ * forever and is the gallery every recognition ranks against. One index would
21618
+ * mean every gallery load and every reconcile carried a filter whose failure
21619
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21620
+ * person's only sample. The dimension they share is not a reason to share an
21621
+ * index; the question they answer is, and it differs.
21374
21622
  *
21375
21623
  * The fix is not a faster loop, it is a different backend — and the backend
21376
21624
  * should be replaceable without touching six callers. So: a singleton
@@ -21475,7 +21723,20 @@ var VectorQueryResultSchema = object({
21475
21723
  */
21476
21724
  scanned: number(),
21477
21725
  /** True when the backend could not consider every row that passed the filter. */
21478
- truncated: boolean()
21726
+ truncated: boolean(),
21727
+ /**
21728
+ * The `topK` the backend actually ran with.
21729
+ *
21730
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21731
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21732
+ * own log rather than in its answer. That is how an audit asking for 20,000
21733
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21734
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21735
+ * MUCH, in the return value, where the caller cannot fail to see it.
21736
+ *
21737
+ * Equals the requested `topK` whenever nothing was lowered.
21738
+ */
21739
+ effectiveTopK: number().int().positive()
21479
21740
  });
21480
21741
  var VectorDeleteInputSchema = object({
21481
21742
  index: string(),
@@ -21504,6 +21765,68 @@ var VectorGetResultSchema = object({ items: array(object({
21504
21765
  id: string(),
21505
21766
  metadata: VectorMetadataSchema
21506
21767
  })) });
21768
+ /**
21769
+ * Ids to read back WITH their vectors.
21770
+ *
21771
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21772
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21773
+ * caller depends on that promise. This one promises the opposite.
21774
+ *
21775
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21776
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21777
+ * a per-face cross-process KNN would be a network round trip inside the
21778
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21779
+ * it requires the index to hand the floats back. Without this method the only
21780
+ * way to keep a readable vector is a JSON column, which is the thing this
21781
+ * capability exists to delete.
21782
+ *
21783
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21784
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21785
+ */
21786
+ var VectorFetchInputSchema = object({
21787
+ index: string(),
21788
+ ids: array(string())
21789
+ });
21790
+ var VectorFetchResultSchema = object({ items: array(object({
21791
+ id: string(),
21792
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21793
+ vector: string(),
21794
+ metadata: VectorMetadataSchema
21795
+ })) });
21796
+ /**
21797
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21798
+ *
21799
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21800
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21801
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21802
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21803
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21804
+ * looked" for as long as anyone cared to read it.
21805
+ *
21806
+ * This is the primitive that question actually needs: a bounded page, ordered
21807
+ * by the backend's own row order, costing no distance computation at all.
21808
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21809
+ * the full-table read this capability was built to stop.
21810
+ */
21811
+ var VectorScanInputSchema = object({
21812
+ index: string(),
21813
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21814
+ cursor: number().int().nonnegative().default(0),
21815
+ limit: number().int().positive()
21816
+ });
21817
+ var VectorScanResultSchema = object({
21818
+ items: array(object({
21819
+ id: string(),
21820
+ metadata: VectorMetadataSchema
21821
+ })),
21822
+ /**
21823
+ * Where the next page starts, or `null` when the walk reached the end.
21824
+ *
21825
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21826
+ * from a short page: a backend is free to return fewer rows than asked.
21827
+ */
21828
+ nextCursor: number().int().nonnegative().nullable()
21829
+ });
21507
21830
  var VectorStatsInputSchema = object({ index: string() });
21508
21831
  var VectorStatsResultSchema = object({
21509
21832
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21522,7 +21845,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21522
21845
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21523
21846
  kind: "mutation",
21524
21847
  auth: "admin"
21525
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21848
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21526
21849
  kind: "mutation",
21527
21850
  auth: "admin"
21528
21851
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26426,6 +26749,9 @@ method(object({
26426
26749
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26427
26750
  kind: "query",
26428
26751
  auth: "admin"
26752
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26753
+ kind: "query",
26754
+ auth: "admin"
26429
26755
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26430
26756
  kind: "mutation",
26431
26757
  auth: "admin"
@@ -31391,6 +31717,12 @@ Object.freeze({
31391
31717
  addonId: null,
31392
31718
  access: "create"
31393
31719
  },
31720
+ "pipelineAnalytics.countRelocatableMedia": {
31721
+ capName: "pipeline-analytics",
31722
+ capScope: "device",
31723
+ addonId: null,
31724
+ access: "view"
31725
+ },
31394
31726
  "pipelineAnalytics.countUnstampedEventMedia": {
31395
31727
  capName: "pipeline-analytics",
31396
31728
  capScope: "device",
@@ -32555,6 +32887,12 @@ Object.freeze({
32555
32887
  addonId: null,
32556
32888
  access: "view"
32557
32889
  },
32890
+ "recording.getRelocateResidue": {
32891
+ capName: "recording",
32892
+ capScope: "system",
32893
+ addonId: null,
32894
+ access: "view"
32895
+ },
32558
32896
  "recording.getStorageMigrationMoveStatus": {
32559
32897
  capName: "recording",
32560
32898
  capScope: "system",
@@ -33101,12 +33439,30 @@ Object.freeze({
33101
33439
  addonId: null,
33102
33440
  access: "create"
33103
33441
  },
33442
+ "storageMigration.drain": {
33443
+ capName: "storage-migration",
33444
+ capScope: "system",
33445
+ addonId: null,
33446
+ access: "create"
33447
+ },
33448
+ "storageMigration.movers": {
33449
+ capName: "storage-migration",
33450
+ capScope: "system",
33451
+ addonId: null,
33452
+ access: "view"
33453
+ },
33104
33454
  "storageMigration.plan": {
33105
33455
  capName: "storage-migration",
33106
33456
  capScope: "system",
33107
33457
  addonId: null,
33108
33458
  access: "view"
33109
33459
  },
33460
+ "storageMigration.residue": {
33461
+ capName: "storage-migration",
33462
+ capScope: "system",
33463
+ addonId: null,
33464
+ access: "view"
33465
+ },
33110
33466
  "storageMigration.start": {
33111
33467
  capName: "storage-migration",
33112
33468
  capScope: "system",
@@ -33941,6 +34297,12 @@ Object.freeze({
33941
34297
  addonId: null,
33942
34298
  access: "delete"
33943
34299
  },
34300
+ "vectorStore.fetchByIds": {
34301
+ capName: "vector-store",
34302
+ capScope: "system",
34303
+ addonId: null,
34304
+ access: "view"
34305
+ },
33944
34306
  "vectorStore.getByIds": {
33945
34307
  capName: "vector-store",
33946
34308
  capScope: "system",
@@ -33953,6 +34315,12 @@ Object.freeze({
33953
34315
  addonId: null,
33954
34316
  access: "view"
33955
34317
  },
34318
+ "vectorStore.scan": {
34319
+ capName: "vector-store",
34320
+ capScope: "system",
34321
+ addonId: null,
34322
+ access: "view"
34323
+ },
33956
34324
  "vectorStore.stats": {
33957
34325
  capName: "vector-store",
33958
34326
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-alexa",
3
- "version": "1.2.49",
3
+ "version": "1.2.52",
4
4
  "description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
5
5
  "keywords": [
6
6
  "camstack",