@camstack/addon-provider-gree 0.2.42 → 0.2.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +387 -19
  2. package/dist/addon.mjs +387 -19
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8006,6 +8006,21 @@ var RelocateJobSchema = object({
8006
8006
  bytesMoved: number().int(),
8007
8007
  /** Total files discovered up front; null while (or when) unknown. */
8008
8008
  filesTotal: number().int().nullable(),
8009
+ /**
8010
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8011
+ * made that nobody asked for, so it is reported where the operator reads the
8012
+ * job rather than only in a log line.
8013
+ *
8014
+ * A footage segment records its byte count in its own NAME, and the durable
8015
+ * hour row derives its aggregates from those names. A file that does not
8016
+ * match its name therefore makes the ledger's sums — and with them quota and
8017
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8018
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8019
+ *
8020
+ * Absent on lanes where the question has no meaning: a media blob's size is
8021
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8022
+ */
8023
+ rowsReconciled: number().int().nonnegative().optional(),
8009
8024
  startedAt: number(),
8010
8025
  finishedAt: number().nullable(),
8011
8026
  error: string().nullable()
@@ -8074,14 +8089,42 @@ var RelocateMediaInputSchema = object({
8074
8089
  /** Omitted = `move`, the pre-existing behaviour. */
8075
8090
  mode: MediaRelocateModeSchema.optional()
8076
8091
  });
8077
- /** How many rows still carry NO `locationId` — the population a repoint would
8078
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8079
- * value that permits a non-blocking `eventMedia` cutover. */
8080
- var UnstampedEventMediaCountSchema = object({
8081
- media: number().int().nonnegative(),
8082
- retrainFrames: number().int().nonnegative(),
8083
- total: number().int().nonnegative()
8092
+ /**
8093
+ * The unstamped population of ONE collection — split, because the gate and the
8094
+ * operator ask two different questions and only one of them has to be cheap.
8095
+ *
8096
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8097
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8098
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8099
+ * that matters — after a seal, when the population is empty.
8100
+ *
8101
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8102
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8103
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8104
+ * and useful answer: "there are some, and this read could not say how many"
8105
+ * still refuses the cutover, which is the whole job.
8106
+ */
8107
+ var UnstampedRowsSchema = object({
8108
+ present: boolean(),
8109
+ rows: number().int().nonnegative().nullable()
8084
8110
  });
8111
+ /**
8112
+ * How many rows still carry NO `locationId` — the population a repoint would
8113
+ * silently re-aim at a disk that does not hold their bytes.
8114
+ *
8115
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8116
+ * over. The gate opens on a measured absence and on nothing else; an unread
8117
+ * collection and an empty one are different facts, and this repo has already
8118
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8119
+ */
8120
+ var UnstampedEventMediaCountSchema = object({
8121
+ media: UnstampedRowsSchema,
8122
+ retrainFrames: UnstampedRowsSchema,
8123
+ /** True when EITHER collection holds one. The refusal reads this. */
8124
+ anyPresent: boolean(),
8125
+ /** Sum across both, or `null` when either lane could not be counted. */
8126
+ total: number().int().nonnegative().nullable()
8127
+ }).nullable();
8085
8128
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8086
8129
  /** The independently selectable logical storage classes — every class
8087
8130
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8167,13 +8210,53 @@ var StorageMigrationParticipantSchema = _enum([
8167
8210
  "recorder",
8168
8211
  "analytics"
8169
8212
  ]);
8213
+ /**
8214
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8215
+ *
8216
+ * The long half of a non-blocking migration is `draining`, and it is measured
8217
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8218
+ * existed the only place those numbers appeared was a Loki line, so an operator
8219
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8220
+ * afternoon.
8221
+ *
8222
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8223
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8224
+ * mover — which is the exact failure this is meant to end. The coordinator's
8225
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8226
+ * read `state`; folding the counters costs no extra read and makes the durable
8227
+ * record say afterwards how far a move actually got.
8228
+ *
8229
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8230
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8231
+ * cannot say M, and a 0 there would render as "100 % done".
8232
+ */
8233
+ var StorageMigrationMoveProgressSchema = object({
8234
+ filesMoved: number().int().nonnegative(),
8235
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8236
+ filesTotal: number().int().nonnegative().nullable(),
8237
+ bytesMoved: number().int().nonnegative(),
8238
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8239
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8240
+ * rows would be the same failure as one that silently skipped them. */
8241
+ rowsReconciled: number().int().nonnegative().optional(),
8242
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8243
+ * crash gets a new mover, and a rate computed from the migration's start
8244
+ * would silently average in the time nothing was running. */
8245
+ startedAt: number(),
8246
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8247
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8248
+ * subtract its own. */
8249
+ observedAt: number()
8250
+ });
8170
8251
  var StorageMigrationMoveSchema = object({
8171
8252
  storageClass: StorageMigrationClassSchema,
8172
8253
  fromLocationId: string(),
8173
8254
  toLocationId: string(),
8174
8255
  moverJobId: string().nullable(),
8175
8256
  state: RelocateJobStateSchema.nullable(),
8176
- error: string().nullable()
8257
+ error: string().nullable(),
8258
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8259
+ progress: StorageMigrationMoveProgressSchema.nullable()
8177
8260
  });
8178
8261
  var StorageMigrationJobSchema = object({
8179
8262
  jobId: string(),
@@ -8219,6 +8302,98 @@ var StorageMigrationPlanSchema = object({
8219
8302
  findings: array(StorageMigrationFindingSchema)
8220
8303
  });
8221
8304
  /**
8305
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8306
+ *
8307
+ * The coordinator's job record is the state of record for a migration, and its
8308
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8309
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8310
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8311
+ * way because no supported UI path existed. A mover armed like that has no job
8312
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8313
+ *
8314
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8315
+ * orchestrated it.
8316
+ */
8317
+ var StorageMigrationMoverSchema = object({
8318
+ lane: _enum(["footage", "media"]),
8319
+ job: RelocateJobSchema,
8320
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8321
+ * directly against the owning addon. */
8322
+ migrationJobId: string().nullable(),
8323
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8324
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8325
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8326
+ * rate made of two different clocks. */
8327
+ observedAt: number()
8328
+ });
8329
+ /**
8330
+ * What a SOURCE still holds for one storage class — the number that makes a
8331
+ * "drain remaining" action honest rather than hopeful.
8332
+ *
8333
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8334
+ * engine's own selection count for media), never from the resident index: a
8335
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8336
+ * never been told about (D295).
8337
+ *
8338
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8339
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8340
+ * because refusing on an unanswerable read would hide exactly the case an
8341
+ * operator needs to act on.
8342
+ */
8343
+ var StorageMigrationResidueSchema = object({
8344
+ storageClass: StorageMigrationClassSchema,
8345
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8346
+ * move from wherever they are rather than from one named source. */
8347
+ fromLocationId: string(),
8348
+ /** Where a drain would move it — the class's CURRENT default. */
8349
+ toLocationId: string(),
8350
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8351
+ items: number().int().nonnegative().nullable(),
8352
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8353
+ bytes: number().int().nonnegative().nullable()
8354
+ });
8355
+ /**
8356
+ * Run the DRAIN half and nothing else.
8357
+ *
8358
+ * A migration that reached `done` has already repointed, so `start` correctly
8359
+ * refuses its destination ("already the default") — there is nothing left to
8360
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8361
+ * or finish against a work list that was a tenth of the archive (D295), and
8362
+ * before this there was no supported way to run only that half: the only way
8363
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8364
+ *
8365
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8366
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8367
+ * re-repoint a class that is already migrated.
8368
+ */
8369
+ var StorageMigrationDrainInputSchema = object({
8370
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8371
+ * a class whose source is already empty is refused rather than started. */
8372
+ classes: array(StorageMigrationClassSchema).min(1),
8373
+ throttleMbps: number().min(1).max(1e3).optional()
8374
+ });
8375
+ /** What a footage source still holds, asked of the durable hour ledger. */
8376
+ var RelocateResidueInputSchema = object({
8377
+ fromLocationId: string().min(1),
8378
+ /** Narrow to one logical class; omit for every profile on the location. */
8379
+ footageClass: RelocateFootageClassSchema.optional()
8380
+ });
8381
+ /** `null` = the archive could not answer (no ledger on this node, or the
8382
+ * aggregate failed). Never conflated with an empty source. */
8383
+ var RelocateResidueSchema = object({
8384
+ segments: number().int().nonnegative(),
8385
+ bytes: number().int().nonnegative()
8386
+ }).nullable();
8387
+ /** How many rows a media pass would still act on against a given target — the
8388
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8389
+ * never disagree. `null` = the count could not be taken. */
8390
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8391
+ var RelocatableMediaCountInputSchema = object({
8392
+ toLocationId: string().min(1),
8393
+ /** Omitted = `move`. */
8394
+ mode: MediaRelocateModeSchema.optional()
8395
+ });
8396
+ /**
8222
8397
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8223
8398
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8224
8399
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8322,6 +8497,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8322
8497
  * two addons declaring the same `id` must agree on `cardinality` (validated
8323
8498
  * at kernel aggregation time, not here).
8324
8499
  */
8500
+ /**
8501
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8502
+ * actually reaches the bytes. It is the constraint that decides which
8503
+ * `storage-provider`s may back a location of that kind.
8504
+ *
8505
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8506
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8507
+ * post-analysis media roots). Only a provider that serves a genuine local
8508
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8509
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8510
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8511
+ * against a same-named local directory that is something else entirely.
8512
+ *
8513
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8514
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8515
+ * service never sees a path, so any provider can back it. `backups` is the
8516
+ * one kind that qualifies today.
8517
+ *
8518
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8519
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8520
+ * refused the configuration; the first write simply went somewhere wrong, and
8521
+ * a recording write that goes wrong surfaces as a silent black window rather
8522
+ * than an error (the read path does not `stat`). This turns that accident into
8523
+ * a declared, enforced, testable refusal.
8524
+ */
8525
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8325
8526
  var StorageLocationDeclarationSchema = object({
8326
8527
  /**
8327
8528
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8341,6 +8542,19 @@ var StorageLocationDeclarationSchema = object({
8341
8542
  */
8342
8543
  cardinality: _enum(["single", "multi"]),
8343
8544
  /**
8545
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8546
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8547
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8548
+ *
8549
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8550
+ * can only over-restrict (refuse a remote provider for a kind that might
8551
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8552
+ * permissive direction and is therefore never inferred — a repo guard
8553
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8554
+ * reached by omission.
8555
+ */
8556
+ access: StorageAccessSchema.optional(),
8557
+ /**
8344
8558
  * When set, the default instance for this location inherits its resolved
8345
8559
  * root from the named location's default instance. Useful for derivative
8346
8560
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18208,8 +18422,10 @@ var TrackSchema = object({
18208
18422
  lastSeen: number(),
18209
18423
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18210
18424
  positions: array(TrackPositionSchema).readonly(),
18211
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18212
- * saveThumbnails policy). */
18425
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18426
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18427
+ * the retired `saveThumbnails` used to gate this and the rolling
18428
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18213
18429
  snapshots: array(TrackSnapshotSchema).readonly(),
18214
18430
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18215
18431
  zonesVisited: array(string()).readonly(),
@@ -19069,7 +19285,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19069
19285
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19070
19286
  kind: "mutation",
19071
19287
  auth: "admin"
19072
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19288
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19289
+ kind: "query",
19290
+ auth: "admin"
19291
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19073
19292
  kind: "query",
19074
19293
  auth: "admin"
19075
19294
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21028,6 +21247,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21028
21247
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21029
21248
  kind: "mutation",
21030
21249
  auth: "admin"
21250
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21251
+ kind: "mutation",
21252
+ auth: "admin"
21031
21253
  });
21032
21254
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21033
21255
  providerId: string().min(1),
@@ -21426,12 +21648,38 @@ response: record(string(), unknown()) }), object({
21426
21648
  *
21427
21649
  * ## Why this is a capability and not a helper
21428
21650
  *
21429
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21430
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21431
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21432
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21433
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21434
- * load 5,000 rows before ranking anything.
21651
+ * This capability was introduced with the claim that SIX stores in
21652
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21653
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21654
+ * claim was never true, and leaving it here made five stores look like pending
21655
+ * work when three of them have no vector at all. Counted column by column on
21656
+ * 2026-08-30, exactly THREE ever held one:
21657
+ *
21658
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21659
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21660
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21661
+ * face, migrated 2026-08-30 into its OWN index (see below).
21662
+ *
21663
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21664
+ * and `identities` store a name; the event store stores no derivative vector.
21665
+ * They are not migration candidates and never were.
21666
+ *
21667
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21668
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21669
+ * rows before ranking anything.
21670
+ *
21671
+ * ## One index per COMPARISON, never per encoder
21672
+ *
21673
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21674
+ * model, and they still get two indexes. An index is a set of things that are
21675
+ * ranked against each other and that live and die together, and these two are
21676
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21677
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21678
+ * forever and is the gallery every recognition ranks against. One index would
21679
+ * mean every gallery load and every reconcile carried a filter whose failure
21680
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21681
+ * person's only sample. The dimension they share is not a reason to share an
21682
+ * index; the question they answer is, and it differs.
21435
21683
  *
21436
21684
  * The fix is not a faster loop, it is a different backend — and the backend
21437
21685
  * should be replaceable without touching six callers. So: a singleton
@@ -21536,7 +21784,20 @@ var VectorQueryResultSchema = object({
21536
21784
  */
21537
21785
  scanned: number(),
21538
21786
  /** True when the backend could not consider every row that passed the filter. */
21539
- truncated: boolean()
21787
+ truncated: boolean(),
21788
+ /**
21789
+ * The `topK` the backend actually ran with.
21790
+ *
21791
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21792
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21793
+ * own log rather than in its answer. That is how an audit asking for 20,000
21794
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21795
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21796
+ * MUCH, in the return value, where the caller cannot fail to see it.
21797
+ *
21798
+ * Equals the requested `topK` whenever nothing was lowered.
21799
+ */
21800
+ effectiveTopK: number().int().positive()
21540
21801
  });
21541
21802
  var VectorDeleteInputSchema = object({
21542
21803
  index: string(),
@@ -21565,6 +21826,68 @@ var VectorGetResultSchema = object({ items: array(object({
21565
21826
  id: string(),
21566
21827
  metadata: VectorMetadataSchema
21567
21828
  })) });
21829
+ /**
21830
+ * Ids to read back WITH their vectors.
21831
+ *
21832
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21833
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21834
+ * caller depends on that promise. This one promises the opposite.
21835
+ *
21836
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21837
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21838
+ * a per-face cross-process KNN would be a network round trip inside the
21839
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21840
+ * it requires the index to hand the floats back. Without this method the only
21841
+ * way to keep a readable vector is a JSON column, which is the thing this
21842
+ * capability exists to delete.
21843
+ *
21844
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21845
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21846
+ */
21847
+ var VectorFetchInputSchema = object({
21848
+ index: string(),
21849
+ ids: array(string())
21850
+ });
21851
+ var VectorFetchResultSchema = object({ items: array(object({
21852
+ id: string(),
21853
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21854
+ vector: string(),
21855
+ metadata: VectorMetadataSchema
21856
+ })) });
21857
+ /**
21858
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21859
+ *
21860
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21861
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21862
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21863
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21864
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21865
+ * looked" for as long as anyone cared to read it.
21866
+ *
21867
+ * This is the primitive that question actually needs: a bounded page, ordered
21868
+ * by the backend's own row order, costing no distance computation at all.
21869
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21870
+ * the full-table read this capability was built to stop.
21871
+ */
21872
+ var VectorScanInputSchema = object({
21873
+ index: string(),
21874
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21875
+ cursor: number().int().nonnegative().default(0),
21876
+ limit: number().int().positive()
21877
+ });
21878
+ var VectorScanResultSchema = object({
21879
+ items: array(object({
21880
+ id: string(),
21881
+ metadata: VectorMetadataSchema
21882
+ })),
21883
+ /**
21884
+ * Where the next page starts, or `null` when the walk reached the end.
21885
+ *
21886
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21887
+ * from a short page: a backend is free to return fewer rows than asked.
21888
+ */
21889
+ nextCursor: number().int().nonnegative().nullable()
21890
+ });
21568
21891
  var VectorStatsInputSchema = object({ index: string() });
21569
21892
  var VectorStatsResultSchema = object({
21570
21893
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21583,7 +21906,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21583
21906
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21584
21907
  kind: "mutation",
21585
21908
  auth: "admin"
21586
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21909
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21587
21910
  kind: "mutation",
21588
21911
  auth: "admin"
21589
21912
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -28202,6 +28525,9 @@ method(object({
28202
28525
  }), method(object({}), array(RelocateJobSchema).readonly(), {
28203
28526
  kind: "query",
28204
28527
  auth: "admin"
28528
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28529
+ kind: "query",
28530
+ auth: "admin"
28205
28531
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28206
28532
  kind: "mutation",
28207
28533
  auth: "admin"
@@ -34836,6 +35162,12 @@ Object.freeze({
34836
35162
  addonId: null,
34837
35163
  access: "create"
34838
35164
  },
35165
+ "pipelineAnalytics.countRelocatableMedia": {
35166
+ capName: "pipeline-analytics",
35167
+ capScope: "device",
35168
+ addonId: null,
35169
+ access: "view"
35170
+ },
34839
35171
  "pipelineAnalytics.countUnstampedEventMedia": {
34840
35172
  capName: "pipeline-analytics",
34841
35173
  capScope: "device",
@@ -36000,6 +36332,12 @@ Object.freeze({
36000
36332
  addonId: null,
36001
36333
  access: "view"
36002
36334
  },
36335
+ "recording.getRelocateResidue": {
36336
+ capName: "recording",
36337
+ capScope: "system",
36338
+ addonId: null,
36339
+ access: "view"
36340
+ },
36003
36341
  "recording.getStorageMigrationMoveStatus": {
36004
36342
  capName: "recording",
36005
36343
  capScope: "system",
@@ -36546,12 +36884,30 @@ Object.freeze({
36546
36884
  addonId: null,
36547
36885
  access: "create"
36548
36886
  },
36887
+ "storageMigration.drain": {
36888
+ capName: "storage-migration",
36889
+ capScope: "system",
36890
+ addonId: null,
36891
+ access: "create"
36892
+ },
36893
+ "storageMigration.movers": {
36894
+ capName: "storage-migration",
36895
+ capScope: "system",
36896
+ addonId: null,
36897
+ access: "view"
36898
+ },
36549
36899
  "storageMigration.plan": {
36550
36900
  capName: "storage-migration",
36551
36901
  capScope: "system",
36552
36902
  addonId: null,
36553
36903
  access: "view"
36554
36904
  },
36905
+ "storageMigration.residue": {
36906
+ capName: "storage-migration",
36907
+ capScope: "system",
36908
+ addonId: null,
36909
+ access: "view"
36910
+ },
36555
36911
  "storageMigration.start": {
36556
36912
  capName: "storage-migration",
36557
36913
  capScope: "system",
@@ -37386,6 +37742,12 @@ Object.freeze({
37386
37742
  addonId: null,
37387
37743
  access: "delete"
37388
37744
  },
37745
+ "vectorStore.fetchByIds": {
37746
+ capName: "vector-store",
37747
+ capScope: "system",
37748
+ addonId: null,
37749
+ access: "view"
37750
+ },
37389
37751
  "vectorStore.getByIds": {
37390
37752
  capName: "vector-store",
37391
37753
  capScope: "system",
@@ -37398,6 +37760,12 @@ Object.freeze({
37398
37760
  addonId: null,
37399
37761
  access: "view"
37400
37762
  },
37763
+ "vectorStore.scan": {
37764
+ capName: "vector-store",
37765
+ capScope: "system",
37766
+ addonId: null,
37767
+ access: "view"
37768
+ },
37401
37769
  "vectorStore.stats": {
37402
37770
  capName: "vector-store",
37403
37771
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -8005,6 +8005,21 @@ var RelocateJobSchema = object({
8005
8005
  bytesMoved: number().int(),
8006
8006
  /** Total files discovered up front; null while (or when) unknown. */
8007
8007
  filesTotal: number().int().nullable(),
8008
+ /**
8009
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8010
+ * made that nobody asked for, so it is reported where the operator reads the
8011
+ * job rather than only in a log line.
8012
+ *
8013
+ * A footage segment records its byte count in its own NAME, and the durable
8014
+ * hour row derives its aggregates from those names. A file that does not
8015
+ * match its name therefore makes the ledger's sums — and with them quota and
8016
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8017
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8018
+ *
8019
+ * Absent on lanes where the question has no meaning: a media blob's size is
8020
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8021
+ */
8022
+ rowsReconciled: number().int().nonnegative().optional(),
8008
8023
  startedAt: number(),
8009
8024
  finishedAt: number().nullable(),
8010
8025
  error: string().nullable()
@@ -8073,14 +8088,42 @@ var RelocateMediaInputSchema = object({
8073
8088
  /** Omitted = `move`, the pre-existing behaviour. */
8074
8089
  mode: MediaRelocateModeSchema.optional()
8075
8090
  });
8076
- /** How many rows still carry NO `locationId` — the population a repoint would
8077
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8078
- * value that permits a non-blocking `eventMedia` cutover. */
8079
- var UnstampedEventMediaCountSchema = object({
8080
- media: number().int().nonnegative(),
8081
- retrainFrames: number().int().nonnegative(),
8082
- total: number().int().nonnegative()
8091
+ /**
8092
+ * The unstamped population of ONE collection — split, because the gate and the
8093
+ * operator ask two different questions and only one of them has to be cheap.
8094
+ *
8095
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8096
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8097
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8098
+ * that matters — after a seal, when the population is empty.
8099
+ *
8100
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8101
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8102
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8103
+ * and useful answer: "there are some, and this read could not say how many"
8104
+ * still refuses the cutover, which is the whole job.
8105
+ */
8106
+ var UnstampedRowsSchema = object({
8107
+ present: boolean(),
8108
+ rows: number().int().nonnegative().nullable()
8083
8109
  });
8110
+ /**
8111
+ * How many rows still carry NO `locationId` — the population a repoint would
8112
+ * silently re-aim at a disk that does not hold their bytes.
8113
+ *
8114
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8115
+ * over. The gate opens on a measured absence and on nothing else; an unread
8116
+ * collection and an empty one are different facts, and this repo has already
8117
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8118
+ */
8119
+ var UnstampedEventMediaCountSchema = object({
8120
+ media: UnstampedRowsSchema,
8121
+ retrainFrames: UnstampedRowsSchema,
8122
+ /** True when EITHER collection holds one. The refusal reads this. */
8123
+ anyPresent: boolean(),
8124
+ /** Sum across both, or `null` when either lane could not be counted. */
8125
+ total: number().int().nonnegative().nullable()
8126
+ }).nullable();
8084
8127
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8085
8128
  /** The independently selectable logical storage classes — every class
8086
8129
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8166,13 +8209,53 @@ var StorageMigrationParticipantSchema = _enum([
8166
8209
  "recorder",
8167
8210
  "analytics"
8168
8211
  ]);
8212
+ /**
8213
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8214
+ *
8215
+ * The long half of a non-blocking migration is `draining`, and it is measured
8216
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8217
+ * existed the only place those numbers appeared was a Loki line, so an operator
8218
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8219
+ * afternoon.
8220
+ *
8221
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8222
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8223
+ * mover — which is the exact failure this is meant to end. The coordinator's
8224
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8225
+ * read `state`; folding the counters costs no extra read and makes the durable
8226
+ * record say afterwards how far a move actually got.
8227
+ *
8228
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8229
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8230
+ * cannot say M, and a 0 there would render as "100 % done".
8231
+ */
8232
+ var StorageMigrationMoveProgressSchema = object({
8233
+ filesMoved: number().int().nonnegative(),
8234
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8235
+ filesTotal: number().int().nonnegative().nullable(),
8236
+ bytesMoved: number().int().nonnegative(),
8237
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8238
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8239
+ * rows would be the same failure as one that silently skipped them. */
8240
+ rowsReconciled: number().int().nonnegative().optional(),
8241
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8242
+ * crash gets a new mover, and a rate computed from the migration's start
8243
+ * would silently average in the time nothing was running. */
8244
+ startedAt: number(),
8245
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8246
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8247
+ * subtract its own. */
8248
+ observedAt: number()
8249
+ });
8169
8250
  var StorageMigrationMoveSchema = object({
8170
8251
  storageClass: StorageMigrationClassSchema,
8171
8252
  fromLocationId: string(),
8172
8253
  toLocationId: string(),
8173
8254
  moverJobId: string().nullable(),
8174
8255
  state: RelocateJobStateSchema.nullable(),
8175
- error: string().nullable()
8256
+ error: string().nullable(),
8257
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8258
+ progress: StorageMigrationMoveProgressSchema.nullable()
8176
8259
  });
8177
8260
  var StorageMigrationJobSchema = object({
8178
8261
  jobId: string(),
@@ -8218,6 +8301,98 @@ var StorageMigrationPlanSchema = object({
8218
8301
  findings: array(StorageMigrationFindingSchema)
8219
8302
  });
8220
8303
  /**
8304
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8305
+ *
8306
+ * The coordinator's job record is the state of record for a migration, and its
8307
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8308
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8309
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8310
+ * way because no supported UI path existed. A mover armed like that has no job
8311
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8312
+ *
8313
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8314
+ * orchestrated it.
8315
+ */
8316
+ var StorageMigrationMoverSchema = object({
8317
+ lane: _enum(["footage", "media"]),
8318
+ job: RelocateJobSchema,
8319
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8320
+ * directly against the owning addon. */
8321
+ migrationJobId: string().nullable(),
8322
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8323
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8324
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8325
+ * rate made of two different clocks. */
8326
+ observedAt: number()
8327
+ });
8328
+ /**
8329
+ * What a SOURCE still holds for one storage class — the number that makes a
8330
+ * "drain remaining" action honest rather than hopeful.
8331
+ *
8332
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8333
+ * engine's own selection count for media), never from the resident index: a
8334
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8335
+ * never been told about (D295).
8336
+ *
8337
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8338
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8339
+ * because refusing on an unanswerable read would hide exactly the case an
8340
+ * operator needs to act on.
8341
+ */
8342
+ var StorageMigrationResidueSchema = object({
8343
+ storageClass: StorageMigrationClassSchema,
8344
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8345
+ * move from wherever they are rather than from one named source. */
8346
+ fromLocationId: string(),
8347
+ /** Where a drain would move it — the class's CURRENT default. */
8348
+ toLocationId: string(),
8349
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8350
+ items: number().int().nonnegative().nullable(),
8351
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8352
+ bytes: number().int().nonnegative().nullable()
8353
+ });
8354
+ /**
8355
+ * Run the DRAIN half and nothing else.
8356
+ *
8357
+ * A migration that reached `done` has already repointed, so `start` correctly
8358
+ * refuses its destination ("already the default") — there is nothing left to
8359
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8360
+ * or finish against a work list that was a tenth of the archive (D295), and
8361
+ * before this there was no supported way to run only that half: the only way
8362
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8363
+ *
8364
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8365
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8366
+ * re-repoint a class that is already migrated.
8367
+ */
8368
+ var StorageMigrationDrainInputSchema = object({
8369
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8370
+ * a class whose source is already empty is refused rather than started. */
8371
+ classes: array(StorageMigrationClassSchema).min(1),
8372
+ throttleMbps: number().min(1).max(1e3).optional()
8373
+ });
8374
+ /** What a footage source still holds, asked of the durable hour ledger. */
8375
+ var RelocateResidueInputSchema = object({
8376
+ fromLocationId: string().min(1),
8377
+ /** Narrow to one logical class; omit for every profile on the location. */
8378
+ footageClass: RelocateFootageClassSchema.optional()
8379
+ });
8380
+ /** `null` = the archive could not answer (no ledger on this node, or the
8381
+ * aggregate failed). Never conflated with an empty source. */
8382
+ var RelocateResidueSchema = object({
8383
+ segments: number().int().nonnegative(),
8384
+ bytes: number().int().nonnegative()
8385
+ }).nullable();
8386
+ /** How many rows a media pass would still act on against a given target — the
8387
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8388
+ * never disagree. `null` = the count could not be taken. */
8389
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8390
+ var RelocatableMediaCountInputSchema = object({
8391
+ toLocationId: string().min(1),
8392
+ /** Omitted = `move`. */
8393
+ mode: MediaRelocateModeSchema.optional()
8394
+ });
8395
+ /**
8221
8396
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8222
8397
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8223
8398
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8321,6 +8496,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8321
8496
  * two addons declaring the same `id` must agree on `cardinality` (validated
8322
8497
  * at kernel aggregation time, not here).
8323
8498
  */
8499
+ /**
8500
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8501
+ * actually reaches the bytes. It is the constraint that decides which
8502
+ * `storage-provider`s may back a location of that kind.
8503
+ *
8504
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8505
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8506
+ * post-analysis media roots). Only a provider that serves a genuine local
8507
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8508
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8509
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8510
+ * against a same-named local directory that is something else entirely.
8511
+ *
8512
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8513
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8514
+ * service never sees a path, so any provider can back it. `backups` is the
8515
+ * one kind that qualifies today.
8516
+ *
8517
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8518
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8519
+ * refused the configuration; the first write simply went somewhere wrong, and
8520
+ * a recording write that goes wrong surfaces as a silent black window rather
8521
+ * than an error (the read path does not `stat`). This turns that accident into
8522
+ * a declared, enforced, testable refusal.
8523
+ */
8524
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8324
8525
  var StorageLocationDeclarationSchema = object({
8325
8526
  /**
8326
8527
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8340,6 +8541,19 @@ var StorageLocationDeclarationSchema = object({
8340
8541
  */
8341
8542
  cardinality: _enum(["single", "multi"]),
8342
8543
  /**
8544
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8545
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8546
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8547
+ *
8548
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8549
+ * can only over-restrict (refuse a remote provider for a kind that might
8550
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8551
+ * permissive direction and is therefore never inferred — a repo guard
8552
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8553
+ * reached by omission.
8554
+ */
8555
+ access: StorageAccessSchema.optional(),
8556
+ /**
8343
8557
  * When set, the default instance for this location inherits its resolved
8344
8558
  * root from the named location's default instance. Useful for derivative
8345
8559
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18207,8 +18421,10 @@ var TrackSchema = object({
18207
18421
  lastSeen: number(),
18208
18422
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18209
18423
  positions: array(TrackPositionSchema).readonly(),
18210
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18211
- * saveThumbnails policy). */
18424
+ /** Periodic snapshots at snapshotIntervalMs cadence — DEBUG media, produced
18425
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18426
+ * the retired `saveThumbnails` used to gate this and the rolling
18427
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18212
18428
  snapshots: array(TrackSnapshotSchema).readonly(),
18213
18429
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18214
18430
  zonesVisited: array(string()).readonly(),
@@ -19068,7 +19284,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19068
19284
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19069
19285
  kind: "mutation",
19070
19286
  auth: "admin"
19071
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19287
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19288
+ kind: "query",
19289
+ auth: "admin"
19290
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19072
19291
  kind: "query",
19073
19292
  auth: "admin"
19074
19293
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21027,6 +21246,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21027
21246
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21028
21247
  kind: "mutation",
21029
21248
  auth: "admin"
21249
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21250
+ kind: "mutation",
21251
+ auth: "admin"
21030
21252
  });
21031
21253
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21032
21254
  providerId: string().min(1),
@@ -21425,12 +21647,38 @@ response: record(string(), unknown()) }), object({
21425
21647
  *
21426
21648
  * ## Why this is a capability and not a helper
21427
21649
  *
21428
- * Six stores in `addon-post-analysis` already hold vectors — object CLIP, face,
21429
- * plate, vehicle, identity, and the event store's derivatives — and every one of
21430
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21431
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21432
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21433
- * load 5,000 rows before ranking anything.
21650
+ * This capability was introduced with the claim that SIX stores in
21651
+ * `addon-post-analysis` held vectors in a `JSON` settings-store column — object
21652
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21653
+ * claim was never true, and leaving it here made five stores look like pending
21654
+ * work when three of them have no vector at all. Counted column by column on
21655
+ * 2026-08-30, exactly THREE ever held one:
21656
+ *
21657
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21658
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21659
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21660
+ * face, migrated 2026-08-30 into its OWN index (see below).
21661
+ *
21662
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21663
+ * and `identities` store a name; the event store stores no derivative vector.
21664
+ * They are not migration candidates and never were.
21665
+ *
21666
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21667
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21668
+ * rows before ranking anything.
21669
+ *
21670
+ * ## One index per COMPARISON, never per encoder
21671
+ *
21672
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21673
+ * model, and they still get two indexes. An index is a set of things that are
21674
+ * ranked against each other and that live and die together, and these two are
21675
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21676
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21677
+ * forever and is the gallery every recognition ranks against. One index would
21678
+ * mean every gallery load and every reconcile carried a filter whose failure
21679
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21680
+ * person's only sample. The dimension they share is not a reason to share an
21681
+ * index; the question they answer is, and it differs.
21434
21682
  *
21435
21683
  * The fix is not a faster loop, it is a different backend — and the backend
21436
21684
  * should be replaceable without touching six callers. So: a singleton
@@ -21535,7 +21783,20 @@ var VectorQueryResultSchema = object({
21535
21783
  */
21536
21784
  scanned: number(),
21537
21785
  /** True when the backend could not consider every row that passed the filter. */
21538
- truncated: boolean()
21786
+ truncated: boolean(),
21787
+ /**
21788
+ * The `topK` the backend actually ran with.
21789
+ *
21790
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21791
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21792
+ * own log rather than in its answer. That is how an audit asking for 20,000
21793
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21794
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21795
+ * MUCH, in the return value, where the caller cannot fail to see it.
21796
+ *
21797
+ * Equals the requested `topK` whenever nothing was lowered.
21798
+ */
21799
+ effectiveTopK: number().int().positive()
21539
21800
  });
21540
21801
  var VectorDeleteInputSchema = object({
21541
21802
  index: string(),
@@ -21564,6 +21825,68 @@ var VectorGetResultSchema = object({ items: array(object({
21564
21825
  id: string(),
21565
21826
  metadata: VectorMetadataSchema
21566
21827
  })) });
21828
+ /**
21829
+ * Ids to read back WITH their vectors.
21830
+ *
21831
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21832
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21833
+ * caller depends on that promise. This one promises the opposite.
21834
+ *
21835
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21836
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21837
+ * a per-face cross-process KNN would be a network round trip inside the
21838
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21839
+ * it requires the index to hand the floats back. Without this method the only
21840
+ * way to keep a readable vector is a JSON column, which is the thing this
21841
+ * capability exists to delete.
21842
+ *
21843
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21844
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21845
+ */
21846
+ var VectorFetchInputSchema = object({
21847
+ index: string(),
21848
+ ids: array(string())
21849
+ });
21850
+ var VectorFetchResultSchema = object({ items: array(object({
21851
+ id: string(),
21852
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21853
+ vector: string(),
21854
+ metadata: VectorMetadataSchema
21855
+ })) });
21856
+ /**
21857
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21858
+ *
21859
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21860
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21861
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21862
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21863
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21864
+ * looked" for as long as anyone cared to read it.
21865
+ *
21866
+ * This is the primitive that question actually needs: a bounded page, ordered
21867
+ * by the backend's own row order, costing no distance computation at all.
21868
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21869
+ * the full-table read this capability was built to stop.
21870
+ */
21871
+ var VectorScanInputSchema = object({
21872
+ index: string(),
21873
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21874
+ cursor: number().int().nonnegative().default(0),
21875
+ limit: number().int().positive()
21876
+ });
21877
+ var VectorScanResultSchema = object({
21878
+ items: array(object({
21879
+ id: string(),
21880
+ metadata: VectorMetadataSchema
21881
+ })),
21882
+ /**
21883
+ * Where the next page starts, or `null` when the walk reached the end.
21884
+ *
21885
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21886
+ * from a short page: a backend is free to return fewer rows than asked.
21887
+ */
21888
+ nextCursor: number().int().nonnegative().nullable()
21889
+ });
21567
21890
  var VectorStatsInputSchema = object({ index: string() });
21568
21891
  var VectorStatsResultSchema = object({
21569
21892
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21582,7 +21905,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21582
21905
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21583
21906
  kind: "mutation",
21584
21907
  auth: "admin"
21585
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21908
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21586
21909
  kind: "mutation",
21587
21910
  auth: "admin"
21588
21911
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -28201,6 +28524,9 @@ method(object({
28201
28524
  }), method(object({}), array(RelocateJobSchema).readonly(), {
28202
28525
  kind: "query",
28203
28526
  auth: "admin"
28527
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28528
+ kind: "query",
28529
+ auth: "admin"
28204
28530
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28205
28531
  kind: "mutation",
28206
28532
  auth: "admin"
@@ -34835,6 +35161,12 @@ Object.freeze({
34835
35161
  addonId: null,
34836
35162
  access: "create"
34837
35163
  },
35164
+ "pipelineAnalytics.countRelocatableMedia": {
35165
+ capName: "pipeline-analytics",
35166
+ capScope: "device",
35167
+ addonId: null,
35168
+ access: "view"
35169
+ },
34838
35170
  "pipelineAnalytics.countUnstampedEventMedia": {
34839
35171
  capName: "pipeline-analytics",
34840
35172
  capScope: "device",
@@ -35999,6 +36331,12 @@ Object.freeze({
35999
36331
  addonId: null,
36000
36332
  access: "view"
36001
36333
  },
36334
+ "recording.getRelocateResidue": {
36335
+ capName: "recording",
36336
+ capScope: "system",
36337
+ addonId: null,
36338
+ access: "view"
36339
+ },
36002
36340
  "recording.getStorageMigrationMoveStatus": {
36003
36341
  capName: "recording",
36004
36342
  capScope: "system",
@@ -36545,12 +36883,30 @@ Object.freeze({
36545
36883
  addonId: null,
36546
36884
  access: "create"
36547
36885
  },
36886
+ "storageMigration.drain": {
36887
+ capName: "storage-migration",
36888
+ capScope: "system",
36889
+ addonId: null,
36890
+ access: "create"
36891
+ },
36892
+ "storageMigration.movers": {
36893
+ capName: "storage-migration",
36894
+ capScope: "system",
36895
+ addonId: null,
36896
+ access: "view"
36897
+ },
36548
36898
  "storageMigration.plan": {
36549
36899
  capName: "storage-migration",
36550
36900
  capScope: "system",
36551
36901
  addonId: null,
36552
36902
  access: "view"
36553
36903
  },
36904
+ "storageMigration.residue": {
36905
+ capName: "storage-migration",
36906
+ capScope: "system",
36907
+ addonId: null,
36908
+ access: "view"
36909
+ },
36554
36910
  "storageMigration.start": {
36555
36911
  capName: "storage-migration",
36556
36912
  capScope: "system",
@@ -37385,6 +37741,12 @@ Object.freeze({
37385
37741
  addonId: null,
37386
37742
  access: "delete"
37387
37743
  },
37744
+ "vectorStore.fetchByIds": {
37745
+ capName: "vector-store",
37746
+ capScope: "system",
37747
+ addonId: null,
37748
+ access: "view"
37749
+ },
37388
37750
  "vectorStore.getByIds": {
37389
37751
  capName: "vector-store",
37390
37752
  capScope: "system",
@@ -37397,6 +37759,12 @@ Object.freeze({
37397
37759
  addonId: null,
37398
37760
  access: "view"
37399
37761
  },
37762
+ "vectorStore.scan": {
37763
+ capName: "vector-store",
37764
+ capScope: "system",
37765
+ addonId: null,
37766
+ access: "view"
37767
+ },
37400
37768
  "vectorStore.stats": {
37401
37769
  capName: "vector-store",
37402
37770
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-gree",
3
- "version": "0.2.42",
3
+ "version": "0.2.45",
4
4
  "description": "Gree air-conditioner device-provider addon for CamStack — wraps the @apocaliss92/nodegree local-UDP client (LAN discovery + AES control), exposing climate-control and fan-control",
5
5
  "keywords": [
6
6
  "camstack",