@camstack/addon-provider-rademacher 0.2.41 → 0.2.44

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 +452 -23
  2. package/dist/addon.mjs +452 -23
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -9024,18 +9024,61 @@ var RelocateFootageInputSchema = object({
9024
9024
  * `RecordingConfig.enabled` or camera wrapper bindings. */
9025
9025
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
9026
9026
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
9027
+ /**
9028
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
9029
+ * mover (the engine already walks both collections with a timestamp cursor and
9030
+ * already has a stamp-without-copy path).
9031
+ *
9032
+ * - `move` — the default and the historical behaviour: event-media and
9033
+ * retrain blobs move to `toLocationId` and their rows are
9034
+ * stamped. The enrolled gallery is skipped (D197).
9035
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
9036
+ * stamped with `toLocationId`. `toLocationId` here is the id the
9037
+ * bytes ALREADY sit on — today's `eventMedia` default — because
9038
+ * a NULL row means "wherever `eventMedia` points *now*", and the
9039
+ * instant a repoint moves that pointer the row reads from the
9040
+ * new disk while its bytes are on the old one.
9041
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
9042
+ * (enrolled-gallery) rows, which `move` deliberately skips.
9043
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
9044
+ * never run beside a live second location: it is stop-the-world
9045
+ * by construction, which is acceptable only because the gallery
9046
+ * is a few KB per enrolled sample.
9047
+ */
9048
+ var MediaRelocateModeSchema = _enum([
9049
+ "move",
9050
+ "seal",
9051
+ "gallery"
9052
+ ]);
9027
9053
  var RelocateMediaInputSchema = object({
9028
9054
  toLocationId: string(),
9029
- throttleMbps: number().min(1).max(1e3).optional()
9055
+ throttleMbps: number().min(1).max(1e3).optional(),
9056
+ /** Omitted = `move`, the pre-existing behaviour. */
9057
+ mode: MediaRelocateModeSchema.optional()
9058
+ });
9059
+ /** How many rows still carry NO `locationId` — the population a repoint would
9060
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
9061
+ * value that permits a non-blocking `eventMedia` cutover. */
9062
+ var UnstampedEventMediaCountSchema = object({
9063
+ media: number().int().nonnegative(),
9064
+ retrainFrames: number().int().nonnegative(),
9065
+ total: number().int().nonnegative()
9030
9066
  });
9031
9067
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9032
- /** The independently selectable logical storage classes. `recordings`
9033
- * encompasses the high and mid segment profiles; `recordingsLow` is low
9034
- * segments; `eventMedia` is post-analysis blobs. */
9068
+ /** The independently selectable logical storage classes — every class
9069
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
9070
+ * Zod enum error where they should meet an explanation.
9071
+ *
9072
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
9073
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
9074
+ * enrolled gallery; `backups` is the system backup archive. The last two have
9075
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
9035
9076
  var StorageMigrationClassSchema = _enum([
9036
9077
  "recordings",
9037
9078
  "recordingsLow",
9038
- "eventMedia"
9079
+ "eventMedia",
9080
+ "backups",
9081
+ "galleryMedia"
9039
9082
  ]);
9040
9083
  /** A destination is always an existing, fully-qualified location id. The
9041
9084
  * migration API intentionally never changes a source location's `basePath`:
@@ -9043,20 +9086,56 @@ var StorageMigrationClassSchema = _enum([
9043
9086
  var StorageMigrationDestinationsSchema = object({
9044
9087
  recordings: string().min(1).optional(),
9045
9088
  recordingsLow: string().min(1).optional(),
9046
- eventMedia: string().min(1).optional()
9089
+ eventMedia: string().min(1).optional(),
9090
+ backups: string().min(1).optional(),
9091
+ galleryMedia: string().min(1).optional()
9047
9092
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
9093
+ /**
9094
+ * How a migration sequences the cutover against the byte move.
9095
+ *
9096
+ * - `blocking` — the historical order: pause, move every byte, repoint,
9097
+ * resume. Recording is stopped for the whole move. Right
9098
+ * for a small or a cold class, and the only legal mode for
9099
+ * a `cardinality: 'single'` class.
9100
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
9101
+ * refresh, resume, then move the past with everything
9102
+ * running. The pause is three bounded instants (a detach +
9103
+ * attach round, a write-gate drain, a lease) instead of one
9104
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
9105
+ * stopped recording under `blocking`; the same move is
9106
+ * seconds of stopped recording under `nonBlocking`.
9107
+ *
9108
+ * The mode is on the JOB, not only on the input, because `status` is where an
9109
+ * operator finds out which one is running.
9110
+ */
9111
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9048
9112
  /** Shared input for planning and starting an orchestrated storage migration. */
9049
9113
  var StorageMigrationInputSchema = object({
9050
9114
  destinations: StorageMigrationDestinationsSchema,
9051
- throttleMbps: number().min(1).max(1e3).optional()
9115
+ throttleMbps: number().min(1).max(1e3).optional(),
9116
+ /** Omitted = `blocking`, which stays the default. */
9117
+ mode: StorageMigrationModeSchema.optional()
9052
9118
  });
9053
- /** The durable coordinator state machine. The only phase that changes default
9054
- * locations is `repointing`, after every selected mover has completed and been
9055
- * verified. */
9119
+ /**
9120
+ * The durable coordinator state machine.
9121
+ *
9122
+ * `blocking`:
9123
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
9124
+ *
9125
+ * `nonBlocking`:
9126
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
9127
+ *
9128
+ * Same phases, different order plus two new ones — not a second mover.
9129
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
9130
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
9131
+ * `repointing` is still the only phase that changes a default location.
9132
+ */
9056
9133
  var StorageMigrationPhaseSchema = _enum([
9057
9134
  "planning",
9135
+ "sealing",
9058
9136
  "pausing",
9059
9137
  "moving",
9138
+ "draining",
9060
9139
  "verifying",
9061
9140
  "repointing",
9062
9141
  "refreshing",
@@ -9070,17 +9149,56 @@ var StorageMigrationParticipantSchema = _enum([
9070
9149
  "recorder",
9071
9150
  "analytics"
9072
9151
  ]);
9152
+ /**
9153
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9154
+ *
9155
+ * The long half of a non-blocking migration is `draining`, and it is measured
9156
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9157
+ * existed the only place those numbers appeared was a Loki line, so an operator
9158
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9159
+ * afternoon.
9160
+ *
9161
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9162
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9163
+ * mover — which is the exact failure this is meant to end. The coordinator's
9164
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9165
+ * read `state`; folding the counters costs no extra read and makes the durable
9166
+ * record say afterwards how far a move actually got.
9167
+ *
9168
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9169
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9170
+ * cannot say M, and a 0 there would render as "100 % done".
9171
+ */
9172
+ var StorageMigrationMoveProgressSchema = object({
9173
+ filesMoved: number().int().nonnegative(),
9174
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9175
+ filesTotal: number().int().nonnegative().nullable(),
9176
+ bytesMoved: number().int().nonnegative(),
9177
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9178
+ * crash gets a new mover, and a rate computed from the migration's start
9179
+ * would silently average in the time nothing was running. */
9180
+ startedAt: number(),
9181
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9182
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9183
+ * subtract its own. */
9184
+ observedAt: number()
9185
+ });
9073
9186
  var StorageMigrationMoveSchema = object({
9074
9187
  storageClass: StorageMigrationClassSchema,
9075
9188
  fromLocationId: string(),
9076
9189
  toLocationId: string(),
9077
9190
  moverJobId: string().nullable(),
9078
9191
  state: RelocateJobStateSchema.nullable(),
9079
- error: string().nullable()
9192
+ error: string().nullable(),
9193
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9194
+ progress: StorageMigrationMoveProgressSchema.nullable()
9080
9195
  });
9081
9196
  var StorageMigrationJobSchema = object({
9082
9197
  jobId: string(),
9083
9198
  phase: StorageMigrationPhaseSchema,
9199
+ /** Which order this job is running. `status` is the only place an operator
9200
+ * can tell a seconds-long cutover from a thirty-hour one. */
9201
+ mode: StorageMigrationModeSchema,
9084
9202
  destinations: StorageMigrationDestinationsSchema,
9085
9203
  throttleMbps: number(),
9086
9204
  moves: array(StorageMigrationMoveSchema),
@@ -9093,13 +9211,122 @@ var StorageMigrationJobSchema = object({
9093
9211
  finishedAt: number().nullable(),
9094
9212
  error: string().nullable()
9095
9213
  });
9214
+ var StorageMigrationFindingSchema = object({
9215
+ code: _enum([
9216
+ "sharesDeviceWithSource",
9217
+ "deviceIdentityUnknown",
9218
+ "unstampedEventMediaRows",
9219
+ "blockingOnly",
9220
+ "noMover"
9221
+ ]),
9222
+ storageClass: StorageMigrationClassSchema,
9223
+ /** Human-readable, already carrying the ids and counts. */
9224
+ message: string()
9225
+ });
9096
9226
  var StorageMigrationPlanSchema = object({
9097
9227
  destinations: StorageMigrationDestinationsSchema,
9228
+ /** The mode this plan was built for. A plan is only valid for its mode: the
9229
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
9230
+ * it. */
9231
+ mode: StorageMigrationModeSchema,
9098
9232
  moves: array(object({
9099
9233
  storageClass: StorageMigrationClassSchema,
9100
9234
  fromLocationId: string(),
9101
9235
  toLocationId: string()
9102
- }))
9236
+ })),
9237
+ findings: array(StorageMigrationFindingSchema)
9238
+ });
9239
+ /**
9240
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9241
+ *
9242
+ * The coordinator's job record is the state of record for a migration, and its
9243
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9244
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9245
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9246
+ * way because no supported UI path existed. A mover armed like that has no job
9247
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9248
+ *
9249
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9250
+ * orchestrated it.
9251
+ */
9252
+ var StorageMigrationMoverSchema = object({
9253
+ lane: _enum(["footage", "media"]),
9254
+ job: RelocateJobSchema,
9255
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9256
+ * directly against the owning addon. */
9257
+ migrationJobId: string().nullable(),
9258
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9259
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9260
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9261
+ * rate made of two different clocks. */
9262
+ observedAt: number()
9263
+ });
9264
+ /**
9265
+ * What a SOURCE still holds for one storage class — the number that makes a
9266
+ * "drain remaining" action honest rather than hopeful.
9267
+ *
9268
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9269
+ * engine's own selection count for media), never from the resident index: a
9270
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9271
+ * never been told about (D295).
9272
+ *
9273
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9274
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9275
+ * because refusing on an unanswerable read would hide exactly the case an
9276
+ * operator needs to act on.
9277
+ */
9278
+ var StorageMigrationResidueSchema = object({
9279
+ storageClass: StorageMigrationClassSchema,
9280
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9281
+ * move from wherever they are rather than from one named source. */
9282
+ fromLocationId: string(),
9283
+ /** Where a drain would move it — the class's CURRENT default. */
9284
+ toLocationId: string(),
9285
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9286
+ items: number().int().nonnegative().nullable(),
9287
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9288
+ bytes: number().int().nonnegative().nullable()
9289
+ });
9290
+ /**
9291
+ * Run the DRAIN half and nothing else.
9292
+ *
9293
+ * A migration that reached `done` has already repointed, so `start` correctly
9294
+ * refuses its destination ("already the default") — there is nothing left to
9295
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9296
+ * or finish against a work list that was a tenth of the archive (D295), and
9297
+ * before this there was no supported way to run only that half: the only way
9298
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9299
+ *
9300
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9301
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9302
+ * re-repoint a class that is already migrated.
9303
+ */
9304
+ var StorageMigrationDrainInputSchema = object({
9305
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9306
+ * a class whose source is already empty is refused rather than started. */
9307
+ classes: array(StorageMigrationClassSchema).min(1),
9308
+ throttleMbps: number().min(1).max(1e3).optional()
9309
+ });
9310
+ /** What a footage source still holds, asked of the durable hour ledger. */
9311
+ var RelocateResidueInputSchema = object({
9312
+ fromLocationId: string().min(1),
9313
+ /** Narrow to one logical class; omit for every profile on the location. */
9314
+ footageClass: RelocateFootageClassSchema.optional()
9315
+ });
9316
+ /** `null` = the archive could not answer (no ledger on this node, or the
9317
+ * aggregate failed). Never conflated with an empty source. */
9318
+ var RelocateResidueSchema = object({
9319
+ segments: number().int().nonnegative(),
9320
+ bytes: number().int().nonnegative()
9321
+ }).nullable();
9322
+ /** How many rows a media pass would still act on against a given target — the
9323
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9324
+ * never disagree. `null` = the count could not be taken. */
9325
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9326
+ var RelocatableMediaCountInputSchema = object({
9327
+ toLocationId: string().min(1),
9328
+ /** Omitted = `move`. */
9329
+ mode: MediaRelocateModeSchema.optional()
9103
9330
  });
9104
9331
  /**
9105
9332
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -9205,6 +9432,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9205
9432
  * two addons declaring the same `id` must agree on `cardinality` (validated
9206
9433
  * at kernel aggregation time, not here).
9207
9434
  */
9435
+ /**
9436
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9437
+ * actually reaches the bytes. It is the constraint that decides which
9438
+ * `storage-provider`s may back a location of that kind.
9439
+ *
9440
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9441
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9442
+ * post-analysis media roots). Only a provider that serves a genuine local
9443
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9444
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9445
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9446
+ * against a same-named local directory that is something else entirely.
9447
+ *
9448
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9449
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9450
+ * service never sees a path, so any provider can back it. `backups` is the
9451
+ * one kind that qualifies today.
9452
+ *
9453
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9454
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9455
+ * refused the configuration; the first write simply went somewhere wrong, and
9456
+ * a recording write that goes wrong surfaces as a silent black window rather
9457
+ * than an error (the read path does not `stat`). This turns that accident into
9458
+ * a declared, enforced, testable refusal.
9459
+ */
9460
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9208
9461
  var StorageLocationDeclarationSchema = object({
9209
9462
  /**
9210
9463
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9224,6 +9477,19 @@ var StorageLocationDeclarationSchema = object({
9224
9477
  */
9225
9478
  cardinality: _enum(["single", "multi"]),
9226
9479
  /**
9480
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9481
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9482
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9483
+ *
9484
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9485
+ * can only over-restrict (refuse a remote provider for a kind that might
9486
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9487
+ * permissive direction and is therefore never inferred — a repo guard
9488
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9489
+ * reached by omission.
9490
+ */
9491
+ access: StorageAccessSchema.optional(),
9492
+ /**
9227
9493
  * When set, the default instance for this location inherits its resolved
9228
9494
  * root from the named location's default instance. Useful for derivative
9229
9495
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19091,8 +19357,10 @@ var TrackSchema = object({
19091
19357
  lastSeen: number(),
19092
19358
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19093
19359
  positions: array(TrackPositionSchema).readonly(),
19094
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19095
- * saveThumbnails policy). */
19360
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19361
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19362
+ * the retired `saveThumbnails` used to gate this and the rolling
19363
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19096
19364
  snapshots: array(TrackSnapshotSchema).readonly(),
19097
19365
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19098
19366
  zonesVisited: array(string()).readonly(),
@@ -19952,6 +20220,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19952
20220
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19953
20221
  kind: "mutation",
19954
20222
  auth: "admin"
20223
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20224
+ kind: "query",
20225
+ auth: "admin"
19955
20226
  }), method(object({}), array(RelocateJobSchema).readonly(), {
19956
20227
  kind: "query",
19957
20228
  auth: "admin"
@@ -21859,7 +22130,10 @@ method(object({
21859
22130
  }), StorageLocationSchema, {
21860
22131
  kind: "mutation",
21861
22132
  auth: "admin"
21862
- }), method(object({ id: string() }), _void(), {
22133
+ }), method(object({
22134
+ id: string(),
22135
+ force: boolean().optional()
22136
+ }), _void(), {
21863
22137
  kind: "mutation",
21864
22138
  auth: "admin"
21865
22139
  }), method(object({ id: string() }), object({
@@ -21908,6 +22182,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21908
22182
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21909
22183
  kind: "mutation",
21910
22184
  auth: "admin"
22185
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22186
+ kind: "mutation",
22187
+ auth: "admin"
21911
22188
  });
21912
22189
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21913
22190
  providerId: string().min(1),
@@ -22306,12 +22583,38 @@ response: record(string(), unknown()) }), object({
22306
22583
  *
22307
22584
  * ## Why this is a capability and not a helper
22308
22585
  *
22309
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22310
- * plate, vehicle, identity, and the event store's derivativesand every one of
22311
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22312
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22313
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22314
- * load 5,000 rows before ranking anything.
22586
+ * This capability was introduced with the claim that SIX stores in
22587
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22588
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22589
+ * claim was never true, and leaving it here made five stores look like pending
22590
+ * work when three of them have no vector at all. Counted column by column on
22591
+ * 2026-08-30, exactly THREE ever held one:
22592
+ *
22593
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22594
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22595
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22596
+ * face, migrated 2026-08-30 into its OWN index (see below).
22597
+ *
22598
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22599
+ * and `identities` store a name; the event store stores no derivative vector.
22600
+ * They are not migration candidates and never were.
22601
+ *
22602
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22603
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22604
+ * rows before ranking anything.
22605
+ *
22606
+ * ## One index per COMPARISON, never per encoder
22607
+ *
22608
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22609
+ * model, and they still get two indexes. An index is a set of things that are
22610
+ * ranked against each other and that live and die together, and these two are
22611
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22612
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22613
+ * forever and is the gallery every recognition ranks against. One index would
22614
+ * mean every gallery load and every reconcile carried a filter whose failure
22615
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22616
+ * person's only sample. The dimension they share is not a reason to share an
22617
+ * index; the question they answer is, and it differs.
22315
22618
  *
22316
22619
  * The fix is not a faster loop, it is a different backend — and the backend
22317
22620
  * should be replaceable without touching six callers. So: a singleton
@@ -22416,7 +22719,20 @@ var VectorQueryResultSchema = object({
22416
22719
  */
22417
22720
  scanned: number(),
22418
22721
  /** True when the backend could not consider every row that passed the filter. */
22419
- truncated: boolean()
22722
+ truncated: boolean(),
22723
+ /**
22724
+ * The `topK` the backend actually ran with.
22725
+ *
22726
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22727
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22728
+ * own log rather than in its answer. That is how an audit asking for 20,000
22729
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22730
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22731
+ * MUCH, in the return value, where the caller cannot fail to see it.
22732
+ *
22733
+ * Equals the requested `topK` whenever nothing was lowered.
22734
+ */
22735
+ effectiveTopK: number().int().positive()
22420
22736
  });
22421
22737
  var VectorDeleteInputSchema = object({
22422
22738
  index: string(),
@@ -22445,6 +22761,68 @@ var VectorGetResultSchema = object({ items: array(object({
22445
22761
  id: string(),
22446
22762
  metadata: VectorMetadataSchema
22447
22763
  })) });
22764
+ /**
22765
+ * Ids to read back WITH their vectors.
22766
+ *
22767
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22768
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22769
+ * caller depends on that promise. This one promises the opposite.
22770
+ *
22771
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22772
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22773
+ * a per-face cross-process KNN would be a network round trip inside the
22774
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22775
+ * it requires the index to hand the floats back. Without this method the only
22776
+ * way to keep a readable vector is a JSON column, which is the thing this
22777
+ * capability exists to delete.
22778
+ *
22779
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22780
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22781
+ */
22782
+ var VectorFetchInputSchema = object({
22783
+ index: string(),
22784
+ ids: array(string())
22785
+ });
22786
+ var VectorFetchResultSchema = object({ items: array(object({
22787
+ id: string(),
22788
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22789
+ vector: string(),
22790
+ metadata: VectorMetadataSchema
22791
+ })) });
22792
+ /**
22793
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22794
+ *
22795
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22796
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22797
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22798
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22799
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22800
+ * looked" for as long as anyone cared to read it.
22801
+ *
22802
+ * This is the primitive that question actually needs: a bounded page, ordered
22803
+ * by the backend's own row order, costing no distance computation at all.
22804
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22805
+ * the full-table read this capability was built to stop.
22806
+ */
22807
+ var VectorScanInputSchema = object({
22808
+ index: string(),
22809
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22810
+ cursor: number().int().nonnegative().default(0),
22811
+ limit: number().int().positive()
22812
+ });
22813
+ var VectorScanResultSchema = object({
22814
+ items: array(object({
22815
+ id: string(),
22816
+ metadata: VectorMetadataSchema
22817
+ })),
22818
+ /**
22819
+ * Where the next page starts, or `null` when the walk reached the end.
22820
+ *
22821
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22822
+ * from a short page: a backend is free to return fewer rows than asked.
22823
+ */
22824
+ nextCursor: number().int().nonnegative().nullable()
22825
+ });
22448
22826
  var VectorStatsInputSchema = object({ index: string() });
22449
22827
  var VectorStatsResultSchema = object({
22450
22828
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22463,7 +22841,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22463
22841
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22464
22842
  kind: "mutation",
22465
22843
  auth: "admin"
22466
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22844
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22467
22845
  kind: "mutation",
22468
22846
  auth: "admin"
22469
22847
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29082,6 +29460,9 @@ method(object({
29082
29460
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29083
29461
  kind: "query",
29084
29462
  auth: "admin"
29463
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29464
+ kind: "query",
29465
+ auth: "admin"
29085
29466
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29086
29467
  kind: "mutation",
29087
29468
  auth: "admin"
@@ -35716,6 +36097,18 @@ Object.freeze({
35716
36097
  addonId: null,
35717
36098
  access: "create"
35718
36099
  },
36100
+ "pipelineAnalytics.countRelocatableMedia": {
36101
+ capName: "pipeline-analytics",
36102
+ capScope: "device",
36103
+ addonId: null,
36104
+ access: "view"
36105
+ },
36106
+ "pipelineAnalytics.countUnstampedEventMedia": {
36107
+ capName: "pipeline-analytics",
36108
+ capScope: "device",
36109
+ addonId: null,
36110
+ access: "view"
36111
+ },
35719
36112
  "pipelineAnalytics.deleteDeviceEvents": {
35720
36113
  capName: "pipeline-analytics",
35721
36114
  capScope: "device",
@@ -36874,6 +37267,12 @@ Object.freeze({
36874
37267
  addonId: null,
36875
37268
  access: "view"
36876
37269
  },
37270
+ "recording.getRelocateResidue": {
37271
+ capName: "recording",
37272
+ capScope: "system",
37273
+ addonId: null,
37274
+ access: "view"
37275
+ },
36877
37276
  "recording.getStorageMigrationMoveStatus": {
36878
37277
  capName: "recording",
36879
37278
  capScope: "system",
@@ -37420,12 +37819,30 @@ Object.freeze({
37420
37819
  addonId: null,
37421
37820
  access: "create"
37422
37821
  },
37822
+ "storageMigration.drain": {
37823
+ capName: "storage-migration",
37824
+ capScope: "system",
37825
+ addonId: null,
37826
+ access: "create"
37827
+ },
37828
+ "storageMigration.movers": {
37829
+ capName: "storage-migration",
37830
+ capScope: "system",
37831
+ addonId: null,
37832
+ access: "view"
37833
+ },
37423
37834
  "storageMigration.plan": {
37424
37835
  capName: "storage-migration",
37425
37836
  capScope: "system",
37426
37837
  addonId: null,
37427
37838
  access: "view"
37428
37839
  },
37840
+ "storageMigration.residue": {
37841
+ capName: "storage-migration",
37842
+ capScope: "system",
37843
+ addonId: null,
37844
+ access: "view"
37845
+ },
37429
37846
  "storageMigration.start": {
37430
37847
  capName: "storage-migration",
37431
37848
  capScope: "system",
@@ -38260,6 +38677,12 @@ Object.freeze({
38260
38677
  addonId: null,
38261
38678
  access: "delete"
38262
38679
  },
38680
+ "vectorStore.fetchByIds": {
38681
+ capName: "vector-store",
38682
+ capScope: "system",
38683
+ addonId: null,
38684
+ access: "view"
38685
+ },
38263
38686
  "vectorStore.getByIds": {
38264
38687
  capName: "vector-store",
38265
38688
  capScope: "system",
@@ -38272,6 +38695,12 @@ Object.freeze({
38272
38695
  addonId: null,
38273
38696
  access: "view"
38274
38697
  },
38698
+ "vectorStore.scan": {
38699
+ capName: "vector-store",
38700
+ capScope: "system",
38701
+ addonId: null,
38702
+ access: "view"
38703
+ },
38275
38704
  "vectorStore.stats": {
38276
38705
  capName: "vector-store",
38277
38706
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -9023,18 +9023,61 @@ var RelocateFootageInputSchema = object({
9023
9023
  * `RecordingConfig.enabled` or camera wrapper bindings. */
9024
9024
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
9025
9025
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
9026
+ /**
9027
+ * What a `relocateMedia` pass DOES. One engine, three passes — never a second
9028
+ * mover (the engine already walks both collections with a timestamp cursor and
9029
+ * already has a stamp-without-copy path).
9030
+ *
9031
+ * - `move` — the default and the historical behaviour: event-media and
9032
+ * retrain blobs move to `toLocationId` and their rows are
9033
+ * stamped. The enrolled gallery is skipped (D197).
9034
+ * - `seal` — ROWS ONLY, no bytes. Every row whose `locationId` is NULL is
9035
+ * stamped with `toLocationId`. `toLocationId` here is the id the
9036
+ * bytes ALREADY sit on — today's `eventMedia` default — because
9037
+ * a NULL row means "wherever `eventMedia` points *now*", and the
9038
+ * instant a repoint moves that pointer the row reads from the
9039
+ * new disk while its bytes are on the old one.
9040
+ * - `gallery` — the inverse selection of `move`: ONLY the retention-exempt
9041
+ * (enrolled-gallery) rows, which `move` deliberately skips.
9042
+ * `galleryMedia` is `cardinality: 'single'`, so this pass can
9043
+ * never run beside a live second location: it is stop-the-world
9044
+ * by construction, which is acceptable only because the gallery
9045
+ * is a few KB per enrolled sample.
9046
+ */
9047
+ var MediaRelocateModeSchema = _enum([
9048
+ "move",
9049
+ "seal",
9050
+ "gallery"
9051
+ ]);
9026
9052
  var RelocateMediaInputSchema = object({
9027
9053
  toLocationId: string(),
9028
- throttleMbps: number().min(1).max(1e3).optional()
9054
+ throttleMbps: number().min(1).max(1e3).optional(),
9055
+ /** Omitted = `move`, the pre-existing behaviour. */
9056
+ mode: MediaRelocateModeSchema.optional()
9057
+ });
9058
+ /** How many rows still carry NO `locationId` — the population a repoint would
9059
+ * silently re-aim at a disk that does not hold their bytes. Zero is the only
9060
+ * value that permits a non-blocking `eventMedia` cutover. */
9061
+ var UnstampedEventMediaCountSchema = object({
9062
+ media: number().int().nonnegative(),
9063
+ retrainFrames: number().int().nonnegative(),
9064
+ total: number().int().nonnegative()
9029
9065
  });
9030
9066
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9031
- /** The independently selectable logical storage classes. `recordings`
9032
- * encompasses the high and mid segment profiles; `recordingsLow` is low
9033
- * segments; `eventMedia` is post-analysis blobs. */
9067
+ /** The independently selectable logical storage classes — every class
9068
+ * `storage.listLocationDeclarations` reports, so an operator never meets a
9069
+ * Zod enum error where they should meet an explanation.
9070
+ *
9071
+ * `recordings` encompasses the high and mid segment profiles; `recordingsLow`
9072
+ * is low segments; `eventMedia` is post-analysis blobs; `galleryMedia` is the
9073
+ * enrolled gallery; `backups` is the system backup archive. The last two have
9074
+ * their own rules — see {@link StorageMigrationFindingCodeSchema}. */
9034
9075
  var StorageMigrationClassSchema = _enum([
9035
9076
  "recordings",
9036
9077
  "recordingsLow",
9037
- "eventMedia"
9078
+ "eventMedia",
9079
+ "backups",
9080
+ "galleryMedia"
9038
9081
  ]);
9039
9082
  /** A destination is always an existing, fully-qualified location id. The
9040
9083
  * migration API intentionally never changes a source location's `basePath`:
@@ -9042,20 +9085,56 @@ var StorageMigrationClassSchema = _enum([
9042
9085
  var StorageMigrationDestinationsSchema = object({
9043
9086
  recordings: string().min(1).optional(),
9044
9087
  recordingsLow: string().min(1).optional(),
9045
- eventMedia: string().min(1).optional()
9088
+ eventMedia: string().min(1).optional(),
9089
+ backups: string().min(1).optional(),
9090
+ galleryMedia: string().min(1).optional()
9046
9091
  }).refine((value) => Object.keys(value).length > 0, { message: "select at least one storage class" });
9092
+ /**
9093
+ * How a migration sequences the cutover against the byte move.
9094
+ *
9095
+ * - `blocking` — the historical order: pause, move every byte, repoint,
9096
+ * resume. Recording is stopped for the whole move. Right
9097
+ * for a small or a cold class, and the only legal mode for
9098
+ * a `cardinality: 'single'` class.
9099
+ * - `nonBlocking` — repoint FIRST, drain behind: seal, pause, repoint,
9100
+ * refresh, resume, then move the past with everything
9101
+ * running. The pause is three bounded instants (a detach +
9102
+ * attach round, a write-gate drain, a lease) instead of one
9103
+ * bounded by bytes. 1.09 TB at 7–14 MB/s is thirty hours of
9104
+ * stopped recording under `blocking`; the same move is
9105
+ * seconds of stopped recording under `nonBlocking`.
9106
+ *
9107
+ * The mode is on the JOB, not only on the input, because `status` is where an
9108
+ * operator finds out which one is running.
9109
+ */
9110
+ var StorageMigrationModeSchema = _enum(["blocking", "nonBlocking"]);
9047
9111
  /** Shared input for planning and starting an orchestrated storage migration. */
9048
9112
  var StorageMigrationInputSchema = object({
9049
9113
  destinations: StorageMigrationDestinationsSchema,
9050
- throttleMbps: number().min(1).max(1e3).optional()
9114
+ throttleMbps: number().min(1).max(1e3).optional(),
9115
+ /** Omitted = `blocking`, which stays the default. */
9116
+ mode: StorageMigrationModeSchema.optional()
9051
9117
  });
9052
- /** The durable coordinator state machine. The only phase that changes default
9053
- * locations is `repointing`, after every selected mover has completed and been
9054
- * verified. */
9118
+ /**
9119
+ * The durable coordinator state machine.
9120
+ *
9121
+ * `blocking`:
9122
+ * planning → pausing → moving → verifying → repointing → refreshing → resuming → done
9123
+ *
9124
+ * `nonBlocking`:
9125
+ * planning → sealing → pausing → repointing → refreshing → resuming → draining → verifying → done
9126
+ *
9127
+ * Same phases, different order plus two new ones — not a second mover.
9128
+ * `sealing` closes the `eventMedia` NULL-row hole BEFORE anything is paused;
9129
+ * `draining` runs the same movers UNLEASED, after every writer is back up.
9130
+ * `repointing` is still the only phase that changes a default location.
9131
+ */
9055
9132
  var StorageMigrationPhaseSchema = _enum([
9056
9133
  "planning",
9134
+ "sealing",
9057
9135
  "pausing",
9058
9136
  "moving",
9137
+ "draining",
9059
9138
  "verifying",
9060
9139
  "repointing",
9061
9140
  "refreshing",
@@ -9069,17 +9148,56 @@ var StorageMigrationParticipantSchema = _enum([
9069
9148
  "recorder",
9070
9149
  "analytics"
9071
9150
  ]);
9151
+ /**
9152
+ * The mover's own numbers, folded onto the coordinator's durable move record.
9153
+ *
9154
+ * The long half of a non-blocking migration is `draining`, and it is measured
9155
+ * in hours: 136 885 files at ~4 MB/s is about five of them. Before this shape
9156
+ * existed the only place those numbers appeared was a Loki line, so an operator
9157
+ * watching the Admin UI saw `phase: draining` and nothing else for a whole
9158
+ * afternoon.
9159
+ *
9160
+ * It is POLLED, never pushed. Events are telemetry and may be dropped
9161
+ * (D8/D11), and a dropped progress event is indistinguishable from a stalled
9162
+ * mover — which is the exact failure this is meant to end. The coordinator's
9163
+ * `waitForMoves` already fetches the whole {@link RelocateJob} on every tick to
9164
+ * read `state`; folding the counters costs no extra read and makes the durable
9165
+ * record say afterwards how far a move actually got.
9166
+ *
9167
+ * `filesTotal` is `null` for "no honest denominator" and is never zero-filled:
9168
+ * a windowed footage job (`sinceMs`) and a node with no ledger both genuinely
9169
+ * cannot say M, and a 0 there would render as "100 % done".
9170
+ */
9171
+ var StorageMigrationMoveProgressSchema = object({
9172
+ filesMoved: number().int().nonnegative(),
9173
+ /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9174
+ filesTotal: number().int().nonnegative().nullable(),
9175
+ bytesMoved: number().int().nonnegative(),
9176
+ /** The MOVER's start, not the migration's: a drain restarted after an addon
9177
+ * crash gets a new mover, and a rate computed from the migration's start
9178
+ * would silently average in the time nothing was running. */
9179
+ startedAt: number(),
9180
+ /** When the coordinator last read these numbers. Paired with `startedAt` it
9181
+ * is the only honest rate: both clocks are the hub's, so a UI never has to
9182
+ * subtract its own. */
9183
+ observedAt: number()
9184
+ });
9072
9185
  var StorageMigrationMoveSchema = object({
9073
9186
  storageClass: StorageMigrationClassSchema,
9074
9187
  fromLocationId: string(),
9075
9188
  toLocationId: string(),
9076
9189
  moverJobId: string().nullable(),
9077
9190
  state: RelocateJobStateSchema.nullable(),
9078
- error: string().nullable()
9191
+ error: string().nullable(),
9192
+ /** Last observed mover counters; `null` until the mover has been polled once. */
9193
+ progress: StorageMigrationMoveProgressSchema.nullable()
9079
9194
  });
9080
9195
  var StorageMigrationJobSchema = object({
9081
9196
  jobId: string(),
9082
9197
  phase: StorageMigrationPhaseSchema,
9198
+ /** Which order this job is running. `status` is the only place an operator
9199
+ * can tell a seconds-long cutover from a thirty-hour one. */
9200
+ mode: StorageMigrationModeSchema,
9083
9201
  destinations: StorageMigrationDestinationsSchema,
9084
9202
  throttleMbps: number(),
9085
9203
  moves: array(StorageMigrationMoveSchema),
@@ -9092,13 +9210,122 @@ var StorageMigrationJobSchema = object({
9092
9210
  finishedAt: number().nullable(),
9093
9211
  error: string().nullable()
9094
9212
  });
9213
+ var StorageMigrationFindingSchema = object({
9214
+ code: _enum([
9215
+ "sharesDeviceWithSource",
9216
+ "deviceIdentityUnknown",
9217
+ "unstampedEventMediaRows",
9218
+ "blockingOnly",
9219
+ "noMover"
9220
+ ]),
9221
+ storageClass: StorageMigrationClassSchema,
9222
+ /** Human-readable, already carrying the ids and counts. */
9223
+ message: string()
9224
+ });
9095
9225
  var StorageMigrationPlanSchema = object({
9096
9226
  destinations: StorageMigrationDestinationsSchema,
9227
+ /** The mode this plan was built for. A plan is only valid for its mode: the
9228
+ * `eventMedia` seal gate and the single-cardinality refusal both depend on
9229
+ * it. */
9230
+ mode: StorageMigrationModeSchema,
9097
9231
  moves: array(object({
9098
9232
  storageClass: StorageMigrationClassSchema,
9099
9233
  fromLocationId: string(),
9100
9234
  toLocationId: string()
9101
- }))
9235
+ })),
9236
+ findings: array(StorageMigrationFindingSchema)
9237
+ });
9238
+ /**
9239
+ * A mover as it exists RIGHT NOW, whether or not a migration job owns it.
9240
+ *
9241
+ * The coordinator's job record is the state of record for a migration, and its
9242
+ * moves carry {@link StorageMigrationMoveProgress}. But the movers are usable
9243
+ * standalone — `recording.relocateFootage` and `pipelineAnalytics.relocateMedia`
9244
+ * are both operator-callable, and on 2026-08-29 a five-hour drain was armed that
9245
+ * way because no supported UI path existed. A mover armed like that has no job
9246
+ * to fold progress into, so it has to be readable on its own or it is invisible.
9247
+ *
9248
+ * `migrationJobId` is what tells the two apart: `null` means nothing here
9249
+ * orchestrated it.
9250
+ */
9251
+ var StorageMigrationMoverSchema = object({
9252
+ lane: _enum(["footage", "media"]),
9253
+ job: RelocateJobSchema,
9254
+ /** The coordinator job that armed this mover, or `null` for a mover armed
9255
+ * directly against the owning addon. */
9256
+ migrationJobId: string().nullable(),
9257
+ /** When the hub read these counters. Stamped here so a rate is `bytesMoved`
9258
+ * over (`observedAt` − `job.startedAt`) with BOTH ends on the hub's clock —
9259
+ * a browser subtracting its own `Date.now()` from a server `startedAt` is a
9260
+ * rate made of two different clocks. */
9261
+ observedAt: number()
9262
+ });
9263
+ /**
9264
+ * What a SOURCE still holds for one storage class — the number that makes a
9265
+ * "drain remaining" action honest rather than hopeful.
9266
+ *
9267
+ * It comes from the archive (`SegmentHourLedger.census` for footage, the media
9268
+ * engine's own selection count for media), never from the resident index: a
9269
+ * drain sized off `RecordingIndex` is what reported `done` over 80.3 GB it had
9270
+ * never been told about (D295).
9271
+ *
9272
+ * `items`/`bytes` are `null` for "the archive could not be asked", which is
9273
+ * deliberately NOT zero: a drain is still offered for an unknown residue,
9274
+ * because refusing on an unanswerable read would hide exactly the case an
9275
+ * operator needs to act on.
9276
+ */
9277
+ var StorageMigrationResidueSchema = object({
9278
+ storageClass: StorageMigrationClassSchema,
9279
+ /** The location still holding the data. `'*'` for the media lane, whose rows
9280
+ * move from wherever they are rather than from one named source. */
9281
+ fromLocationId: string(),
9282
+ /** Where a drain would move it — the class's CURRENT default. */
9283
+ toLocationId: string(),
9284
+ /** Segments (footage lane) or rows (media lane) still on the source. */
9285
+ items: number().int().nonnegative().nullable(),
9286
+ /** Bytes on the source; `null` when the lane counts rows rather than bytes. */
9287
+ bytes: number().int().nonnegative().nullable()
9288
+ });
9289
+ /**
9290
+ * Run the DRAIN half and nothing else.
9291
+ *
9292
+ * A migration that reached `done` has already repointed, so `start` correctly
9293
+ * refuses its destination ("already the default") — there is nothing left to
9294
+ * repoint. But the drain can fail, be cancelled, be interrupted by a restart,
9295
+ * or finish against a work list that was a tenth of the archive (D295), and
9296
+ * before this there was no supported way to run only that half: the only way
9297
+ * through was calling `recording.relocateFootage` by hand over admin tRPC.
9298
+ *
9299
+ * `drain` NEVER calls `setDefaultLocations`. That is what keeps `start`'s
9300
+ * refusal meaningful: the two verbs are disjoint, so nothing here can silently
9301
+ * re-repoint a class that is already migrated.
9302
+ */
9303
+ var StorageMigrationDrainInputSchema = object({
9304
+ /** The classes to drain. Each must appear in `storageMigration.residue`, so
9305
+ * a class whose source is already empty is refused rather than started. */
9306
+ classes: array(StorageMigrationClassSchema).min(1),
9307
+ throttleMbps: number().min(1).max(1e3).optional()
9308
+ });
9309
+ /** What a footage source still holds, asked of the durable hour ledger. */
9310
+ var RelocateResidueInputSchema = object({
9311
+ fromLocationId: string().min(1),
9312
+ /** Narrow to one logical class; omit for every profile on the location. */
9313
+ footageClass: RelocateFootageClassSchema.optional()
9314
+ });
9315
+ /** `null` = the archive could not answer (no ledger on this node, or the
9316
+ * aggregate failed). Never conflated with an empty source. */
9317
+ var RelocateResidueSchema = object({
9318
+ segments: number().int().nonnegative(),
9319
+ bytes: number().int().nonnegative()
9320
+ }).nullable();
9321
+ /** How many rows a media pass would still act on against a given target — the
9322
+ * media lane's denominator AND its residue, from ONE derivation so the two can
9323
+ * never disagree. `null` = the count could not be taken. */
9324
+ var RelocatableMediaCountSchema = object({ rows: number().int().nonnegative() }).nullable();
9325
+ var RelocatableMediaCountInputSchema = object({
9326
+ toLocationId: string().min(1),
9327
+ /** Omitted = `move`. */
9328
+ mode: MediaRelocateModeSchema.optional()
9102
9329
  });
9103
9330
  /**
9104
9331
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -9204,6 +9431,32 @@ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(
9204
9431
  * two addons declaring the same `id` must agree on `cardinality` (validated
9205
9432
  * at kernel aggregation time, not here).
9206
9433
  */
9434
+ /**
9435
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
9436
+ * actually reaches the bytes. It is the constraint that decides which
9437
+ * `storage-provider`s may back a location of that kind.
9438
+ *
9439
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
9440
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9441
+ * post-analysis media roots). Only a provider that serves a genuine local
9442
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9443
+ * remote provider's `resolve` returns a path on the REMOTE host, and
9444
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
9445
+ * against a same-named local directory that is something else entirely.
9446
+ *
9447
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
9448
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9449
+ * service never sees a path, so any provider can back it. `backups` is the
9450
+ * one kind that qualifies today.
9451
+ *
9452
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9453
+ * an EMERGENT property of how the recorder happened to be written. Nothing
9454
+ * refused the configuration; the first write simply went somewhere wrong, and
9455
+ * a recording write that goes wrong surfaces as a silent black window rather
9456
+ * than an error (the read path does not `stat`). This turns that accident into
9457
+ * a declared, enforced, testable refusal.
9458
+ */
9459
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9207
9460
  var StorageLocationDeclarationSchema = object({
9208
9461
  /**
9209
9462
  * Global location identifier, e.g. `recordings` or `recordingsLow`.
@@ -9223,6 +9476,19 @@ var StorageLocationDeclarationSchema = object({
9223
9476
  */
9224
9477
  cardinality: _enum(["single", "multi"]),
9225
9478
  /**
9479
+ * HOW the declaring service reaches the bytes — and therefore WHICH
9480
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
9481
+ * and {@link STORAGE_ACCESS_FALLBACK}.
9482
+ *
9483
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9484
+ * can only over-restrict (refuse a remote provider for a kind that might
9485
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9486
+ * permissive direction and is therefore never inferred — a repo guard
9487
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9488
+ * reached by omission.
9489
+ */
9490
+ access: StorageAccessSchema.optional(),
9491
+ /**
9226
9492
  * When set, the default instance for this location inherits its resolved
9227
9493
  * root from the named location's default instance. Useful for derivative
9228
9494
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
@@ -19090,8 +19356,10 @@ var TrackSchema = object({
19090
19356
  lastSeen: number(),
19091
19357
  /** Frame-rate position history (subject to maxPositionHistory cap). */
19092
19358
  positions: array(TrackPositionSchema).readonly(),
19093
- /** Periodic snapshots at snapshotIntervalMs cadence (subject to
19094
- * saveThumbnails policy). */
19359
+ /** Periodic snapshots at snapshotIntervalMs cadence DEBUG media, produced
19360
+ * only while `MediaSettings.debugMediaEnabled` is on for the camera (D299;
19361
+ * the retired `saveThumbnails` used to gate this and the rolling
19362
+ * `lastFrame` together). Empty is the healthy default, not a capture gap. */
19095
19363
  snapshots: array(TrackSnapshotSchema).readonly(),
19096
19364
  /** Deduplicated zones the track has entered at least once. Zone IDS. */
19097
19365
  zonesVisited: array(string()).readonly(),
@@ -19951,6 +20219,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19951
20219
  }), method(RelocateMediaInputSchema, object({ jobId: string() }), {
19952
20220
  kind: "mutation",
19953
20221
  auth: "admin"
20222
+ }), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
20223
+ kind: "query",
20224
+ auth: "admin"
19954
20225
  }), method(object({}), array(RelocateJobSchema).readonly(), {
19955
20226
  kind: "query",
19956
20227
  auth: "admin"
@@ -21858,7 +22129,10 @@ method(object({
21858
22129
  }), StorageLocationSchema, {
21859
22130
  kind: "mutation",
21860
22131
  auth: "admin"
21861
- }), method(object({ id: string() }), _void(), {
22132
+ }), method(object({
22133
+ id: string(),
22134
+ force: boolean().optional()
22135
+ }), _void(), {
21862
22136
  kind: "mutation",
21863
22137
  auth: "admin"
21864
22138
  }), method(object({ id: string() }), object({
@@ -21907,6 +22181,9 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
21907
22181
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
21908
22182
  kind: "mutation",
21909
22183
  auth: "admin"
22184
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
22185
+ kind: "mutation",
22186
+ auth: "admin"
21910
22187
  });
21911
22188
  var ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [object({
21912
22189
  providerId: string().min(1),
@@ -22305,12 +22582,38 @@ response: record(string(), unknown()) }), object({
22305
22582
  *
22306
22583
  * ## Why this is a capability and not a helper
22307
22584
  *
22308
- * Six stores in `addon-post-analysis` already hold vectors object CLIP, face,
22309
- * plate, vehicle, identity, and the event store's derivativesand every one of
22310
- * them keeps its vectors in a `JSON` settings-store column and ranks them by
22311
- * brute-force cosine in JS. Measured on the live hub that costs ~11.7 KB per row
22312
- * (512 floats as TEXT, `JSON.parse`d on every search) and made semantic search
22313
- * load 5,000 rows before ranking anything.
22585
+ * This capability was introduced with the claim that SIX stores in
22586
+ * `addon-post-analysis` held vectors in a `JSON` settings-store columnobject
22587
+ * CLIP, face, plate, vehicle, identity, and the event store's derivatives. That
22588
+ * claim was never true, and leaving it here made five stores look like pending
22589
+ * work when three of them have no vector at all. Counted column by column on
22590
+ * 2026-08-30, exactly THREE ever held one:
22591
+ *
22592
+ * - `object-clip` — 512-dim CLIP image embedding, migrated 2026-08-06.
22593
+ * - `faces.embedding` — 512-dim ArcFace face embedding, migrated 2026-08-30.
22594
+ * - `identity-samples.embedding` — the same ArcFace vector for an ENROLLED
22595
+ * face, migrated 2026-08-30 into its OWN index (see below).
22596
+ *
22597
+ * `plates` and `vehicle-samples` store a plate STRING and a score; `vehicles`
22598
+ * and `identities` store a name; the event store stores no derivative vector.
22599
+ * They are not migration candidates and never were.
22600
+ *
22601
+ * Measured on the live hub the JSON encoding cost ~11.7 KB per row (512 floats
22602
+ * as TEXT, `JSON.parse`d on every search) and made semantic search load 5,000
22603
+ * rows before ranking anything.
22604
+ *
22605
+ * ## One index per COMPARISON, never per encoder
22606
+ *
22607
+ * `faces` and `identity-samples` hold the same 512 ArcFace dims from the same
22608
+ * model, and they still get two indexes. An index is a set of things that are
22609
+ * ranked against each other and that live and die together, and these two are
22610
+ * neither: a `faces` row is TRACK-OWNED and cascades away with its track under
22611
+ * a per-camera capacity cap, an `identity-samples` row is retention-EXEMPT
22612
+ * forever and is the gallery every recognition ranks against. One index would
22613
+ * mean every gallery load and every reconcile carried a filter whose failure
22614
+ * mode is either ranking a candidate against itself or reclaiming an enrolled
22615
+ * person's only sample. The dimension they share is not a reason to share an
22616
+ * index; the question they answer is, and it differs.
22314
22617
  *
22315
22618
  * The fix is not a faster loop, it is a different backend — and the backend
22316
22619
  * should be replaceable without touching six callers. So: a singleton
@@ -22415,7 +22718,20 @@ var VectorQueryResultSchema = object({
22415
22718
  */
22416
22719
  scanned: number(),
22417
22720
  /** True when the backend could not consider every row that passed the filter. */
22418
- truncated: boolean()
22721
+ truncated: boolean(),
22722
+ /**
22723
+ * The `topK` the backend actually ran with.
22724
+ *
22725
+ * Every backend has a ceiling — sqlite-vec's is 4,096 — and a caller asking
22726
+ * past it used to learn nothing but a boolean, from a WARN in the provider's
22727
+ * own log rather than in its answer. That is how an audit asking for 20,000
22728
+ * consumed 4,096 and reported `examined: 4096` as if it had walked the index,
22729
+ * for weeks. `truncated` says THAT the answer was short; this says BY HOW
22730
+ * MUCH, in the return value, where the caller cannot fail to see it.
22731
+ *
22732
+ * Equals the requested `topK` whenever nothing was lowered.
22733
+ */
22734
+ effectiveTopK: number().int().positive()
22419
22735
  });
22420
22736
  var VectorDeleteInputSchema = object({
22421
22737
  index: string(),
@@ -22444,6 +22760,68 @@ var VectorGetResultSchema = object({ items: array(object({
22444
22760
  id: string(),
22445
22761
  metadata: VectorMetadataSchema
22446
22762
  })) });
22763
+ /**
22764
+ * Ids to read back WITH their vectors.
22765
+ *
22766
+ * The sibling of {@link VectorGetResultSchema}, and deliberately a separate
22767
+ * method rather than a flag on it: `getByIds` promises no vectors and its one
22768
+ * caller depends on that promise. This one promises the opposite.
22769
+ *
22770
+ * It exists because a store cannot put its vectors here otherwise. An ArcFace
22771
+ * gallery is ranked IN PROCESS, per detection, against every enrolled sample —
22772
+ * a per-face cross-process KNN would be a network round trip inside the
22773
+ * recognition loop. So the gallery is loaded once and held in RAM, and loading
22774
+ * it requires the index to hand the floats back. Without this method the only
22775
+ * way to keep a readable vector is a JSON column, which is the thing this
22776
+ * capability exists to delete.
22777
+ *
22778
+ * BOUNDED BY THE CALLER: ids are named, never "everything". Enumerating an
22779
+ * index is {@link VectorScanInputSchema}'s job, and it returns no vectors.
22780
+ */
22781
+ var VectorFetchInputSchema = object({
22782
+ index: string(),
22783
+ ids: array(string())
22784
+ });
22785
+ var VectorFetchResultSchema = object({ items: array(object({
22786
+ id: string(),
22787
+ /** base64 Float32LE — the same wire form `upsert` accepts. */
22788
+ vector: string(),
22789
+ metadata: VectorMetadataSchema
22790
+ })) });
22791
+ /**
22792
+ * ENUMERATE an index: one page of rows in a stable order, no ranking.
22793
+ *
22794
+ * A reconcile does not want the nearest rows, it wants ALL of them, and asking
22795
+ * a KNN for "all" is the wrong question twice over. It hits the backend's `k`
22796
+ * ceiling — 4,096 on sqlite-vec against a 22,128-row index — and it needs a
22797
+ * probe vector it does not have, so the audit passed a ZERO vector whose cosine
22798
+ * distance to every row is degenerate. `examined: 4096` then read as "we
22799
+ * looked" for as long as anyone cared to read it.
22800
+ *
22801
+ * This is the primitive that question actually needs: a bounded page, ordered
22802
+ * by the backend's own row order, costing no distance computation at all.
22803
+ * Vectors are NOT returned — an enumeration that shipped 2 KB per row would be
22804
+ * the full-table read this capability was built to stop.
22805
+ */
22806
+ var VectorScanInputSchema = object({
22807
+ index: string(),
22808
+ /** Opaque resume point. `0` starts at the top; pass back `nextCursor`. */
22809
+ cursor: number().int().nonnegative().default(0),
22810
+ limit: number().int().positive()
22811
+ });
22812
+ var VectorScanResultSchema = object({
22813
+ items: array(object({
22814
+ id: string(),
22815
+ metadata: VectorMetadataSchema
22816
+ })),
22817
+ /**
22818
+ * Where the next page starts, or `null` when the walk reached the end.
22819
+ *
22820
+ * `null` is the ONLY end-of-index signal. A caller must not infer the end
22821
+ * from a short page: a backend is free to return fewer rows than asked.
22822
+ */
22823
+ nextCursor: number().int().nonnegative().nullable()
22824
+ });
22447
22825
  var VectorStatsInputSchema = object({ index: string() });
22448
22826
  var VectorStatsResultSchema = object({
22449
22827
  /** Provider id, so an operator can tell brute force from an ANN index. */
@@ -22462,7 +22840,7 @@ method(VectorDeclareIndexInputSchema, _void(), {
22462
22840
  }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
22463
22841
  kind: "mutation",
22464
22842
  auth: "admin"
22465
- }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22843
+ }), method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }), method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }), method(VectorFetchInputSchema, VectorFetchResultSchema, { auth: "admin" }), method(VectorScanInputSchema, VectorScanResultSchema, { auth: "admin" }), method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
22466
22844
  kind: "mutation",
22467
22845
  auth: "admin"
22468
22846
  }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
@@ -29081,6 +29459,9 @@ method(object({
29081
29459
  }), method(object({}), array(RelocateJobSchema).readonly(), {
29082
29460
  kind: "query",
29083
29461
  auth: "admin"
29462
+ }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
29463
+ kind: "query",
29464
+ auth: "admin"
29084
29465
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
29085
29466
  kind: "mutation",
29086
29467
  auth: "admin"
@@ -35715,6 +36096,18 @@ Object.freeze({
35715
36096
  addonId: null,
35716
36097
  access: "create"
35717
36098
  },
36099
+ "pipelineAnalytics.countRelocatableMedia": {
36100
+ capName: "pipeline-analytics",
36101
+ capScope: "device",
36102
+ addonId: null,
36103
+ access: "view"
36104
+ },
36105
+ "pipelineAnalytics.countUnstampedEventMedia": {
36106
+ capName: "pipeline-analytics",
36107
+ capScope: "device",
36108
+ addonId: null,
36109
+ access: "view"
36110
+ },
35718
36111
  "pipelineAnalytics.deleteDeviceEvents": {
35719
36112
  capName: "pipeline-analytics",
35720
36113
  capScope: "device",
@@ -36873,6 +37266,12 @@ Object.freeze({
36873
37266
  addonId: null,
36874
37267
  access: "view"
36875
37268
  },
37269
+ "recording.getRelocateResidue": {
37270
+ capName: "recording",
37271
+ capScope: "system",
37272
+ addonId: null,
37273
+ access: "view"
37274
+ },
36876
37275
  "recording.getStorageMigrationMoveStatus": {
36877
37276
  capName: "recording",
36878
37277
  capScope: "system",
@@ -37419,12 +37818,30 @@ Object.freeze({
37419
37818
  addonId: null,
37420
37819
  access: "create"
37421
37820
  },
37821
+ "storageMigration.drain": {
37822
+ capName: "storage-migration",
37823
+ capScope: "system",
37824
+ addonId: null,
37825
+ access: "create"
37826
+ },
37827
+ "storageMigration.movers": {
37828
+ capName: "storage-migration",
37829
+ capScope: "system",
37830
+ addonId: null,
37831
+ access: "view"
37832
+ },
37422
37833
  "storageMigration.plan": {
37423
37834
  capName: "storage-migration",
37424
37835
  capScope: "system",
37425
37836
  addonId: null,
37426
37837
  access: "view"
37427
37838
  },
37839
+ "storageMigration.residue": {
37840
+ capName: "storage-migration",
37841
+ capScope: "system",
37842
+ addonId: null,
37843
+ access: "view"
37844
+ },
37428
37845
  "storageMigration.start": {
37429
37846
  capName: "storage-migration",
37430
37847
  capScope: "system",
@@ -38259,6 +38676,12 @@ Object.freeze({
38259
38676
  addonId: null,
38260
38677
  access: "delete"
38261
38678
  },
38679
+ "vectorStore.fetchByIds": {
38680
+ capName: "vector-store",
38681
+ capScope: "system",
38682
+ addonId: null,
38683
+ access: "view"
38684
+ },
38262
38685
  "vectorStore.getByIds": {
38263
38686
  capName: "vector-store",
38264
38687
  capScope: "system",
@@ -38271,6 +38694,12 @@ Object.freeze({
38271
38694
  addonId: null,
38272
38695
  access: "view"
38273
38696
  },
38697
+ "vectorStore.scan": {
38698
+ capName: "vector-store",
38699
+ capScope: "system",
38700
+ addonId: null,
38701
+ access: "view"
38702
+ },
38274
38703
  "vectorStore.stats": {
38275
38704
  capName: "vector-store",
38276
38705
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rademacher",
3
- "version": "0.2.41",
3
+ "version": "0.2.44",
4
4
  "description": "Rademacher HomePilot device-provider addon for CamStack — wraps the @apocaliss92/noderademacher local-hub client (roller shutters over the cover cap)",
5
5
  "keywords": [
6
6
  "camstack",