@camstack/addon-export-google 0.1.7 → 0.1.10

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.
@@ -8196,6 +8196,21 @@ var RelocateJobSchema = object({
8196
8196
  bytesMoved: number().int(),
8197
8197
  /** Total files discovered up front; null while (or when) unknown. */
8198
8198
  filesTotal: number().int().nullable(),
8199
+ /**
8200
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8201
+ * made that nobody asked for, so it is reported where the operator reads the
8202
+ * job rather than only in a log line.
8203
+ *
8204
+ * A footage segment records its byte count in its own NAME, and the durable
8205
+ * hour row derives its aggregates from those names. A file that does not
8206
+ * match its name therefore makes the ledger's sums — and with them quota and
8207
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8208
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8209
+ *
8210
+ * Absent on lanes where the question has no meaning: a media blob's size is
8211
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8212
+ */
8213
+ rowsReconciled: number().int().nonnegative().optional(),
8199
8214
  startedAt: number(),
8200
8215
  finishedAt: number().nullable(),
8201
8216
  error: string().nullable()
@@ -8264,14 +8279,42 @@ var RelocateMediaInputSchema = object({
8264
8279
  /** Omitted = `move`, the pre-existing behaviour. */
8265
8280
  mode: MediaRelocateModeSchema.optional()
8266
8281
  });
8267
- /** How many rows still carry NO `locationId` — the population a repoint would
8268
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8269
- * value that permits a non-blocking `eventMedia` cutover. */
8270
- var UnstampedEventMediaCountSchema = object({
8271
- media: number().int().nonnegative(),
8272
- retrainFrames: number().int().nonnegative(),
8273
- total: number().int().nonnegative()
8282
+ /**
8283
+ * The unstamped population of ONE collection split, because the gate and the
8284
+ * operator ask two different questions and only one of them has to be cheap.
8285
+ *
8286
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8287
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8288
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8289
+ * that matters — after a seal, when the population is empty.
8290
+ *
8291
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8292
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8293
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8294
+ * and useful answer: "there are some, and this read could not say how many"
8295
+ * still refuses the cutover, which is the whole job.
8296
+ */
8297
+ var UnstampedRowsSchema = object({
8298
+ present: boolean(),
8299
+ rows: number().int().nonnegative().nullable()
8274
8300
  });
8301
+ /**
8302
+ * How many rows still carry NO `locationId` — the population a repoint would
8303
+ * silently re-aim at a disk that does not hold their bytes.
8304
+ *
8305
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8306
+ * over. The gate opens on a measured absence and on nothing else; an unread
8307
+ * collection and an empty one are different facts, and this repo has already
8308
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8309
+ */
8310
+ var UnstampedEventMediaCountSchema = object({
8311
+ media: UnstampedRowsSchema,
8312
+ retrainFrames: UnstampedRowsSchema,
8313
+ /** True when EITHER collection holds one. The refusal reads this. */
8314
+ anyPresent: boolean(),
8315
+ /** Sum across both, or `null` when either lane could not be counted. */
8316
+ total: number().int().nonnegative().nullable()
8317
+ }).nullable();
8275
8318
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8276
8319
  /** The independently selectable logical storage classes — every class
8277
8320
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8357,13 +8400,53 @@ var StorageMigrationParticipantSchema = _enum([
8357
8400
  "recorder",
8358
8401
  "analytics"
8359
8402
  ]);
8403
+ /**
8404
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8405
+ *
8406
+ * The long half of a non-blocking migration is `draining`, and it is measured
8407
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8408
+ * existed the only place those numbers appeared was a Loki line, so an operator
8409
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8410
+ * afternoon.
8411
+ *
8412
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8413
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8414
+ * mover — which is the exact failure this is meant to end. The coordinator's
8415
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8416
+ * read `state`; folding the counters costs no extra read and makes the durable
8417
+ * record say afterwards how far a move actually got.
8418
+ *
8419
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8420
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8421
+ * cannot say M, and a 0 there would render as "100 % done".
8422
+ */
8423
+ var StorageMigrationMoveProgressSchema = object({
8424
+ filesMoved: number().int().nonnegative(),
8425
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8426
+ filesTotal: number().int().nonnegative().nullable(),
8427
+ bytesMoved: number().int().nonnegative(),
8428
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8429
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8430
+ * rows would be the same failure as one that silently skipped them. */
8431
+ rowsReconciled: number().int().nonnegative().optional(),
8432
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8433
+ * crash gets a new mover, and a rate computed from the migration's start
8434
+ * would silently average in the time nothing was running. */
8435
+ startedAt: number(),
8436
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8437
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8438
+ * subtract its own. */
8439
+ observedAt: number()
8440
+ });
8360
8441
  var StorageMigrationMoveSchema = object({
8361
8442
  storageClass: StorageMigrationClassSchema,
8362
8443
  fromLocationId: string(),
8363
8444
  toLocationId: string(),
8364
8445
  moverJobId: string().nullable(),
8365
8446
  state: RelocateJobStateSchema.nullable(),
8366
- error: string().nullable()
8447
+ error: string().nullable(),
8448
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8449
+ progress: StorageMigrationMoveProgressSchema.nullable()
8367
8450
  });
8368
8451
  var StorageMigrationJobSchema = object({
8369
8452
  jobId: string(),
@@ -8409,6 +8492,98 @@ var StorageMigrationPlanSchema = object({
8409
8492
  findings: array(StorageMigrationFindingSchema)
8410
8493
  });
8411
8494
  /**
8495
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8496
+ *
8497
+ * The coordinator's job record is the state of record for a migration, and its
8498
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8499
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8500
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8501
+ * way because no supported UI path existed. A mover armed like that has no job
8502
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8503
+ *
8504
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8505
+ * orchestrated it.
8506
+ */
8507
+ var StorageMigrationMoverSchema = object({
8508
+ lane: _enum(["footage", "media"]),
8509
+ job: RelocateJobSchema,
8510
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8511
+ * directly against the owning addon. */
8512
+ migrationJobId: string().nullable(),
8513
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8514
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8515
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8516
+ * rate made of two different clocks. */
8517
+ observedAt: number()
8518
+ });
8519
+ /**
8520
+ * What a SOURCE still holds for one storage class — the number that makes a
8521
+ * "drain remaining" action honest rather than hopeful.
8522
+ *
8523
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8524
+ * engine's own selection count for media), never from the resident index: a
8525
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8526
+ * never been told about (D295).
8527
+ *
8528
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8529
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8530
+ * because refusing on an unanswerable read would hide exactly the case an
8531
+ * operator needs to act on.
8532
+ */
8533
+ var StorageMigrationResidueSchema = object({
8534
+ storageClass: StorageMigrationClassSchema,
8535
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8536
+ * move from wherever they are rather than from one named source. */
8537
+ fromLocationId: string(),
8538
+ /** Where a drain would move it — the class's CURRENT default. */
8539
+ toLocationId: string(),
8540
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8541
+ items: number().int().nonnegative().nullable(),
8542
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8543
+ bytes: number().int().nonnegative().nullable()
8544
+ });
8545
+ /**
8546
+ * Run the DRAIN half and nothing else.
8547
+ *
8548
+ * A migration that reached `done` has already repointed, so `start` correctly
8549
+ * refuses its destination ("already the default") — there is nothing left to
8550
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8551
+ * or finish against a work list that was a tenth of the archive (D295), and
8552
+ * before this there was no supported way to run only that half: the only way
8553
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8554
+ *
8555
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8556
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8557
+ * re-repoint a class that is already migrated.
8558
+ */
8559
+ var StorageMigrationDrainInputSchema = object({
8560
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8561
+ * a class whose source is already empty is refused rather than started. */
8562
+ classes: array(StorageMigrationClassSchema).min(1),
8563
+ throttleMbps: number().min(1).max(1e3).optional()
8564
+ });
8565
+ /** What a footage source still holds, asked of the durable hour ledger. */
8566
+ var RelocateResidueInputSchema = object({
8567
+ fromLocationId: string().min(1),
8568
+ /** Narrow to one logical class; omit for every profile on the location. */
8569
+ footageClass: RelocateFootageClassSchema.optional()
8570
+ });
8571
+ /** `null` = the archive could not answer (no ledger on this node, or the
8572
+ * aggregate failed). Never conflated with an empty source. */
8573
+ var RelocateResidueSchema = object({
8574
+ segments: number().int().nonnegative(),
8575
+ bytes: number().int().nonnegative()
8576
+ }).nullable();
8577
+ /** How many rows a media pass would still act on against a given target — the
8578
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8579
+ * never disagree. `null` = the count could not be taken. */
8580
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8581
+ var RelocatableMediaCountInputSchema = object({
8582
+ toLocationId: string().min(1),
8583
+ /** Omitted = `move`. */
8584
+ mode: MediaRelocateModeSchema.optional()
8585
+ });
8586
+ /**
8412
8587
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8413
8588
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8414
8589
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8512,6 +8687,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8512
8687
  * two addons declaring the same `id` must agree on `cardinality` (validated
8513
8688
  * at kernel aggregation time, not here).
8514
8689
  */
8690
+ /**
8691
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8692
+ * actually reaches the bytes. It is the constraint that decides which
8693
+ * `storage-provider`s may back a location of that kind.
8694
+ *
8695
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8696
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8697
+ * post-analysis media roots). Only a provider that serves a genuine local
8698
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8699
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8700
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8701
+ * against a same-named local directory that is something else entirely.
8702
+ *
8703
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8704
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8705
+ * service never sees a path, so any provider can back it. `backups` is the
8706
+ * one kind that qualifies today.
8707
+ *
8708
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8709
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8710
+ * refused the configuration; the first write simply went somewhere wrong, and
8711
+ * a recording write that goes wrong surfaces as a silent black window rather
8712
+ * than an error (the read path does not `stat`). This turns that accident into
8713
+ * a declared, enforced, testable refusal.
8714
+ */
8715
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8515
8716
  var StorageLocationDeclarationSchema = object({
8516
8717
  /**
8517
8718
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8531,6 +8732,19 @@ var StorageLocationDeclarationSchema = object({
8531
8732
  */
8532
8733
  cardinality: _enum(["single", "multi"]),
8533
8734
  /**
8735
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8736
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8737
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8738
+ *
8739
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8740
+ * can only over-restrict (refuse a remote provider for a kind that might
8741
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8742
+ * permissive direction and is therefore never inferred — a repo guard
8743
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8744
+ * reached by omission.
8745
+ */
8746
+ access: StorageAccessSchema.optional(),
8747
+ /**
8534
8748
  * When set, the default instance for this location inherits its resolved
8535
8749
  * root from the named location's default instance. Useful for derivative
8536
8750
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18145,8 +18359,10 @@ var TrackSchema = object({
18145
18359
  lastSeen: number(),
18146
18360
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18147
18361
  positions: array(TrackPositionSchema).readonly(),
18148
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18149
- * saveThumbnails policy). */
18362
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
18363
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18364
+ * the retired `saveThumbnails` used to gate this and the rolling
18365
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18150
18366
  snapshots: array(TrackSnapshotSchema).readonly(),
18151
18367
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18152
18368
  zonesVisited: array(string()).readonly(),
@@ -19006,7 +19222,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19006
19222
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19007
19223
  kind: "mutation",
19008
19224
  auth: "admin"
19009
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19225
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19226
+ kind: "query",
19227
+ auth: "admin"
19228
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19010
19229
  kind: "query",
19011
19230
  auth: "admin"
19012
19231
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -20965,6 +21184,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20965
21184
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20966
21185
  kind: "mutation",
20967
21186
  auth: "admin"
21187
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21188
+ kind: "mutation",
21189
+ auth: "admin"
20968
21190
  });
20969
21191
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20970
21192
  providerId: string().min(1),
@@ -21363,12 +21585,38 @@ response: record(string(), unknown()) }), object({
21363
21585
  *
21364
21586
  * ## Why this is a capability and not a helper
21365
21587
  *
21366
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
21367
- * plate, vehicle, identity, and the event store's derivativesand every one of
21368
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21369
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21370
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21371
- * load 5,000 rows before ranking anything.
21588
+ * This capability was introduced with the claim that SIX stores in
21589
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
21590
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21591
+ * claim was never true, and leaving it here made five stores look like pending
21592
+ * work when three of them have no vector at all. Counted column by column on
21593
+ * 2026-08-30, exactly THREE ever held one:
21594
+ *
21595
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21596
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21597
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21598
+ * face, migrated 2026-08-30 into its OWN index (see below).
21599
+ *
21600
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21601
+ * and `identities` store a name; the event store stores no derivative vector.
21602
+ * They are not migration candidates and never were.
21603
+ *
21604
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21605
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21606
+ * rows before ranking anything.
21607
+ *
21608
+ * ## One index per COMPARISON, never per encoder
21609
+ *
21610
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21611
+ * model, and they still get two indexes. An index is a set of things that are
21612
+ * ranked against each other and that live and die together, and these two are
21613
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21614
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21615
+ * forever and is the gallery every recognition ranks against. One index would
21616
+ * mean every gallery load and every reconcile carried a filter whose failure
21617
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21618
+ * person's only sample. The dimension they share is not a reason to share an
21619
+ * index; the question they answer is, and it differs.
21372
21620
  *
21373
21621
  * The fix is not a faster loop, it is a different backend — and the backend
21374
21622
  * should be replaceable without touching six callers. So: a singleton
@@ -21473,7 +21721,20 @@ var VectorQueryResultSchema = object({
21473
21721
  */
21474
21722
  scanned: number(),
21475
21723
  /** True when the backend could not consider every row that passed the filter. */
21476
- truncated: boolean()
21724
+ truncated: boolean(),
21725
+ /**
21726
+ * The `topK` the backend actually ran with.
21727
+ *
21728
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21729
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21730
+ * own log rather than in its answer. That is how an audit asking for 20,000
21731
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21732
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21733
+ * MUCH, in the return value, where the caller cannot fail to see it.
21734
+ *
21735
+ * Equals the requested `topK` whenever nothing was lowered.
21736
+ */
21737
+ effectiveTopK: number().int().positive()
21477
21738
  });
21478
21739
  var VectorDeleteInputSchema = object({
21479
21740
  index: string(),
@@ -21502,6 +21763,68 @@ var VectorGetResultSchema = object({ items: array(object({
21502
21763
  id: string(),
21503
21764
  metadata: VectorMetadataSchema
21504
21765
  })) });
21766
+ /**
21767
+ * Ids to read back WITH their vectors.
21768
+ *
21769
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21770
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21771
+ * caller depends on that promise. This one promises the opposite.
21772
+ *
21773
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21774
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21775
+ * a per-face cross-process KNN would be a network round trip inside the
21776
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21777
+ * it requires the index to hand the floats back. Without this method the only
21778
+ * way to keep a readable vector is a JSON column, which is the thing this
21779
+ * capability exists to delete.
21780
+ *
21781
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21782
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21783
+ */
21784
+ var VectorFetchInputSchema = object({
21785
+ index: string(),
21786
+ ids: array(string())
21787
+ });
21788
+ var VectorFetchResultSchema = object({ items: array(object({
21789
+ id: string(),
21790
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21791
+ vector: string(),
21792
+ metadata: VectorMetadataSchema
21793
+ })) });
21794
+ /**
21795
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21796
+ *
21797
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21798
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21799
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21800
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21801
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21802
+ * looked" for as long as anyone cared to read it.
21803
+ *
21804
+ * This is the primitive that question actually needs: a bounded page, ordered
21805
+ * by the backend's own row order, costing no distance computation at all.
21806
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21807
+ * the full-table read this capability was built to stop.
21808
+ */
21809
+ var VectorScanInputSchema = object({
21810
+ index: string(),
21811
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21812
+ cursor: number().int().nonnegative().default(0),
21813
+ limit: number().int().positive()
21814
+ });
21815
+ var VectorScanResultSchema = object({
21816
+ items: array(object({
21817
+ id: string(),
21818
+ metadata: VectorMetadataSchema
21819
+ })),
21820
+ /**
21821
+ * Where the next page starts, or `null` when the walk reached the end.
21822
+ *
21823
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21824
+ * from a short page: a backend is free to return fewer rows than asked.
21825
+ */
21826
+ nextCursor: number().int().nonnegative().nullable()
21827
+ });
21505
21828
  var VectorStatsInputSchema = object({ index: string() });
21506
21829
  var VectorStatsResultSchema = object({
21507
21830
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21520,7 +21843,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21520
21843
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21521
21844
  kind: "mutation",
21522
21845
  auth: "admin"
21523
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21846
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21524
21847
  kind: "mutation",
21525
21848
  auth: "admin"
21526
21849
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26436,6 +26759,9 @@ method(object({
26436
26759
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26437
26760
  kind: "query",
26438
26761
  auth: "admin"
26762
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26763
+ kind: "query",
26764
+ auth: "admin"
26439
26765
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26440
26766
  kind: "mutation",
26441
26767
  auth: "admin"
@@ -31408,6 +31734,12 @@ Object.freeze({
31408
31734
  addonId: null,
31409
31735
  access: "create"
31410
31736
  },
31737
+ "pipelineAnalytics.countRelocatableMedia": {
31738
+ capName: "pipeline-analytics",
31739
+ capScope: "device",
31740
+ addonId: null,
31741
+ access: "view"
31742
+ },
31411
31743
  "pipelineAnalytics.countUnstampedEventMedia": {
31412
31744
  capName: "pipeline-analytics",
31413
31745
  capScope: "device",
@@ -32572,6 +32904,12 @@ Object.freeze({
32572
32904
  addonId: null,
32573
32905
  access: "view"
32574
32906
  },
32907
+ "recording.getRelocateResidue": {
32908
+ capName: "recording",
32909
+ capScope: "system",
32910
+ addonId: null,
32911
+ access: "view"
32912
+ },
32575
32913
  "recording.getStorageMigrationMoveStatus": {
32576
32914
  capName: "recording",
32577
32915
  capScope: "system",
@@ -33118,12 +33456,30 @@ Object.freeze({
33118
33456
  addonId: null,
33119
33457
  access: "create"
33120
33458
  },
33459
+ "storageMigration.drain": {
33460
+ capName: "storage-migration",
33461
+ capScope: "system",
33462
+ addonId: null,
33463
+ access: "create"
33464
+ },
33465
+ "storageMigration.movers": {
33466
+ capName: "storage-migration",
33467
+ capScope: "system",
33468
+ addonId: null,
33469
+ access: "view"
33470
+ },
33121
33471
  "storageMigration.plan": {
33122
33472
  capName: "storage-migration",
33123
33473
  capScope: "system",
33124
33474
  addonId: null,
33125
33475
  access: "view"
33126
33476
  },
33477
+ "storageMigration.residue": {
33478
+ capName: "storage-migration",
33479
+ capScope: "system",
33480
+ addonId: null,
33481
+ access: "view"
33482
+ },
33127
33483
  "storageMigration.start": {
33128
33484
  capName: "storage-migration",
33129
33485
  capScope: "system",
@@ -33958,6 +34314,12 @@ Object.freeze({
33958
34314
  addonId: null,
33959
34315
  access: "delete"
33960
34316
  },
34317
+ "vectorStore.fetchByIds": {
34318
+ capName: "vector-store",
34319
+ capScope: "system",
34320
+ addonId: null,
34321
+ access: "view"
34322
+ },
33961
34323
  "vectorStore.getByIds": {
33962
34324
  capName: "vector-store",
33963
34325
  capScope: "system",
@@ -33970,6 +34332,12 @@ Object.freeze({
33970
34332
  addonId: null,
33971
34333
  access: "view"
33972
34334
  },
34335
+ "vectorStore.scan": {
34336
+ capName: "vector-store",
34337
+ capScope: "system",
34338
+ addonId: null,
34339
+ access: "view"
34340
+ },
33973
34341
  "vectorStore.stats": {
33974
34342
  capName: "vector-store",
33975
34343
  capScope: "system",
@@ -8192,6 +8192,21 @@ var RelocateJobSchema = object({
8192
8192
  bytesMoved: number().int(),
8193
8193
  /** Total files discovered up front; null while (or when) unknown. */
8194
8194
  filesTotal: number().int().nullable(),
8195
+ /**
8196
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8197
+ * made that nobody asked for, so it is reported where the operator reads the
8198
+ * job rather than only in a log line.
8199
+ *
8200
+ * A footage segment records its byte count in its own NAME, and the durable
8201
+ * hour row derives its aggregates from those names. A file that does not
8202
+ * match its name therefore makes the ledger's sums — and with them quota and
8203
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8204
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8205
+ *
8206
+ * Absent on lanes where the question has no meaning: a media blob's size is
8207
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8208
+ */
8209
+ rowsReconciled: number().int().nonnegative().optional(),
8195
8210
  startedAt: number(),
8196
8211
  finishedAt: number().nullable(),
8197
8212
  error: string().nullable()
@@ -8260,14 +8275,42 @@ var RelocateMediaInputSchema = object({
8260
8275
  /** Omitted = `move`, the pre-existing behaviour. */
8261
8276
  mode: MediaRelocateModeSchema.optional()
8262
8277
  });
8263
- /** How many rows still carry NO `locationId` — the population a repoint would
8264
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8265
- * value that permits a non-blocking `eventMedia` cutover. */
8266
- var UnstampedEventMediaCountSchema = object({
8267
- media: number().int().nonnegative(),
8268
- retrainFrames: number().int().nonnegative(),
8269
- total: number().int().nonnegative()
8278
+ /**
8279
+ * The unstamped population of ONE collection split, because the gate and the
8280
+ * operator ask two different questions and only one of them has to be cheap.
8281
+ *
8282
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8283
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8284
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8285
+ * that matters — after a seal, when the population is empty.
8286
+ *
8287
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8288
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8289
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8290
+ * and useful answer: "there are some, and this read could not say how many"
8291
+ * still refuses the cutover, which is the whole job.
8292
+ */
8293
+ var UnstampedRowsSchema = object({
8294
+ present: boolean(),
8295
+ rows: number().int().nonnegative().nullable()
8270
8296
  });
8297
+ /**
8298
+ * How many rows still carry NO `locationId` — the population a repoint would
8299
+ * silently re-aim at a disk that does not hold their bytes.
8300
+ *
8301
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8302
+ * over. The gate opens on a measured absence and on nothing else; an unread
8303
+ * collection and an empty one are different facts, and this repo has already
8304
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8305
+ */
8306
+ var UnstampedEventMediaCountSchema = object({
8307
+ media: UnstampedRowsSchema,
8308
+ retrainFrames: UnstampedRowsSchema,
8309
+ /** True when EITHER collection holds one. The refusal reads this. */
8310
+ anyPresent: boolean(),
8311
+ /** Sum across both, or `null` when either lane could not be counted. */
8312
+ total: number().int().nonnegative().nullable()
8313
+ }).nullable();
8271
8314
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8272
8315
  /** The independently selectable logical storage classes — every class
8273
8316
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8353,13 +8396,53 @@ var StorageMigrationParticipantSchema = _enum([
8353
8396
  "recorder",
8354
8397
  "analytics"
8355
8398
  ]);
8399
+ /**
8400
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8401
+ *
8402
+ * The long half of a non-blocking migration is `draining`, and it is measured
8403
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8404
+ * existed the only place those numbers appeared was a Loki line, so an operator
8405
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8406
+ * afternoon.
8407
+ *
8408
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8409
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8410
+ * mover — which is the exact failure this is meant to end. The coordinator's
8411
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8412
+ * read `state`; folding the counters costs no extra read and makes the durable
8413
+ * record say afterwards how far a move actually got.
8414
+ *
8415
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8416
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8417
+ * cannot say M, and a 0 there would render as "100 % done".
8418
+ */
8419
+ var StorageMigrationMoveProgressSchema = object({
8420
+ filesMoved: number().int().nonnegative(),
8421
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8422
+ filesTotal: number().int().nonnegative().nullable(),
8423
+ bytesMoved: number().int().nonnegative(),
8424
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8425
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8426
+ * rows would be the same failure as one that silently skipped them. */
8427
+ rowsReconciled: number().int().nonnegative().optional(),
8428
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8429
+ * crash gets a new mover, and a rate computed from the migration's start
8430
+ * would silently average in the time nothing was running. */
8431
+ startedAt: number(),
8432
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8433
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8434
+ * subtract its own. */
8435
+ observedAt: number()
8436
+ });
8356
8437
  var StorageMigrationMoveSchema = object({
8357
8438
  storageClass: StorageMigrationClassSchema,
8358
8439
  fromLocationId: string(),
8359
8440
  toLocationId: string(),
8360
8441
  moverJobId: string().nullable(),
8361
8442
  state: RelocateJobStateSchema.nullable(),
8362
- error: string().nullable()
8443
+ error: string().nullable(),
8444
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8445
+ progress: StorageMigrationMoveProgressSchema.nullable()
8363
8446
  });
8364
8447
  var StorageMigrationJobSchema = object({
8365
8448
  jobId: string(),
@@ -8405,6 +8488,98 @@ var StorageMigrationPlanSchema = object({
8405
8488
  findings: array(StorageMigrationFindingSchema)
8406
8489
  });
8407
8490
  /**
8491
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8492
+ *
8493
+ * The coordinator's job record is the state of record for a migration, and its
8494
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8495
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8496
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8497
+ * way because no supported UI path existed. A mover armed like that has no job
8498
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8499
+ *
8500
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8501
+ * orchestrated it.
8502
+ */
8503
+ var StorageMigrationMoverSchema = object({
8504
+ lane: _enum(["footage", "media"]),
8505
+ job: RelocateJobSchema,
8506
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8507
+ * directly against the owning addon. */
8508
+ migrationJobId: string().nullable(),
8509
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8510
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8511
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8512
+ * rate made of two different clocks. */
8513
+ observedAt: number()
8514
+ });
8515
+ /**
8516
+ * What a SOURCE still holds for one storage class — the number that makes a
8517
+ * "drain remaining" action honest rather than hopeful.
8518
+ *
8519
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8520
+ * engine's own selection count for media), never from the resident index: a
8521
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8522
+ * never been told about (D295).
8523
+ *
8524
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8525
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8526
+ * because refusing on an unanswerable read would hide exactly the case an
8527
+ * operator needs to act on.
8528
+ */
8529
+ var StorageMigrationResidueSchema = object({
8530
+ storageClass: StorageMigrationClassSchema,
8531
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8532
+ * move from wherever they are rather than from one named source. */
8533
+ fromLocationId: string(),
8534
+ /** Where a drain would move it — the class's CURRENT default. */
8535
+ toLocationId: string(),
8536
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8537
+ items: number().int().nonnegative().nullable(),
8538
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8539
+ bytes: number().int().nonnegative().nullable()
8540
+ });
8541
+ /**
8542
+ * Run the DRAIN half and nothing else.
8543
+ *
8544
+ * A migration that reached `done` has already repointed, so `start` correctly
8545
+ * refuses its destination ("already the default") — there is nothing left to
8546
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8547
+ * or finish against a work list that was a tenth of the archive (D295), and
8548
+ * before this there was no supported way to run only that half: the only way
8549
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8550
+ *
8551
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8552
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8553
+ * re-repoint a class that is already migrated.
8554
+ */
8555
+ var StorageMigrationDrainInputSchema = object({
8556
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8557
+ * a class whose source is already empty is refused rather than started. */
8558
+ classes: array(StorageMigrationClassSchema).min(1),
8559
+ throttleMbps: number().min(1).max(1e3).optional()
8560
+ });
8561
+ /** What a footage source still holds, asked of the durable hour ledger. */
8562
+ var RelocateResidueInputSchema = object({
8563
+ fromLocationId: string().min(1),
8564
+ /** Narrow to one logical class; omit for every profile on the location. */
8565
+ footageClass: RelocateFootageClassSchema.optional()
8566
+ });
8567
+ /** `null` = the archive could not answer (no ledger on this node, or the
8568
+ * aggregate failed). Never conflated with an empty source. */
8569
+ var RelocateResidueSchema = object({
8570
+ segments: number().int().nonnegative(),
8571
+ bytes: number().int().nonnegative()
8572
+ }).nullable();
8573
+ /** How many rows a media pass would still act on against a given target — the
8574
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8575
+ * never disagree. `null` = the count could not be taken. */
8576
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8577
+ var RelocatableMediaCountInputSchema = object({
8578
+ toLocationId: string().min(1),
8579
+ /** Omitted = `move`. */
8580
+ mode: MediaRelocateModeSchema.optional()
8581
+ });
8582
+ /**
8408
8583
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8409
8584
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8410
8585
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8508,6 +8683,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8508
8683
  * two addons declaring the same `id` must agree on `cardinality` (validated
8509
8684
  * at kernel aggregation time, not here).
8510
8685
  */
8686
+ /**
8687
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8688
+ * actually reaches the bytes. It is the constraint that decides which
8689
+ * `storage-provider`s may back a location of that kind.
8690
+ *
8691
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8692
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8693
+ * post-analysis media roots). Only a provider that serves a genuine local
8694
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8695
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8696
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8697
+ * against a same-named local directory that is something else entirely.
8698
+ *
8699
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8700
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8701
+ * service never sees a path, so any provider can back it. `backups` is the
8702
+ * one kind that qualifies today.
8703
+ *
8704
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8705
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8706
+ * refused the configuration; the first write simply went somewhere wrong, and
8707
+ * a recording write that goes wrong surfaces as a silent black window rather
8708
+ * than an error (the read path does not `stat`). This turns that accident into
8709
+ * a declared, enforced, testable refusal.
8710
+ */
8711
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8511
8712
  var StorageLocationDeclarationSchema = object({
8512
8713
  /**
8513
8714
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8527,6 +8728,19 @@ var StorageLocationDeclarationSchema = object({
8527
8728
  */
8528
8729
  cardinality: _enum(["single", "multi"]),
8529
8730
  /**
8731
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8732
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8733
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8734
+ *
8735
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8736
+ * can only over-restrict (refuse a remote provider for a kind that might
8737
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8738
+ * permissive direction and is therefore never inferred — a repo guard
8739
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8740
+ * reached by omission.
8741
+ */
8742
+ access: StorageAccessSchema.optional(),
8743
+ /**
8530
8744
  * When set, the default instance for this location inherits its resolved
8531
8745
  * root from the named location's default instance. Useful for derivative
8532
8746
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18141,8 +18355,10 @@ var TrackSchema = object({
18141
18355
  lastSeen: number(),
18142
18356
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18143
18357
  positions: array(TrackPositionSchema).readonly(),
18144
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18145
- * saveThumbnails policy). */
18358
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
18359
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18360
+ * the retired `saveThumbnails` used to gate this and the rolling
18361
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18146
18362
  snapshots: array(TrackSnapshotSchema).readonly(),
18147
18363
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18148
18364
  zonesVisited: array(string()).readonly(),
@@ -19002,7 +19218,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19002
19218
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19003
19219
  kind: "mutation",
19004
19220
  auth: "admin"
19005
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19221
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19222
+ kind: "query",
19223
+ auth: "admin"
19224
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19006
19225
  kind: "query",
19007
19226
  auth: "admin"
19008
19227
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -20961,6 +21180,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
20961
21180
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20962
21181
  kind: "mutation",
20963
21182
  auth: "admin"
21183
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21184
+ kind: "mutation",
21185
+ auth: "admin"
20964
21186
  });
20965
21187
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
20966
21188
  providerId: string().min(1),
@@ -21359,12 +21581,38 @@ response: record(string(), unknown()) }), object({
21359
21581
  *
21360
21582
  * ## Why this is a capability and not a helper
21361
21583
  *
21362
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
21363
- * plate, vehicle, identity, and the event store's derivativesand every one of
21364
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21365
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21366
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21367
- * load 5,000 rows before ranking anything.
21584
+ * This capability was introduced with the claim that SIX stores in
21585
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
21586
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21587
+ * claim was never true, and leaving it here made five stores look like pending
21588
+ * work when three of them have no vector at all. Counted column by column on
21589
+ * 2026-08-30, exactly THREE ever held one:
21590
+ *
21591
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21592
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21593
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21594
+ * face, migrated 2026-08-30 into its OWN index (see below).
21595
+ *
21596
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21597
+ * and `identities` store a name; the event store stores no derivative vector.
21598
+ * They are not migration candidates and never were.
21599
+ *
21600
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21601
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21602
+ * rows before ranking anything.
21603
+ *
21604
+ * ## One index per COMPARISON, never per encoder
21605
+ *
21606
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21607
+ * model, and they still get two indexes. An index is a set of things that are
21608
+ * ranked against each other and that live and die together, and these two are
21609
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21610
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21611
+ * forever and is the gallery every recognition ranks against. One index would
21612
+ * mean every gallery load and every reconcile carried a filter whose failure
21613
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21614
+ * person's only sample. The dimension they share is not a reason to share an
21615
+ * index; the question they answer is, and it differs.
21368
21616
  *
21369
21617
  * The fix is not a faster loop, it is a different backend — and the backend
21370
21618
  * should be replaceable without touching six callers. So: a singleton
@@ -21469,7 +21717,20 @@ var VectorQueryResultSchema = object({
21469
21717
  */
21470
21718
  scanned: number(),
21471
21719
  /** True when the backend could not consider every row that passed the filter. */
21472
- truncated: boolean()
21720
+ truncated: boolean(),
21721
+ /**
21722
+ * The `topK` the backend actually ran with.
21723
+ *
21724
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21725
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21726
+ * own log rather than in its answer. That is how an audit asking for 20,000
21727
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21728
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21729
+ * MUCH, in the return value, where the caller cannot fail to see it.
21730
+ *
21731
+ * Equals the requested `topK` whenever nothing was lowered.
21732
+ */
21733
+ effectiveTopK: number().int().positive()
21473
21734
  });
21474
21735
  var VectorDeleteInputSchema = object({
21475
21736
  index: string(),
@@ -21498,6 +21759,68 @@ var VectorGetResultSchema = object({ items: array(object({
21498
21759
  id: string(),
21499
21760
  metadata: VectorMetadataSchema
21500
21761
  })) });
21762
+ /**
21763
+ * Ids to read back WITH their vectors.
21764
+ *
21765
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21766
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21767
+ * caller depends on that promise. This one promises the opposite.
21768
+ *
21769
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21770
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21771
+ * a per-face cross-process KNN would be a network round trip inside the
21772
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21773
+ * it requires the index to hand the floats back. Without this method the only
21774
+ * way to keep a readable vector is a JSON column, which is the thing this
21775
+ * capability exists to delete.
21776
+ *
21777
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21778
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21779
+ */
21780
+ var VectorFetchInputSchema = object({
21781
+ index: string(),
21782
+ ids: array(string())
21783
+ });
21784
+ var VectorFetchResultSchema = object({ items: array(object({
21785
+ id: string(),
21786
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21787
+ vector: string(),
21788
+ metadata: VectorMetadataSchema
21789
+ })) });
21790
+ /**
21791
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21792
+ *
21793
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21794
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21795
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21796
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21797
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21798
+ * looked" for as long as anyone cared to read it.
21799
+ *
21800
+ * This is the primitive that question actually needs: a bounded page, ordered
21801
+ * by the backend's own row order, costing no distance computation at all.
21802
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21803
+ * the full-table read this capability was built to stop.
21804
+ */
21805
+ var VectorScanInputSchema = object({
21806
+ index: string(),
21807
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21808
+ cursor: number().int().nonnegative().default(0),
21809
+ limit: number().int().positive()
21810
+ });
21811
+ var VectorScanResultSchema = object({
21812
+ items: array(object({
21813
+ id: string(),
21814
+ metadata: VectorMetadataSchema
21815
+ })),
21816
+ /**
21817
+ * Where the next page starts, or `null` when the walk reached the end.
21818
+ *
21819
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21820
+ * from a short page: a backend is free to return fewer rows than asked.
21821
+ */
21822
+ nextCursor: number().int().nonnegative().nullable()
21823
+ });
21501
21824
  var VectorStatsInputSchema = object({ index: string() });
21502
21825
  var VectorStatsResultSchema = object({
21503
21826
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21516,7 +21839,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21516
21839
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21517
21840
  kind: "mutation",
21518
21841
  auth: "admin"
21519
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21842
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21520
21843
  kind: "mutation",
21521
21844
  auth: "admin"
21522
21845
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -26432,6 +26755,9 @@ method(object({
26432
26755
  }), method(object({}), array(RelocateJobSchema).readonly(), {
26433
26756
  kind: "query",
26434
26757
  auth: "admin"
26758
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26759
+ kind: "query",
26760
+ auth: "admin"
26435
26761
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26436
26762
  kind: "mutation",
26437
26763
  auth: "admin"
@@ -31404,6 +31730,12 @@ Object.freeze({
31404
31730
  addonId: null,
31405
31731
  access: "create"
31406
31732
  },
31733
+ "pipelineAnalytics.countRelocatableMedia": {
31734
+ capName: "pipeline-analytics",
31735
+ capScope: "device",
31736
+ addonId: null,
31737
+ access: "view"
31738
+ },
31407
31739
  "pipelineAnalytics.countUnstampedEventMedia": {
31408
31740
  capName: "pipeline-analytics",
31409
31741
  capScope: "device",
@@ -32568,6 +32900,12 @@ Object.freeze({
32568
32900
  addonId: null,
32569
32901
  access: "view"
32570
32902
  },
32903
+ "recording.getRelocateResidue": {
32904
+ capName: "recording",
32905
+ capScope: "system",
32906
+ addonId: null,
32907
+ access: "view"
32908
+ },
32571
32909
  "recording.getStorageMigrationMoveStatus": {
32572
32910
  capName: "recording",
32573
32911
  capScope: "system",
@@ -33114,12 +33452,30 @@ Object.freeze({
33114
33452
  addonId: null,
33115
33453
  access: "create"
33116
33454
  },
33455
+ "storageMigration.drain": {
33456
+ capName: "storage-migration",
33457
+ capScope: "system",
33458
+ addonId: null,
33459
+ access: "create"
33460
+ },
33461
+ "storageMigration.movers": {
33462
+ capName: "storage-migration",
33463
+ capScope: "system",
33464
+ addonId: null,
33465
+ access: "view"
33466
+ },
33117
33467
  "storageMigration.plan": {
33118
33468
  capName: "storage-migration",
33119
33469
  capScope: "system",
33120
33470
  addonId: null,
33121
33471
  access: "view"
33122
33472
  },
33473
+ "storageMigration.residue": {
33474
+ capName: "storage-migration",
33475
+ capScope: "system",
33476
+ addonId: null,
33477
+ access: "view"
33478
+ },
33123
33479
  "storageMigration.start": {
33124
33480
  capName: "storage-migration",
33125
33481
  capScope: "system",
@@ -33954,6 +34310,12 @@ Object.freeze({
33954
34310
  addonId: null,
33955
34311
  access: "delete"
33956
34312
  },
34313
+ "vectorStore.fetchByIds": {
34314
+ capName: "vector-store",
34315
+ capScope: "system",
34316
+ addonId: null,
34317
+ access: "view"
34318
+ },
33957
34319
  "vectorStore.getByIds": {
33958
34320
  capName: "vector-store",
33959
34321
  capScope: "system",
@@ -33966,6 +34328,12 @@ Object.freeze({
33966
34328
  addonId: null,
33967
34329
  access: "view"
33968
34330
  },
34331
+ "vectorStore.scan": {
34332
+ capName: "vector-store",
34333
+ capScope: "system",
34334
+ addonId: null,
34335
+ access: "view"
34336
+ },
33969
34337
  "vectorStore.stats": {
33970
34338
  capName: "vector-store",
33971
34339
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-google",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Google Home export — hub-side smart-home fulfillment (SYNC / QUERY / EXECUTE / DISCONNECT) for the non-camera fleet, served over the hub's own OAuth account link. No Google credential is stored, sent or required.",
5
5
  "keywords": [
6
6
  "camstack",