@camstack/system 1.2.164 → 1.2.166

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.
Files changed (68) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +1 -1
  36. package/dist/builtins/snapshot/index.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/index.d.ts +1 -1
  40. package/dist/builtins/sqlite-storage/index.js +2 -0
  41. package/dist/builtins/sqlite-storage/index.mjs +2 -2
  42. package/dist/builtins/sqlite-storage/query-profiler.d.ts +21 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  45. package/dist/builtins/storage-orchestrator/location-occupancy.d.ts +19 -0
  46. package/dist/builtins/storage-orchestrator/storage-cleanup-coordinator.d.ts +68 -0
  47. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +14 -0
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +13 -8
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +441 -100
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +443 -100
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +11 -13
  52. package/dist/builtins/system-config/system-config.addon.js +1 -1
  53. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  54. package/dist/builtins/winston-logging/index.js +1 -1
  55. package/dist/builtins/winston-logging/index.mjs +1 -1
  56. package/dist/{dist-BhE8zNfY.js → dist-CFTKQGym.js} +185 -7
  57. package/dist/{dist-BpsHt11f.mjs → dist-Dw9cm1NS.mjs} +180 -8
  58. package/dist/index.js +2 -2
  59. package/dist/index.mjs +2 -2
  60. package/dist/kernel/deps/manifest-system-deps.d.ts +5 -2
  61. package/dist/kernel/deps/system-package-allowlist.d.ts +12 -0
  62. package/dist/kernel/deps/system-package-broker.d.ts +31 -0
  63. package/dist/kernel/deps/system-package-client.d.ts +9 -0
  64. package/dist/{manifest-system-deps-CJLqfJUs.js → manifest-system-deps-BIxY1Y5e.js} +140 -5
  65. package/dist/{manifest-system-deps-Bk_XLYbB.mjs → manifest-system-deps-DTz_zNQf.mjs} +142 -6
  66. package/dist/{retired-settings-keys-CqDbI-vK.js → retired-settings-keys-dehyu2N3.js} +1 -1
  67. package/dist/{retired-settings-keys-DLwieFXR.mjs → retired-settings-keys-diW1NmyP.mjs} +1 -1
  68. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
- import { Mt as BaseAddon, N as collectSecretConfigKeys, Qt as parseJsonObject, S as StorageMigrationJobSchema, St as settingsStoreCapability, Tt as storageMigrationCapability, m as REDACTED_SECRET, wt as storageCapability, x as StorageLocationTypeSchema } from "../../dist-BpsHt11f.mjs";
1
+ import { $t as parseJsonObject, C as StorageMigrationJobSchema, Ct as settingsStoreCapability, Et as storageMigrationCapability, Nt as BaseAddon, P as collectSecretConfigKeys, S as StorageLocationTypeSchema, Tt as storageCapability, m as REDACTED_SECRET, x as StorageCleanupJobSchema } from "../../dist-Dw9cm1NS.mjs";
2
+ import { z } from "zod";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import * as fs from "node:fs/promises";
5
+ import { opendir, readdir } from "node:fs/promises";
4
6
  import * as path$1 from "node:path";
7
+ import path from "node:path";
5
8
  //#region src/builtins/storage-orchestrator/location-store.ts
