@camstack/addon-post-analysis 1.2.144 → 1.2.147

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.
@@ -1,4 +1,4 @@
1
- import { $ as buildEventKindDescriptor, A as NcSnoozeSuppressedSchema, At as hydrateSchema, B as SCENE_DIVERGED, Bt as object, C as NcRuleInputSchema, Ct as vectorDimFromBase64, D as NcScheduleSchema, Dt as BaseAddon, E as NcRuleTargetSchema, Et as errMsg, F as RECORDING_EXPORT_MAX_READ_BYTES, Ft as array, G as TimelapseRuleSchema, Gt as EventCategory, H as TIMELAPSE_DENSE_FLOOR_SEC, Ht as record, I as RetrainStatusSchema, It as boolean, J as addonWidgetsSourceCapability, K as TrackSourceSchema, Lt as discriminatedUnion, M as NcTaxonomySchema, Mt as nodePin, N as OpsLogEntrySchema, Nt as sleep, O as NcSnoozeInputSchema, Ot as DeviceType, Pt as _enum, Q as audioModeOf, R as SCENE_DEFAULT_ANCHOR_THRESHOLD, Rt as literal, S as NcConditionDescriptorSchema, St as systemEventFilterApplies, T as NcRuleSchema, Tt as zoneAnalyticsCapability, U as TimelapseRuleInputSchema, Ut as string, V as SceneMonitorSchema, Vt as partialRecord, W as TimelapseRulePatchSchema, Wt as unknown, X as assertTimelapseCadences, Y as alarmPanelCapability, Z as audioMetricsCapability, _ as NC_ALARM_SYSTEM_EVENT_KINDS, _t as readDeviceStateFrom, a as COCO_TO_MACRO, at as encodeVectorBase64, bt as sceneMonitorCapability, c as DETECTION_MACRO_CLASSES, d as EVENT_KIND_BY_CAP, dt as kebabToCamel, et as cosineSimilarity$1, f as EVENT_PAD_MS, ft as notificationRulesCapability, g as MACRO_LABELS, gt as plateGalleryCapability, h as LabelAttributionSchema, ht as pipelineAnalyticsCapability, i as CLUSTER_MODEL_SCOPED_STEPS, j as NcSystemEventKindSchema, jt as isDeviceScopedCap, k as NcSnoozeSchema, kt as createEvent, lt as isDetectionMacroClass, m as FailureCounters, mt as pickClusterStepModels, n as AnalyticsGroupRecordSchema, nt as defineCustomActions, o as DEFAULT_EVENT_COLOR, ot as faceGalleryCapability, p as FULL_IMAGE_BBOX, q as VISIT_MERGE_GAP_MS, r as BaseDevice, rt as deriveRecordingMode, st as failureContributionCapability, t as AnalyticsGroupDetailSchema, tt as customAction, u as DeclaredDevices, ut as isScheduleActive, v as NC_CONDITION_CATALOG, vt as readTimelapseGeneratedAt, w as NcRulePatchSchema, wt as videoclipsCapability, x as NC_TAXONOMY, xt as subKindsOf, y as NC_DEFAULT_SNOOZE_MINUTES, z as SCENE_DEFAULT_UNCOVERED_POLICY, zt as number } from "../dist-3FJkk7Mx.mjs";
1
+ import { $ as buildEventKindDescriptor, A as NcSnoozeSuppressedSchema, At as hydrateSchema, B as SCENE_DIVERGED, Bt as object, C as NcRuleInputSchema, Ct as vectorDimFromBase64, D as NcScheduleSchema, Dt as BaseAddon, E as NcRuleTargetSchema, Et as errMsg, F as RECORDING_EXPORT_MAX_READ_BYTES, Ft as array, G as TimelapseRuleSchema, Gt as EventCategory, H as TIMELAPSE_DENSE_FLOOR_SEC, Ht as record, I as RetrainStatusSchema, It as boolean, J as addonWidgetsSourceCapability, K as TrackSourceSchema, Lt as discriminatedUnion, M as NcTaxonomySchema, Mt as nodePin, N as OpsLogEntrySchema, Nt as sleep, O as NcSnoozeInputSchema, Ot as DeviceType, Pt as _enum, Q as audioModeOf, R as SCENE_DEFAULT_ANCHOR_THRESHOLD, Rt as literal, S as NcConditionDescriptorSchema, St as systemEventFilterApplies, T as NcRuleSchema, Tt as zoneAnalyticsCapability, U as TimelapseRuleInputSchema, Ut as string, V as SceneMonitorSchema, Vt as partialRecord, W as TimelapseRulePatchSchema, Wt as unknown, X as assertTimelapseCadences, Y as alarmPanelCapability, Z as audioMetricsCapability, _ as NC_ALARM_SYSTEM_EVENT_KINDS, _t as readDeviceStateFrom, a as COCO_TO_MACRO, at as encodeVectorBase64, bt as sceneMonitorCapability, c as DETECTION_MACRO_CLASSES, d as EVENT_KIND_BY_CAP, dt as kebabToCamel, et as cosineSimilarity$1, f as EVENT_PAD_MS, ft as notificationRulesCapability, g as MACRO_LABELS, gt as plateGalleryCapability, h as LabelAttributionSchema, ht as pipelineAnalyticsCapability, i as CLUSTER_MODEL_SCOPED_STEPS, j as NcSystemEventKindSchema, jt as isDeviceScopedCap, k as NcSnoozeSchema, kt as createEvent, lt as isDetectionMacroClass, m as FailureCounters, mt as pickClusterStepModels, n as AnalyticsGroupRecordSchema, nt as defineCustomActions, o as DEFAULT_EVENT_COLOR, ot as faceGalleryCapability, p as FULL_IMAGE_BBOX, q as VISIT_MERGE_GAP_MS, r as BaseDevice, rt as deriveRecordingMode, st as failureContributionCapability, t as AnalyticsGroupDetailSchema, tt as customAction, u as DeclaredDevices, ut as isScheduleActive, v as NC_CONDITION_CATALOG, vt as readTimelapseGeneratedAt, w as NcRulePatchSchema, wt as videoclipsCapability, x as NC_TAXONOMY, xt as subKindsOf, y as NC_DEFAULT_SNOOZE_MINUTES, z as SCENE_DEFAULT_UNCOVERED_POLICY, zt as number } from "../dist-4eP-xmjY.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -40150,9 +40150,6 @@ function readJpegSize(data) {
40150
40150
  * table or the disk it consumes."
