@camstack/addon-post-analysis 1.2.62 → 1.2.64

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 array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-CdqESw05.mjs";
1
+ import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-CSTtfEOC.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";
@@ -5589,6 +5589,12 @@ function frozenOccupancy(rule, occ) {
5589
5589
  occupied: occ.occupied
5590
5590
  };
5591
5591
  }
5592
+ function notificationZones(rule, subject) {
5593
+ const configured = rule.conditions.zones?.ids;
5594
+ if (configured === void 0 || configured.length === 0) return subject.zones;
5595
+ const allowed = new Set(configured);
5596
+ return subject.zones.filter((zoneId) => allowed.has(zoneId));
5597
+ }
5592
5598
  /** Classify an object-event row as a package delivery / pick-up, or `null` when
5593
5599
  * it is an ordinary detection. */
5594
5600
  function packagePhaseOf(ev) {
@@ -6561,6 +6567,7 @@ var NotificationCenter = class NotificationCenter {
6561
6567
  const isTrackScoped = kind === "object-event" || kind === "track-end";
6562
6568
  const isSystemEvent = kind === "system-event";
6563
6569
  const direct = new Set(rule.targets.map((t) => t.targetId));
6570
+ const renderedZones = notificationZones(rule, subject);
6564
6571
  return [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))].filter((target) => !rule.disabledTargetIds.includes(target.targetId)).map((target) => {
6565
6572
  const payload = {
6566
6573
  ruleName: rule.name,
@@ -6586,7 +6593,7 @@ var NotificationCenter = class NotificationCenter {
6586
6593
  className: subject.classNames[0] ?? subject.sensorKind ?? "event",
6587
6594
  ...subject.label !== void 0 ? { label: subject.label } : {},
6588
6595
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
6589
- zones: subject.zones,
6596
+ zones: renderedZones,
6590
6597
  timestamp: subject.timestamp,
6591
6598
  ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
6592
6599
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
@@ -15764,6 +15771,7 @@ function resolveFaceSettings(raw) {
15764
15771
  */
15765
15772
  function createLocationAwareMediaStorage(deps) {
15766
15773
  const roots = /* @__PURE__ */ new Map();
15774
+ let defaultBypassInvalidated = false;
15767
15775
  const rootOf = async (locationId) => {
15768
15776
  const cached = roots.get(locationId);
15769
15777
  if (cached !== void 0) return cached;
@@ -15774,21 +15782,55 @@ function createLocationAwareMediaStorage(deps) {
15774
15782
  const absOf = async (locationId, relativePath) => path.join(await rootOf(locationId), relativePath);
15775
15783
  return {
15776
15784
  write: async (input) => {
15777
- if (input.location === deps.defaultLocation) return deps.base.write(input);
15785
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.write(input);
15778
15786
  const abs = await absOf(input.location, input.relativePath);
15779
15787
  await promises.mkdir(path.dirname(abs), { recursive: true });
15780
15788
  await promises.writeFile(abs, input.data);
15781
15789
  },
15782
15790
  read: async (input) => {
15783
- if (input.location === deps.defaultLocation) return deps.base.read(input);
15791
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.read(input);
15784
15792
  return promises.readFile(await absOf(input.location, input.relativePath));
15785
15793
  },
15786
15794
  delete: async (input) => {
15787
- if (input.location === deps.defaultLocation) return deps.base.delete(input);
15795
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.delete(input);
15788
15796
  await promises.rm(await absOf(input.location, input.relativePath), { force: true });
15797
+ },
15798
+ invalidateRoots: () => {
15799
+ roots.clear();
15800
+ defaultBypassInvalidated = true;
15789
15801
  }
15790
15802
  };
15791
15803
  }
15804
+ //#endregion
15805
+ //#region src/pipeline-analytics/media-write-gate.ts
15806
+ /**
15807
+ * Reversible writer gate for storage maintenance. `pause()` closes admission
15808
+ * first, then waits for writes already admitted to settle; callers never
15809
+ * mutate device bindings or analytics configuration to quiesce media.
15810
+ */
15811
+ var MediaWriteGate = class {
15812
+ paused = false;
15813
+ active = 0;
15814
+ drainedWaiters = [];
15815
+ async run(write) {
15816
+ if (this.paused) throw new Error("event-media writes are paused for storage migration");
15817
+ this.active++;
15818
+ try {
15819
+ return await write();
15820
+ } finally {
15821
+ this.active--;
15822
+ if (this.active === 0) for (const resolve of this.drainedWaiters.splice(0)) resolve();
15823
+ }
15824
+ }
15825
+ pause() {
15826
+ this.paused = true;
15827
+ if (this.active === 0) return Promise.resolve();
15828
+ return new Promise((resolve) => this.drainedWaiters.push(resolve));
15829
+ }
15830
+ resume() {
15831
+ this.paused = false;
15832
+ }
15833
+ };
15792
15834
  var MACRO_SET = new Set([
15793
15835
  "person",
15794
15836
  "vehicle",
@@ -16659,7 +16701,14 @@ var RETRAIN_FRAME_COLUMNS = [
16659
16701
  name: "copiedAt",
16660
16702
  type: "INTEGER",
16661
16703
  notNull: true
16662
- }
16704
+ },
16705
+ (
16706
+ /** NULL = current eventMedia default; stamped when migration moves a copy
16707
+ * before the logical default is repointed. */
16708
+ {
16709
+ name: "locationId",
16710
+ type: "TEXT"
16711
+ })
16663
16712
  ];
16664
16713
  var RETRAIN_FRAME_INDEXES = [{
16665
16714
  name: "idx_retrain_frames_track",
@@ -16688,12 +16737,14 @@ var RetrainFrameStore = class {
16688
16737
  logger;
16689
16738
  readSource;
16690
16739
  now;
16740
+ writeGate;
16691
16741
  constructor(deps) {
16692
16742
  this.store = deps.store;
16693
16743
  this.storage = deps.storage;
16694
16744
  this.logger = deps.logger;
16695
16745
  this.readSource = deps.readSource;
16696
16746
  this.now = deps.now;
16747
+ this.writeGate = deps.writeGate;
16697
16748
  }
16698
16749
  static async declare(store) {
16699
16750
  await store.declareCollection.mutate({
@@ -16712,7 +16763,8 @@ var RetrainFrameStore = class {
16712
16763
  sizeBytes: Number(data["sizeBytes"] ?? 0),
16713
16764
  width: Number(data["width"] ?? 0),
16714
16765
  height: Number(data["height"] ?? 0),
16715
- copiedAt: Number(data["copiedAt"] ?? 0)
16766
+ copiedAt: Number(data["copiedAt"] ?? 0),
16767
+ locationId: typeof data["locationId"] === "string" && data["locationId"].length > 0 ? data["locationId"] : null
16716
16768
  };
16717
16769
  }
16718
16770
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
@@ -16780,7 +16832,7 @@ var RetrainFrameStore = class {
16780
16832
  async readFrame(frame) {
16781
16833
  try {
16782
16834
  return await this.storage.read({
16783
- location: RETRAIN_MEDIA_LOCATION,
16835
+ location: frame.locationId ?? "eventMedia",
16784
16836
  relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16785
16837
  });
16786
16838
  } catch (err) {
@@ -16860,9 +16912,10 @@ var RetrainFrameStore = class {
16860
16912
  sizeBytes: bytes.length,
16861
16913
  width: size.width,
16862
16914
  height: size.height,
16863
- copiedAt: this.now()
16915
+ copiedAt: this.now(),
16916
+ locationId: null
16864
16917
  };
16865
- try {
16918
+ const persist = async () => {
16866
16919
  await this.storage.write({
16867
16920
  location: RETRAIN_MEDIA_LOCATION,
16868
16921
  relativePath: path,
@@ -16880,9 +16933,14 @@ var RetrainFrameStore = class {
16880
16933
  sizeBytes: frame.sizeBytes,
16881
16934
  width: frame.width,
16882
16935
  height: frame.height,
16883
- copiedAt: frame.copiedAt
16936
+ copiedAt: frame.copiedAt,
16937
+ locationId: null
16884
16938
  }
16885
16939
  });
16940
+ };
16941
+ try {
16942
+ if (this.writeGate) await this.writeGate.run(persist);
16943
+ else await persist();
16886
16944
  } catch (err) {
16887
16945
  this.logger.error("retrain copy failed to persist", {
16888
16946
  tags: { deviceId: input.deviceId },
@@ -16923,26 +16981,30 @@ var RetrainFrameStore = class {
16923
16981
  * best-effort write is how a half-deleted annotation set happens.
16924
16982
  */
16925
16983
  async deleteFrame(frame) {
16926
- try {
16927
- await this.storage.delete({
16928
- location: RETRAIN_MEDIA_LOCATION,
16929
- relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16930
- });
16931
- } catch {}
16932
- try {
16933
- await this.store.delete.mutate({
16934
- collection: RETRAIN_FRAMES_COLLECTION,
16935
- key: frame.frameId
16936
- });
16937
- } catch (err) {
16938
- this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
16939
- tags: { deviceId: frame.deviceId },
16940
- meta: {
16941
- frameId: frame.frameId,
16942
- error: String(err)
16943
- }
16944
- });
16945
- }
16984
+ const remove = async () => {
16985
+ try {
16986
+ await this.storage.delete({
16987
+ location: frame.locationId ?? "eventMedia",
16988
+ relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16989
+ });
16990
+ } catch {}
16991
+ try {
16992
+ await this.store.delete.mutate({
16993
+ collection: RETRAIN_FRAMES_COLLECTION,
16994
+ key: frame.frameId
16995
+ });
16996
+ } catch (err) {
16997
+ this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
16998
+ tags: { deviceId: frame.deviceId },
16999
+ meta: {
17000
+ frameId: frame.frameId,
17001
+ error: String(err)
17002
+ }
17003
+ });
17004
+ }
17005
+ };
17006
+ if (this.writeGate) await this.writeGate.run(remove);
17007
+ else await remove();
16946
17008
  }
16947
17009
  };
16948
17010
  //#endregion
@@ -17981,10 +18043,15 @@ var MediaStore = class {
17981
18043
  storage;
17982
18044
  store;
17983
18045
  logger;
18046
+ writeGate;
17984
18047
  constructor(deps) {
17985
18048
  this.storage = deps.storage;
17986
18049
  this.store = deps.store;
17987
18050
  this.logger = deps.logger;
18051
+ this.writeGate = deps.writeGate;
18052
+ }
18053
+ gated(mutation) {
18054
+ return this.writeGate ? this.writeGate.run(mutation) : mutation();
17988
18055
  }
17989
18056
  static async declare(store) {
17990
18057
  await store.declareCollection.mutate({
@@ -17996,6 +18063,10 @@ var MediaStore = class {
17996
18063
  /** Persist a media blob and register its metadata row. Returns the
17997
18064
  * collection key (MediaFile.key) that callers store as reference. */
17998
18065
  async put(params) {
18066
+ if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
18067
+ return this.putUnchecked(params);
18068
+ }
18069
+ async putUnchecked(params) {
17999
18070
  const key = buildKey(params);
18000
18071
  const path = buildPath(params);
18001
18072
  const record = {
@@ -18066,6 +18137,9 @@ var MediaStore = class {
18066
18137
  * scheme (transition back-compat); once migrated it is a no-op.
18067
18138
  */
18068
18139
  async putReplacing(params) {
18140
+ return this.gated(() => this.putReplacingUnchecked(params));
18141
+ }
18142
+ async putReplacingUnchecked(params) {
18069
18143
  const existing = await this.store.query.query({
18070
18144
  collection: MEDIA_COLLECTION,
18071
18145
  filter: { where: {
@@ -18074,7 +18148,7 @@ var MediaStore = class {
18074
18148
  kind: params.kind
18075
18149
  } }
18076
18150
  });
18077
- const newKey = await this.put(params);
18151
+ const newKey = await this.putUnchecked(params);
18078
18152
  for (const row of existing) {
18079
18153
  if (row.id === newKey) continue;
18080
18154
  const rowData = row.data;
@@ -18117,8 +18191,11 @@ var MediaStore = class {
18117
18191
  * own kind.
18118
18192
  */
18119
18193
  async promoteToLastFrame(input) {
18194
+ return this.gated(() => this.promoteToLastFrameUnchecked(input));
18195
+ }
18196
+ async promoteToLastFrameUnchecked(input) {
18120
18197
  const data = Buffer.from(input.snapshot.base64, "base64");
18121
- const newKey = await this.putReplacing({
18198
+ const newKey = await this.putReplacingUnchecked({
18122
18199
  deviceId: input.deviceId,
18123
18200
  ownerKind: "track",
18124
18201
  ownerId: input.trackId,
@@ -18126,7 +18203,7 @@ var MediaStore = class {
18126
18203
  timestamp: input.snapshot.timestamp,
18127
18204
  data
18128
18205
  });
18129
- if (input.keepSource !== true) await this.deleteByKey(input.snapshot.key);
18206
+ if (input.keepSource !== true) await this.deleteByKeyUnchecked(input.snapshot.key);
18130
18207
  return newKey;
18131
18208
  }
18132
18209
  /**
@@ -18164,6 +18241,9 @@ var MediaStore = class {
18164
18241
  * Deletes the blob from storage (best-effort) and removes the index row.
18165
18242
  */
18166
18243
  async deleteByKey(key) {
18244
+ await this.gated(() => this.deleteByKeyUnchecked(key));
18245
+ }
18246
+ async deleteByKeyUnchecked(key) {
18167
18247
  const row = await this.store.get.query({
18168
18248
  collection: MEDIA_COLLECTION,
18169
18249
  key
@@ -18274,6 +18354,9 @@ var MediaStore = class {
18274
18354
  * Best-effort per row; returns the number of index rows removed.
18275
18355
  */
18276
18356
  async deleteForOwner(ownerKind, ownerIds) {
18357
+ return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
18358
+ }
18359
+ async deleteForOwnerUnchecked(ownerKind, ownerIds) {
18277
18360
  let removed = 0;
18278
18361
  for (const ownerId of ownerIds) {
18279
18362
  const rows = await this.store.query.query({
@@ -18355,6 +18438,9 @@ var MediaStore = class {
18355
18438
  * The old-entry deletion is best-effort: on failure a warning is logged
18356
18439
  * and the method still returns the new key. */
18357
18440
  async reown(input) {
18441
+ return this.gated(() => this.reownUnchecked(input));
18442
+ }
18443
+ async reownUnchecked(input) {
18358
18444
  const { fromOwnerKind, fromOwnerId, toOwnerKind, toOwnerId, key } = input;
18359
18445
  const row = (await this.store.query.query({
18360
18446
  collection: MEDIA_COLLECTION,
@@ -18388,7 +18474,7 @@ var MediaStore = class {
18388
18474
  relativePath: oldPath
18389
18475
  })
18390
18476
  };
18391
- const newKey = await this.put(newParams);
18477
+ const newKey = await this.putUnchecked(newParams);
18392
18478
  try {
18393
18479
  await this.storage.delete({
18394
18480
  location: mediaRowLocation(meta),
@@ -18442,8 +18528,10 @@ var MediaStore = class {
18442
18528
  * batch.
18443
18529
  */
18444
18530
  async deleteByTracks(trackIds) {
18445
- await this.deleteForOwner("track", trackIds);
18446
- await this.deleteForOwner("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
18531
+ await this.gated(async () => {
18532
+ await this.deleteForOwnerUnchecked("track", trackIds);
18533
+ await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
18534
+ });
18447
18535
  await this.deleteForOwner("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
18448
18536
  }
18449
18537
  /**
@@ -18485,7 +18573,7 @@ function snapshot(j) {
18485
18573
  fromLocationId: j.fromLocationId,
18486
18574
  toLocationId: j.toLocationId,
18487
18575
  deviceId: j.deviceId,
18488
- entities: ["media"],
18576
+ entities: ["media", "retrainFrames"],
18489
18577
  filesMoved: j.filesMoved,
18490
18578
  bytesMoved: j.bytesMoved,
18491
18579
  filesTotal: null,
@@ -18516,7 +18604,7 @@ var MediaRelocateEngine = class {
18516
18604
  state: "running",
18517
18605
  fromLocationId: "*",
18518
18606
  toLocationId: input.toLocationId,
18519
- deviceId: input.deviceId ?? null,
18607
+ deviceId: null,
18520
18608
  filesMoved: 0,
18521
18609
  bytesMoved: 0,
18522
18610
  startedAt: this.deps.now(),
@@ -18533,24 +18621,25 @@ var MediaRelocateEngine = class {
18533
18621
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
18534
18622
  try {
18535
18623
  await this.deps.resolveTargetRoot(job.toLocationId);
18624
+ let failedRows = 0;
18536
18625
  let cursor = 0;
18537
- let seenAtCursor = /* @__PURE__ */ new Set();
18626
+ let offsetAtCursor = 0;
18538
18627
  for (;;) {
18539
18628
  if (job.cancelRequested) break;
18540
- const fresh = (await this.deps.store.query.query({
18629
+ const rows = await this.deps.store.query.query({
18541
18630
  collection: MEDIA_COLLECTION,
18542
18631
  filter: {
18543
- ...job.deviceId !== null ? { where: { deviceId: job.deviceId } } : {},
18544
18632
  whereBetween: { timestamp: [cursor, Number.MAX_SAFE_INTEGER] },
18545
18633
  orderBy: {
18546
18634
  field: "timestamp",
18547
18635
  direction: "asc"
18548
18636
  },
18549
- limit: PAGE_SIZE
18637
+ limit: PAGE_SIZE,
18638
+ offset: offsetAtCursor
18550
18639
  }
18551
- })).filter((r) => !seenAtCursor.has(r.id));
18552
- if (fresh.length === 0) break;
18553
- for (const row of fresh) {
18640
+ });
18641
+ if (rows.length === 0) break;
18642
+ for (const row of rows) {
18554
18643
  if (job.cancelRequested) break;
18555
18644
  const data = row.data;
18556
18645
  const from = mediaRowLocation(data);
@@ -18558,6 +18647,17 @@ var MediaRelocateEngine = class {
18558
18647
  const relativePath = String(data["path"] ?? "");
18559
18648
  if (relativePath.length === 0) continue;
18560
18649
  try {
18650
+ if (await this.samePhysicalLocation(from, job.toLocationId)) {
18651
+ await this.deps.store.set.mutate({
18652
+ collection: MEDIA_COLLECTION,
18653
+ key: row.id,
18654
+ value: {
18655
+ ...data,
18656
+ locationId: job.toLocationId
18657
+ }
18658
+ });
18659
+ continue;
18660
+ }
18561
18661
  const bytes = await this.deps.storage.read({
18562
18662
  location: from,
18563
18663
  relativePath
@@ -18567,6 +18667,10 @@ var MediaRelocateEngine = class {
18567
18667
  relativePath,
18568
18668
  data: bytes
18569
18669
  });
18670
+ if ((await this.deps.storage.read({
18671
+ location: job.toLocationId,
18672
+ relativePath
18673
+ })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
18570
18674
  await this.deps.store.set.mutate({
18571
18675
  collection: MEDIA_COLLECTION,
18572
18676
  key: row.id,
@@ -18583,20 +18687,99 @@ var MediaRelocateEngine = class {
18583
18687
  job.bytesMoved += bytes.length;
18584
18688
  await sleep(bytes.length / bytesPerMs);
18585
18689
  } catch (err) {
18690
+ failedRows++;
18586
18691
  this.deps.logger.debug("media relocate row failed", { meta: {
18587
18692
  key: row.id,
18588
18693
  error: String(err)
18589
18694
  } });
18590
18695
  }
18591
18696
  }
18592
- const last = fresh[fresh.length - 1];
18697
+ const last = rows[rows.length - 1];
18593
18698
  const lastTs = Number(last.data["timestamp"] ?? cursor);
18594
- if (lastTs === cursor) for (const r of fresh) seenAtCursor.add(r.id);
18699
+ if (lastTs === cursor) offsetAtCursor += rows.length;
18595
18700
  else {
18596
18701
  cursor = lastTs;
18597
- seenAtCursor = new Set(fresh.filter((r) => Number(r.data["timestamp"]) === lastTs).map((r) => r.id));
18702
+ offsetAtCursor = rows.filter((row) => Number(row.data["timestamp"]) === lastTs).length;
18598
18703
  }
18704
+ if (rows.length < PAGE_SIZE) break;
18599
18705
  }
18706
+ let retrainOffset = 0;
18707
+ for (;;) {
18708
+ if (job.cancelRequested) break;
18709
+ const rows = await this.deps.store.query.query({
18710
+ collection: RETRAIN_FRAMES_COLLECTION,
18711
+ filter: {
18712
+ orderBy: {
18713
+ field: "copiedAt",
18714
+ direction: "asc"
18715
+ },
18716
+ limit: PAGE_SIZE,
18717
+ offset: retrainOffset
18718
+ }
18719
+ });
18720
+ if (rows.length === 0) break;
18721
+ for (const row of rows) {
18722
+ if (job.cancelRequested) break;
18723
+ const data = row.data;
18724
+ const from = mediaRowLocation(data);
18725
+ if (from === job.toLocationId) continue;
18726
+ const relativePath = String(data["path"] ?? "");
18727
+ if (relativePath.length === 0) {
18728
+ failedRows++;
18729
+ continue;
18730
+ }
18731
+ try {
18732
+ if (await this.samePhysicalLocation(from, job.toLocationId)) {
18733
+ await this.deps.store.set.mutate({
18734
+ collection: RETRAIN_FRAMES_COLLECTION,
18735
+ key: row.id,
18736
+ value: {
18737
+ ...data,
18738
+ locationId: job.toLocationId
18739
+ }
18740
+ });
18741
+ continue;
18742
+ }
18743
+ const bytes = await this.deps.storage.read({
18744
+ location: from,
18745
+ relativePath
18746
+ });
18747
+ await this.deps.storage.write({
18748
+ location: job.toLocationId,
18749
+ relativePath,
18750
+ data: bytes
18751
+ });
18752
+ if ((await this.deps.storage.read({
18753
+ location: job.toLocationId,
18754
+ relativePath
18755
+ })).length !== bytes.length) throw new Error(`relocated retrain frame size mismatch for ${relativePath}`);
18756
+ await this.deps.store.set.mutate({
18757
+ collection: RETRAIN_FRAMES_COLLECTION,
18758
+ key: row.id,
18759
+ value: {
18760
+ ...data,
18761
+ locationId: job.toLocationId
18762
+ }
18763
+ });
18764
+ await this.deps.storage.delete({
18765
+ location: from,
18766
+ relativePath
18767
+ });
18768
+ job.filesMoved++;
18769
+ job.bytesMoved += bytes.length;
18770
+ await sleep(bytes.length / bytesPerMs);
18771
+ } catch (err) {
18772
+ failedRows++;
18773
+ this.deps.logger.debug("retrain frame relocate row failed", { meta: {
18774
+ key: row.id,
18775
+ error: String(err)
18776
+ } });
18777
+ }
18778
+ }
18779
+ retrainOffset += rows.length;
18780
+ if (rows.length < PAGE_SIZE) break;
18781
+ }
18782
+ if (failedRows > 0) throw new Error(`event-media relocation left ${failedRows} row(s) unmoved`);
18600
18783
  job.state = job.cancelRequested ? "cancelled" : "done";
18601
18784
  } catch (err) {
18602
18785
  job.state = "failed";
@@ -18610,6 +18793,18 @@ var MediaRelocateEngine = class {
18610
18793
  this.deps.onFinished?.(snapshot(job));
18611
18794
  }
18612
18795
  }
18796
+ async samePhysicalLocation(from, to) {
18797
+ if (from === to) return true;
18798
+ try {
18799
+ const [fromRoot, toRoot] = await Promise.all([this.deps.resolveTargetRoot(from), this.deps.resolveTargetRoot(to)]);
18800
+ const [fromStat, toStat] = await Promise.all([promises.stat(fromRoot), promises.stat(toRoot)]);
18801
+ if (fromStat.dev === toStat.dev && fromStat.ino === toStat.ino) return true;
18802
+ const [fromReal, toReal] = await Promise.all([promises.realpath(path.resolve(fromRoot)), promises.realpath(path.resolve(toRoot))]);
18803
+ return fromReal === toReal;
18804
+ } catch {
18805
+ return false;
18806
+ }
18807
+ }
18613
18808
  };
18614
18809
  //#endregion
18615
18810
  //#region src/pipeline-analytics/media-settings.ts
@@ -31162,6 +31357,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31162
31357
  eventsOpsLog = null;
31163
31358
  /** Event-media relocation engine (entity-routing Phase 4). */
31164
31359
  mediaRelocate = null;
31360
+ mediaLocationStorage = null;
31361
+ mediaWriteGate = new MediaWriteGate();
31362
+ storageMigrationLeaseId = null;
31165
31363
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
31166
31364
  sensorEventStore = null;
31167
31365
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
@@ -31962,7 +32160,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31962
32160
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31963
32161
  }
31964
32162
  if (!storage) throw new Error("pipeline-analytics requires ctx.kernel.storage");
31965
- return createLocationAwareMediaStorage({
32163
+ const routed = createLocationAwareMediaStorage({
31966
32164
  base: storage,
31967
32165
  defaultLocation: "eventMedia",
31968
32166
  resolveRoot: (locationId) => this.ctx.api.storage.resolve.query({
@@ -31970,6 +32168,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31970
32168
  relativePath: ""
31971
32169
  })
31972
32170
  });
32171
+ this.mediaLocationStorage = routed;
32172
+ return routed;
31973
32173
  }
31974
32174
  /** Constructs every SQLite-backed store plus the stationary/package-drop/
31975
32175
  * sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
@@ -32014,7 +32214,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
32014
32214
  const mediaStore = new MediaStore({
32015
32215
  storage,
32016
32216
  store: api.settingsStore,
32017
- logger: logger.child("MediaStore")
32217
+ logger: logger.child("MediaStore"),
32218
+ writeGate: this.mediaWriteGate
32018
32219
  });
32019
32220
  this.mediaStore = mediaStore;
32020
32221
  const retrainFrames = new RetrainFrameStore({
@@ -32022,6 +32223,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
32022
32223
  storage,
32023
32224
  logger: logger.child("RetrainFrames"),
32024
32225
  now: () => Date.now(),
32226
+ writeGate: this.mediaWriteGate,
32025
32227
  readSource: async (mediaKey) => {
32026
32228
  const file = await mediaStore.getByKey(mediaKey);
32027
32229
  return file === null ? null : Buffer.from(file.base64, "base64");
@@ -36263,14 +36465,38 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
36263
36465
  return this.queryFacade.deleteDeviceEvents(input);
36264
36466
  }
36265
36467
  /** The events ops-log rows (newest-first), optionally scoped to one camera. */
36266
- async relocateMedia(input) {
36468
+ async pauseForStorageMigration(input) {
36469
+ if (this.storageMigrationLeaseId !== null && this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media writes are already paused by another storage migration");
36470
+ this.storageMigrationLeaseId = input.leaseId;
36471
+ try {
36472
+ await this.mediaWriteGate.pause();
36473
+ return { paused: true };
36474
+ } catch (err) {
36475
+ this.storageMigrationLeaseId = null;
36476
+ throw err;
36477
+ }
36478
+ }
36479
+ async resumeForStorageMigration(input) {
36480
+ if (this.storageMigrationLeaseId === null) return { resumed: true };
36481
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36482
+ this.mediaWriteGate.resume();
36483
+ this.storageMigrationLeaseId = null;
36484
+ return { resumed: true };
36485
+ }
36486
+ async refreshStorageLocationsForMigration(input) {
36487
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36488
+ this.mediaLocationStorage?.invalidateRoots();
36489
+ return { refreshed: true };
36490
+ }
36491
+ async startStorageMigrationMove(input) {
36267
36492
  if (!this.mediaRelocate) throw new Error("media relocation unavailable");
36493
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36268
36494
  return { jobId: this.mediaRelocate.start(input) };
36269
36495
  }
36270
- async getMediaRelocateStatus() {
36271
- return this.mediaRelocate?.list() ?? [];
36496
+ async getStorageMigrationMoveStatus(input) {
36497
+ return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
36272
36498
  }
36273
- async cancelMediaRelocate(input) {
36499
+ async cancelStorageMigrationMove(input) {
36274
36500
  return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
36275
36501
  }
36276
36502
  async listOpsLog(input) {
@@ -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-DrgptocJ.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-lSMqbbY-.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.62",
3
+ "version": "1.2.64",
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",
@@ -99,7 +99,7 @@
99
99
  {
100
100
  "id": "eventMedia",
101
101
  "displayName": "Event Media",
102
- "cardinality": "single",
102
+ "cardinality": "multi",
103
103
  "description": "Detection snapshots, thumbnails and crops.",
104
104
  "defaultsTo": "recordings"
105
105
  }