6
9
  /**
7
10
  * @durable class=config owner=storage-orchestrator
@@ -285,17 +288,22 @@ var StorageMigrationCoordinator = class {
285
288
  for (const storageClass of STORAGE_CLASSES) {
286
289
  const targetId = input.destinations[storageClass];
287
290
  if (targetId === void 0) continue;
288
- const source = this.deps.locations.getDefaultLocation(storageClass);
289
- if (!source) throw new Error(`No default storage location for "${storageClass}"`);
291
+ const namedSourceId = input.sources?.[storageClass];
292
+ const defaultLoc = this.deps.locations.getDefaultLocation(storageClass);
293
+ const source = namedSourceId ? this.deps.locations.getLocationById(namedSourceId) : defaultLoc;
294
+ if (!source) throw new Error(namedSourceId ? `Storage location "${namedSourceId}" not found` : `No default storage location for "${storageClass}"`);
295
+ if (source.type !== storageClass) throw new Error(`Storage location "${source.id}" is type "${source.type}", expected "${storageClass}"`);
290
296
  const target = this.deps.locations.getLocationById(targetId);
291
297
  if (!target) throw new Error(`Storage location "${targetId}" not found`);
292
298
  if (target.type !== storageClass) throw new Error(`Storage location "${targetId}" is type "${target.type}", expected "${storageClass}"`);
293
- if (source.id === target.id) throw new Error(`Storage location "${targetId}" is already the "${storageClass}" default`);
299
+ if (source.id === target.id) throw new Error(namedSourceId ? `Source and destination are the same location ("${targetId}")` : `Storage location "${targetId}" is already the "${storageClass}" default`);
300
+ const freezeSource = defaultLoc !== void 0 && source.id !== defaultLoc.id;
294
301
  if (!MOVER_CLASSES.includes(storageClass)) throw new Error(`No mover owns "${storageClass}" — the migration can repoint its default but cannot move its bytes. Move it by hand, then repoint the default with upsertLocation.`);
295
302
  moves.push({
296
303
  storageClass,
297
304
  fromLocationId: source.id,
298
305
  toLocationId: target.id,
306
+ freezeSource,
299
307
  moverJobId: null,
300
308
  state: null,
301
309
  error: null,
@@ -326,11 +334,13 @@ var StorageMigrationCoordinator = class {
326
334
  }
327
335
  return {
328
336
  destinations: input.destinations,
337
+ sources: input.sources,
329
338
  mode,
330
- moves: moves.map(({ storageClass, fromLocationId, toLocationId }) => ({
339
+ moves: moves.map(({ storageClass, fromLocationId, toLocationId, freezeSource }) => ({
331
340
  storageClass,
332
341
  fromLocationId,
333
- toLocationId
342
+ toLocationId,
343
+ freezeSource
334
344
  })),
335
345
  findings
336
346
  };
@@ -360,12 +370,12 @@ var StorageMigrationCoordinator = class {
360
370
  this.startReserved = true;
361
371
  try {
362
372
  const existing = await this.status();
363
- if (existing && isTerminal(existing) && existing.pausedParticipants.length > 0) {
373
+ if (existing && isTerminal$1(existing) && existing.pausedParticipants.length > 0) {
364
374
  await this.releaseAfterTerminal(existing);
365
375
  await this.persist(existing);
366
376
  if (existing.pausedParticipants.length > 0) throw new Error(`storage migration ${existing.jobId} still holds maintenance leases`);
367
377
  }
368
- if (existing && !isTerminal(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
378
+ if (existing && !isTerminal$1(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
369
379
  const plan = await this.plan(input);
370
380
  const now = this.deps.now();
371
381
  const job = {
@@ -373,6 +383,7 @@ var StorageMigrationCoordinator = class {
373
383
  phase: "planning",
374
384
  mode: plan.mode,
375
385
  destinations: input.destinations,
386
+ sources: input.sources,
376
387
  throttleMbps: input.throttleMbps ?? 40,
377
388
  moves: plan.moves.map((move) => ({
378
389
  ...move,
@@ -404,10 +415,15 @@ var StorageMigrationCoordinator = class {
404
415
  if (jobId !== void 0 && job?.jobId !== jobId) return null;
405
416
  return job;
406
417
  }
418
+ /** Finished jobs, newest first, excluding the one `status` currently shows. */
419
+ async history() {
420
+ const current = await this.status();
421
+ return (await this.deps.history?.get() ?? []).filter((job) => job.jobId !== current?.jobId);
422
+ }
407
423
  async cancel(jobId) {
408
424
  const job = await this.status(jobId);
409
425
  const cutoverInFlight = job !== null && job.repointed && (job.phase === "refreshing" || job.phase === "resuming");
410
- if (!job || isTerminal(job) || cutoverInFlight) return false;
426
+ if (!job || isTerminal$1(job) || cutoverInFlight) return false;
411
427
  job.cancelRequested = true;
412
428
  await this.persist(job);
413
429
  await Promise.all(job.moves.filter((move) => move.moverJobId !== null).map((move) => this.cancelMove(move)));
@@ -464,18 +480,22 @@ var StorageMigrationCoordinator = class {
464
480
  const target = this.deps.locations.getDefaultLocation(storageClass);
465
481
  if (!target) continue;
466
482
  if (laneOf(storageClass) === "media") {
467
- const count = await unanswerable(this.deps.participants.analytics.residue({
468
- toLocationId: target.id,
469
- mode: "move"
470
- }));
471
- if (count !== null && count.rows === 0) continue;
472
- out.push({
473
- storageClass,
474
- fromLocationId: "*",
475
- toLocationId: target.id,
476
- items: count?.rows ?? null,
477
- bytes: null
478
- });
483
+ for (const source of this.deps.locations.listLocations({ type: storageClass })) {
484
+ if (source.id === target.id) continue;
485
+ const count = await unanswerable(this.deps.participants.analytics.residue({
486
+ toLocationId: target.id,
487
+ fromLocationId: source.id,
488
+ mode: "move"
489
+ }));
490
+ if (count !== null && count.rows === 0) continue;
491
+ out.push({
492
+ storageClass,
493
+ fromLocationId: source.id,
494
+ toLocationId: target.id,
495
+ items: count?.rows ?? null,
496
+ bytes: null
497
+ });
498
+ }
479
499
  continue;
480
500
  }
481
501
  for (const source of this.deps.locations.listLocations({ type: storageClass })) {
@@ -529,12 +549,12 @@ var StorageMigrationCoordinator = class {
529
549
  this.startReserved = true;
530
550
  try {
531
551
  const existing = await this.status();
532
- if (existing && isTerminal(existing) && existing.pausedParticipants.length > 0) {
552
+ if (existing && isTerminal$1(existing) && existing.pausedParticipants.length > 0) {
533
553
  await this.releaseAfterTerminal(existing);
534
554
  await this.persist(existing);
535
555
  if (existing.pausedParticipants.length > 0) throw new Error(`storage migration ${existing.jobId} still holds maintenance leases`);
536
556
  }
537
- if (existing && !isTerminal(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
557
+ if (existing && !isTerminal$1(existing)) throw new Error(`storage migration is already active (${existing.jobId})`);
538
558
  for (const storageClass of input.classes) {
539
559
  if (MOVER_CLASSES.includes(storageClass)) continue;
540
560
  throw new Error(`No mover owns "${storageClass}" — there is nothing that can drain it. Move it by hand.`);
@@ -599,7 +619,7 @@ var StorageMigrationCoordinator = class {
599
619
  await this.persist(job);
600
620
  return;
601
621
  }
602
- if (isTerminal(job)) {
622
+ if (isTerminal$1(job)) {
603
623
  if (job.pausedParticipants.length > 0) {
604
624
  await this.releaseAfterTerminal(job);
605
625
  await this.persist(job);
@@ -660,11 +680,12 @@ var StorageMigrationCoordinator = class {
660
680
  if (job.phase === "verifying" && !nonBlocking) {
661
681
  await this.verifyMoves(job);
662
682
  if (job.cancelRequested) return this.finishCancelled(job);
683
+ await this.freezeDrainedSources(job);
663
684
  await this.setPhase(job, "repointing");
664
685
  }
665
686
  if (job.phase === "repointing") {
666
- const targets = new Map(job.moves.map((move) => [move.storageClass, move.toLocationId]));
667
- await this.deps.locations.setDefaultLocations(targets);
687
+ const targets = new Map(job.moves.filter((move) => move.freezeSource !== true).map((move) => [move.storageClass, move.toLocationId]));
688
+ if (targets.size > 0) await this.deps.locations.setDefaultLocations(targets);
668
689
  job.repointed = true;
669
690
  await this.setPhase(job, "refreshing");
670
691
  }
@@ -687,6 +708,7 @@ var StorageMigrationCoordinator = class {
687
708
  if (job.phase === "verifying" && nonBlocking) {
688
709
  await this.verifyMoves(job);
689
710
  if (job.cancelRequested) return this.finishCancelled(job);
711
+ await this.freezeDrainedSources(job);
690
712
  await this.setPhase(job, "done");
691
713
  }
692
714
  } catch (err) {
@@ -702,7 +724,7 @@ var StorageMigrationCoordinator = class {
702
724
  await this.releaseAfterTerminal(job);
703
725
  await this.persist(job);
704
726
  } finally {
705
- if (isTerminal(job)) {
727
+ if (isTerminal$1(job)) {
706
728
  await this.releaseAfterTerminal(job);
707
729
  this.active = job;
708
730
  }
@@ -862,6 +884,24 @@ var StorageMigrationCoordinator = class {
862
884
  for (const move of job.moves) if (move.state !== "done") throw new Error(`${move.storageClass} move did not complete verification`);
863
885
  }
864
886
  /**
887
+ * After a from→to copy whose source was NOT the class default: stop new
888
+ * writes to `from`. The default is unchanged (that is the whole point of
889
+ * naming a non-default source). A location that is already disabled is
890
+ * left alone.
891
+ */
892
+ freezeDrainedSources(job) {
893
+ for (const move of job.moves) {
894
+ if (move.freezeSource !== true) continue;
895
+ const loc = this.deps.locations.getLocationById(move.fromLocationId);
896
+ if (!loc || loc.enabled === false) continue;
897
+ const { createdAt: _c, updatedAt: _u, capacity: _cap, ...rest } = loc;
898
+ this.deps.locations.upsertLocation({
899
+ ...rest,
900
+ enabled: false
901
+ });
902
+ }
903
+ }
904
+ /**
865
905
  * Arm one class's mover.
866
906
  *
867
907
  * `draining` runs after every writer has been resumed, so there is no lease
@@ -874,6 +914,7 @@ var StorageMigrationCoordinator = class {
874
914
  if (laneOf(move.storageClass) === "media") {
875
915
  const input = {
876
916
  toLocationId: move.toLocationId,
917
+ fromLocationId: move.fromLocationId,
877
918
  throttleMbps: job.throttleMbps,
878
919
  mode: "move"
879
920
  };
@@ -907,7 +948,7 @@ var StorageMigrationCoordinator = class {
907
948
  }
908
949
  async setPhase(job, phase) {
909
950
  job.phase = phase;
910
- if (isTerminal(job)) job.finishedAt = this.deps.now();
951
+ if (isTerminal$1(job)) job.finishedAt = this.deps.now();
911
952
  await this.persist(job);
912
953
  }
913
954
  async finishCancelled(job) {
@@ -919,6 +960,9 @@ var StorageMigrationCoordinator = class {
919
960
  async persist(job) {
920
961
  job.updatedAt = this.deps.now();
921
962
  await this.deps.state.set(job);
963
+ if (!isTerminal$1(job) || this.deps.history === void 0) return;
964
+ const prev = [...await this.deps.history.get() ?? []];
965
+ await this.deps.history.set([job, ...prev.filter((row) => row.jobId !== job.jobId)].slice(0, HISTORY_CAP));
922
966
  }
923
967
  };
924
968
  /**
@@ -944,9 +988,10 @@ function requireLease(job) {
944
988
  if (job.pauseLeaseId === null) throw new Error("storage migration has no maintenance lease");
945
989
  return job.pauseLeaseId;
946
990
  }
947
- function isTerminal(job) {
991
+ function isTerminal$1(job) {
948
992
  return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
949
993
  }
994
+ var HISTORY_CAP = 20;
950
995
  /**
951
996
  * Which single-flight engine owns a class.
952
997
  */
@@ -1068,6 +1113,78 @@ function canProbeOccupancyLocally(locality) {
1068
1113
  return locality === true;
1069
1114
  }
1070
1115
  //#endregion
1116
+ //#region src/builtins/storage-orchestrator/location-occupancy.ts
1117
+ /**
1118
+ * Delete-time occupancy of a location's on-disk root.
1119
+ *
1120
+ * A recordings disk may host several classes at one `basePath`
1121
+ * (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
1122
+ * a class is therefore the class subtree, not the shared root — otherwise a
1123
+ * drained `eventMedia:default` still looks full because high footage is
1124
+ * writing next to it (observed 2026-09-01).
1125
+ *
1126
+ * The walk is shallow: one `readdir` of the root (one dirent per camera)
1127
+ * plus `opendir`+first-entry of each named subtree. No recursive find of
1128
+ * segments — that is the walk that saturates a USB disk.
1129
+ */
1130
+ /**
1131
+ * Class → directories under `<root>/<camera>/`. Same map as the discard
1132
+ * wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
1133
+ * Types not listed keep the legacy "any non-dot entry at the root" rule
1134
+ * (backups, addon-data, …).
1135
+ */
1136
+ var STORAGE_CLASS_SUBTREES = {
1137
+ eventMedia: ["events"],
1138
+ recordingsLow: ["low"],
1139
+ recordings: ["high", "mid"]
1140
+ };
1141
+ function isDotEntry(name) {
1142
+ return name.startsWith(".");
1143
+ }
1144
+ /**
1145
+ * True when `dir` exists and contains at least one dirent. Missing → false.
1146
+ * Any other errno throws so the caller can answer `unknown` (D49).
1147
+ */
1148
+ async function dirHasAnyEntry(dir) {
1149
+ let handle;
1150
+ try {
1151
+ handle = await opendir(dir);
1152
+ } catch (err) {
1153
+ if (err.code === "ENOENT") return false;
1154
+ throw err;
1155
+ }
1156
+ try {
1157
+ return await handle.read() !== null;
1158
+ } finally {
1159
+ await handle.close();
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Occupancy of `basePath` for a location of `type`.
1164
+ *
1165
+ * - missing root → `empty` (nothing this delete can strand)
1166
+ * - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
1167
+ * - other types → occupied iff the root has a non-dot entry
1168
+ */
1169
+ async function probeLocalDirectoryOccupancy(input) {
1170
+ let names;
1171
+ try {
1172
+ names = await readdir(input.basePath);
1173
+ } catch (err) {
1174
+ if (err.code === "ENOENT") return "empty";
1175
+ return "unknown";
1176
+ }
1177
+ const visible = names.filter((name) => !isDotEntry(name));
1178
+ const subtrees = STORAGE_CLASS_SUBTREES[input.type];
1179
+ if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
1180
+ try {
1181
+ for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(path.join(input.basePath, camera, subtree))) return "occupied";
1182
+ } catch {
1183
+ return "unknown";
1184
+ }
1185
+ return "empty";
1186
+ }
1187
+ //#endregion
1071
1188
  //#region src/builtins/storage-orchestrator/data-store-dispatch.ts
1072
1189
  /**
1073
1190
  * Pick the engine to serve a call.
@@ -1246,6 +1363,173 @@ async function collectProviderInfos(providers, onError) {
1246
1363
  return out;
1247
1364
  }
1248
1365
  //#endregion
1366
+ //#region src/builtins/storage-orchestrator/storage-cleanup-coordinator.ts
1367
+ var FOOTAGE_TYPES = new Set(["recordings", "recordingsLow"]);
1368
+ function isTerminal(job) {
1369
+ return job.phase === "done" || job.phase === "failed" || job.phase === "cancelled";
1370
+ }
1371
+ function footageFrozen(location) {
1372
+ return location.enabled === false || location.config["readOnly"] === true;
1373
+ }
1374
+ /**
1375
+ * One operator-facing cleanup of leftover analytics rows/blobs, optional
1376
+ * debug media, and ghost ledger rows on frozen footage locations.
1377
+ *
1378
+ * Each addon call either returns immediately (start+poll) or is a bounded
1379
+ * ledger walk. The HTTP caller never waits on the full pass.
1380
+ */
1381
+ var StorageCleanupCoordinator = class {
1382
+ deps;
1383
+ active = null;
1384
+ startReserved = false;
1385
+ constructor(deps) {
1386
+ this.deps = deps;
1387
+ }
1388
+ async start(input) {
1389
+ if (this.startReserved) throw new Error("storage cleanup is already active");
1390
+ this.startReserved = true;
1391
+ try {
1392
+ const existing = await this.status();
1393
+ if (existing && !isTerminal(existing)) throw new Error(`storage cleanup is already active (${existing.jobId})`);
1394
+ const now = this.deps.now();
1395
+ const job = {
1396
+ jobId: this.deps.newId(),
1397
+ phase: "orphans",
1398
+ includeDebugMedia: input.includeDebugMedia === true,
1399
+ orphansReclaimed: 0,
1400
+ orphanBytesReclaimed: 0,
1401
+ debugMediaReclaimed: 0,
1402
+ debugMediaBytesReclaimed: 0,
1403
+ ghostsForgotten: 0,
1404
+ ghostBytesForgotten: 0,
1405
+ detail: "Starting orphan reclaim",
1406
+ cancelRequested: false,
1407
+ startedAt: now,
1408
+ updatedAt: now,
1409
+ finishedAt: null,
1410
+ error: null
1411
+ };
1412
+ await this.persist(job);
1413
+ this.active = job;
1414
+ this.run(job);
1415
+ return job.jobId;
1416
+ } finally {
1417
+ this.startReserved = false;
1418
+ }
1419
+ }
1420
+ async status(jobId) {
1421
+ const job = this.active ?? await this.deps.state.get();
1422
+ if (jobId !== void 0 && job?.jobId !== jobId) return null;
1423
+ return job;
1424
+ }
1425
+ async cancel(jobId) {
1426
+ const job = await this.status(jobId);
1427
+ if (!job || isTerminal(job)) return false;
1428
+ job.cancelRequested = true;
1429
+ job.detail = "Cancel requested — finishing the current step";
1430
+ await this.persist(job);
1431
+ return true;
1432
+ }
1433
+ async run(job) {
1434
+ try {
1435
+ await this.reclaimOrphans(job);
1436
+ if (await this.stopped(job)) return;
1437
+ if (job.includeDebugMedia) {
1438
+ await this.setPhase(job, "debug-media", "Reclaiming debug media");
1439
+ await this.reclaimDebugMedia(job);
1440
+ if (await this.stopped(job)) return;
1441
+ }
1442
+ await this.setPhase(job, "ghost-ledger", "Forgetting ghost footage rows");
1443
+ await this.forgetGhosts(job);
1444
+ if (await this.stopped(job)) return;
1445
+ await this.finish(job, "done", null);
1446
+ } catch (err) {
1447
+ const message = err instanceof Error ? err.message : String(err);
1448
+ await this.finish(job, "failed", message);
1449
+ }
1450
+ }
1451
+ async reclaimOrphans(job) {
1452
+ let restart = true;
1453
+ for (;;) {
1454
+ if (await this.stopped(job)) return;
1455
+ const start = await this.deps.participants.startOrphan(restart);
1456
+ restart = false;
1457
+ if (!start.started && !start.alreadyRunning) throw new Error("orphan reclaim did not start");
1458
+ const status = await this.waitUntilIdle(() => this.deps.participants.orphanStatus(), job, (s) => `Orphan reclaim: ${s.totalReclaimed} rows / ${String(s.totalBytesReclaimed)} bytes`);
1459
+ job.orphansReclaimed += status.totalReclaimed;
1460
+ job.orphanBytesReclaimed += status.totalBytesReclaimed;
1461
+ job.detail = `Orphans: ${job.orphansReclaimed} rows, ${String(job.orphanBytesReclaimed)} bytes`;
1462
+ await this.persist(job);
1463
+ if (status.error) throw new Error(status.error);
1464
+ if (status.complete === true) return;
1465
+ }
1466
+ }
1467
+ async reclaimDebugMedia(job) {
1468
+ const start = await this.deps.participants.startDebugMedia();
1469
+ if (!start.started && !start.alreadyRunning) throw new Error("debug-media reclaim did not start");
1470
+ const status = await this.waitUntilIdle(() => this.deps.participants.debugMediaStatus(), job, (s) => `Debug media: ${s.totalReclaimed} files`);
1471
+ job.debugMediaReclaimed = status.totalReclaimed;
1472
+ job.debugMediaBytesReclaimed = status.totalBytesReclaimed;
1473
+ await this.persist(job);
1474
+ if (status.error) throw new Error(status.error);
1475
+ }
1476
+ async forgetGhosts(job) {
1477
+ const frozen = this.deps.locations.listLocations().filter((l) => FOOTAGE_TYPES.has(l.type) && footageFrozen(l));
1478
+ if (frozen.length === 0) {
1479
+ job.detail = "No frozen footage locations";
1480
+ await this.persist(job);
1481
+ return;
1482
+ }
1483
+ for (const loc of frozen) {
1484
+ if (await this.stopped(job)) return;
1485
+ job.detail = `Ghost ledger: ${loc.id}`;
1486
+ await this.persist(job);
1487
+ const report = await this.deps.participants.walkLedger({
1488
+ locationId: loc.id,
1489
+ apply: true
1490
+ });
1491
+ job.ghostsForgotten += report.forgottenSegments;
1492
+ job.ghostBytesForgotten += report.forgottenBytes;
1493
+ await this.persist(job);
1494
+ }
1495
+ }
1496
+ async waitUntilIdle(read, job, detail) {
1497
+ const pollMs = this.deps.pollMs ?? 2e3;
1498
+ const sleep = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1499
+ for (;;) {
1500
+ const status = await read();
1501
+ job.detail = detail(status);
1502
+ await this.persist(job);
1503
+ if (!status.running) return status;
1504
+ await sleep(pollMs);
1505
+ }
1506
+ }
1507
+ async stopped(job) {
1508
+ if ((await this.status(job.jobId))?.cancelRequested === true) {
1509
+ await this.finish(job, "cancelled", null);
1510
+ return true;
1511
+ }
1512
+ return false;
1513
+ }
1514
+ async setPhase(job, phase, detail) {
1515
+ job.phase = phase;
1516
+ job.detail = detail;
1517
+ await this.persist(job);
1518
+ }
1519
+ async finish(job, phase, error) {
1520
+ job.phase = phase;
1521
+ job.error = error;
1522
+ job.finishedAt = this.deps.now();
1523
+ job.detail = phase === "done" ? "Cleanup finished" : job.detail;
1524
+ await this.persist(job);
1525
+ }
1526
+ async persist(job) {
1527
+ job.updatedAt = this.deps.now();
1528
+ this.active = job;
1529
+ await this.deps.state.set(job);
1530
+ }
1531
+ };
1532
+ //#endregion
1249
1533
  //#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
1250
1534
  /**
1251
1535
  * Routing core for the `storage-orchestrator` builtin.
@@ -1368,7 +1652,7 @@ var StorageOrchestratorService = class {
1368
1652
  * Reconciles both directions so operator edits survive a reboot:
1369
1653
  * 1. hydrate — DB rows win over any early in-memory seed (restores
1370
1654
  * operator config like `minFreePercent` that the pre-store boot
1371
- * can't see), with the same `isSystem` upgrade as {@link initialize};
1655
+ * can't see);
1372
1656
  * 2. backfill — in-memory locations the DB doesn't have yet (the
1373
1657
  * pre-store seed defaults on a fresh install) are persisted, so the
1374
1658
  * store becomes the durable source of truth from here on.
@@ -1390,14 +1674,7 @@ var StorageOrchestratorService = class {
1390
1674
  const rows = await store.loadAll();
1391
1675
  this.locationStore = store;
1392
1676
  const dbIds = new Set(rows.map((r) => r.id));
1393
- for (const loc of rows) {
1394
- const upgraded = !loc.isSystem && loc.id === `${loc.type}:default` ? {
1395
- ...loc,
1396
- isSystem: true
1397
- } : loc;
1398
- this.locations.set(upgraded.id, upgraded);
1399
- if (upgraded !== loc) store.upsert(upgraded).catch(() => {});
1400
- }
1677
+ for (const loc of rows) this.locations.set(loc.id, loc);
1401
1678
  const backfill = [...this.locations.values()].filter((l) => !dbIds.has(l.id));
1402
1679
  for (const loc of backfill) store.upsert(loc).catch((err) => {
1403
1680
  this.logger.warn("storage-orchestrator: attachStore backfill persist failed", { meta: {
@@ -1489,25 +1766,8 @@ var StorageOrchestratorService = class {
1489
1766
  async initialize() {
1490
1767
  if (!this.locationStore) return;
1491
1768
  const rows = await this.locationStore.loadAll();
1492
- let upgraded = 0;
1493
- for (const loc of rows) if (!loc.isSystem && loc.id === `${loc.type}:default`) {
1494
- const upgradedLoc = {
1495
- ...loc,
1496
- isSystem: true
1497
- };
1498
- this.locations.set(upgradedLoc.id, upgradedLoc);
1499
- this.locationStore.upsert(upgradedLoc).catch((err) => {
1500
- this.logger.warn("storage-orchestrator: isSystem upgrade persist failed", { meta: {
1501
- id: upgradedLoc.id,
1502
- error: err instanceof Error ? err.message : String(err)
1503
- } });
1504
- });
1505
- upgraded++;
1506
- } else this.locations.set(loc.id, loc);
1507
- this.logger.info("storage-orchestrator: hydrated locations from store", { meta: {
1508
- loaded: this.locations.size,
1509
- isSystemUpgraded: upgraded
1510
- } });
1769
+ for (const loc of rows) this.locations.set(loc.id, loc);
1770
+ this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
1511
1771
  this.reportDisabledDefaults();
1512
1772
  }
1513
1773
  /**
@@ -1692,10 +1952,6 @@ var StorageOrchestratorService = class {
1692
1952
  ...input,
1693
1953
  nodeId: "hub"
1694
1954
  };
1695
- if (existing?.isSystem === true) input = {
1696
- ...input,
1697
- isSystem: true
1698
- };
1699
1955
  input = {
1700
1956
  ...input,
1701
1957
  ...resolveEnabled(input, existing, this.hasAnyLocationOfType(input.type))
@@ -1742,21 +1998,22 @@ var StorageOrchestratorService = class {
1742
1998
  return next;
1743
1999
  }
1744
2000
  /**
1745
- * Remove a location. Refuses to remove the default of a type unless a
1746
- * sibling default exists (defensive `upsertLocation` already ensures
1747
- * at most one default per type, but the logic guards against future
1748
- * bypass paths e.g. SQLite migration that imports two defaults).
1749
- *
1750
- * Persistence (Task 6) mirrors the delete asynchronously, with errors
1751
- * routed to the logger — see `upsertLocation` for the rationale.
2001
+ * Remove a location. Refuses the last location of a type and the last
2002
+ * enabled location of a type. `isSystem` is informational — a frozen
2003
+ * recordings `:default` is deletable once a sibling is live. Deleting
2004
+ * the flagged default promotes an enabled sibling. Occupancy still
2005
+ * refuses unless `force` is set; force never overrides uniqueness /
2006
+ * last-enabled.
1752
2007
  */
1753
2008
  async deleteLocation(id, options) {
1754
2009
  const loc = this.locations.get(id);
1755
2010
  if (!loc) throw new Error(`Storage location "${id}" not found`);
1756
- if (loc.isSystem) throw new Error(`Storage location "${id}" is system-managed and cannot be deleted. Edit its config (path / providerId) instead.`);
1757
- if (loc.isDefault) {
1758
- if (![...this.locations.values()].find((l) => l.type === loc.type && l.id !== id && l.isDefault)) throw new Error(`Cannot delete default location "${id}" for type "${loc.type}" — promote another location to default first`);
1759
- }
2011
+ const ofType = [...this.locations.values()].filter((l) => l.type === loc.type);
2012
+ if (ofType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only location for type "${loc.type}"`);
2013
+ const enabledOfType = ofType.filter((l) => l.enabled !== false);
2014
+ if (loc.enabled !== false && enabledOfType.length <= 1) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
2015
+ const successor = loc.isDefault === true ? ofType.find((l) => l.id !== id && l.enabled !== false) : void 0;
2016
+ if (loc.isDefault === true && successor === void 0) throw new Error(`Cannot delete "${id}" — it is the only enabled location for type "${loc.type}"`);
1760
2017
  const occupancy = await this.probeOccupancy(loc);
1761
2018
  if (occupancy !== "empty") {
1762
2019
  if (options?.force !== true) throw new Error(occupancy === "occupied" ? `Storage location "${id}" still holds data — drain it first (storage migration / relocate), or pass force to delete the record anyway and strand what is on it` : `Storage location "${id}" could not be checked for remaining data (unreachable provider, another node, or an unmounted root) — pass force to delete the record anyway`);
@@ -1766,6 +2023,21 @@ var StorageOrchestratorService = class {
1766
2023
  occupancy
1767
2024
  } });
1768
2025
  }
2026
+ if (successor !== void 0 && successor.isDefault !== true) {
2027
+ const now = Date.now();
2028
+ const promoted = {
2029
+ ...successor,
2030
+ isDefault: true,
2031
+ updatedAt: now
2032
+ };
2033
+ this.locations.set(successor.id, promoted);
2034
+ if (this.locationStore) this.locationStore.upsert(promoted).catch((err) => {
2035
+ this.logger.error("storage-orchestrator: default promotion persist failed", { meta: {
2036
+ id: successor.id,
2037
+ error: err instanceof Error ? err.message : String(err)
2038
+ } });
2039
+ });
2040
+ }
1769
2041
  this.locations.delete(id);
1770
2042
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
1771
2043
  this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
@@ -1792,15 +2064,14 @@ var StorageOrchestratorService = class {
1792
2064
  }
1793
2065
  }
1794
2066
  /**
1795
- * Remove system-seeded locations whose type is no longer declared by any
1796
- * addon (stale defaults from a removed location type). Operator-added
1797
- * (non-system) locations of an undeclared type are KEPT but warned — the
1798
- * operator owns them. Requires the registry to be set first.
2067
+ * Remove seed-shaped locations (`<type>:default`) whose type is no longer
2068
+ * declared by any addon (stale defaults from a removed location type).
2069
+ * Operator-added locations of an undeclared type are KEPT but warned.
1799
2070
  *
1800
2071
  * FAIL-SAFE: if the registry is EMPTY (no addon declared any location), we
1801
2072
  * refuse to prune anything. An empty registry almost always means
1802
2073
  * declarations failed to load (boot ordering, a stale install) — pruning
1803
- * "everything undeclared" in that state would wipe every system location
2074
+ * "everything undeclared" in that state would wipe every seeded location
1804
2075
  * (data/logs/recordings/…). Better to keep stale rows than destroy live ones.
1805
2076
  */
1806
2077
  pruneUndeclaredSystemLocations() {
@@ -1811,7 +2082,7 @@ var StorageOrchestratorService = class {
1811
2082
  }
1812
2083
  for (const [id, loc] of Array.from(this.locations)) {
1813
2084
  if (this.registry.cardinalityOf(loc.type) !== null) continue;
1814
- if (loc.isSystem) {
2085
+ if (loc.id === `${loc.type}:default`) {
1815
2086
  this.locations.delete(id);
1816
2087
  if (this.locationStore) this.locationStore.delete(id).catch((err) => {
1817
2088
  this.logger.error("storage-orchestrator: prune persistence failed", { meta: {
@@ -1819,7 +2090,7 @@ var StorageOrchestratorService = class {
1819
2090
  error: err instanceof Error ? err.message : String(err)
1820
2091
  } });
1821
2092
  });
1822
- this.logger.info("storage-orchestrator: pruned stale system location", { meta: {
2093
+ this.logger.info("storage-orchestrator: pruned stale seeded location", { meta: {
1823
2094
  id,
1824
2095
  type: loc.type
1825
2096
  } });
@@ -1979,7 +2250,7 @@ var StorageOrchestratorService = class {
1979
2250
  providerId: input.providerId,
1980
2251
  config: { basePath: base },
1981
2252
  isDefault: true,
1982
- isSystem: true
2253
+ isSystem: false
1983
2254
  });
1984
2255
  added++;
1985
2256
  }
@@ -2209,6 +2480,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2209
2480
  pressureTimer = null;
2210
2481
  pressureSweepInFlight = false;
2211
2482
  migration = null;
2483
+ cleanup = null;
2212
2484
  /**
2213
2485
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
2214
2486
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
@@ -2266,7 +2538,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2266
2538
  const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
2267
2539
  return Promise.all(rows.map(async (loc) => ({
2268
2540
  ...this.redacted(loc),
2269
- capacity: await this.localCapacityOf(loc)
2541
+ capacity: loc.enabled === false ? null : await this.localCapacityOf(loc)
2270
2542
  })));
2271
2543
  },
2272
2544
  getDefaultLocation: async ({ type }) => {
@@ -2434,6 +2706,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2434
2706
  const migration = new StorageMigrationCoordinator({
2435
2707
  locations: service,
2436
2708
  state: this.state("storage-migration", StorageMigrationJobSchema.nullable(), null),
2709
+ history: this.state("storage-migration-history", z.array(StorageMigrationJobSchema), []),
2437
2710
  participants: {
2438
2711
  pipeline: {
2439
2712
  pause: async (leaseId) => {
@@ -2484,6 +2757,36 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2484
2757
  deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
2485
2758
  });
2486
2759
  this.migration = migration;
2760
+ const cleanup = new StorageCleanupCoordinator({
2761
+ locations: service,
2762
+ state: this.state("storage-cleanup", StorageCleanupJobSchema.nullable(), null),
2763
+ participants: {
2764
+ startOrphan: async (restart) => {
2765
+ return parseStarted(await this.ctx.api.addons.custom.mutate({
2766
+ addonId: "pipeline-analytics",
2767
+ action: "retention.orphanAudit",
2768
+ input: {
2769
+ mode: "reclaim",
2770
+ restart,
2771
+ maxRowsPerScope: 2e4
2772
+ }
2773
+ }));
2774
+ },
2775
+ orphanStatus: async () => {
2776
+ return parseOrphanStatus(await this.ctx.api.addons.custom.mutate({
2777
+ addonId: "pipeline-analytics",
2778
+ action: "retention.orphanAuditStatus",
2779
+ input: {}
2780
+ }));
2781
+ },
2782
+ startDebugMedia: () => this.ctx.api.pipelineAnalytics.reclaimDebugMedia.mutate({ mode: "reclaim" }),
2783
+ debugMediaStatus: () => this.ctx.api.pipelineAnalytics.getMediaReclaimStatus.query({}),
2784
+ walkLedger: (input) => this.ctx.api.recording.reconcileLedgerAgainstDisk.mutate(input)
2785
+ },
2786
+ now: () => Date.now(),
2787
+ newId: () => randomUUID()
2788
+ });
2789
+ this.cleanup = cleanup;
2487
2790
  const migrationProvider = {
2488
2791
  plan: (input) => migration.plan(input),
2489
2792
  start: async (input) => ({ jobId: await migration.start(input) }),
@@ -2491,7 +2794,11 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2491
2794
  cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
2492
2795
  movers: () => migration.movers(),
2493
2796
  residue: () => migration.residue(),
2494
- drain: async (input) => ({ jobId: await migration.drain(input) })
2797
+ drain: async (input) => ({ jobId: await migration.drain(input) }),
2798
+ cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
2799
+ cleanupStatus: ({ jobId }) => cleanup.status(jobId),
2800
+ cleanupCancel: async ({ jobId }) => ({ cancelled: await cleanup.cancel(jobId) }),
2801
+ history: () => migration.history()
2495
2802
  };
2496
2803
  await this.seedFromDeclarations();
2497
2804
  const eventBus = this.ctx.eventBus;
@@ -2734,11 +3041,14 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2734
3041
  }
2735
3042
  }
2736
3043
  /**
2737
- * Does this location still hold anything? A SHALLOW `readdir` of its
2738
- * `basePath` — one syscall, never a walk. `/recordings` holds one entry per
2739
- * camera, so this is cheap on a volume with millions of segments, and a
2740
- * `find`-style walk of a saturated recordings disk is precisely what must
2741
- * not happen here.
3044
+ * Does this location still hold anything of ITS class?
3045
+ *
3046
+ * Shared roots (`/recordings` hosting high + low + events) are scored by
3047
+ * class subtree (`<camera>/high|mid`, `/low`, `/events`), not by a raw
3048
+ * `readdir` of the root — camera dirs and `.camstack-location` would
3049
+ * otherwise make every class look occupied. The walk stays shallow: one
3050
+ * dirent per camera, then first-entry of the named subtree. A `find` of
3051
+ * segments is precisely what must not happen here.
2742
3052
  *
2743
3053
  * **What it cannot see, stated rather than papered over:**
2744
3054
  *
@@ -2747,9 +3057,10 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2747
3057
  * `unknown`, never `empty`.
2748
3058
  * - A location with no `basePath` — same answer.
2749
3059
  * - The difference between "holds live footage" and "holds one stray
2750
- * lock-file". It measures BYTES ON DISK, not durable rows, so it refuses
2751
- * a location holding orphan files that no index names. That direction is
2752
- * the safe one, and `force` is the escape hatch for it.
3060
+ * lock-file". It measures ON-DISK entries of this class, not durable
3061
+ * rows, so it refuses a location holding orphan files that no index
3062
+ * names. That direction is the safe one, and `force` is the escape
3063
+ * hatch for it.
2753
3064
  *
2754
3065
  * A missing root (`ENOENT`) is `empty`, not `unknown`: an unmounted disk and
2755
3066
  * a deleted directory are indistinguishable here, and if the record's own
@@ -2767,16 +3078,16 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2767
3078
  if ((location.nodeId === void 0 || location.nodeId === "" ? HUB_NODE_ID : location.nodeId) !== (this.service?.getLocalNodeId() ?? HUB_NODE_ID)) return "unknown";
2768
3079
  const basePath = this.locationBasePath(location.id);
2769
3080
  if (basePath === null) return "unknown";
2770
- try {
2771
- return (await fs.readdir(basePath)).length > 0 ? "occupied" : "empty";
2772
- } catch (err) {
2773
- if (err instanceof Error && "code" in err && err.code === "ENOENT") return "empty";
2774
- this.ctx.logger.warn("storage-orchestrator: could not read a location root to check it", { meta: {
2775
- id: location.id,
2776
- error: err instanceof Error ? err.message : String(err)
2777
- } });
2778
- return "unknown";
2779
- }
3081
+ const occupancy = await probeLocalDirectoryOccupancy({
3082
+ type: location.type,
3083
+ basePath
3084
+ });
3085
+ if (occupancy === "unknown") this.ctx.logger.warn("storage-orchestrator: could not read a location root to check it", { meta: {
3086
+ id: location.id,
3087
+ basePath,
3088
+ type: location.type
3089
+ } });
3090
+ return occupancy;
2780
3091
  }
2781
3092
  /** Free capacity (%) on a location's volume via `statfs`; 100 (guard inert) when unstattable. */
2782
3093
  async locationFreePercent(locationId) {
@@ -2852,5 +3163,37 @@ var StorageOrchestratorAddon = class extends BaseAddon {
2852
3163
  }
2853
3164
  }
2854
3165
  };
3166
+ function parseStarted(raw) {
3167
+ if (raw !== null && typeof raw === "object") {
3168
+ const o = raw;
3169
+ if (typeof o["started"] === "boolean" && typeof o["alreadyRunning"] === "boolean") return {
3170
+ started: o["started"],
3171
+ alreadyRunning: o["alreadyRunning"]
3172
+ };
3173
+ }
3174
+ return {
3175
+ started: false,
3176
+ alreadyRunning: false
3177
+ };
3178
+ }
3179
+ function parseOrphanStatus(raw) {
3180
+ if (raw !== null && typeof raw === "object") {
3181
+ const o = raw;
3182
+ return {
3183
+ running: o["running"] === true,
3184
+ complete: typeof o["complete"] === "boolean" ? o["complete"] : null,
3185
+ totalReclaimed: typeof o["totalReclaimed"] === "number" ? o["totalReclaimed"] : 0,
3186
+ totalBytesReclaimed: typeof o["totalBytesReclaimed"] === "number" ? o["totalBytesReclaimed"] : 0,
3187
+ error: typeof o["error"] === "string" ? o["error"] : null
3188
+ };
3189
+ }
3190
+ return {
3191
+ running: false,
3192
+ complete: true,
3193
+ totalReclaimed: 0,
3194
+ totalBytesReclaimed: 0,
3195
+ error: "orphan audit status unreadable"
3196
+ };
3197
+ }
2855
3198
  //#endregion
2856
3199
  export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, SqliteLocationStore as i, buildStorageLocationRegistry as n, StorageMigrationCoordinator as r, StorageOrchestratorService as t };