@camstack/addon-post-analysis 1.2.63 → 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");
@@ -15784,6 +15784,7 @@ function resolveFaceSettings(raw) {
15784
15784
  */
15785
15785
  function createLocationAwareMediaStorage(deps) {
15786
15786
  const roots = /* @__PURE__ */ new Map();
15787
+ let defaultBypassInvalidated = false;
15787
15788
  const rootOf = async (locationId) => {
15788
15789
  const cached = roots.get(locationId);
15789
15790
  if (cached !== void 0) return cached;
@@ -15794,21 +15795,55 @@ function createLocationAwareMediaStorage(deps) {
15794
15795
  const absOf = async (locationId, relativePath) => node_path.default.join(await rootOf(locationId), relativePath);
15795
15796
  return {
15796
15797
  write: async (input) => {
15797
- if (input.location === deps.defaultLocation) return deps.base.write(input);
15798
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.write(input);
15798
15799
  const abs = await absOf(input.location, input.relativePath);
15799
15800
  await node_fs.promises.mkdir(node_path.default.dirname(abs), { recursive: true });
15800
15801
  await node_fs.promises.writeFile(abs, input.data);
15801
15802
  },
15802
15803
  read: async (input) => {
15803
- if (input.location === deps.defaultLocation) return deps.base.read(input);
15804
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.read(input);
15804
15805
  return node_fs.promises.readFile(await absOf(input.location, input.relativePath));
15805
15806
  },
15806
15807
  delete: async (input) => {
15807
- if (input.location === deps.defaultLocation) return deps.base.delete(input);
15808
+ if (input.location === deps.defaultLocation && !defaultBypassInvalidated) return deps.base.delete(input);
15808
15809
  await node_fs.promises.rm(await absOf(input.location, input.relativePath), { force: true });
15810
+ },
15811
+ invalidateRoots: () => {
15812
+ roots.clear();
15813
+ defaultBypassInvalidated = true;
15809
15814
  }
15810
15815
  };
15811
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
+ };
15812
15847
  var MACRO_SET = new Set([
15813
15848
  "person",
15814
15849
  "vehicle",
@@ -16679,7 +16714,14 @@ var RETRAIN_FRAME_COLUMNS = [
16679
16714
  name: "copiedAt",
16680
16715
  type: "INTEGER",
16681
16716
  notNull: true
16682
- }
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
+ })
16683
16725
  ];
16684
16726
  var RETRAIN_FRAME_INDEXES = [{
16685
16727
  name: "idx_retrain_frames_track",
@@ -16708,12 +16750,14 @@ var RetrainFrameStore = class {
16708
16750
  logger;
16709
16751
  readSource;
16710
16752
  now;
16753
+ writeGate;
16711
16754
  constructor(deps) {
16712
16755
  this.store = deps.store;
16713
16756
  this.storage = deps.storage;
16714
16757
  this.logger = deps.logger;
16715
16758
  this.readSource = deps.readSource;
16716
16759
  this.now = deps.now;
16760
+ this.writeGate = deps.writeGate;
16717
16761
  }
16718
16762
  static async declare(store) {
16719
16763
  await store.declareCollection.mutate({
@@ -16732,7 +16776,8 @@ var RetrainFrameStore = class {
16732
16776
  sizeBytes: Number(data["sizeBytes"] ?? 0),
16733
16777
  width: Number(data["width"] ?? 0),
16734
16778
  height: Number(data["height"] ?? 0),
16735
- copiedAt: Number(data["copiedAt"] ?? 0)
16779
+ copiedAt: Number(data["copiedAt"] ?? 0),
16780
+ locationId: typeof data["locationId"] === "string" && data["locationId"].length > 0 ? data["locationId"] : null
16736
16781
  };
16737
16782
  }
16738
16783
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
@@ -16800,7 +16845,7 @@ var RetrainFrameStore = class {
16800
16845
  async readFrame(frame) {
16801
16846
  try {
16802
16847
  return await this.storage.read({
16803
- location: RETRAIN_MEDIA_LOCATION,
16848
+ location: frame.locationId ?? "eventMedia",
16804
16849
  relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16805
16850
  });
16806
16851
  } catch (err) {
@@ -16880,9 +16925,10 @@ var RetrainFrameStore = class {
16880
16925
  sizeBytes: bytes.length,
16881
16926
  width: size.width,
16882
16927
  height: size.height,
16883
- copiedAt: this.now()
16928
+ copiedAt: this.now(),
16929
+ locationId: null
16884
16930
  };
16885
- try {
16931
+ const persist = async () => {
16886
16932
  await this.storage.write({
16887
16933
  location: RETRAIN_MEDIA_LOCATION,
16888
16934
  relativePath: path,
@@ -16900,9 +16946,14 @@ var RetrainFrameStore = class {
16900
16946
  sizeBytes: frame.sizeBytes,
16901
16947
  width: frame.width,
16902
16948
  height: frame.height,
16903
- copiedAt: frame.copiedAt
16949
+ copiedAt: frame.copiedAt,
16950
+ locationId: null
16904
16951
  }
16905
16952
  });
16953
+ };
16954
+ try {
16955
+ if (this.writeGate) await this.writeGate.run(persist);
16956
+ else await persist();
16906
16957
  } catch (err) {
16907
16958
  this.logger.error("retrain copy failed to persist", {
16908
16959
  tags: { deviceId: input.deviceId },
@@ -16943,26 +16994,30 @@ var RetrainFrameStore = class {
16943
16994
  * best-effort write is how a half-deleted annotation set happens.
16944
16995
  */
16945
16996
  async deleteFrame(frame) {
16946
- try {
16947
- await this.storage.delete({
16948
- location: RETRAIN_MEDIA_LOCATION,
16949
- relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
16950
- });
16951
- } catch {}
16952
- try {
16953
- await this.store.delete.mutate({
16954
- collection: RETRAIN_FRAMES_COLLECTION,
16955
- key: frame.frameId
16956
- });
16957
- } catch (err) {
16958
- this.logger.warn("RetrainFrameStore.deleteFrame row delete failed", {
16959
- tags: { deviceId: frame.deviceId },
16960
- meta: {
16961
- frameId: frame.frameId,
16962
- error: String(err)
16963
- }
16964
- });
16965
- }
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();
16966
17021
  }
16967
17022
  };
16968
17023
  //#endregion
@@ -18001,10 +18056,15 @@ var MediaStore = class {
18001
18056
  storage;
18002
18057
  store;
18003
18058
  logger;
18059
+ writeGate;
18004
18060
  constructor(deps) {
18005
18061
  this.storage = deps.storage;
18006
18062
  this.store = deps.store;
18007
18063
  this.logger = deps.logger;
18064
+ this.writeGate = deps.writeGate;
18065
+ }
18066
+ gated(mutation) {
18067
+ return this.writeGate ? this.writeGate.run(mutation) : mutation();
18008
18068
  }
18009
18069
  static async declare(store) {
18010
18070
  await store.declareCollection.mutate({
@@ -18016,6 +18076,10 @@ var MediaStore = class {
18016
18076
  /** Persist a media blob and register its metadata row. Returns the
18017
18077
  * collection key (MediaFile.key) that callers store as reference. */
18018
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) {
18019
18083
  const key = buildKey(params);
18020
18084
  const path = buildPath(params);
18021
18085
  const record = {
@@ -18086,6 +18150,9 @@ var MediaStore = class {
18086
18150
  * scheme (transition back-compat); once migrated it is a no-op.
18087
18151
  */
18088
18152
  async putReplacing(params) {
18153
+ return this.gated(() => this.putReplacingUnchecked(params));
18154
+ }
18155
+ async putReplacingUnchecked(params) {
18089
18156
  const existing = await this.store.query.query({
18090
18157
  collection: MEDIA_COLLECTION,
18091
18158
  filter: { where: {
@@ -18094,7 +18161,7 @@ var MediaStore = class {
18094
18161
  kind: params.kind
18095
18162
  } }
18096
18163
  });
18097
- const newKey = await this.put(params);
18164
+ const newKey = await this.putUnchecked(params);
18098
18165
  for (const row of existing) {
18099
18166
  if (row.id === newKey) continue;
18100
18167
  const rowData = row.data;
@@ -18137,8 +18204,11 @@ var MediaStore = class {
18137
18204
  * own kind.
18138
18205
  */
18139
18206
  async promoteToLastFrame(input) {
18207
+ return this.gated(() => this.promoteToLastFrameUnchecked(input));
18208
+ }
18209
+ async promoteToLastFrameUnchecked(input) {
18140
18210
  const data = Buffer.from(input.snapshot.base64, "base64");
18141
- const newKey = await this.putReplacing({
18211
+ const newKey = await this.putReplacingUnchecked({
18142
18212
  deviceId: input.deviceId,
18143
18213
  ownerKind: "track",
18144
18214
  ownerId: input.trackId,
@@ -18146,7 +18216,7 @@ var MediaStore = class {
18146
18216
  timestamp: input.snapshot.timestamp,
18147
18217
  data
18148
18218
  });
18149
- if (input.keepSource !== true) await this.deleteByKey(input.snapshot.key);
18219
+ if (input.keepSource !== true) await this.deleteByKeyUnchecked(input.snapshot.key);
18150
18220
  return newKey;
18151
18221
  }
18152
18222
  /**
@@ -18184,6 +18254,9 @@ var MediaStore = class {
18184
18254
  * Deletes the blob from storage (best-effort) and removes the index row.
18185
18255
  */
18186
18256
  async deleteByKey(key) {
18257
+ await this.gated(() => this.deleteByKeyUnchecked(key));
18258
+ }
18259
+ async deleteByKeyUnchecked(key) {
18187
18260
  const row = await this.store.get.query({
18188
18261
  collection: MEDIA_COLLECTION,
18189
18262
  key
@@ -18294,6 +18367,9 @@ var MediaStore = class {
18294
18367
  * Best-effort per row; returns the number of index rows removed.
18295
18368
  */
18296
18369
  async deleteForOwner(ownerKind, ownerIds) {
18370
+ return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
18371
+ }
18372
+ async deleteForOwnerUnchecked(ownerKind, ownerIds) {
18297
18373
  let removed = 0;
18298
18374
  for (const ownerId of ownerIds) {
18299
18375
  const rows = await this.store.query.query({
@@ -18375,6 +18451,9 @@ var MediaStore = class {
18375
18451
  * The old-entry deletion is best-effort: on failure a warning is logged
18376
18452
  * and the method still returns the new key. */
18377
18453
  async reown(input) {
18454
+ return this.gated(() => this.reownUnchecked(input));
18455
+ }
18456
+ async reownUnchecked(input) {
18378
18457
  const { fromOwnerKind, fromOwnerId, toOwnerKind, toOwnerId, key } = input;
18379
18458
  const row = (await this.store.query.query({
18380
18459
  collection: MEDIA_COLLECTION,
@@ -18408,7 +18487,7 @@ var MediaStore = class {
18408
18487
  relativePath: oldPath
18409
18488
  })
18410
18489
  };
18411
- const newKey = await this.put(newParams);
18490
+ const newKey = await this.putUnchecked(newParams);
18412
18491
  try {
18413
18492
  await this.storage.delete({
18414
18493
  location: mediaRowLocation(meta),
@@ -18462,8 +18541,10 @@ var MediaStore = class {
18462
18541
  * batch.
18463
18542
  */
18464
18543
  async deleteByTracks(trackIds) {
18465
- await this.deleteForOwner("track", trackIds);
18466
- 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
+ });
18467
18548
  await this.deleteForOwner("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
18468
18549
  }
18469
18550
  /**
@@ -18505,7 +18586,7 @@ function snapshot(j) {
18505
18586
  fromLocationId: j.fromLocationId,
18506
18587
  toLocationId: j.toLocationId,
18507
18588
  deviceId: j.deviceId,
18508
- entities: ["media"],
18589
+ entities: ["media", "retrainFrames"],
18509
18590
  filesMoved: j.filesMoved,
18510
18591
  bytesMoved: j.bytesMoved,
18511
18592
  filesTotal: null,
@@ -18536,7 +18617,7 @@ var MediaRelocateEngine = class {
18536
18617
  state: "running",
18537
18618
  fromLocationId: "*",
18538
18619
  toLocationId: input.toLocationId,
18539
- deviceId: input.deviceId ?? null,
18620
+ deviceId: null,
18540
18621
  filesMoved: 0,
18541
18622
  bytesMoved: 0,
18542
18623
  startedAt: this.deps.now(),
@@ -18553,24 +18634,25 @@ var MediaRelocateEngine = class {
18553
18634
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
18554
18635
  try {
18555
18636
  await this.deps.resolveTargetRoot(job.toLocationId);
18637
+ let failedRows = 0;
18556
18638
  let cursor = 0;
18557
- let seenAtCursor = /* @__PURE__ */ new Set();
18639
+ let offsetAtCursor = 0;
18558
18640
  for (;;) {
18559
18641
  if (job.cancelRequested) break;
18560
- const fresh = (await this.deps.store.query.query({
18642
+ const rows = await this.deps.store.query.query({
18561
18643
  collection: MEDIA_COLLECTION,
18562
18644
  filter: {
18563
- ...job.deviceId !== null ? { where: { deviceId: job.deviceId } } : {},
18564
18645
  whereBetween: { timestamp: [cursor, Number.MAX_SAFE_INTEGER] },
18565
18646
  orderBy: {
18566
18647
  field: "timestamp",
18567
18648
  direction: "asc"
18568
18649
  },
18569
- limit: PAGE_SIZE
18650
+ limit: PAGE_SIZE,
18651
+ offset: offsetAtCursor
18570
18652
  }
18571
- })).filter((r) => !seenAtCursor.has(r.id));
18572
- if (fresh.length === 0) break;
18573
- for (const row of fresh) {
18653
+ });
18654
+ if (rows.length === 0) break;
18655
+ for (const row of rows) {
18574
18656
  if (job.cancelRequested) break;
18575
18657
  const data = row.data;
18576
18658
  const from = mediaRowLocation(data);
@@ -18578,6 +18660,17 @@ var MediaRelocateEngine = class {
18578
18660
  const relativePath = String(data["path"] ?? "");
18579
18661
  if (relativePath.length === 0) continue;
18580
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
+ }
18581
18674
  const bytes = await this.deps.storage.read({
18582
18675
  location: from,
18583
18676
  relativePath
@@ -18587,6 +18680,10 @@ var MediaRelocateEngine = class {
18587
18680
  relativePath,
18588
18681
  data: bytes
18589
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}`);
18590
18687
  await this.deps.store.set.mutate({
18591
18688
  collection: MEDIA_COLLECTION,
18592
18689
  key: row.id,
@@ -18603,20 +18700,99 @@ var MediaRelocateEngine = class {
18603
18700
  job.bytesMoved += bytes.length;
18604
18701
  await sleep(bytes.length / bytesPerMs);
18605
18702
  } catch (err) {
18703
+ failedRows++;
18606
18704
  this.deps.logger.debug("media relocate row failed", { meta: {
18607
18705
  key: row.id,
18608
18706
  error: String(err)
18609
18707
  } });
18610
18708
  }
18611
18709
  }
18612
- const last = fresh[fresh.length - 1];
18710
+ const last = rows[rows.length - 1];
18613
18711
  const lastTs = Number(last.data["timestamp"] ?? cursor);
18614
- if (lastTs === cursor) for (const r of fresh) seenAtCursor.add(r.id);
18712
+ if (lastTs === cursor) offsetAtCursor += rows.length;
18615
18713
  else {
18616
18714
  cursor = lastTs;
18617
- 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;
18618
18716
  }
18717
+ if (rows.length < PAGE_SIZE) break;
18619
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`);
18620
18796
  job.state = job.cancelRequested ? "cancelled" : "done";
18621
18797
  } catch (err) {
18622
18798
  job.state = "failed";
@@ -18630,6 +18806,18 @@ var MediaRelocateEngine = class {
18630
18806
  this.deps.onFinished?.(snapshot(job));
18631
18807
  }
18632
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
+ }
18633
18821
  };
18634
18822
  //#endregion
18635
18823
  //#region src/pipeline-analytics/media-settings.ts
@@ -31182,6 +31370,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31182
31370
  eventsOpsLog = null;
31183
31371
  /** Event-media relocation engine (entity-routing Phase 4). */
31184
31372
  mediaRelocate = null;
31373
+ mediaLocationStorage = null;
31374
+ mediaWriteGate = new MediaWriteGate();
31375
+ storageMigrationLeaseId = null;
31185
31376
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
31186
31377
  sensorEventStore = null;
31187
31378
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
@@ -31982,7 +32173,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31982
32173
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31983
32174
  }
31984
32175
  if (!storage) throw new Error("pipeline-analytics requires ctx.kernel.storage");
31985
- return createLocationAwareMediaStorage({
32176
+ const routed = createLocationAwareMediaStorage({
31986
32177
  base: storage,
31987
32178
  defaultLocation: "eventMedia",
31988
32179
  resolveRoot: (locationId) => this.ctx.api.storage.resolve.query({
@@ -31990,6 +32181,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
31990
32181
  relativePath: ""
31991
32182
  })
31992
32183
  });
32184
+ this.mediaLocationStorage = routed;
32185
+ return routed;
31993
32186
  }
31994
32187
  /** Constructs every SQLite-backed store plus the stationary/package-drop/
31995
32188
  * sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
@@ -32034,7 +32227,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
32034
32227
  const mediaStore = new MediaStore({
32035
32228
  storage,
32036
32229
  store: api.settingsStore,
32037
- logger: logger.child("MediaStore")
32230
+ logger: logger.child("MediaStore"),
32231
+ writeGate: this.mediaWriteGate
32038
32232
  });
32039
32233
  this.mediaStore = mediaStore;
32040
32234
  const retrainFrames = new RetrainFrameStore({
@@ -32042,6 +32236,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
32042
32236
  storage,
32043
32237
  logger: logger.child("RetrainFrames"),
32044
32238
  now: () => Date.now(),
32239
+ writeGate: this.mediaWriteGate,
32045
32240
  readSource: async (mediaKey) => {
32046
32241
  const file = await mediaStore.getByKey(mediaKey);
32047
32242
  return file === null ? null : Buffer.from(file.base64, "base64");
@@ -36283,14 +36478,38 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
36283
36478
  return this.queryFacade.deleteDeviceEvents(input);
36284
36479
  }
36285
36480
  /** The events ops-log rows (newest-first), optionally scoped to one camera. */
36286
- 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) {
36287
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");
36288
36507
  return { jobId: this.mediaRelocate.start(input) };
36289
36508
  }
36290
- async getMediaRelocateStatus() {
36291
- return this.mediaRelocate?.list() ?? [];
36509
+ async getStorageMigrationMoveStatus(input) {
36510
+ return this.mediaRelocate?.list().find((job) => job.jobId === input.jobId) ?? null;
36292
36511
  }
36293
- async cancelMediaRelocate(input) {
36512
+ async cancelStorageMigrationMove(input) {
36294
36513
  return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
36295
36514
  }
36296
36515
  async listOpsLog(input) {