@camstack/addon-post-analysis 1.2.613 → 1.2.614

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.
@@ -9499,7 +9499,9 @@ var MigrationMarkerBodySchema = require_clip_model_registry.object({
9499
9499
  });
9500
9500
  /** The orphan audit's resume position: one row, the whole map in one value on purpose. */
9501
9501
  var SweepCursorBodySchema = require_clip_model_registry.object({
9502
- scopes: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.number()),
9502
+ /** A number is an offset (a settings-store scope); a string is the records
9503
+ * engine's opaque keyset cursor (a scope walked through its kind). */
9504
+ scopes: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.union([require_clip_model_registry.number(), require_clip_model_registry.string()])),
9503
9505
  /** Absent on a row written before the walk existed — absent means "from the top". */
9504
9506
  faceVectorCursor: require_clip_model_registry.number().optional(),
9505
9507
  /** One cursor per CLIP index, keyed by index name. Absent on an older row. */
@@ -9973,7 +9975,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9973
9975
  //#endregion
9974
9976
  //#region src/pipeline-analytics/scene/scene-config-ledger.ts
9975
9977
  /** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
9976
- var PAGE_SIZE$1 = 1e3;
9978
+ var PAGE_SIZE$2 = 1e3;
9977
9979
  /**
9978
9980
  * A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
9979
9981
  * half-parsed scene would report a verdict about a region it cannot describe.
@@ -10011,7 +10013,7 @@ var SceneConfigLedger = class {
10011
10013
  let seen = 0;
10012
10014
  let cursor;
10013
10015
  for (;;) {
10014
- const limit = Math.min(PAGE_SIZE$1, this.loadLimit - seen);
10016
+ const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
10015
10017
  if (limit <= 0) break;
10016
10018
  const page = await this.kind.query({
10017
10019
  limit,
@@ -12058,6 +12060,28 @@ var EventLedgerCollection = class EventLedgerCollection {
12058
12060
  if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
12059
12061
  return out;
12060
12062
  }
12063
+ /**
12064
+ * One page of the whole kind in `id` order, resumed from the records
12065
+ * engine's OPAQUE keyset cursor (`null` = from the top). The answer's
12066
+ * `nextCursor` is `null` at the end. A keyset cursor names the last row
12067
+ * read, so deleting rows behind it never shifts what comes next — the
12068
+ * reason a bulk walk that reclaims as it goes needs this, not an offset.
12069
+ *
12070
+ * Records only: without `ctx.records` the caller pages the settings store
12071
+ * itself, and asking here throws rather than inventing a cursor.
12072
+ */
12073
+ async pageById(cursor, limit) {
12074
+ if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
12075
+ const page = await this.kind.query({
12076
+ order: "id",
12077
+ limit,
12078
+ ...cursor === null ? {} : { cursor }
12079
+ });
12080
+ return {
12081
+ rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
12082
+ nextCursor: page.nextCursor
12083
+ };
12084
+ }
12061
12085
  async getManyByIds(ids) {
12062
12086
  if (ids.length === 0) return [];
12063
12087
  if (this.kind === null) return (await this.store.query.query({
@@ -13184,11 +13208,35 @@ var FaceStore = class {
13184
13208
  * carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
13185
13209
  * that is what they are stamped with — a KNOWN historical fact, not a
13186
13210
  * fallback guess. A row that names some other model keeps its own.
13211
+ *
13212
+ * On a records hub the rows are paged through the faces LEDGER: the legacy
13213
+ * table is dropped there, and the adoption copied each row's body whole —
13214
+ * an `embedding` the pass had not drained yet included — so a records-era
13215
+ * row can still need converting (the same shape as `IdentityStore`).
13187
13216
  */
13188
13217
  conversionSpec() {
13218
+ const ledger = this.ledger;
13219
+ const recordsPaging = ledger.usesRecords ? {
13220
+ pageRows: (limit, offset) => ledger.query({
13221
+ orderBy: {
13222
+ field: "id",
13223
+ direction: "asc"
13224
+ },
13225
+ limit,
13226
+ offset
13227
+ }, [
13228
+ "deviceId",
13229
+ "timestamp",
13230
+ "trackId",
13231
+ "embeddingModelId",
13232
+ "embedding"
13233
+ ]),
13234
+ drainEmbedding: (id) => ledger.update(id, { embedding: [] })
13235
+ } : {};
13189
13236
  return {
13190
13237
  collection: FACES_COLLECTION,
13191
13238
  vectors: this.vectors,
13239
+ ...recordsPaging,
13192
13240
  write: (id, embedding, data) => {
13193
13241
  const deviceId = data["deviceId"];
13194
13242
  const timestamp = data["timestamp"];
@@ -67474,7 +67522,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
67474
67522
  });
67475
67523
  return "old-removal-retried";
67476
67524
  }
67477
- function tally$1(samples, outcomes, dryRun) {
67525
+ function tally$2(samples, outcomes, dryRun) {
67478
67526
  const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
67479
67527
  const refusals = [];
67480
67528
  for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
@@ -67546,7 +67594,7 @@ async function reenrolPhotoSamples(deps, options) {
67546
67594
  outcomes.push(outcomeOf(sample, verdict));
67547
67595
  await deps.pause(pacingMs);
67548
67596
  }
67549
- const result = tally$1(samples, outcomes, dryRun);
67597
+ const result = tally$2(samples, outcomes, dryRun);
67550
67598
  if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
67551
67599
  const { outcomes: _perSample, ...summary } = result;
67552
67600
  deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
@@ -78963,24 +79011,11 @@ function createDiscardJob(deps) {
78963
79011
  });
78964
79012
  await sleep(0);
78965
79013
  }
78966
- for (;;) {
79014
+ const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
79015
+ for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
78967
79016
  if (cancelRequested) return owners;
78968
- const rows = await deps.store.query.query({
78969
- collection: RETRAIN_FRAMES_COLLECTION,
78970
- columns: ["id"],
78971
- filter: {
78972
- where: { locationId },
78973
- limit: MEDIA_PAGE
78974
- }
78975
- });
78976
- if (rows.length === 0) break;
78977
- for (const row of rows) {
78978
- await deps.store.delete.mutate({
78979
- collection: RETRAIN_FRAMES_COLLECTION,
78980
- key: row.id
78981
- });
78982
- deleted += 1;
78983
- }
79017
+ const page = retrainIds.slice(i, i + MEDIA_PAGE);
79018
+ deleted += await deps.retrainFrameLedger.deleteByIds(page);
78984
79019
  report({
78985
79020
  dbRowsDeleted: deleted,
78986
79021
  detail: `deleted ${deleted} media/retrain rows`
@@ -79032,6 +79067,383 @@ function createDiscardJob(deps) {
79032
79067
  };
79033
79068
  }
79034
79069
  //#endregion
79070
+ //#region src/pipeline-analytics/retention/media-forget-actions.ts
79071
+ /**
79072
+ * The operator door for the NARROW media-row forget (`media-row-forget.ts`).
79073
+ *
79074
+ * It forgets `pipeline-analytics:media` rows stamped with ONE location whose
79075
+ * file is provably gone, and nothing else: no event, track, object-event,
79076
+ * face, plate or identity row is read for deletion, ever. Contrast
79077
+ * `storage.discardLocation`, which wipes the disk AND cascades owners.
79078
+ *
79079
+ * `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
79080
+ * also requires `expectAbsent` — the `absent` count a dry run just reported —
79081
+ * so the forget only ever deletes a population a human has already read.
79082
+ */
79083
+ /** Hard ceiling on rows one call examines — the call is synchronous. */
79084
+ var MEDIA_FORGET_MAX_ROWS = 5e4;
79085
+ var MediaForgetInputSchema = require_clip_model_registry.object({
79086
+ locationId: require_clip_model_registry.string().min(1),
79087
+ /** Restrict to these cameras. Omitted = every row stamped with the location. */
79088
+ deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int()).min(1).optional(),
79089
+ dryRun: require_clip_model_registry.boolean().default(true),
79090
+ /**
79091
+ * Required when `dryRun` is false: the `totals.absent` of the dry run. A
79092
+ * different count at apply time refuses the whole call before any delete.
79093
+ */
79094
+ expectAbsent: require_clip_model_registry.number().int().nonnegative().optional(),
79095
+ /** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
79096
+ maxRows: require_clip_model_registry.number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
79097
+ /** Detailed rows returned per verdict. Default 20. */
79098
+ sampleSize: require_clip_model_registry.number().int().min(0).max(500).optional()
79099
+ });
79100
+ var VerdictCountsSchema = require_clip_model_registry.object({
79101
+ rows: require_clip_model_registry.number(),
79102
+ /** File answered ENOENT — the only verdict that may be forgotten. */
79103
+ absent: require_clip_model_registry.number(),
79104
+ /** File is there: relocation is incomplete. Never forgotten. */
79105
+ present: require_clip_model_registry.number(),
79106
+ /** The stat failed with any errno other than ENOENT. Never forgotten. */
79107
+ error: require_clip_model_registry.number(),
79108
+ /** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
79109
+ kept: require_clip_model_registry.number(),
79110
+ /** Rows actually deleted (0 on a dry run). */
79111
+ deleted: require_clip_model_registry.number()
79112
+ });
79113
+ var MediaForgetSampleSchema = require_clip_model_registry.object({
79114
+ mediaId: require_clip_model_registry.number(),
79115
+ deviceId: require_clip_model_registry.number(),
79116
+ kind: require_clip_model_registry.string(),
79117
+ ownerKind: require_clip_model_registry.string(),
79118
+ ownerId: require_clip_model_registry.string(),
79119
+ path: require_clip_model_registry.string(),
79120
+ /** The errno (error) or the reason (kept); absent otherwise. */
79121
+ reason: require_clip_model_registry.string().optional()
79122
+ });
79123
+ var MediaForgetResultSchema = require_clip_model_registry.object({
79124
+ locationId: require_clip_model_registry.string(),
79125
+ dryRun: require_clip_model_registry.boolean(),
79126
+ root: require_clip_model_registry.string(),
79127
+ /** Rows of this location that were not examined because `maxRows` was hit. */
79128
+ truncated: require_clip_model_registry.boolean(),
79129
+ totals: VerdictCountsSchema,
79130
+ byDevice: require_clip_model_registry.array(VerdictCountsSchema.extend({ deviceId: require_clip_model_registry.number() })),
79131
+ byKind: require_clip_model_registry.array(VerdictCountsSchema.extend({ kind: require_clip_model_registry.string() })),
79132
+ /** Every id the call deletes (dry run: would delete). The exact list. */
79133
+ absentIds: require_clip_model_registry.array(require_clip_model_registry.number()),
79134
+ samples: require_clip_model_registry.object({
79135
+ absent: require_clip_model_registry.array(MediaForgetSampleSchema),
79136
+ present: require_clip_model_registry.array(MediaForgetSampleSchema),
79137
+ error: require_clip_model_registry.array(MediaForgetSampleSchema),
79138
+ kept: require_clip_model_registry.array(MediaForgetSampleSchema)
79139
+ }),
79140
+ /** Absent at classification, no longer ENOENT at delete time: kept. */
79141
+ changedBeforeDelete: require_clip_model_registry.number()
79142
+ });
79143
+ var mediaForgetActions = require_clip_model_registry.defineCustomActions({ "storage.forgetMissingMedia": require_clip_model_registry.customAction(MediaForgetInputSchema, MediaForgetResultSchema, {
79144
+ kind: "mutation",
79145
+ auth: "admin"
79146
+ }) });
79147
+ //#endregion
79148
+ //#region src/pipeline-analytics/retention/media-row-forget.ts
79149
+ /**
79150
+ * The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
79151
+ * with ONE storage location whose file is provably gone — and nothing else.
79152
+ *
79153
+ * Why it exists: a location whose bytes were lost keeps rows naming it, and a
79154
+ * cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
79155
+ * once cascaded ~132 k events away from a read it did not double-check. Here:
79156
+ *
79157
+ * - **Only media rows.** No event, track, object-event, face, plate or
79158
+ * identity row is deleted. A parent keeps existing with that media kind
79159
+ * missing — which is exactly what it already serves (`getBytesById` answers
79160
+ * `null` for a missing row as it does for a missing blob).
79161
+ * - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
79162
+ * location's own root on THIS node. Any other errno keeps the row and is
79163
+ * reported; a present file keeps the row and is reported (relocation is
79164
+ * incomplete). A row with no path, a path that escapes the root, or a
79165
+ * retention-exempt owner (the enrolled gallery) is kept.
79166
+ * - **Only rows stamped with the id.** `locationId IS NULL` rows mean
79167
+ * "wherever eventMedia points now" and are never selected.
79168
+ * - **Only a location nothing writes to.** An active (writable) location is
79169
+ * refused: new rows could land while the walk runs.
79170
+ * - **Dry run first, then the same count.** Applying requires
79171
+ * `expectAbsent`; the whole population is classified before any delete,
79172
+ * and a different absent count refuses the call with nothing written.
79173
+ * Each id is re-`stat`ed right before its batch is deleted, and the delete
79174
+ * predicate repeats `locationId`, so a row repointed in between survives.
79175
+ *
79176
+ * Occupancy needs no separate bookkeeping: `storage-occupancy` sums
79177
+ * `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
79178
+ */
79179
+ var PAGE_SIZE$1 = 200;
79180
+ var DELETE_BATCH = 200;
79181
+ var DEFAULT_SAMPLE_SIZE = 20;
79182
+ function errnoOf(err) {
79183
+ if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
79184
+ return err instanceof Error ? err.message : String(err);
79185
+ }
79186
+ function emptyCounts() {
79187
+ return {
79188
+ rows: 0,
79189
+ absent: 0,
79190
+ present: 0,
79191
+ error: 0,
79192
+ kept: 0,
79193
+ deleted: 0
79194
+ };
79195
+ }
79196
+ function tally$1(counts, verdict) {
79197
+ const rows = counts.rows + 1;
79198
+ switch (verdict) {
79199
+ case "absent": return {
79200
+ ...counts,
79201
+ rows,
79202
+ absent: counts.absent + 1
79203
+ };
79204
+ case "present": return {
79205
+ ...counts,
79206
+ rows,
79207
+ present: counts.present + 1
79208
+ };
79209
+ case "error": return {
79210
+ ...counts,
79211
+ rows,
79212
+ error: counts.error + 1
79213
+ };
79214
+ case "kept": return {
79215
+ ...counts,
79216
+ rows,
79217
+ kept: counts.kept + 1
79218
+ };
79219
+ }
79220
+ }
79221
+ /** The file's absolute path under `root`, or `null` when it would escape it. */
79222
+ function resolveUnder(root, relativePath) {
79223
+ const base = node_path.default.resolve(root);
79224
+ const full = node_path.default.resolve(base, relativePath);
79225
+ return full.startsWith(base + node_path.default.sep) ? full : null;
79226
+ }
79227
+ function toSample(id, data) {
79228
+ return {
79229
+ mediaId: Number(id),
79230
+ deviceId: Number(data["deviceId"] ?? 0),
79231
+ kind: String(data["kind"] ?? ""),
79232
+ ownerKind: String(data["ownerKind"] ?? ""),
79233
+ ownerId: String(data["ownerId"] ?? ""),
79234
+ path: String(data["path"] ?? "")
79235
+ };
79236
+ }
79237
+ async function probe(stat, absolutePath) {
79238
+ try {
79239
+ await stat(absolutePath);
79240
+ return { verdict: "present" };
79241
+ } catch (err) {
79242
+ const code = errnoOf(err);
79243
+ return code === "ENOENT" ? { verdict: "absent" } : {
79244
+ verdict: "error",
79245
+ reason: code
79246
+ };
79247
+ }
79248
+ }
79249
+ async function classify(stat, root, id, data) {
79250
+ const sample = toSample(id, data);
79251
+ const ownerKind = sample.ownerKind;
79252
+ if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
79253
+ key: id,
79254
+ sample: {
79255
+ ...sample,
79256
+ reason: "retention-exempt owner"
79257
+ },
79258
+ verdict: "kept",
79259
+ absolutePath: null
79260
+ };
79261
+ if (sample.path.length === 0) return {
79262
+ key: id,
79263
+ sample: {
79264
+ ...sample,
79265
+ reason: "no path"
79266
+ },
79267
+ verdict: "kept",
79268
+ absolutePath: null
79269
+ };
79270
+ const absolutePath = resolveUnder(root, sample.path);
79271
+ if (absolutePath === null) return {
79272
+ key: id,
79273
+ sample: {
79274
+ ...sample,
79275
+ reason: "path escapes the root"
79276
+ },
79277
+ verdict: "kept",
79278
+ absolutePath
79279
+ };
79280
+ const result = await probe(stat, absolutePath);
79281
+ return {
79282
+ key: id,
79283
+ sample: result.reason === void 0 ? sample : {
79284
+ ...sample,
79285
+ reason: result.reason
79286
+ },
79287
+ verdict: result.verdict,
79288
+ absolutePath
79289
+ };
79290
+ }
79291
+ function resolveTarget(locations, locationId, localNodeId) {
79292
+ const location = locations.find((row) => row.id === locationId);
79293
+ if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
79294
+ if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
79295
+ if (require_clip_model_registry.mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
79296
+ const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
79297
+ if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
79298
+ return {
79299
+ location,
79300
+ root: locationBasePath(location)
79301
+ };
79302
+ }
79303
+ /**
79304
+ * Classify every row stamped with `input.locationId` (bounded by `maxRows`),
79305
+ * and — only when `dryRun` is false and the absent count equals
79306
+ * `expectAbsent` — delete the absent ones, per camera, in batches.
79307
+ */
79308
+ async function forgetMissingMediaRows(deps, rawInput) {
79309
+ const input = MediaForgetInputSchema.parse(rawInput);
79310
+ if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
79311
+ const stat = deps.stat ?? ((p) => node_fs.promises.stat(p));
79312
+ const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
79313
+ const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
79314
+ const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
79315
+ const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
79316
+ const classified = [];
79317
+ let cursor = 0;
79318
+ let truncated = false;
79319
+ for (;;) {
79320
+ const rows = await deps.store.query.query({
79321
+ collection: MEDIA_COLLECTION,
79322
+ columns: [
79323
+ "id",
79324
+ "deviceId",
79325
+ "ownerKind",
79326
+ "ownerId",
79327
+ "kind",
79328
+ "path"
79329
+ ],
79330
+ filter: {
79331
+ where: { locationId: input.locationId },
79332
+ ...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
79333
+ whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
79334
+ orderBy: {
79335
+ field: "id",
79336
+ direction: "asc"
79337
+ },
79338
+ limit: PAGE_SIZE$1
79339
+ }
79340
+ });
79341
+ if (rows.length === 0) break;
79342
+ for (const row of rows) {
79343
+ if (classified.length >= maxRows) {
79344
+ truncated = true;
79345
+ break;
79346
+ }
79347
+ classified.push(await classify(stat, root, row.id, row.data));
79348
+ cursor = Math.max(cursor, Number(row.id));
79349
+ }
79350
+ if (truncated || rows.length < PAGE_SIZE$1) break;
79351
+ await pace();
79352
+ }
79353
+ const absentRows = classified.filter((c) => c.verdict === "absent");
79354
+ const deletedIds = /* @__PURE__ */ new Set();
79355
+ let changedBeforeDelete = 0;
79356
+ if (!input.dryRun) {
79357
+ if (absentRows.length !== input.expectAbsent) throw new Error(`forgetMissingMedia refused: ${String(absentRows.length)} rows are absent now, expectAbsent was ${String(input.expectAbsent)} — run the dry run again; nothing was deleted`);
79358
+ const byDevice = /* @__PURE__ */ new Map();
79359
+ for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
79360
+ for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
79361
+ const batch = [];
79362
+ for (const row of rows.slice(at, at + DELETE_BATCH)) if ((row.absolutePath === null ? null : await probe(stat, row.absolutePath))?.verdict === "absent") batch.push(row);
79363
+ else changedBeforeDelete += 1;
79364
+ if (batch.length === 0) continue;
79365
+ const { deleted } = await deps.store.deleteWhere.mutate({
79366
+ collection: MEDIA_COLLECTION,
79367
+ filter: {
79368
+ where: {
79369
+ locationId: input.locationId,
79370
+ deviceId
79371
+ },
79372
+ whereIn: { id: batch.map((row) => row.key) }
79373
+ }
79374
+ });
79375
+ for (const row of batch) deletedIds.add(row.sample.mediaId);
79376
+ deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
79377
+ tags: { deviceId },
79378
+ meta: {
79379
+ locationId: input.locationId,
79380
+ batch: batch.length,
79381
+ deleted,
79382
+ firstId: batch[0]?.sample.mediaId,
79383
+ lastId: batch[batch.length - 1]?.sample.mediaId
79384
+ }
79385
+ });
79386
+ if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
79387
+ tags: { deviceId },
79388
+ meta: {
79389
+ locationId: input.locationId,
79390
+ batch: batch.length,
79391
+ deleted
79392
+ }
79393
+ });
79394
+ await pace();
79395
+ }
79396
+ }
79397
+ let totals = emptyCounts();
79398
+ const perDevice = /* @__PURE__ */ new Map();
79399
+ const perKind = /* @__PURE__ */ new Map();
79400
+ for (const row of classified) {
79401
+ const isDeleted = deletedIds.has(row.sample.mediaId);
79402
+ const bump = (c) => {
79403
+ const next = tally$1(c, row.verdict);
79404
+ return isDeleted ? {
79405
+ ...next,
79406
+ deleted: next.deleted + 1
79407
+ } : next;
79408
+ };
79409
+ totals = bump(totals);
79410
+ perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
79411
+ perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
79412
+ }
79413
+ const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
79414
+ for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
79415
+ tags: { deviceId },
79416
+ meta: {
79417
+ locationId: input.locationId,
79418
+ dryRun: input.dryRun,
79419
+ ...counts
79420
+ }
79421
+ });
79422
+ return {
79423
+ locationId: input.locationId,
79424
+ dryRun: input.dryRun,
79425
+ root,
79426
+ truncated,
79427
+ totals,
79428
+ byDevice: [...perDevice].map(([deviceId, c]) => ({
79429
+ deviceId,
79430
+ ...c
79431
+ })),
79432
+ byKind: [...perKind].map(([kind, c]) => ({
79433
+ kind,
79434
+ ...c
79435
+ })),
79436
+ absentIds: absentRows.map((r) => r.sample.mediaId),
79437
+ samples: {
79438
+ absent: samplesOf("absent"),
79439
+ present: samplesOf("present"),
79440
+ error: samplesOf("error"),
79441
+ kept: samplesOf("kept")
79442
+ },
79443
+ changedBeforeDelete
79444
+ };
79445
+ }
79446
+ //#endregion
79035
79447
  //#region src/pipeline-analytics/retention/media-reclaim.ts
