@camstack/addon-provider-amcrest 0.2.44 → 0.2.47

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
@@ -7996,6 +7996,21 @@ var RelocateJobSchema = object({
7996
7996
  bytesMoved: number().int(),
7997
7997
  /** Total files discovered up front; null while (or when) unknown. */
7998
7998
  filesTotal: number().int().nullable(),
7999
+ /**
8000
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8001
+ * made that nobody asked for, so it is reported where the operator reads the
8002
+ * job rather than only in a log line.
8003
+ *
8004
+ * A footage segment records its byte count in its own NAME, and the durable
8005
+ * hour row derives its aggregates from those names. A file that does not
8006
+ * match its name therefore makes the ledger's sums — and with them quota and
8007
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8008
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8009
+ *
8010
+ * Absent on lanes where the question has no meaning: a media blob's size is
8011
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8012
+ */
8013
+ rowsReconciled: number().int().nonnegative().optional(),
7999
8014
  startedAt: number(),
8000
8015
  finishedAt: number().nullable(),
8001
8016
  error: string().nullable()
@@ -8064,14 +8079,42 @@ var RelocateMediaInputSchema = object({
8064
8079
  /** Omitted = `move`, the pre-existing behaviour. */
8065
8080
  mode: MediaRelocateModeSchema.optional()
8066
8081
  });
8067
- /** How many rows still carry NO `locationId` — the population a repoint would
8068
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8069
- * value that permits a non-blocking `eventMedia` cutover. */
8070
- var UnstampedEventMediaCountSchema = object({
8071
- media: number().int().nonnegative(),
8072
- retrainFrames: number().int().nonnegative(),
8073
- total: number().int().nonnegative()
8082
+ /**
8083
+ * The unstamped population of ONE collection split, because the gate and the
8084
+ * operator ask two different questions and only one of them has to be cheap.
8085
+ *
8086
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8087
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8088
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8089
+ * that matters — after a seal, when the population is empty.
8090
+ *
8091
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8092
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8093
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8094
+ * and useful answer: "there are some, and this read could not say how many"
8095
+ * still refuses the cutover, which is the whole job.
8096
+ */
8097
+ var UnstampedRowsSchema = object({
8098
+ present: boolean(),
8099
+ rows: number().int().nonnegative().nullable()
8074
8100
  });
8101
+ /**
8102
+ * How many rows still carry NO `locationId` — the population a repoint would
8103
+ * silently re-aim at a disk that does not hold their bytes.
8104
+ *
8105
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8106
+ * over. The gate opens on a measured absence and on nothing else; an unread
8107
+ * collection and an empty one are different facts, and this repo has already
8108
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8109
+ */
8110
+ var UnstampedEventMediaCountSchema = object({
8111
+ media: UnstampedRowsSchema,
8112
+ retrainFrames: UnstampedRowsSchema,
8113
+ /** True when EITHER collection holds one. The refusal reads this. */
8114
+ anyPresent: boolean(),
8115
+ /** Sum across both, or `null` when either lane could not be counted. */
8116
+ total: number().int().nonnegative().nullable()
8117
+ }).nullable();
8075
8118
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8076
8119
  /** The independently selectable logical storage classes — every class
8077
8120
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8157,13 +8200,53 @@ var StorageMigrationParticipantSchema = _enum([
8157
8200
  "recorder",
8158
8201
  "analytics"
8159
8202
  ]);
8203
+ /**
8204
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8205
+ *
8206
+ * The long half of a non-blocking migration is `draining`, and it is measured
8207
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8208
+ * existed the only place those numbers appeared was a Loki line, so an operator
8209
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8210
+ * afternoon.
8211
+ *
8212
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8213
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8214
+ * mover — which is the exact failure this is meant to end. The coordinator's
8215
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8216
+ * read `state`; folding the counters costs no extra read and makes the durable
8217
+ * record say afterwards how far a move actually got.
8218
+ *
8219
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8220
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8221
+ * cannot say M, and a 0 there would render as "100 % done".
8222
+ */
8223
+ var StorageMigrationMoveProgressSchema = object({
8224
+ filesMoved: number().int().nonnegative(),
8225
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8226
+ filesTotal: number().int().nonnegative().nullable(),
8227
+ bytesMoved: number().int().nonnegative(),
8228
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8229
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8230
+ * rows would be the same failure as one that silently skipped them. */
8231
+ rowsReconciled: number().int().nonnegative().optional(),
8232
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8233
+ * crash gets a new mover, and a rate computed from the migration's start
8234
+ * would silently average in the time nothing was running. */
8235
+ startedAt: number(),
8236
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8237
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8238
+ * subtract its own. */
8239
+ observedAt: number()
8240
+ });
8160
8241
  var StorageMigrationMoveSchema = object({
8161
8242
  storageClass: StorageMigrationClassSchema,
8162
8243
  fromLocationId: string(),
8163
8244
  toLocationId: string(),
8164
8245
  moverJobId: string().nullable(),
8165
8246
  state: RelocateJobStateSchema.nullable(),
8166
- error: string().nullable()
8247
+ error: string().nullable(),
8248
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8249
+ progress: StorageMigrationMoveProgressSchema.nullable()
8167
8250
  });
8168
8251
  var StorageMigrationJobSchema = object({
8169
8252
  jobId: string(),
@@ -8209,6 +8292,98 @@ var StorageMigrationPlanSchema = object({
8209
8292
  findings: array(StorageMigrationFindingSchema)
8210
8293
  });
8211
8294
  /**
8295
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8296
+ *
8297
+ * The coordinator's job record is the state of record for a migration, and its
8298
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8299
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8300
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8301
+ * way because no supported UI path existed. A mover armed like that has no job
8302
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8303
+ *
8304
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8305
+ * orchestrated it.
8306
+ */
8307
+ var StorageMigrationMoverSchema = object({
8308
+ lane: _enum(["footage", "media"]),
8309
+ job: RelocateJobSchema,
8310
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8311
+ * directly against the owning addon. */
8312
+ migrationJobId: string().nullable(),
8313
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8314
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8315
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8316
+ * rate made of two different clocks. */
8317
+ observedAt: number()
8318
+ });
8319
+ /**
8320
+ * What a SOURCE still holds for one storage class — the number that makes a
8321
+ * "drain remaining" action honest rather than hopeful.
8322
+ *
8323
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8324
+ * engine's own selection count for media), never from the resident index: a
8325
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8326
+ * never been told about (D295).
8327
+ *
8328
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8329
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8330
+ * because refusing on an unanswerable read would hide exactly the case an
8331
+ * operator needs to act on.
8332
+ */
8333
+ var StorageMigrationResidueSchema = object({
8334
+ storageClass: StorageMigrationClassSchema,
8335
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8336
+ * move from wherever they are rather than from one named source. */
8337
+ fromLocationId: string(),
8338
+ /** Where a drain would move it — the class's CURRENT default. */
8339
+ toLocationId: string(),
8340
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8341
+ items: number().int().nonnegative().nullable(),
8342
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8343
+ bytes: number().int().nonnegative().nullable()
8344
+ });
8345
+ /**
8346
+ * Run the DRAIN half and nothing else.
8347
+ *
8348
+ * A migration that reached `done` has already repointed, so `start` correctly
8349
+ * refuses its destination ("already the default") — there is nothing left to
8350
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8351
+ * or finish against a work list that was a tenth of the archive (D295), and
8352
+ * before this there was no supported way to run only that half: the only way
8353
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8354
+ *
8355
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8356
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8357
+ * re-repoint a class that is already migrated.
8358
+ */
8359
+ var StorageMigrationDrainInputSchema = object({
8360
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8361
+ * a class whose source is already empty is refused rather than started. */
8362
+ classes: array(StorageMigrationClassSchema).min(1),
8363
+ throttleMbps: number().min(1).max(1e3).optional()
8364
+ });
8365
+ /** What a footage source still holds, asked of the durable hour ledger. */
8366
+ var RelocateResidueInputSchema = object({
8367
+ fromLocationId: string().min(1),
8368
+ /** Narrow to one logical class; omit for every profile on the location. */
8369
+ footageClass: RelocateFootageClassSchema.optional()
8370
+ });
8371
+ /** `null` = the archive could not answer (no ledger on this node, or the
8372
+ * aggregate failed). Never conflated with an empty source. */
8373
+ var RelocateResidueSchema = object({
8374
+ segments: number().int().nonnegative(),
8375
+ bytes: number().int().nonnegative()
8376
+ }).nullable();
8377
+ /** How many rows a media pass would still act on against a given target — the
8378
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8379
+ * never disagree. `null` = the count could not be taken. */
8380
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8381
+ var RelocatableMediaCountInputSchema = object({
8382
+ toLocationId: string().min(1),
8383
+ /** Omitted = `move`. */
8384
+ mode: MediaRelocateModeSchema.optional()
8385
+ });
8386
+ /**
8212
8387
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8213
8388
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8214
8389
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8312,6 +8487,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8312
8487
  * two addons declaring the same `id` must agree on `cardinality` (validated
8313
8488
  * at kernel aggregation time, not here).
8314
8489
  */
8490
+ /**
8491
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8492
+ * actually reaches the bytes. It is the constraint that decides which
8493
+ * `storage-provider`s may back a location of that kind.
8494
+ *
8495
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8496
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8497
+ * post-analysis media roots). Only a provider that serves a genuine local
8498
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8499
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8500
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8501
+ * against a same-named local directory that is something else entirely.
8502
+ *
8503
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8504
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8505
+ * service never sees a path, so any provider can back it. `backups` is the
8506
+ * one kind that qualifies today.
8507
+ *
8508
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8509
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8510
+ * refused the configuration; the first write simply went somewhere wrong, and
8511
+ * a recording write that goes wrong surfaces as a silent black window rather
8512
+ * than an error (the read path does not `stat`). This turns that accident into
8513
+ * a declared, enforced, testable refusal.
8514
+ */
8515
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8315
8516
  var StorageLocationDeclarationSchema = object({
8316
8517
  /**
8317
8518
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8331,6 +8532,19 @@ var StorageLocationDeclarationSchema = object({
8331
8532
  */
8332
8533
  cardinality: _enum(["single", "multi"]),
8333
8534
  /**
8535
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8536
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8537
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8538
+ *
8539
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8540
+ * can only over-restrict (refuse a remote provider for a kind that might
8541
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8542
+ * permissive direction and is therefore never inferred — a repo guard
8543
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8544
+ * reached by omission.
8545
+ */
8546
+ access: StorageAccessSchema.optional(),
8547
+ /**
8334
8548
  * When set, the default instance for this location inherits its resolved
8335
8549
  * root from the named location's default instance. Useful for derivative
8336
8550
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18198,8 +18412,10 @@ var TrackSchema = object({
18198
18412
  lastSeen: number(),
18199
18413
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18200
18414
  positions: array(TrackPositionSchema).readonly(),
18201
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18202
- * saveThumbnails policy). */
18415
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
18416
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18417
+ * the retired `saveThumbnails` used to gate this and the rolling
18418
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18203
18419
  snapshots: array(TrackSnapshotSchema).readonly(),
18204
18420
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18205
18421
  zonesVisited: array(string()).readonly(),
@@ -19059,7 +19275,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19059
19275
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19060
19276
  kind: "mutation",
19061
19277
  auth: "admin"
19062
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19278
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19279
+ kind: "query",
19280
+ auth: "admin"
19281
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19063
19282
  kind: "query",
19064
19283
  auth: "admin"
19065
19284
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21122,6 +21341,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21122
21341
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21123
21342
  kind: "mutation",
21124
21343
  auth: "admin"
21344
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21345
+ kind: "mutation",
21346
+ auth: "admin"
21125
21347
  });
21126
21348
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21127
21349
  providerId: string().min(1),
@@ -21520,12 +21742,38 @@ response: record(string(), unknown()) }), object({
21520
21742
  *
21521
21743
  * ## Why this is a capability and not a helper
21522
21744
  *
21523
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
21524
- * plate, vehicle, identity, and the event store's derivativesand every one of
21525
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21526
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21527
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21528
- * load 5,000 rows before ranking anything.
21745
+ * This capability was introduced with the claim that SIX stores in
21746
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
21747
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21748
+ * claim was never true, and leaving it here made five stores look like pending
21749
+ * work when three of them have no vector at all. Counted column by column on
21750
+ * 2026-08-30, exactly THREE ever held one:
21751
+ *
21752
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21753
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21754
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21755
+ * face, migrated 2026-08-30 into its OWN index (see below).
21756
+ *
21757
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21758
+ * and `identities` store a name; the event store stores no derivative vector.
21759
+ * They are not migration candidates and never were.
21760
+ *
21761
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21762
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21763
+ * rows before ranking anything.
21764
+ *
21765
+ * ## One index per COMPARISON, never per encoder
21766
+ *
21767
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21768
+ * model, and they still get two indexes. An index is a set of things that are
21769
+ * ranked against each other and that live and die together, and these two are
21770
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21771
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21772
+ * forever and is the gallery every recognition ranks against. One index would
21773
+ * mean every gallery load and every reconcile carried a filter whose failure
21774
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21775
+ * person's only sample. The dimension they share is not a reason to share an
21776
+ * index; the question they answer is, and it differs.
21529
21777
  *
21530
21778
  * The fix is not a faster loop, it is a different backend — and the backend
21531
21779
  * should be replaceable without touching six callers. So: a singleton
@@ -21630,7 +21878,20 @@ var VectorQueryResultSchema = object({
21630
21878
  */
21631
21879
  scanned: number(),
21632
21880
  /** True when the backend could not consider every row that passed the filter. */
21633
- truncated: boolean()
21881
+ truncated: boolean(),
21882
+ /**
21883
+ * The `topK` the backend actually ran with.
21884
+ *
21885
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21886
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21887
+ * own log rather than in its answer. That is how an audit asking for 20,000
21888
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21889
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21890
+ * MUCH, in the return value, where the caller cannot fail to see it.
21891
+ *
21892
+ * Equals the requested `topK` whenever nothing was lowered.
21893
+ */
21894
+ effectiveTopK: number().int().positive()
21634
21895
  });
21635
21896
  var VectorDeleteInputSchema = object({
21636
21897
  index: string(),
@@ -21659,6 +21920,68 @@ var VectorGetResultSchema = object({ items: array(object({
21659
21920
  id: string(),
21660
21921
  metadata: VectorMetadataSchema
21661
21922
  })) });
21923
+ /**
21924
+ * Ids to read back WITH their vectors.
21925
+ *
21926
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21927
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21928
+ * caller depends on that promise. This one promises the opposite.
21929
+ *
21930
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21931
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21932
+ * a per-face cross-process KNN would be a network round trip inside the
21933
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21934
+ * it requires the index to hand the floats back. Without this method the only
21935
+ * way to keep a readable vector is a JSON column, which is the thing this
21936
+ * capability exists to delete.
21937
+ *
21938
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21939
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21940
+ */
21941
+ var VectorFetchInputSchema = object({
21942
+ index: string(),
21943
+ ids: array(string())
21944
+ });
21945
+ var VectorFetchResultSchema = object({ items: array(object({
21946
+ id: string(),
21947
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21948
+ vector: string(),
21949
+ metadata: VectorMetadataSchema
21950
+ })) });
21951
+ /**
21952
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21953
+ *
21954
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21955
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21956
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21957
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21958
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21959
+ * looked" for as long as anyone cared to read it.
21960
+ *
21961
+ * This is the primitive that question actually needs: a bounded page, ordered
21962
+ * by the backend's own row order, costing no distance computation at all.
21963
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21964
+ * the full-table read this capability was built to stop.
21965
+ */
21966
+ var VectorScanInputSchema = object({
21967
+ index: string(),
21968
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21969
+ cursor: number().int().nonnegative().default(0),
21970
+ limit: number().int().positive()
21971
+ });
21972
+ var VectorScanResultSchema = object({
21973
+ items: array(object({
21974
+ id: string(),
21975
+ metadata: VectorMetadataSchema
21976
+ })),
21977
+ /**
21978
+ * Where the next page starts, or `null` when the walk reached the end.
21979
+ *
21980
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21981
+ * from a short page: a backend is free to return fewer rows than asked.
21982
+ */
21983
+ nextCursor: number().int().nonnegative().nullable()
21984
+ });
21662
21985
  var VectorStatsInputSchema = object({ index: string() });
21663
21986
  var VectorStatsResultSchema = object({
21664
21987
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21677,7 +22000,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21677
22000
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21678
22001
  kind: "mutation",
21679
22002
  auth: "admin"
21680
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22003
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21681
22004
  kind: "mutation",
21682
22005
  auth: "admin"
21683
22006
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -28384,6 +28707,9 @@ method(object({
28384
28707
  }), method(object({}), array(RelocateJobSchema).readonly(), {
28385
28708
  kind: "query",
28386
28709
  auth: "admin"
28710
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28711
+ kind: "query",
28712
+ auth: "admin"
28387
28713
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28388
28714
  kind: "mutation",
28389
28715
  auth: "admin"
@@ -35422,6 +35748,12 @@ Object.freeze({
35422
35748
  addonId: null,
35423
35749
  access: "create"
35424
35750
  },
35751
+ "pipelineAnalytics.countRelocatableMedia": {
35752
+ capName: "pipeline-analytics",
35753
+ capScope: "device",
35754
+ addonId: null,
35755
+ access: "view"
35756
+ },
35425
35757
  "pipelineAnalytics.countUnstampedEventMedia": {
35426
35758
  capName: "pipeline-analytics",
35427
35759
  capScope: "device",
@@ -36586,6 +36918,12 @@ Object.freeze({
36586
36918
  addonId: null,
36587
36919
  access: "view"
36588
36920
  },
36921
+ "recording.getRelocateResidue": {
36922
+ capName: "recording",
36923
+ capScope: "system",
36924
+ addonId: null,
36925
+ access: "view"
36926
+ },
36589
36927
  "recording.getStorageMigrationMoveStatus": {
36590
36928
  capName: "recording",
36591
36929
  capScope: "system",
@@ -37132,12 +37470,30 @@ Object.freeze({
37132
37470
  addonId: null,
37133
37471
  access: "create"
37134
37472
  },
37473
+ "storageMigration.drain": {
37474
+ capName: "storage-migration",
37475
+ capScope: "system",
37476
+ addonId: null,
37477
+ access: "create"
37478
+ },
37479
+ "storageMigration.movers": {
37480
+ capName: "storage-migration",
37481
+ capScope: "system",
37482
+ addonId: null,
37483
+ access: "view"
37484
+ },
37135
37485
  "storageMigration.plan": {
37136
37486
  capName: "storage-migration",
37137
37487
  capScope: "system",
37138
37488
  addonId: null,
37139
37489
  access: "view"
37140
37490
  },
37491
+ "storageMigration.residue": {
37492
+ capName: "storage-migration",
37493
+ capScope: "system",
37494
+ addonId: null,
37495
+ access: "view"
37496
+ },
37141
37497
  "storageMigration.start": {
37142
37498
  capName: "storage-migration",
37143
37499
  capScope: "system",
@@ -37972,6 +38328,12 @@ Object.freeze({
37972
38328
  addonId: null,
37973
38329
  access: "delete"
37974
38330
  },
38331
+ "vectorStore.fetchByIds": {
38332
+ capName: "vector-store",
38333
+ capScope: "system",
38334
+ addonId: null,
38335
+ access: "view"
38336
+ },
37975
38337
  "vectorStore.getByIds": {
37976
38338
  capName: "vector-store",
37977
38339
  capScope: "system",
@@ -37984,6 +38346,12 @@ Object.freeze({
37984
38346
  addonId: null,
37985
38347
  access: "view"
37986
38348
  },
38349
+ "vectorStore.scan": {
38350
+ capName: "vector-store",
38351
+ capScope: "system",
38352
+ addonId: null,
38353
+ access: "view"
38354
+ },
37987
38355
  "vectorStore.stats": {
37988
38356
  capName: "vector-store",
37989
38357
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -7997,6 +7997,21 @@ var RelocateJobSchema = object({
7997
7997
  bytesMoved: number().int(),
7998
7998
  /** Total files discovered up front; null while (or when) unknown. */
7999
7999
  filesTotal: number().int().nullable(),
8000
+ /**
8001
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8002
+ * made that nobody asked for, so it is reported where the operator reads the
8003
+ * job rather than only in a log line.
8004
+ *
8005
+ * A footage segment records its byte count in its own NAME, and the durable
8006
+ * hour row derives its aggregates from those names. A file that does not
8007
+ * match its name therefore makes the ledger's sums — and with them quota and
8008
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8009
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8010
+ *
8011
+ * Absent on lanes where the question has no meaning: a media blob's size is
8012
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8013
+ */
8014
+ rowsReconciled: number().int().nonnegative().optional(),
8000
8015
  startedAt: number(),
8001
8016
  finishedAt: number().nullable(),
8002
8017
  error: string().nullable()
@@ -8065,14 +8080,42 @@ var RelocateMediaInputSchema = object({
8065
8080
  /** Omitted = `move`, the pre-existing behaviour. */
8066
8081
  mode: MediaRelocateModeSchema.optional()
8067
8082
  });
8068
- /** How many rows still carry NO `locationId` — the population a repoint would
8069
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8070
- * value that permits a non-blocking `eventMedia` cutover. */
8071
- var UnstampedEventMediaCountSchema = object({
8072
- media: number().int().nonnegative(),
8073
- retrainFrames: number().int().nonnegative(),
8074
- total: number().int().nonnegative()
8083
+ /**
8084
+ * The unstamped population of ONE collection split, because the gate and the
8085
+ * operator ask two different questions and only one of them has to be cheap.
8086
+ *
8087
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8088
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8089
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8090
+ * that matters — after a seal, when the population is empty.
8091
+ *
8092
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8093
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8094
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8095
+ * and useful answer: "there are some, and this read could not say how many"
8096
+ * still refuses the cutover, which is the whole job.
8097
+ */
8098
+ var UnstampedRowsSchema = object({
8099
+ present: boolean(),
8100
+ rows: number().int().nonnegative().nullable()
8075
8101
  });
8102
+ /**
8103
+ * How many rows still carry NO `locationId` — the population a repoint would
8104
+ * silently re-aim at a disk that does not hold their bytes.
8105
+ *
8106
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8107
+ * over. The gate opens on a measured absence and on nothing else; an unread
8108
+ * collection and an empty one are different facts, and this repo has already
8109
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8110
+ */
8111
+ var UnstampedEventMediaCountSchema = object({
8112
+ media: UnstampedRowsSchema,
8113
+ retrainFrames: UnstampedRowsSchema,
8114
+ /** True when EITHER collection holds one. The refusal reads this. */
8115
+ anyPresent: boolean(),
8116
+ /** Sum across both, or `null` when either lane could not be counted. */
8117
+ total: number().int().nonnegative().nullable()
8118
+ }).nullable();
8076
8119
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8077
8120
  /** The independently selectable logical storage classes — every class
8078
8121
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8158,13 +8201,53 @@ var StorageMigrationParticipantSchema = _enum([
8158
8201
  "recorder",
8159
8202
  "analytics"
8160
8203
  ]);
8204
+ /**
8205
+ * The mover's own numbers, folded onto the coordinator's durable move record.
8206
+ *
8207
+ * The long half of a non-blocking migration is `draining`, and it is measured
8208
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
8209
+ * existed the only place those numbers appeared was a Loki line, so an operator
8210
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
8211
+ * afternoon.
8212
+ *
8213
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
8214
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
8215
+ * mover — which is the exact failure this is meant to end. The coordinator's
8216
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
8217
+ * read `state`; folding the counters costs no extra read and makes the durable
8218
+ * record say afterwards how far a move actually got.
8219
+ *
8220
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
8221
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
8222
+ * cannot say M, and a 0 there would render as "100 % done".
8223
+ */
8224
+ var StorageMigrationMoveProgressSchema = object({
8225
+ filesMoved: number().int().nonnegative(),
8226
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8227
+ filesTotal: number().int().nonnegative().nullable(),
8228
+ bytesMoved: number().int().nonnegative(),
8229
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8230
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8231
+ * rows would be the same failure as one that silently skipped them. */
8232
+ rowsReconciled: number().int().nonnegative().optional(),
8233
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
8234
+ * crash gets a new mover, and a rate computed from the migration's start
8235
+ * would silently average in the time nothing was running. */
8236
+ startedAt: number(),
8237
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
8238
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
8239
+ * subtract its own. */
8240
+ observedAt: number()
8241
+ });
8161
8242
  var StorageMigrationMoveSchema = object({
8162
8243
  storageClass: StorageMigrationClassSchema,
8163
8244
  fromLocationId: string(),
8164
8245
  toLocationId: string(),
8165
8246
  moverJobId: string().nullable(),
8166
8247
  state: RelocateJobStateSchema.nullable(),
8167
- error: string().nullable()
8248
+ error: string().nullable(),
8249
+ /** Last observed mover counters; `null` until the mover has been polled once. */
8250
+ progress: StorageMigrationMoveProgressSchema.nullable()
8168
8251
  });
8169
8252
  var StorageMigrationJobSchema = object({
8170
8253
  jobId: string(),
@@ -8210,6 +8293,98 @@ var StorageMigrationPlanSchema = object({
8210
8293
  findings: array(StorageMigrationFindingSchema)
8211
8294
  });
8212
8295
  /**
8296
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
8297
+ *
8298
+ * The coordinator's job record is the state of record for a migration, and its
8299
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
8300
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
8301
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
8302
+ * way because no supported UI path existed. A mover armed like that has no job
8303
+ * to fold progress into, so it has to be readable on its own or it is invisible.
8304
+ *
8305
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
8306
+ * orchestrated it.
8307
+ */
8308
+ var StorageMigrationMoverSchema = object({
8309
+ lane: _enum(["footage", "media"]),
8310
+ job: RelocateJobSchema,
8311
+ /** The coordinator job that armed this mover, or `null` for a mover armed
8312
+ * directly against the owning addon. */
8313
+ migrationJobId: string().nullable(),
8314
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
8315
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
8316
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
8317
+ * rate made of two different clocks. */
8318
+ observedAt: number()
8319
+ });
8320
+ /**
8321
+ * What a SOURCE still holds for one storage class — the number that makes a
8322
+ * "drain remaining" action honest rather than hopeful.
8323
+ *
8324
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
8325
+ * engine's own selection count for media), never from the resident index: a
8326
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
8327
+ * never been told about (D295).
8328
+ *
8329
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
8330
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
8331
+ * because refusing on an unanswerable read would hide exactly the case an
8332
+ * operator needs to act on.
8333
+ */
8334
+ var StorageMigrationResidueSchema = object({
8335
+ storageClass: StorageMigrationClassSchema,
8336
+ /** The location still holding the data. `'*'` for the media lane, whose rows
8337
+ * move from wherever they are rather than from one named source. */
8338
+ fromLocationId: string(),
8339
+ /** Where a drain would move it — the class's CURRENT default. */
8340
+ toLocationId: string(),
8341
+ /** Segments (footage lane) or rows (media lane) still on the source. */
8342
+ items: number().int().nonnegative().nullable(),
8343
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
8344
+ bytes: number().int().nonnegative().nullable()
8345
+ });
8346
+ /**
8347
+ * Run the DRAIN half and nothing else.
8348
+ *
8349
+ * A migration that reached `done` has already repointed, so `start` correctly
8350
+ * refuses its destination ("already the default") — there is nothing left to
8351
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
8352
+ * or finish against a work list that was a tenth of the archive (D295), and
8353
+ * before this there was no supported way to run only that half: the only way
8354
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
8355
+ *
8356
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
8357
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
8358
+ * re-repoint a class that is already migrated.
8359
+ */
8360
+ var StorageMigrationDrainInputSchema = object({
8361
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
8362
+ * a class whose source is already empty is refused rather than started. */
8363
+ classes: array(StorageMigrationClassSchema).min(1),
8364
+ throttleMbps: number().min(1).max(1e3).optional()
8365
+ });
8366
+ /** What a footage source still holds, asked of the durable hour ledger. */
8367
+ var RelocateResidueInputSchema = object({
8368
+ fromLocationId: string().min(1),
8369
+ /** Narrow to one logical class; omit for every profile on the location. */
8370
+ footageClass: RelocateFootageClassSchema.optional()
8371
+ });
8372
+ /** `null` = the archive could not answer (no ledger on this node, or the
8373
+ * aggregate failed). Never conflated with an empty source. */
8374
+ var RelocateResidueSchema = object({
8375
+ segments: number().int().nonnegative(),
8376
+ bytes: number().int().nonnegative()
8377
+ }).nullable();
8378
+ /** How many rows a media pass would still act on against a given target — the
8379
+ * media lane's denominator AND its residue, from ONE derivation so the two can
8380
+ * never disagree. `null` = the count could not be taken. */
8381
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
8382
+ var RelocatableMediaCountInputSchema = object({
8383
+ toLocationId: string().min(1),
8384
+ /** Omitted = `move`. */
8385
+ mode: MediaRelocateModeSchema.optional()
8386
+ });
8387
+ /**
8213
8388
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
8214
8389
  * storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
8215
8390
  * so the persisted record schema and the consumer-facing cap can both consume it
@@ -8313,6 +8488,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
8313
8488
  * two addons declaring the same `id` must agree on `cardinality` (validated
8314
8489
  * at kernel aggregation time, not here).
8315
8490
  */
8491
+ /**
8492
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
8493
+ * actually reaches the bytes. It is the constraint that decides which
8494
+ * `storage-provider`s may back a location of that kind.
8495
+ *
8496
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
8497
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
8498
+ * post-analysis media roots). Only a provider that serves a genuine local
8499
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
8500
+ * remote provider's `resolve` returns a path on the REMOTE host, and
8501
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
8502
+ * against a same-named local directory that is something else entirely.
8503
+ *
8504
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
8505
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
8506
+ * service never sees a path, so any provider can back it. `backups` is the
8507
+ * one kind that qualifies today.
8508
+ *
8509
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
8510
+ * an EMERGENT property of how the recorder happened to be written. Nothing
8511
+ * refused the configuration; the first write simply went somewhere wrong, and
8512
+ * a recording write that goes wrong surfaces as a silent black window rather
8513
+ * than an error (the read path does not `stat`). This turns that accident into
8514
+ * a declared, enforced, testable refusal.
8515
+ */
8516
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
8316
8517
  var StorageLocationDeclarationSchema = object({
8317
8518
  /**
8318
8519
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -8332,6 +8533,19 @@ var StorageLocationDeclarationSchema = object({
8332
8533
  */
8333
8534
  cardinality: _enum(["single", "multi"]),
8334
8535
  /**
8536
+ * HOW the declaring service reaches the bytes — and therefore WHICH
8537
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
8538
+ * and {@link STORAGE_ACCESS_FALLBACK}.
8539
+ *
8540
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
8541
+ * can only over-restrict (refuse a remote provider for a kind that might
8542
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
8543
+ * permissive direction and is therefore never inferred — a repo guard
8544
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
8545
+ * reached by omission.
8546
+ */
8547
+ access: StorageAccessSchema.optional(),
8548
+ /**
8335
8549
  * When set, the default instance for this location inherits its resolved
8336
8550
  * root from the named location's default instance. Useful for derivative
8337
8551
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -18199,8 +18413,10 @@ var TrackSchema = object({
18199
18413
  lastSeen: number(),
18200
18414
  /** Frame-rate position history (subject to maxPositionHistory cap). */
18201
18415
  positions: array(TrackPositionSchema).readonly(),
18202
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
18203
- * saveThumbnails policy). */
18416
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
18417
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
18418
+ * the retired `saveThumbnails` used to gate this and the rolling
18419
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
18204
18420
  snapshots: array(TrackSnapshotSchema).readonly(),
18205
18421
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
18206
18422
  zonesVisited: array(string()).readonly(),
@@ -19060,7 +19276,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19060
19276
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19061
19277
  kind: "mutation",
19062
19278
  auth: "admin"
19063
- }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(object({}), array(RelocateJobSchema).readonly(), {
19279
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
19280
+ kind: "query",
19281
+ auth: "admin"
19282
+ }), method(object({}), array(RelocateJobSchema).readonly(), {
19064
19283
  kind: "query",
19065
19284
  auth: "admin"
19066
19285
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
@@ -21123,6 +21342,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21123
21342
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21124
21343
  kind: "mutation",
21125
21344
  auth: "admin"
21345
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
21346
+ kind: "mutation",
21347
+ auth: "admin"
21126
21348
  });
21127
21349
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21128
21350
  providerId: string().min(1),
@@ -21521,12 +21743,38 @@ response: record(string(), unknown()) }), object({
21521
21743
  *
21522
21744
  * ## Why this is a capability and not a helper
21523
21745
  *
21524
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
21525
- * plate, vehicle, identity, and the event store's derivativesand every one of
21526
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
21527
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
21528
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
21529
- * load 5,000 rows before ranking anything.
21746
+ * This capability was introduced with the claim that SIX stores in
21747
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
21748
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
21749
+ * claim was never true, and leaving it here made five stores look like pending
21750
+ * work when three of them have no vector at all. Counted column by column on
21751
+ * 2026-08-30, exactly THREE ever held one:
21752
+ *
21753
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
21754
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
21755
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
21756
+ * face, migrated 2026-08-30 into its OWN index (see below).
21757
+ *
21758
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
21759
+ * and `identities` store a name; the event store stores no derivative vector.
21760
+ * They are not migration candidates and never were.
21761
+ *
21762
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
21763
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
21764
+ * rows before ranking anything.
21765
+ *
21766
+ * ## One index per COMPARISON, never per encoder
21767
+ *
21768
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
21769
+ * model, and they still get two indexes. An index is a set of things that are
21770
+ * ranked against each other and that live and die together, and these two are
21771
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
21772
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
21773
+ * forever and is the gallery every recognition ranks against. One index would
21774
+ * mean every gallery load and every reconcile carried a filter whose failure
21775
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
21776
+ * person's only sample. The dimension they share is not a reason to share an
21777
+ * index; the question they answer is, and it differs.
21530
21778
  *
21531
21779
  * The fix is not a faster loop, it is a different backend — and the backend
21532
21780
  * should be replaceable without touching six callers. So: a singleton
@@ -21631,7 +21879,20 @@ var VectorQueryResultSchema = object({
21631
21879
  */
21632
21880
  scanned: number(),
21633
21881
  /** True when the backend could not consider every row that passed the filter. */
21634
- truncated: boolean()
21882
+ truncated: boolean(),
21883
+ /**
21884
+ * The `topK` the backend actually ran with.
21885
+ *
21886
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
21887
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
21888
+ * own log rather than in its answer. That is how an audit asking for 20,000
21889
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
21890
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
21891
+ * MUCH, in the return value, where the caller cannot fail to see it.
21892
+ *
21893
+ * Equals the requested `topK` whenever nothing was lowered.
21894
+ */
21895
+ effectiveTopK: number().int().positive()
21635
21896
  });
21636
21897
  var VectorDeleteInputSchema = object({
21637
21898
  index: string(),
@@ -21660,6 +21921,68 @@ var VectorGetResultSchema = object({ items: array(object({
21660
21921
  id: string(),
21661
21922
  metadata: VectorMetadataSchema
21662
21923
  })) });
21924
+ /**
21925
+ * Ids to read back WITH their vectors.
21926
+ *
21927
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
21928
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
21929
+ * caller depends on that promise. This one promises the opposite.
21930
+ *
21931
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
21932
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
21933
+ * a per-face cross-process KNN would be a network round trip inside the
21934
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
21935
+ * it requires the index to hand the floats back. Without this method the only
21936
+ * way to keep a readable vector is a JSON column, which is the thing this
21937
+ * capability exists to delete.
21938
+ *
21939
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
21940
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
21941
+ */
21942
+ var VectorFetchInputSchema = object({
21943
+ index: string(),
21944
+ ids: array(string())
21945
+ });
21946
+ var VectorFetchResultSchema = object({ items: array(object({
21947
+ id: string(),
21948
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
21949
+ vector: string(),
21950
+ metadata: VectorMetadataSchema
21951
+ })) });
21952
+ /**
21953
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
21954
+ *
21955
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
21956
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
21957
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
21958
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
21959
+ * distance to every row is degenerate. `examined: 4096` then read as "we
21960
+ * looked" for as long as anyone cared to read it.
21961
+ *
21962
+ * This is the primitive that question actually needs: a bounded page, ordered
21963
+ * by the backend's own row order, costing no distance computation at all.
21964
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
21965
+ * the full-table read this capability was built to stop.
21966
+ */
21967
+ var VectorScanInputSchema = object({
21968
+ index: string(),
21969
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
21970
+ cursor: number().int().nonnegative().default(0),
21971
+ limit: number().int().positive()
21972
+ });
21973
+ var VectorScanResultSchema = object({
21974
+ items: array(object({
21975
+ id: string(),
21976
+ metadata: VectorMetadataSchema
21977
+ })),
21978
+ /**
21979
+ * Where the next page starts, or `null` when the walk reached the end.
21980
+ *
21981
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
21982
+ * from a short page: a backend is free to return fewer rows than asked.
21983
+ */
21984
+ nextCursor: number().int().nonnegative().nullable()
21985
+ });
21663
21986
  var VectorStatsInputSchema = object({ index: string() });
21664
21987
  var VectorStatsResultSchema = object({
21665
21988
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -21678,7 +22001,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
21678
22001
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
21679
22002
  kind: "mutation",
21680
22003
  auth: "admin"
21681
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22004
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
21682
22005
  kind: "mutation",
21683
22006
  auth: "admin"
21684
22007
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -28385,6 +28708,9 @@ method(object({
28385
28708
  }), method(object({}), array(RelocateJobSchema).readonly(), {
28386
28709
  kind: "query",
28387
28710
  auth: "admin"
28711
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
28712
+ kind: "query",
28713
+ auth: "admin"
28388
28714
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
28389
28715
  kind: "mutation",
28390
28716
  auth: "admin"
@@ -35423,6 +35749,12 @@ Object.freeze({
35423
35749
  addonId: null,
35424
35750
  access: "create"
35425
35751
  },
35752
+ "pipelineAnalytics.countRelocatableMedia": {
35753
+ capName: "pipeline-analytics",
35754
+ capScope: "device",
35755
+ addonId: null,
35756
+ access: "view"
35757
+ },
35426
35758
  "pipelineAnalytics.countUnstampedEventMedia": {
35427
35759
  capName: "pipeline-analytics",
35428
35760
  capScope: "device",
@@ -36587,6 +36919,12 @@ Object.freeze({
36587
36919
  addonId: null,
36588
36920
  access: "view"
36589
36921
  },
36922
+ "recording.getRelocateResidue": {
36923
+ capName: "recording",
36924
+ capScope: "system",
36925
+ addonId: null,
36926
+ access: "view"
36927
+ },
36590
36928
  "recording.getStorageMigrationMoveStatus": {
36591
36929
  capName: "recording",
36592
36930
  capScope: "system",
@@ -37133,12 +37471,30 @@ Object.freeze({
37133
37471
  addonId: null,
37134
37472
  access: "create"
37135
37473
  },
37474
+ "storageMigration.drain": {
37475
+ capName: "storage-migration",
37476
+ capScope: "system",
37477
+ addonId: null,
37478
+ access: "create"
37479
+ },
37480
+ "storageMigration.movers": {
37481
+ capName: "storage-migration",
37482
+ capScope: "system",
37483
+ addonId: null,
37484
+ access: "view"
37485
+ },
37136
37486
  "storageMigration.plan": {
37137
37487
  capName: "storage-migration",
37138
37488
  capScope: "system",
37139
37489
  addonId: null,
37140
37490
  access: "view"
37141
37491
  },
37492
+ "storageMigration.residue": {
37493
+ capName: "storage-migration",
37494
+ capScope: "system",
37495
+ addonId: null,
37496
+ access: "view"
37497
+ },
37142
37498
  "storageMigration.start": {
37143
37499
  capName: "storage-migration",
37144
37500
  capScope: "system",
@@ -37973,6 +38329,12 @@ Object.freeze({
37973
38329
  addonId: null,
37974
38330
  access: "delete"
37975
38331
  },
38332
+ "vectorStore.fetchByIds": {
38333
+ capName: "vector-store",
38334
+ capScope: "system",
38335
+ addonId: null,
38336
+ access: "view"
38337
+ },
37976
38338
  "vectorStore.getByIds": {
37977
38339
  capName: "vector-store",
37978
38340
  capScope: "system",
@@ -37985,6 +38347,12 @@ Object.freeze({
37985
38347
  addonId: null,
37986
38348
  access: "view"
37987
38349
  },
38350
+ "vectorStore.scan": {
38351
+ capName: "vector-store",
38352
+ capScope: "system",
38353
+ addonId: null,
38354
+ access: "view"
38355
+ },
37988
38356
  "vectorStore.stats": {
37989
38357
  capName: "vector-store",
37990
38358
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.44",
3
+ "version": "0.2.47",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",