@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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-D4fGaA71.js");
5
+ const require_dist = require("../dist-BQ-M7jn9.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -5602,6 +5602,12 @@ function frozenOccupancy(rule, occ) {
5602
5602
  occupied: occ.occupied
5603
5603
  };
5604
5604
  }
5605
+ function notificationZones(rule, subject) {
5606
+ const configured = rule.conditions.zones?.ids;
5607
+ if (configured === void 0 || configured.length === 0) return subject.zones;
5608
+ const allowed = new Set(configured);
5609
+ return subject.zones.filter((zoneId) => allowed.has(zoneId));
5610
+ }
5605
5611
  /** Classify an object-event row as a package delivery / pick-up, or `null` when
5606
5612
  * it is an ordinary detection. */
5607
5613
  function packagePhaseOf(ev) {
@@ -6574,6 +6580,7 @@ var NotificationCenter = class NotificationCenter {
6574
6580
  const isTrackScoped = kind === "object-event" || kind === "track-end";
6575
6581
  const isSystemEvent = kind === "system-event";
6576
6582
  const direct = new Set(rule.targets.map((t) => t.targetId));
6583
+ const renderedZones = notificationZones(rule, subject);
6577
6584
  return [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))].filter((target) => !rule.disabledTargetIds.includes(target.targetId)).map((target) => {
6578
6585
  const payload = {
6579
6586
  ruleName: rule.name,
@@ -6599,7 +6606,7 @@ var NotificationCenter = class NotificationCenter {
6599
6606
  className: subject.classNames[0] ?? subject.sensorKind ?? "event",
6600
6607
  ...subject.label !== void 0 ? { label: subject.label } : {},
6601
6608
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
6602
- zones: subject.zones,
6609
+ zones: renderedZones,
6603
6610
  timestamp: subject.timestamp,
6604
6611
  ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
6605
6612
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
@@ -15777,6 +15784,7 @@ function resolveFaceSettings(raw) {
15777
15784
  */
15778
15785
  function createLocationAwareMediaStorage(deps) {
15779
15786
  const roots = /* @__PURE__ */ new Map();
15787
+ let defaultBypassInvalidated = false;
15780
15788
  const rootOf = async (locationId) => {
15781
15789
  const cached = roots.get(locationId);
15782
15790
  if (cached !== void 0) return cached;
@@ -15787,21 +15795,55 @@ function createLocationAwareMediaStorage(deps) {
15787
15795
  const absOf = async (locationId, relativePath) => node_path.default.join(await rootOf(locationId), relativePath);
15788
15796
  return {
15789
15797
  write: async (input) => {
15790
- if (input.location === deps.defaultLocation) return deps.base.write(input);
15798
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.write(input);
15791
15799
  const abs = await absOf(input.location, input.relativePath);
15792
15800
  await node_fs.promises.mkdir(node_path.default.dirname(abs), { recursive: true });
15793
15801
  await node_fs.promises.writeFile(abs, input.data);
15794
15802
  },
15795
15803
  read: async (input) => {
15796
- if (input.location === deps.defaultLocation) return deps.base.read(input);
15804
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.read(input);
15797
15805
  return node_fs.promises.readFile(await absOf(input.location, input.relativePath));
15798
15806
  },
15799
15807
  delete: async (input) => {
15800
- if (input.location === deps.defaultLocation) return deps.base.delete(input);
15808
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.delete(input);
15801
15809
  await node_fs.promises.rm(await absOf(input.location, input.relativePath), { force: true });
15810
+ },
15811
+ invalidateRoots: () => {
15812
+ roots.clear();
15813
+ defaultBypassInvalidated = true;
15802
15814
  }
15803
15815
  };
15804
15816
  }
15817
+ //#endregion
15818
+ //#region src/pipeline-analytics/media-write-gate.ts
15819
+ /**
15820
+ * Reversible writer gate for storage maintenance. `pause()` closes admission
15821
+ * first, then waits for writes already admitted to settle; callers never
15822
+ * mutate device bindings or analytics configuration to quiesce media.
15823
+ */
15824
+ var MediaWriteGate = class {
15825
+ paused = false;
15826
+ active = 0;
15827
+ drainedWaiters = [];
15828
+ async run(write) {
15829
+ if (this.paused) throw new Error("event-media writes are paused for storage migration");
15830
+ this.active++;
15831
+ try {
15832
+ return await write();
15833
+ } finally {
15834
+ this.active--;
15835
+ if (this.active === 0) for (const resolve of this.drainedWaiters.splice(0)) resolve();
15836
+ }
15837
+ }
15838
+ pause() {
15839
+ this.paused = true;
15840
+ if (this.active === 0) return Promise.resolve();
15841
+ return new Promise((resolve) => this.drainedWaiters.push(resolve));
15842
+ }
15843
+ resume() {
15844
+ this.paused = false;
15845
+ }
15846
+ };
15805
15847
  var MACRO_SET = new Set([
15806
15848
  "person",
15807
15849
  "vehicle",
@@ -16672,7 +16714,14 @@ var RETRAIN_FRAME_COLUMNS = [
16672
16714
  name: "copiedAt",
16673
16715
  type: "INTEGER",
16674
16716
  notNull: true
16675
- }
16717
+ },
16718
+ (
16719
+ /** NULL = current eventMedia default; stamped when migration moves a copy
16720
+ * before the logical default is repointed. */
16721
+ {
16722
+ name: "locationId",
16723
+ type: "TEXT"
16724
+ })
16676
16725
  ];
16677
16726
  var RETRAIN_FRAME_INDEXES = [{
16678
16727
  name: "idx_retrain_frames_track",
@@ -16701,12 +16750,14 @@ var RetrainFrameStore = class {
16701
16750
  logger;
16702
16751
  readSource;
16703
16752
  now;
16753
+ writeGate;
16704
16754
  constructor(deps) {
16705
16755
  this.store = deps.store;
16706
16756
  this.storage = deps.storage;
16707
16757
  this.logger = deps.logger;
16708
16758
  this.readSource = deps.readSource;
16709
16759
  this.now = deps.now;
16760
+ this.writeGate = deps.writeGate;
16710
16761
  }
16711
16762
  static async declare(store) {
16712
16763
  await store.declareCollection.mutate({
@@ -16725,7 +16776,8 @@ var RetrainFrameStore = class {
16725
16776
  sizeBytes: Number(data["sizeBytes"] ?? 0),
16726
16777
  width: Number(data["width"] ?? 0),
16727
16778
  height: Number(data["height"] ?? 0),
16728
- copiedAt: Number(data["copiedAt"] ?? 0)
16779
+ copiedAt: Number(data["copiedAt"] ?? 0),
16780
+ locationId: typeof data["locationId"] === "string" && data["locationId"].length > 0 ? data["locationId"] : null
16729
16781
  };
16730
16782
  }
16731
16783
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
@@ -16793,7 +16845,7 @@ var RetrainFrameStore = class {
16793
16845
  async readFrame(frame) {
16794
16846
  try {
16795
16847
  return await this.storage.read({
16796
- location: RETRAIN_MEDIA_LOCATION,
16848
+ location: frame.locationId ?? "eventMedia",
16797
16849
  relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16798
16850
  });
16799
16851
  } catch (err) {
@@ -16873,9 +16925,10 @@ var RetrainFrameStore = class {
16873
16925
  sizeBytes: bytes.length,
16874
16926
  width: size.width,
16875
16927
  height: size.height,
16876
- copiedAt: this.now()
16928
+ copiedAt: this.now(),
16929
+ locationId: null
16877
16930
  };
16878
- try {
16931
+ const persist = async () => {
16879
16932
  await this.storage.write({
16880
16933
  location: RETRAIN_MEDIA_LOCATION,
16881
16934
  relativePath: path,
@@ -16893,9 +16946,14 @@ var RetrainFrameStore = class {
16893
16946
  sizeBytes: frame.sizeBytes,
16894
16947
  width: frame.width,
16895
16948
  height: frame.height,
16896
- copiedAt: frame.copiedAt
16949
+ copiedAt: frame.copiedAt,
16950
+ locationId: null
16897
16951
  }
16898
16952
  });
16953
+ };
16954
+ try {
16955
+ if (this.writeGate) await this.writeGate.run(persist);
16956
+ else await persist();
16899
16957
  } catch (err) {
16900
16958
  this.logger.error("retrain copy failed to persist", {
16901
16959
  tags: { deviceId: input.deviceId },
@@ -16936,26 +16994,30 @@ var RetrainFrameStore = class {
16936
16994
  * best-effort write is how a half-deleted annotation set happens.
16937
16995
  */
16938
16996
  async deleteFrame(frame) {
16939
- try {
16940
- await this.storage.delete({
16941
- location: RETRAIN_MEDIA_LOCATION,
16942
- relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16943
- });
16944
- } catch {}
16945
- try {
16946
- await this.store.delete.mutate({
16947
- collection: RETRAIN_FRAMES_COLLECTION,
16948
- key: frame.frameId
16949
- });
16950
- } catch (err) {
16951
- this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
16952
- tags: { deviceId: frame.deviceId },
16953
- meta: {
16954
- frameId: frame.frameId,
16955
- error: String(err)
16956
- }
16957
- });
16958
- }
16997
+ const remove = async () => {
16998
+ try {
16999
+ await this.storage.delete({
17000
+ location: frame.locationId ?? "eventMedia",
17001
+ relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
17002
+ });
17003
+ } catch {}
17004
+ try {
17005
+ await this.store.delete.mutate({
17006
+ collection: RETRAIN_FRAMES_COLLECTION,
17007
+ key: frame.frameId
17008
+ });
17009
+ } catch (err) {
17010
+ this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
17011
+ tags: { deviceId: frame.deviceId },
17012
+ meta: {
17013
+ frameId: frame.frameId,
17014
+ error: String(err)
17015
+ }
17016
+ });
17017
+ }
17018
+ };
17019
+ if (this.writeGate) await this.writeGate.run(remove);
17020
+ else await remove();
16959
17021
  }
16960
17022
  };
16961
17023
  //#endregion
@@ -17994,10 +18056,15 @@ var MediaStore = class {
17994
18056
  storage;
17995
18057
  store;
17996
18058
  logger;
18059
+ writeGate;
17997
18060
  constructor(deps) {
17998
18061
  this.storage = deps.storage;
17999
18062
  this.store = deps.store;
18000
18063
  this.logger = deps.logger;
18064
+ this.writeGate = deps.writeGate;
18065
+ }
18066
+ gated(mutation) {
18067
+ return this.writeGate ? this.writeGate.run(mutation) : mutation();
18001
18068
  }
18002
18069
  static async declare(store) {
18003
18070
  await store.declareCollection.mutate({
@@ -18009,6 +18076,10 @@ var MediaStore = class {
18009
18076
  /** Persist a media blob and register its metadata row. Returns the
18010
18077
  * collection key (MediaFile.key) that callers store as reference. */
18011
18078
  async put(params) {
18079
+ if (this.writeGate) return this.writeGate.run(() => this.putUnchecked(params));
18080
+ return this.putUnchecked(params);
18081
+ }
18082
+ async putUnchecked(params) {
18012
18083
  const key = buildKey(params);
18013
18084
  const path = buildPath(params);
18014
18085
  const record = {
@@ -18079,6 +18150,9 @@ var MediaStore = class {
18079
18150
  * scheme (transition back-compat); once migrated it is a no-op.
18080
18151
  */
18081
18152
  async putReplacing(params) {
18153
+ return this.gated(() => this.putReplacingUnchecked(params));
18154
+ }
18155
+ async putReplacingUnchecked(params) {
18082
18156
  const existing = await this.store.query.query({
18083
18157
  collection: MEDIA_COLLECTION,
18084
18158
  filter: { where: {
@@ -18087,7 +18161,7 @@ var MediaStore = class {
18087
18161
  kind: params.kind
18088
18162
  } }
18089
18163
  });
18090
- const newKey = await this.put(params);
18164
+ const newKey = await this.putUnchecked(params);
18091
18165
  for (const row of existing) {
18092
18166
  if (row.id === newKey) continue;
18093
18167
  const rowData = row.data;
@@ -18130,8 +18204,11 @@ var MediaStore = class {
18130
18204
  * own kind.
18131
18205
  */
18132
18206
  async promoteToLastFrame(input) {
18207
+ return this.gated(() => this.promoteToLastFrameUnchecked(input));
18208
+ }
18209
+ async promoteToLastFrameUnchecked(input) {
18133
18210
  const data = Buffer.from(input.snapshot.base64, "base64");
18134
- const newKey = await this.putReplacing({
18211
+ const newKey = await this.putReplacingUnchecked({
18135
18212
  deviceId: input.deviceId,
18136
18213
  ownerKind: "track",
18137
18214
  ownerId: input.trackId,
@@ -18139,7 +18216,7 @@ var MediaStore = class {
18139
18216
  timestamp: input.snapshot.timestamp,
18140
18217
  data
18141
18218
  });
18142
- if (input.keepSource !== true) await this.deleteByKey(input.snapshot.key);
18219
+ if (input.keepSource !== true) await this.deleteByKeyUnchecked(input.snapshot.key);
18143
18220
  return newKey;
18144
18221
  }
18145
18222
  /**
@@ -18177,6 +18254,9 @@ var MediaStore = class {
18177
18254
  * Deletes the blob from storage (best-effort) and removes the index row.
18178
18255
  */
18179
18256
  async deleteByKey(key) {
18257
+ await this.gated(() => this.deleteByKeyUnchecked(key));
18258
+ }
18259
+ async deleteByKeyUnchecked(key) {
18180
18260
  const row = await this.store.get.query({
18181
18261
  collection: MEDIA_COLLECTION,
18182
18262
  key
@@ -18287,6 +18367,9 @@ var MediaStore = class {
18287
18367
  * Best-effort per row; returns the number of index rows removed.
18288
18368
  */
18289
18369
  async deleteForOwner(ownerKind, ownerIds) {
18370
+ return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
18371
+ }
18372
+ async deleteForOwnerUnchecked(ownerKind, ownerIds) {
18290
18373
  let removed = 0;
18291
18374
  for (const ownerId of ownerIds) {
18292
18375
  const rows = await this.store.query.query({
@@ -18368,6 +18451,9 @@ var MediaStore = class {
18368
18451
  * The old-entry deletion is best-effort: on failure a warning is logged
18369
18452
  * and the method still returns the new key. */
18370
18453
  async reown(input) {
18454
+ return this.gated(() => this.reownUnchecked(input));
18455
+ }
18456
+ async reownUnchecked(input) {
18371
18457
  const { fromOwnerKind, fromOwnerId, toOwnerKind, toOwnerId, key } = input;
18372
18458
  const row = (await this.store.query.query({
18373
18459
  collection: MEDIA_COLLECTION,
@@ -18401,7 +18487,7 @@ var MediaStore = class {
18401
18487
  relativePath: oldPath
18402
18488
  })
18403
18489
  };
18404
- const newKey = await this.put(newParams);
18490
+ const newKey = await this.putUnchecked(newParams);
18405
18491
  try {
18406
18492
  await this.storage.delete({
18407
18493
  location: mediaRowLocation(meta),
@@ -18455,8 +18541,10 @@ var MediaStore = class {
18455
18541
  * batch.
18456
18542
  */
18457
18543
  async deleteByTracks(trackIds) {
18458
- await this.deleteForOwner("track", trackIds);
18459
- await this.deleteForOwner("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
18544
+ await this.gated(async () => {
18545
+ await this.deleteForOwnerUnchecked("track", trackIds);
18546
+ await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
18547
+ });
18460
18548
  await this.deleteForOwner("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
18461
18549
  }
18462
18550
  /**
@@ -18498,7 +18586,7 @@ function snapshot(j) {
18498
18586
  fromLocationId: j.fromLocationId,
18499
18587
  toLocationId: j.toLocationId,
18500
18588
  deviceId: j.deviceId,
18501
- entities: ["media"],
18589
+ entities: ["media", "retrainFrames"],
18502
18590
  filesMoved: j.filesMoved,
18503
18591
  bytesMoved: j.bytesMoved,
18504
18592
  filesTotal: null,
@@ -18529,7 +18617,7 @@ var MediaRelocateEngine = class {
18529
18617
  state: "running",
18530
18618
  fromLocationId: "*",
18531
18619
  toLocationId: input.toLocationId,
18532
- deviceId: input.deviceId ?? null,
18620
+ deviceId: null,
18533
18621
  filesMoved: 0,
18534
18622
  bytesMoved: 0,
18535
18623
  startedAt: this.deps.now(),
@@ -18546,24 +18634,25 @@ var MediaRelocateEngine = class {
18546
18634
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
18547
18635
  try {
18548
18636
  await this.deps.resolveTargetRoot(job.toLocationId);
18637
+ let failedRows = 0;
18549
18638
  let cursor = 0;
18550
- let seenAtCursor = /* @__PURE__ */ new Set();
18639
+ let offsetAtCursor = 0;
18551
18640
  for (;;) {
18552
18641
  if (job.cancelRequested) break;
18553
- const fresh = (await this.deps.store.query.query({
18642
+ const rows = await this.deps.store.query.query({
18554
18643
  collection: MEDIA_COLLECTION,
18555
18644
  filter: {
18556
- ...job.deviceId !== null ? { where: { deviceId: job.deviceId } } : {},
18557
18645
  whereBetween: { timestamp: [cursor, Number.MAX_SAFE_INTEGER] },
18558
18646
  orderBy: {
18559
18647
  field: "timestamp",
18560
18648
  direction: "asc"
18561
18649
  },
18562
- limit: PAGE_SIZE
18650
+ limit: PAGE_SIZE,
18651
+ offset: offsetAtCursor
18563
18652
  }
18564
- })).filter((r) => !seenAtCursor.has(r.id));
18565
- if (fresh.length === 0) break;
18566
- for (const row of fresh) {
18653
+ });
18654
+ if (rows.length === 0) break;
18655
+ for (const row of rows) {
18567
18656
  if (job.cancelRequested) break;
18568
18657
  const data = row.data;
18569
18658
  const from = mediaRowLocation(data);
@@ -18571,6 +18660,17 @@ var MediaRelocateEngine = class {
18571
18660
  const relativePath = String(data["path"] ?? "");
18572
18661
  if (relativePath.length === 0) continue;
18573
18662
  try {
18663
+ if (await this.samePhysicalLocation(from, job.toLocationId)) {
18664
+ await this.deps.store.set.mutate({
18665
+ collection: MEDIA_COLLECTION,
18666
+ key: row.id,
18667
+ value: {
18668
+ ...data,
18669
+ locationId: job.toLocationId
18670
+ }
18671
+ });
18672
+ continue;
18673
+ }
18574
18674
  const bytes = await this.deps.storage.read({
18575
18675
  location: from,
18576
18676
  relativePath
@@ -18580,6 +18680,10 @@ var MediaRelocateEngine = class {
18580
18680
  relativePath,
18581
18681
  data: bytes
18582
18682
  });
18683
+ if ((await this.deps.storage.read({
18684
+ location: job.toLocationId,
18685
+ relativePath
18686
+ })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
18583
18687
  await this.deps.store.set.mutate({
18584
18688
  collection: MEDIA_COLLECTION,
18585
18689
  key: row.id,
@@ -18596,20 +18700,99 @@ var MediaRelocateEngine = class {
18596
18700
  job.bytesMoved += bytes.length;
18597
18701
  await sleep(bytes.length / bytesPerMs);
18598
18702
  } catch (err) {
18703
+ failedRows++;
18599
18704
  this.deps.logger.debug("media relocate row failed", { meta: {
18600
18705
  key: row.id,
18601
18706
  error: String(err)
18602
18707
  } });
18603
18708
  }
18604
18709
  }
18605
- const last = fresh[fresh.length - 1];
18710
+ const last = rows[rows.length - 1];
18606
18711
  const lastTs = Number(last.data["timestamp"] ?? cursor);
18607
- if (lastTs === cursor) for (const r of fresh) seenAtCursor.add(r.id);
18712
+ if (lastTs === cursor) offsetAtCursor += rows.length;
18608
18713
  else {
18609
18714
  cursor = lastTs;
18610
- seenAtCursor = new Set(fresh.filter((r) => Number(r.data["timestamp"]) === lastTs).map((r) => r.id));
18715
+ offsetAtCursor = rows.filter((row) => Number(row.data["timestamp"]) === lastTs).length;
18611
18716
  }
18717
+ if (rows.length < PAGE_SIZE) break;
18612
18718
  }
18719
+ let retrainOffset = 0;
18720
+ for (;;) {
18721
+ if (job.cancelRequested) break;
18722
+ const rows = await this.deps.store.query.query({
18723
+ collection: RETRAIN_FRAMES_COLLECTION,
18724
+ filter: {
18725
+ orderBy: {
18726
+ field: "copiedAt",
18727
+ direction: "asc"
18728
+ },
18729
+ limit: PAGE_SIZE,
18730
+ offset: retrainOffset
18731
+ }
18732
+ });
18733
+ if (rows.length === 0) break;
18734
+ for (const row of rows) {
18735
+ if (job.cancelRequested) break;
18736
+ const data = row.data;
18737
+ const from = mediaRowLocation(data);
18738
+ if (from === job.toLocationId) continue;
18739
+ const relativePath = String(data["path"] ?? "");
18740
+ if (relativePath.length === 0) {
18741
+ failedRows++;
18742
+ continue;
18743
+ }
18744
+ try {
18745
+ if (await this.samePhysicalLocation(from, job.toLocationId)) {
18746
+ await this.deps.store.set.mutate({
18747
+ collection: RETRAIN_FRAMES_COLLECTION,
18748
+ key: row.id,
18749
+ value: {
18750
+ ...data,
18751
+ locationId: job.toLocationId
18752
+ }
18753
+ });
18754
+ continue;
18755
+ }
18756
+ const bytes = await this.deps.storage.read({
18757
+ location: from,
18758
+ relativePath
18759
+ });
18760
+ await this.deps.storage.write({
18761
+ location: job.toLocationId,
18762
+ relativePath,
18763
+ data: bytes
18764
+ });
18765
+ if ((await this.deps.storage.read({
18766
+ location: job.toLocationId,
18767
+ relativePath
18768
+ })).length !== bytes.length) throw new Error(`relocated retrain frame size mismatch for ${relativePath}`);
18769
+ await this.deps.store.set.mutate({
18770
+ collection: RETRAIN_FRAMES_COLLECTION,
18771
+ key: row.id,
18772
+ value: {
18773
+ ...data,
18774
+ locationId: job.toLocationId
18775
+ }
18776
+ });
18777
+ await this.deps.storage.delete({
18778
+ location: from,
18779
+ relativePath
18780
+ });
18781
+ job.filesMoved++;
18782
+ job.bytesMoved += bytes.length;
18783
+ await sleep(bytes.length / bytesPerMs);
18784
+ } catch (err) {
18785
+ failedRows++;
18786
+ this.deps.logger.debug("retrain frame relocate row failed", { meta: {
18787
+ key: row.id,
18788
+ error: String(err)
18789
+ } });
18790
+ }
18791
+ }
18792
+ retrainOffset += rows.length;
18793
+ if (rows.length < PAGE_SIZE) break;
18794
+ }
18795
+ if (failedRows > 0) throw new Error(`event-media relocation left ${failedRows} row(s) unmoved`);
18613
18796
  job.state = job.cancelRequested ? "cancelled" : "done";
18614
18797
  } catch (err) {
18615
18798
  job.state = "failed";
@@ -18623,6 +18806,18 @@ var MediaRelocateEngine = class {
18623
18806
  this.deps.onFinished?.(snapshot(job));
18624
18807
  }
18625
18808
  }
18809
+ async samePhysicalLocation(from, to) {
18810
+ if (from === to) return true;
18811
+ try {
18812
+ const [fromRoot, toRoot] = await Promise.all([this.deps.resolveTargetRoot(from), this.deps.resolveTargetRoot(to)]);
18813
+ const [fromStat, toStat] = await Promise.all([node_fs.promises.stat(fromRoot), node_fs.promises.stat(toRoot)]);
18814
+ if (fromStat.dev === toStat.dev && fromStat.ino === toStat.ino) return true;
18815
+ const [fromReal, toReal] = await Promise.all([node_fs.promises.realpath(node_path.default.resolve(fromRoot)), node_fs.promises.realpath(node_path.default.resolve(toRoot))]);
18816
+ return fromReal === toReal;
18817
+ } catch {
18818
+ return false;
18819
+ }
18820
+ }
18626
18821
  };
18627
18822
  //#endregion
18628
18823
  //#region src/pipeline-analytics/media-settings.ts
@@ -31175,6 +31370,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31175
31370
  eventsOpsLog = null;
31176
31371
  /** Event-media relocation engine (entity-routing Phase 4). */
31177
31372
  mediaRelocate = null;
31373
+ mediaLocationStorage = null;
31374
+ mediaWriteGate = new MediaWriteGate();
31375
+ storageMigrationLeaseId = null;
31178
31376
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
31179
31377
  sensorEventStore = null;
31180
31378
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
@@ -31975,7 +32173,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31975
32173
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31976
32174
  }
31977
32175
  if (!storage) throw new Error("pipeline-analytics requires ctx.kernel.storage");
31978
- return createLocationAwareMediaStorage({
32176
+ const routed = createLocationAwareMediaStorage({
31979
32177
  base: storage,
31980
32178
  defaultLocation: "eventMedia",
31981
32179
  resolveRoot: (locationId) => this.ctx.api.storage.resolve.query({
@@ -31983,6 +32181,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31983
32181
  relativePath: ""
31984
32182
  })
31985
32183
  });
32184
+ this.mediaLocationStorage = routed;
32185
+ return routed;
31986
32186
  }
31987
32187
  /** Constructs every SQLite-backed store plus the stationary/package-drop/
31988
32188
  * sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
@@ -32027,7 +32227,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
32027
32227
  const mediaStore = new MediaStore({
32028
32228
  storage,
32029
32229
  store: api.settingsStore,
32030
- logger: logger.child("MediaStore")
32230
+ logger: logger.child("MediaStore"),
32231
+ writeGate: this.mediaWriteGate
32031
32232
  });
32032
32233
  this.mediaStore = mediaStore;
32033
32234
  const retrainFrames = new RetrainFrameStore({
@@ -32035,6 +32236,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
32035
32236
  storage,
32036
32237
  logger: logger.child("RetrainFrames"),
32037
32238
  now: () => Date.now(),
32239
+ writeGate: this.mediaWriteGate,
32038
32240
  readSource: async (mediaKey) => {
32039
32241
  const file = await mediaStore.getByKey(mediaKey);
32040
32242
  return file === null ? null : Buffer.from(file.base64, "base64");
@@ -36276,14 +36478,38 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
36276
36478
  return this.queryFacade.deleteDeviceEvents(input);
36277
36479
  }
36278
36480
  /** The events ops-log rows (newest-first), optionally scoped to one camera. */
36279
- async relocateMedia(input) {
36481
+ async pauseForStorageMigration(input) {
36482
+ if (this.storageMigrationLeaseId !== null && this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media writes are already paused by another storage migration");
36483
+ this.storageMigrationLeaseId = input.leaseId;
36484
+ try {
36485
+ await this.mediaWriteGate.pause();
36486
+ return { paused: true };
36487
+ } catch (err) {
36488
+ this.storageMigrationLeaseId = null;
36489
+ throw err;
36490
+ }
36491
+ }
36492
+ async resumeForStorageMigration(input) {
36493
+ if (this.storageMigrationLeaseId === null) return { resumed: true };
36494
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36495
+ this.mediaWriteGate.resume();
36496
+ this.storageMigrationLeaseId = null;
36497
+ return { resumed: true };
36498
+ }
36499
+ async refreshStorageLocationsForMigration(input) {
36500
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36501
+ this.mediaLocationStorage?.invalidateRoots();
36502
+ return { refreshed: true };
36503
+ }
36504
+ async startStorageMigrationMove(input) {
36280
36505
  if (!this.mediaRelocate) throw new Error("media relocation unavailable");
36506
+ if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
36281
36507
  return { jobId: this.mediaRelocate.start(input) };
36282
36508
  }
36283
- async getMediaRelocateStatus() {
36284
- return this.mediaRelocate?.list() ?? [];
36509
+ async getStorageMigrationMoveStatus(input) {
36510
+ return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
36285
36511
  }
36286
- async cancelMediaRelocate(input) {
36512
+ async cancelStorageMigrationMove(input) {
36287
36513
  return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
36288
36514
  }
36289
36515
  async listOpsLog(input) {