79036
79448
  var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
79037
79449
  var DEFAULT_MEDIA_RECLAIM_LIMITS = {
@@ -93359,17 +93771,8 @@ function rowIsGallery(data) {
93359
93771
  * D295 calls "a guard nobody believes".
93360
93772
  */
93361
93773
  function collectionsFor(mode) {
93362
- if (mode === "gallery") return [{
93363
- name: MEDIA_COLLECTION,
93364
- order: "timestamp"
93365
- }];
93366
- return [{
93367
- name: MEDIA_COLLECTION,
93368
- order: "timestamp"
93369
- }, {
93370
- name: RETRAIN_FRAMES_COLLECTION,
93371
- order: "copiedAt"
93372
- }];
93774
+ if (mode === "gallery") return [{ lane: "media" }];
93775
+ return [{ lane: "media" }, { lane: "retrainFrames" }];
93373
93776
  }
93374
93777
  /**
93375
93778
  * Does this pass act on this row at all?
@@ -93454,7 +93857,7 @@ var MediaRelocateEngine = class {
93454
93857
  async countUnstamped() {
93455
93858
  try {
93456
93859
  const media = await this.countUnstampedIn(MEDIA_COLLECTION);
93457
- const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
93860
+ const retrainFrames = await this.countUnstampedRetrainFrames();
93458
93861
  return {
93459
93862
  media,
93460
93863
  retrainFrames,
@@ -93506,6 +93909,28 @@ var MediaRelocateEngine = class {
93506
93909
  };
93507
93910
  }
93508
93911
  }
93912
+ /**
93913
+ * The retrain lane's unstamped population. The ledger's records `where` is
93914
+ * equality-only (a `null` predicate is dropped, which would count the whole
93915
+ * dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
93916
+ * dataset is operator-selected and small.
93917
+ */
93918
+ async countUnstampedRetrainFrames() {
93919
+ const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
93920
+ return {
93921
+ present: rows > 0,
93922
+ rows
93923
+ };
93924
+ }
93925
+ /** Every row of one lane, oldest-first. */
93926
+ rowsOf(collection) {
93927
+ return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
93928
+ }
93929
+ /** The retrain-frame ledger, read whole and yielded oldest copy first. */
93930
+ async *retrainFrameRows() {
93931
+ const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
93932
+ for (const row of ordered) yield row;
93933
+ }
93509
93934
  start(input) {
93510
93935
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
93511
93936
  this.cancelCensuses("a relocate pass started");
@@ -93609,22 +94034,22 @@ var MediaRelocateEngine = class {
93609
94034
  let sealable = 0;
93610
94035
  for (const collection of collectionsFor(mode)) {
93611
94036
  if (stop()) return null;
93612
- sealable += await this.deps.store.count.query({
93613
- collection: collection.name,
94037
+ sealable += collection.lane === "media" ? await this.deps.store.count.query({
94038
+ collection: MEDIA_COLLECTION,
93614
94039
  filter: { where: { [LOCATION_ID_FIELD]: null } }
93615
- });
94040
+ }) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
93616
94041
  }
93617
94042
  return { rows: sealable };
93618
94043
  }
93619
94044
  let rows = 0;
93620
94045
  for (const collection of collectionsFor(mode)) {
93621
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93622
- for await (const row of this.pageRows(collection.name, collection.order)) {
94046
+ const isRetrain = collection.lane === "retrainFrames";
94047
+ for await (const row of this.rowsOf(collection)) {
93623
94048
  if (stop()) {
93624
94049
  this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
93625
94050
  mode,
93626
94051
  toLocationId,
93627
- collection: collection.name
94052
+ collection: collection.lane
93628
94053
  } });
93629
94054
  return null;
93630
94055
  }
@@ -93632,7 +94057,7 @@ var MediaRelocateEngine = class {
93632
94057
  this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
93633
94058
  mode,
93634
94059
  toLocationId,
93635
- collection: collection.name,
94060
+ collection: collection.lane,
93636
94061
  rowsVisited: budget.rowsVisited,
93637
94062
  rowBudget: MEDIA_CENSUS_ROW_BUDGET,
93638
94063
  deadlineMs: MEDIA_CENSUS_DEADLINE_MS
@@ -93668,15 +94093,15 @@ var MediaRelocateEngine = class {
93668
94093
  if (job.mode === "seal") {
93669
94094
  for (const collection of collectionsFor(job.mode)) {
93670
94095
  if (job.cancelRequested) break;
93671
- await this.sealCollection(collection.name, job);
94096
+ await this.sealCollection(collection, job);
93672
94097
  }
93673
94098
  job.state = job.cancelRequested ? "cancelled" : "done";
93674
94099
  return;
93675
94100
  }
93676
94101
  for (const collection of collectionsFor(job.mode)) {
93677
94102
  if (job.cancelRequested) break;
93678
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93679
- for await (const row of this.pageRows(collection.name, collection.order)) {
94103
+ const isRetrain = collection.lane === "retrainFrames";
94104
+ for await (const row of this.rowsOf(collection)) {
93680
94105
  if (job.cancelRequested) break;
93681
94106
  const data = row.data;
93682
94107
  if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
@@ -93688,7 +94113,7 @@ var MediaRelocateEngine = class {
93688
94113
  }
93689
94114
  try {
93690
94115
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
93691
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94116
+ await this.stamp(collection, row.id, data, job.toLocationId);
93692
94117
  continue;
93693
94118
  }
93694
94119
  const bytes = await this.deps.storage.read({
@@ -93704,7 +94129,7 @@ var MediaRelocateEngine = class {
93704
94129
  location: job.toLocationId,
93705
94130
  relativePath
93706
94131
  })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
93707
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94132
+ await this.stamp(collection, row.id, data, job.toLocationId);
93708
94133
  await this.deps.storage.delete({
93709
94134
  location: from,
93710
94135
  relativePath
@@ -93715,7 +94140,7 @@ var MediaRelocateEngine = class {
93715
94140
  } catch (err) {
93716
94141
  failedRows++;
93717
94142
  this.deps.logger.debug("media relocate row failed", { meta: {
93718
- collection: collection.name,
94143
+ collection: collection.lane,
93719
94144
  key: row.id,
93720
94145
  error: String(err)
93721
94146
  } });
@@ -93788,7 +94213,17 @@ var MediaRelocateEngine = class {
93788
94213
  * reports a completed seal having stamped nothing. Zero unstamped rows is the
93789
94214
  * single value the migration gate reads as "safe to cut over".
93790
94215
  */
93791
- async sealCollection(collection, job) {
94216
+ async sealCollection(lane, job) {
94217
+ if (lane.lane === "retrainFrames") {
94218
+ for (const row of await this.deps.retrainFrameLedger.query({})) {
94219
+ if (job.cancelRequested) return;
94220
+ if (!rowIsUnstamped(row.data)) continue;
94221
+ await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
94222
+ job.filesMoved += 1;
94223
+ }
94224
+ return;
94225
+ }
94226
+ const collection = MEDIA_COLLECTION;
93792
94227
  for (;;) {
93793
94228
  if (job.cancelRequested) return;
93794
94229
  const batch = await this.deps.store.query.query({
@@ -93809,9 +94244,13 @@ var MediaRelocateEngine = class {
93809
94244
  job.filesMoved += updated;
93810
94245
  }
93811
94246
  }
93812
- async stamp(collection, key, data, locationId) {
94247
+ async stamp(lane, key, data, locationId) {
94248
+ if (lane.lane === "retrainFrames") {
94249
+ await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
94250
+ return;
94251
+ }
93813
94252
  await this.deps.store.set.mutate({
93814
- collection,
94253
+ collection: MEDIA_COLLECTION,
93815
94254
  key,
93816
94255
  value: {
93817
94256
  ...data,
@@ -102258,6 +102697,18 @@ function decodeCursorMap(raw) {
102258
102697
  function nonNegativeInt(value) {
102259
102698
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
102260
102699
  }
102700
+ function decodeScopeCursors(raw) {
102701
+ const out = {};
102702
+ if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
102703
+ if (typeof cursor === "string" && cursor.length > 0) {
102704
+ out[key] = cursor;
102705
+ continue;
102706
+ }
102707
+ const parsed = nonNegativeInt(cursor);
102708
+ if (parsed !== null) out[key] = parsed;
102709
+ }
102710
+ return out;
102711
+ }
102261
102712
  /** Read a stored row back into a cursor map, ignoring anything unreadable. */
102262
102713
  function decodeOrphanAuditCursors(value) {
102263
102714
  const empty = {
@@ -102269,7 +102720,7 @@ function decodeOrphanAuditCursors(value) {
102269
102720
  if (value === null || typeof value !== "object") return empty;
102270
102721
  const row = { ...value };
102271
102722
  return {
102272
- scopes: decodeCursorMap(row["scopes"]),
102723
+ scopes: decodeScopeCursors(row["scopes"]),
102273
102724
  faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
102274
102725
  clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
102275
102726
  updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
@@ -103842,7 +104293,9 @@ async function auditScope(scope, deps, limits, cursor) {
103842
104293
  };
103843
104294
  const deviceIds = /* @__PURE__ */ new Set();
103844
104295
  const graceFloor = deps.now() - limits.graceMs;
103845
- let offset = Math.max(0, cursor);
104296
+ let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
104297
+ let keysetCursor = typeof cursor === "string" ? cursor : null;
104298
+ let keyed = keysetCursor !== null;
103846
104299
  let stoppedBy = "exhausted";
103847
104300
  let collectionSize = null;
103848
104301
  try {
@@ -103859,9 +104312,16 @@ async function auditScope(scope, deps, limits, cursor) {
103859
104312
  stoppedBy = "row-ceiling";
103860
104313
  break;
103861
104314
  }
103862
- const rows = await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
104315
+ if (deps.stop?.() === true) {
104316
+ stoppedBy = "stopped";
104317
+ break;
104318
+ }
104319
+ const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
104320
+ keyed = keyedPage !== null;
104321
+ const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
103863
104322
  if (rows.length === 0) {
103864
104323
  offset = 0;
104324
+ keysetCursor = null;
103865
104325
  break;
103866
104326
  }
103867
104327
  const refs = rows.map((row) => scope.resolve(row));
@@ -103948,9 +104408,17 @@ async function auditScope(scope, deps, limits, cursor) {
103948
104408
  if (deps.breathe) await deps.breathe();
103949
104409
  if (ceilingHit) {
103950
104410
  stoppedBy = "reclaim-ceiling";
103951
- offset += rows.length - reclaimedInPage;
104411
+ if (!keyed) offset += rows.length - reclaimedInPage;
103952
104412
  break;
103953
104413
  }
104414
+ if (keyedPage !== null) {
104415
+ if (keyedPage.nextCursor === null) {
104416
+ keysetCursor = null;
104417
+ break;
104418
+ }
104419
+ keysetCursor = keyedPage.nextCursor;
104420
+ continue;
104421
+ }
103954
104422
  const advanced = rows.length - reclaimedInPage;
103955
104423
  if (advanced === 0 && rows.length < limits.pageSize) {
103956
104424
  offset = 0;
@@ -103966,7 +104434,7 @@ async function auditScope(scope, deps, limits, cursor) {
103966
104434
  collectionSize,
103967
104435
  coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
103968
104436
  stoppedBy,
103969
- nextCursor: offset
104437
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103970
104438
  } });
103971
104439
  const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
103972
104440
  if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
@@ -103993,7 +104461,7 @@ async function auditScope(scope, deps, limits, cursor) {
103993
104461
  unresolvable: tally.unresolvable,
103994
104462
  deviceIds: [...deviceIds],
103995
104463
  stoppedBy,
103996
- nextCursor: offset
104464
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103997
104465
  };
103998
104466
  }
103999
104467
  /**
@@ -104085,7 +104553,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
104085
104553
  //#endregion
104086
104554
  //#region src/pipeline-analytics/retention/orphan-reclaimers.ts
104087
104555
  var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
104088
- var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION, "pipeline-analytics:plates"]);
104556
+ var PLATES_COLLECTION = "pipeline-analytics:plates";
104557
+ var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
104089
104558
  /**
104090
104559
  * The `deleteRow` port: ONE row by its primary key `id` (what every
104091
104560
  * raw-delete collection above declares), answering whether a row went.
@@ -104159,6 +104628,13 @@ function buildOrphanReclaimer(ports) {
104159
104628
  rows: await ports.deleteTrackAlias(key) ? 1 : 0,
104160
104629
  bytes: null
104161
104630
  };
104631
+ if (collection === PLATES_COLLECTION) {
104632
+ const assert = trackIdAssertOf(data);
104633
+ return {
104634
+ rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
104635
+ bytes: null
104636
+ };
104637
+ }
104162
104638
  if (RAW_DELETE_COLLECTIONS.has(collection)) {
104163
104639
  const assert = trackIdAssertOf(data);
104164
104640
  if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
@@ -104180,6 +104656,87 @@ function buildOrphanReclaimer(ports) {
104180
104656
  };
104181
104657
  }
104182
104658
  //#endregion
104659
+ //#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
104660
+ /** Audited collection → its records-kind ledger. Empty without records. */
104661
+ function orphanAuditLedgers(wiring) {
104662
+ if (wiring.records === void 0) return /* @__PURE__ */ new Map();
104663
+ return new Map([
104664
+ [FACES_COLLECTION, wireFaceLedger(wiring)],
104665
+ [PLATES_COLLECTION$1, wirePlateLedger(wiring)],
104666
+ [TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
104667
+ ]);
104668
+ }
104669
+ /** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
104670
+ function keysetPager(ledgers) {
104671
+ return async (collection, cursor, limit) => {
104672
+ const ledger = ledgers.get(collection);
104673
+ if (ledger === void 0) return null;
104674
+ const page = await ledger.pageById(cursor, limit);
104675
+ return {
104676
+ rows: page.rows.map((row) => ({
104677
+ key: row.id,
104678
+ data: row.data
104679
+ })),
104680
+ nextCursor: page.nextCursor
104681
+ };
104682
+ };
104683
+ }
104684
+ /** The kind's count for a ledger-backed collection, `undefined` for any other. */
104685
+ async function ledgerSize(ledgers, collection) {
104686
+ const ledger = ledgers.get(collection);
104687
+ return ledger === void 0 ? void 0 : ledger.count();
104688
+ }
104689
+ /**
104690
+ * The plate reclaim through the plate ledger — records on a hub, the legacy
104691
+ * collection only without records. With `assert`, a row whose `trackId` is not
104692
+ * the dead track's is KEPT (logged), never deleted.
104693
+ */
104694
+ function plateDeleter(plates, logger) {
104695
+ return async (plateId, assert) => {
104696
+ const row = await plates.get(plateId);
104697
+ if (row === null) return false;
104698
+ if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
104699
+ const deviceId = row.data["deviceId"];
104700
+ logger.warn("orphan audit: plate row now names another track — kept", {
104701
+ ...typeof deviceId === "number" ? { tags: { deviceId } } : {},
104702
+ meta: {
104703
+ plateId,
104704
+ expectedTrackId: assert.trackId
104705
+ }
104706
+ });
104707
+ return false;
104708
+ }
104709
+ await plates.deleteKey(plateId);
104710
+ return true;
104711
+ };
104712
+ }
104713
+ function auditCollectionPorts(store, ledgers) {
104714
+ return {
104715
+ pageKeyset: keysetPager(ledgers),
104716
+ pageByOffset: async (collection, offset, limit) => {
104717
+ return (await store.query.query({
104718
+ collection,
104719
+ filter: {
104720
+ orderBy: {
104721
+ field: "id",
104722
+ direction: "asc"
104723
+ },
104724
+ limit,
104725
+ offset
104726
+ }
104727
+ })).map((row) => ({
104728
+ key: row.id,
104729
+ data: row.data
104730
+ }));
104731
+ },
104732
+ size: async (collection) => {
104733
+ const counted = await ledgerSize(ledgers, collection);
104734
+ if (counted !== void 0) return counted;
104735
+ return store.count.query({ collection });
104736
+ }
104737
+ };
104738
+ }
104739
+ //#endregion
104183
104740
  //#region src/pipeline-analytics/retention/retired-device-settings.ts
104184
104741
  /**
104185
104742
  * Every retired knob, with the sentence that explains it. The list is the
@@ -114937,6 +115494,7 @@ var customActions = {
114937
115494
  ...orphanAuditActions,
114938
115495
  ...mediaReclaimActions,
114939
115496
  ...locationDiscardActions,
115497
+ ...mediaForgetActions,
114940
115498
  ...photoEnrollActions,
114941
115499
  ...faceRescoreActions,
114942
115500
  ...stillnessCalibrationActions
@@ -116727,6 +117285,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116727
117285
  orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
116728
117286
  /** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
116729
117287
  discardJob = null;
117288
+ /** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
117289
+ mediaForgetDeps = null;
116730
117290
  /**
116731
117291
  * The CLIP indexes' walks (D649), one resume cursor per index, kept in their
116732
117292
  * OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
@@ -116935,10 +117495,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116935
117495
  const isHub = ownNodeId === "hub";
116936
117496
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
116937
117497
  if (isHub) this.reconcileTapePinsInBackground();
117498
+ this.mediaForgetDeps = {
117499
+ listLocations: async () => api.storage.listLocations.query({}),
117500
+ localNodeId: ownNodeId,
117501
+ store: api.settingsStore,
117502
+ logger: logger.child("MediaForget")
117503
+ };
116938
117504
  this.discardJob = createDiscardJob({
116939
117505
  listLocations: async () => api.storage.listLocations.query({}),
116940
117506
  localNodeId: ownNodeId,
116941
117507
  store: api.settingsStore,
117508
+ retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
116942
117509
  deleteTrackEvents: async (trackId) => {
116943
117510
  await this.eventStore?.deleteByTrack(trackId);
116944
117511
  },
@@ -116959,6 +117526,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116959
117526
  ...orphanAuditActions,
116960
117527
  ...mediaReclaimActions,
116961
117528
  ...locationDiscardActions,
117529
+ ...mediaForgetActions,
116962
117530
  ...photoEnrollActions,
116963
117531
  ...faceRescoreActions,
116964
117532
  ...stillnessCalibrationActions
@@ -117554,6 +118122,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
117554
118122
  });
117555
118123
  this.mediaRelocate = new MediaRelocateEngine({
117556
118124
  store: api.settingsStore,
118125
+ retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
117557
118126
  storage,
117558
118127
  logger: logger.child("MediaRelocate"),
117559
118128
  now: () => Date.now(),
@@ -125296,36 +125865,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125296
125865
  const trackStore = this.trackStore;
125297
125866
  const eventStore = this.eventStore;
125298
125867
  const mediaStore = this.mediaStore;
125299
- if (!trackStore || !eventStore || !mediaStore) {
125868
+ const settingsStore = this.settingsStore;
125869
+ if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
125300
125870
  if (!alreadyHeld) this.orphanAuditRunning = false;
125301
125871
  return null;
125302
125872
  }
125303
125873
  try {
125304
125874
  const limits = await this.orphanAuditLimits(overrides);
125875
+ const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
125876
+ const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
125877
+ const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
125305
125878
  const deps = {
125879
+ pageKeyset: collectionPorts.pageKeyset,
125880
+ stop: () => this.shuttingDown,
125306
125881
  page: async (collection, shard, offset, limit) => {
125307
125882
  if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
125308
125883
  key: String(row.mediaId),
125309
125884
  data: row.data
125310
125885
  }));
125311
- return (await this.settingsStore?.query.query({
125312
- collection,
125313
- filter: {
125314
- orderBy: {
125315
- field: "id",
125316
- direction: "asc"
125317
- },
125318
- limit,
125319
- offset
125320
- }
125321
- }) ?? []).map((row) => ({
125322
- key: row.id,
125323
- data: row.data
125324
- }));
125886
+ return collectionPorts.pageByOffset(collection, offset, limit);
125325
125887
  },
125326
125888
  size: async (collection, shard) => {
125327
125889
  if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
125328
- return await this.settingsStore?.count.query({ collection }) ?? null;
125890
+ return collectionPorts.size(collection);
125329
125891
  },
125330
125892
  liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
125331
125893
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
@@ -125348,6 +125910,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125348
125910
  if (aliasStore === null) throw new Error("track alias store not bound");
125349
125911
  return await aliasStore.deleteAlias(aliasId);
125350
125912
  },
125913
+ deletePlate: plateDeleter(plateLedger, this.ctx.logger),
125351
125914
  deleteRow: settingsRowDeleterOf(() => this.settingsStore),
125352
125915
  logger: this.ctx.logger
125353
125916
  });
@@ -126769,7 +127332,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
126769
127332
  jobId: null
126770
127333
  },
126771
127334
  "storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
126772
- "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
127335
+ "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
127336
+ "storage.forgetMissingMedia": async (input) => {
127337
+ const deps = this.mediaForgetDeps;
127338
+ if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
127339
+ return forgetMissingMediaRows(deps, input);
127340
+ }
126773
127341
  };
126774
127342
  }
126775
127343
  /**