40151
40151
  */
40152
40152
  var RETRAIN_FRAMES_COLLECTION = "pipeline-analytics:retrain-frames";
40153
- /** Storage location for retrain copies — the same default media root the event
40154
- * media lives on, under a prefix nothing else enumerates. */
40155
- var RETRAIN_MEDIA_LOCATION = "eventMedia";
40156
40153
  /** Path prefix of every retrain copy. Never `<deviceId>/events/…`: the orphan
40157
40154
  * audit resolves ownership from INDEX rows, but a future filesystem-side tool
40158
40155
  * must be able to tell the two corpora apart by path alone. */
@@ -40245,6 +40242,7 @@ var RetrainFrameStore = class {
40245
40242
  readSource;
40246
40243
  now;
40247
40244
  writeGate;
40245
+ resolveEventMediaLocation;
40248
40246
  constructor(deps) {
40249
40247
  this.store = deps.store;
40250
40248
  this.storage = deps.storage;
@@ -40252,6 +40250,29 @@ var RetrainFrameStore = class {
40252
40250
  this.readSource = deps.readSource;
40253
40251
  this.now = deps.now;
40254
40252
  this.writeGate = deps.writeGate;
40253
+ this.resolveEventMediaLocation = deps.resolveEventMediaLocation;
40254
+ }
40255
+ /**
40256
+ * The location a NEW copy is written to, and the id its row NAMES.
40257
+ *
40258
+ * `null` = "no concrete location was resolved", which persists as the legacy
40259
+ * NULL row. A resolver that throws must not refuse the copy — losing an
40260
+ * operator's selection is worse than an unstamped row the seal can fix — but
40261
+ * it says so, because that branch re-creates the exact population a
40262
+ * non-blocking migration is gated on.
40263
+ */
40264
+ async resolveWriteLocation(deviceId) {
40265
+ if (!this.resolveEventMediaLocation) return null;
40266
+ try {
40267
+ const resolved = await this.resolveEventMediaLocation(deviceId);
40268
+ return resolved === "eventMedia" ? null : resolved;
40269
+ } catch (err) {
40270
+ this.logger.warn("retrain copy could not resolve its eventMedia location — the row is written UNSTAMPED and a storage repoint would re-aim it at a disk that does not hold it", {
40271
+ tags: { deviceId },
40272
+ meta: { error: String(err) }
40273
+ });
40274
+ return null;
40275
+ }
40255
40276
  }
40256
40277
  static async declare(store) {
40257
40278
  await store.declareCollection.mutate({
@@ -40410,6 +40431,7 @@ var RetrainFrameStore = class {
40410
40431
  };
40411
40432
  }
40412
40433
  const path = framePath(input.deviceId, input.trackId, frameId);
40434
+ const locationId = await this.resolveWriteLocation(input.deviceId);
40413
40435
  const frame = {
40414
40436
  frameId,
40415
40437
  deviceId: input.deviceId,
@@ -40420,11 +40442,11 @@ var RetrainFrameStore = class {
40420
40442
  width: size.width,
40421
40443
  height: size.height,
40422
40444
  copiedAt: this.now(),
40423
- locationId: null
40445
+ locationId
40424
40446
  };
40425
40447
  const persist = async () => {
40426
40448
  await this.storage.write({
40427
- location: RETRAIN_MEDIA_LOCATION,
40449
+ location: locationId ?? "eventMedia",
40428
40450
  relativePath: path,
40429
40451
  data: bytes
40430
40452
  });
@@ -40441,7 +40463,7 @@ var RetrainFrameStore = class {
40441
40463
  width: frame.width,
40442
40464
  height: frame.height,
40443
40465
  copiedAt: frame.copiedAt,
40444
- locationId: null
40466
+ locationId: frame.locationId
40445
40467
  }
40446
40468
  });
40447
40469
  };
@@ -43439,6 +43461,13 @@ function pickWritableMediaLocation(locations, currentDefaultId) {
43439
43461
  //#region src/pipeline-analytics/media-relocate-engine.ts
43440
43462
  var PAGE_SIZE = 200;
43441
43463
  var DEFAULT_THROTTLE_MBPS = 40;
43464
+ /** What a pass touches, reported on the job so an ops-log row says which of the
43465
+ * three passes ran without anyone reading this file. */
43466
+ function entitiesFor(mode) {
43467
+ if (mode === "seal") return ["mediaRows", "retrainFrameRows"];
43468
+ if (mode === "gallery") return ["galleryMedia"];
43469
+ return ["media", "retrainFrames"];
43470
+ }
43442
43471
  function snapshot(j) {
43443
43472
  return {
43444
43473
  jobId: j.jobId,
@@ -43446,7 +43475,7 @@ function snapshot(j) {
43446
43475
  fromLocationId: j.fromLocationId,
43447
43476
  toLocationId: j.toLocationId,
43448
43477
  deviceId: j.deviceId,
43449
- entities: ["media", "retrainFrames"],
43478
+ entities: [...entitiesFor(j.mode)],
43450
43479
  filesMoved: j.filesMoved,
43451
43480
  bytesMoved: j.bytesMoved,
43452
43481
  filesTotal: null,
@@ -43455,6 +43484,22 @@ function snapshot(j) {
43455
43484
  error: j.error
43456
43485
  };
43457
43486
  }
43487
+ /**
43488
+ * True when the row names NO location — the population `seal` exists for.
43489
+ *
43490
+ * Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
43491
+ * helper collapses "unstamped" and "stamped with the bare type ref" into one
43492
+ * answer, and only the first is a row whose meaning changes under a repoint.
43493
+ */
43494
+ function rowIsUnstamped(data) {
43495
+ const id = data["locationId"];
43496
+ return !(typeof id === "string" && id.length > 0);
43497
+ }
43498
+ /** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
43499
+ function rowIsGallery(data) {
43500
+ const ownerKind = String(data["ownerKind"] ?? "");
43501
+ return isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind);
43502
+ }
43458
43503
  var MediaRelocateEngine = class {
43459
43504
  deps;
43460
43505
  jobs = /* @__PURE__ */ new Map();
@@ -43470,11 +43515,34 @@ var MediaRelocateEngine = class {
43470
43515
  job.cancelRequested = true;
43471
43516
  return true;
43472
43517
  }
43518
+ /**
43519
+ * How many rows still carry no `locationId`, per collection.
43520
+ *
43521
+ * Read-only, and the ONLY number the migration planner's non-blocking gate
43522
+ * is allowed to trust: it is counted here, against the rows themselves,
43523
+ * rather than inferred from "a seal job reported done" — a seal that failed
43524
+ * some rows still reports a terminal state.
43525
+ */
43526
+ async countUnstamped() {
43527
+ const media = await this.countUnstampedIn(MEDIA_COLLECTION, "timestamp");
43528
+ const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION, "copiedAt");
43529
+ return {
43530
+ media,
43531
+ retrainFrames,
43532
+ total: media + retrainFrames
43533
+ };
43534
+ }
43535
+ async countUnstampedIn(collection, orderField) {
43536
+ let unstamped = 0;
43537
+ for await (const row of this.pageRows(collection, orderField)) if (rowIsUnstamped(row.data)) unstamped++;
43538
+ return unstamped;
43539
+ }
43473
43540
  start(input) {
43474
43541
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
43475
43542
  const job = {
43476
43543
  jobId: this.deps.newId(),
43477
43544
  state: "running",
43545
+ mode: input.mode ?? "move",
43478
43546
  fromLocationId: "*",
43479
43547
  toLocationId: input.toLocationId,
43480
43548
  deviceId: null,
@@ -43489,130 +43557,104 @@ var MediaRelocateEngine = class {
43489
43557
  this.run(job, input.throttleMbps ?? DEFAULT_THROTTLE_MBPS);
43490
43558
  return job.jobId;
43491
43559
  }
43560
+ /**
43561
+ * Page one collection oldest-first, yielding every row exactly once.
43562
+ *
43563
+ * The cursor is `(orderField, offset-within-ties)` rather than a plain
43564
+ * offset: 201 rows sharing one timestamp is a real shape here (a burst of
43565
+ * snapshots for one track), and a plain `offset` walk over a collection being
43566
+ * mutated underneath is how rows get skipped. Rows only ever gain a
43567
+ * `locationId` during a pass — never a new order key — so appending inserts
43568
+ * land after the cursor and are picked up, and nothing is missed.
43569
+ */
43570
+ async *pageRows(collection, orderField) {
43571
+ let cursor = 0;
43572
+ let offsetAtCursor = 0;
43573
+ for (;;) {
43574
+ const rows = await this.deps.store.query.query({
43575
+ collection,
43576
+ filter: {
43577
+ whereBetween: { [orderField]: [cursor, Number.MAX_SAFE_INTEGER] },
43578
+ orderBy: {
43579
+ field: orderField,
43580
+ direction: "asc"
43581
+ },
43582
+ limit: PAGE_SIZE,
43583
+ offset: offsetAtCursor
43584
+ }
43585
+ });
43586
+ if (rows.length === 0) return;
43587
+ for (const row of rows) yield row;
43588
+ const last = rows[rows.length - 1];
43589
+ if (last === void 0) return;
43590
+ const lastKey = Number(last.data[orderField] ?? cursor);
43591
+ if (lastKey === cursor) offsetAtCursor += rows.length;
43592
+ else {
43593
+ cursor = lastKey;
43594
+ offsetAtCursor = rows.filter((row) => Number(row.data[orderField]) === lastKey).length;
43595
+ }
43596
+ if (rows.length < PAGE_SIZE) return;
43597
+ }
43598
+ }
43599
+ /** Does this pass act on this row at all? */
43600
+ selects(job, data, isRetrain) {
43601
+ if (job.mode === "seal") return rowIsUnstamped(data);
43602
+ if (mediaRowLocation(data) === job.toLocationId) return false;
43603
+ if (job.mode === "gallery") return !isRetrain && rowIsGallery(data);
43604
+ return isRetrain || !rowIsGallery(data);
43605
+ }
43492
43606
  async run(job, throttleMbps) {
43493
43607
  const sleep = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
43494
43608
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
43495
43609
  try {
43496
43610
  await this.deps.resolveTargetRoot(job.toLocationId);
43497
43611
  let failedRows = 0;
43498
- let cursor = 0;
43499
- let offsetAtCursor = 0;
43500
- for (;;) {
43612
+ /** Selected rows that name no blob path. Counted and reported ONCE per
43613
+ * pass rather than per row: a row with no path names no bytes anywhere,
43614
+ * so there is nothing a repoint can orphan — but a pass that steps over
43615
+ * rows in silence reads as "there were none", which is how a migration
43616
+ * gets believed. */
43617
+ let pathlessRows = 0;
43618
+ const collections = job.mode === "gallery" ? [{
43619
+ name: MEDIA_COLLECTION,
43620
+ order: "timestamp"
43621
+ }] : [{
43622
+ name: MEDIA_COLLECTION,
43623
+ order: "timestamp"
43624
+ }, {
43625
+ name: RETRAIN_FRAMES_COLLECTION,
43626
+ order: "copiedAt"
43627
+ }];
43628
+ for (const collection of collections) {
43501
43629
  if (job.cancelRequested) break;
43502
- const rows = await this.deps.store.query.query({
43503
- collection: MEDIA_COLLECTION,
43504
- filter: {
43505
- whereBetween: { timestamp: [cursor, Number.MAX_SAFE_INTEGER] },
43506
- orderBy: {
43507
- field: "timestamp",
43508
- direction: "asc"
43509
- },
43510
- limit: PAGE_SIZE,
43511
- offset: offsetAtCursor
43512
- }
43513
- });
43514
- if (rows.length === 0) break;
43515
- for (const row of rows) {
43630
+ const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
43631
+ for await (const row of this.pageRows(collection.name, collection.order)) {
43516
43632
  if (job.cancelRequested) break;
43517
43633
  const data = row.data;
43518
- const ownerKind = String(data["ownerKind"] ?? "");
43519
- if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) continue;
43520
- const from = mediaRowLocation(data);
43521
- if (from === job.toLocationId) continue;
43522
- const relativePath = String(data["path"] ?? "");
43523
- if (relativePath.length === 0) continue;
43524
- try {
43525
- if (await this.samePhysicalLocation(from, job.toLocationId)) {
43526
- await this.deps.store.set.mutate({
43527
- collection: MEDIA_COLLECTION,
43634
+ if (!this.selects(job, data, isRetrain)) continue;
43635
+ if (job.mode === "seal") {
43636
+ try {
43637
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43638
+ job.filesMoved++;
43639
+ } catch (err) {
43640
+ failedRows++;
43641
+ this.deps.logger.debug("event-media seal row failed", { meta: {
43642
+ collection: collection.name,
43528
43643
  key: row.id,
43529
- value: {
43530
- ...data,
43531
- locationId: job.toLocationId
43532
- }
43533
- });
43534
- continue;
43644
+ error: String(err)
43645
+ } });
43535
43646
  }
43536
- const bytes = await this.deps.storage.read({
43537
- location: from,
43538
- relativePath
43539
- });
43540
- await this.deps.storage.write({
43541
- location: job.toLocationId,
43542
- relativePath,
43543
- data: bytes
43544
- });
43545
- if ((await this.deps.storage.read({
43546
- location: job.toLocationId,
43547
- relativePath
43548
- })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
43549
- await this.deps.store.set.mutate({
43550
- collection: MEDIA_COLLECTION,
43551
- key: row.id,
43552
- value: {
43553
- ...data,
43554
- locationId: job.toLocationId
43555
- }
43556
- });
43557
- await this.deps.storage.delete({
43558
- location: from,
43559
- relativePath
43560
- });
43561
- job.filesMoved++;
43562
- job.bytesMoved += bytes.length;
43563
- await sleep(bytes.length / bytesPerMs);
43564
- } catch (err) {
43565
- failedRows++;
43566
- this.deps.logger.debug("media relocate row failed", { meta: {
43567
- key: row.id,
43568
- error: String(err)
43569
- } });
43570
- }
43571
- }
43572
- const last = rows[rows.length - 1];
43573
- const lastTs = Number(last.data["timestamp"] ?? cursor);
43574
- if (lastTs === cursor) offsetAtCursor += rows.length;
43575
- else {
43576
- cursor = lastTs;
43577
- offsetAtCursor = rows.filter((row) => Number(row.data["timestamp"]) === lastTs).length;
43578
- }
43579
- if (rows.length < PAGE_SIZE) break;
43580
- }
43581
- let retrainOffset = 0;
43582
- for (;;) {
43583
- if (job.cancelRequested) break;
43584
- const rows = await this.deps.store.query.query({
43585
- collection: RETRAIN_FRAMES_COLLECTION,
43586
- filter: {
43587
- orderBy: {
43588
- field: "copiedAt",
43589
- direction: "asc"
43590
- },
43591
- limit: PAGE_SIZE,
43592
- offset: retrainOffset
43647
+ continue;
43593
43648
  }
43594
- });
43595
- if (rows.length === 0) break;
43596
- for (const row of rows) {
43597
- if (job.cancelRequested) break;
43598
- const data = row.data;
43599
43649
  const from = mediaRowLocation(data);
43600
- if (from === job.toLocationId) continue;
43601
43650
  const relativePath = String(data["path"] ?? "");
43602
43651
  if (relativePath.length === 0) {
43603
- failedRows++;
43652
+ pathlessRows++;
43604
43653
  continue;
43605
43654
  }
43606
43655
  try {
43607
43656
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
43608
- await this.deps.store.set.mutate({
43609
- collection: RETRAIN_FRAMES_COLLECTION,
43610
- key: row.id,
43611
- value: {
43612
- ...data,
43613
- locationId: job.toLocationId
43614
- }
43615
- });
43657
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43616
43658
  continue;
43617
43659
  }
43618
43660
  const bytes = await this.deps.storage.read({
@@ -43627,15 +43669,8 @@ var MediaRelocateEngine = class {
43627
43669
  if ((await this.deps.storage.read({
43628
43670
  location: job.toLocationId,
43629
43671
  relativePath
43630
- })).length !== bytes.length) throw new Error(`relocated retrain frame size mismatch for ${relativePath}`);
43631
- await this.deps.store.set.mutate({
43632
- collection: RETRAIN_FRAMES_COLLECTION,
43633
- key: row.id,
43634
- value: {
43635
- ...data,
43636
- locationId: job.toLocationId
43637
- }
43638
- });
43672
+ })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
43673
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43639
43674
  await this.deps.storage.delete({
43640
43675
  location: from,
43641
43676
  relativePath
@@ -43645,22 +43680,27 @@ var MediaRelocateEngine = class {
43645
43680
  await sleep(bytes.length / bytesPerMs);
43646
43681
  } catch (err) {
43647
43682
  failedRows++;
43648
- this.deps.logger.debug("retrain frame relocate row failed", { meta: {
43683
+ this.deps.logger.debug("media relocate row failed", { meta: {
43684
+ collection: collection.name,
43649
43685
  key: row.id,
43650
43686
  error: String(err)
43651
43687
  } });
43652
43688
  }
43653
43689
  }
43654
- retrainOffset += rows.length;
43655
- if (rows.length < PAGE_SIZE) break;
43656
43690
  }
43657
- if (failedRows > 0) throw new Error(`event-media relocation left ${failedRows} row(s) unmoved`);
43691
+ if (pathlessRows > 0) this.deps.logger.warn("media relocate stepped over rows that name no blob path", { meta: {
43692
+ jobId: job.jobId,
43693
+ mode: job.mode,
43694
+ rows: pathlessRows
43695
+ } });
43696
+ if (failedRows > 0) throw new Error(`event-media ${job.mode} left ${failedRows} row(s) unmoved`);
43658
43697
  job.state = job.cancelRequested ? "cancelled" : "done";
43659
43698
  } catch (err) {
43660
43699
  job.state = "failed";
43661
43700
  job.error = err instanceof Error ? err.message : String(err);
43662
43701
  this.deps.logger.warn("media relocate job failed", { meta: {
43663
43702
  jobId: job.jobId,
43703
+ mode: job.mode,
43664
43704
  error: job.error
43665
43705
  } });
43666
43706
  } finally {
@@ -43668,6 +43708,16 @@ var MediaRelocateEngine = class {
43668
43708
  this.deps.onFinished?.(snapshot(job));
43669
43709
  }
43670
43710
  }
43711
+ async stamp(collection, key, data, locationId) {
43712
+ await this.deps.store.set.mutate({
43713
+ collection,
43714
+ key,
43715
+ value: {
43716
+ ...data,
43717
+ locationId
43718
+ }
43719
+ });
43720
+ }
43671
43721
  async samePhysicalLocation(from, to) {
43672
43722
  if (from === to) return true;
43673
43723
  try {
@@ -62235,6 +62285,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62235
62285
  logger: logger.child("RetrainFrames"),
62236
62286
  now: () => Date.now(),
62237
62287
  writeGate: this.mediaWriteGate,
62288
+ resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId),
62238
62289
  readSource: async (mediaKey) => {
62239
62290
  const file = await mediaStore.getByKey(mediaKey);
62240
62291
  return file === null ? null : Buffer.from(file.base64, "base64");
@@ -68911,10 +68962,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68911
68962
  const jobId = this.mediaRelocate.start(input);
68912
68963
  this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
68913
68964
  jobId,
68965
+ mode: input.mode ?? "move",
68914
68966
  to: input.toLocationId
68915
68967
  } });
68916
68968
  return { jobId };
68917
68969
  }
68970
+ /**
68971
+ * How many media / retrain rows still carry no `locationId`.
68972
+ *
68973
+ * Counted against the rows, never inferred from a seal job's terminal state:
68974
+ * the migration planner refuses a non-blocking `eventMedia` cutover on this
68975
+ * number, and a seal that failed some rows still finishes.
68976
+ */
68977
+ async countUnstampedEventMedia() {
68978
+ if (!this.mediaRelocate) throw new Error("media relocation unavailable");
68979
+ return this.mediaRelocate.countUnstamped();
68980
+ }
68918
68981
  /** Every relocate job this addon knows about, newest first (in RAM: the
68919
68982
  * move is resumable, so a lost list costs nothing but the display). */
68920
68983
  async listRelocateMediaJobs() {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bb5cXFSY.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B8VSQDMs.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.144",
3
+ "version": "1.2.147",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",