@camstack/system 1.2.452 → 1.2.454

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 (96) hide show
  1. package/dist/{agent-https-CMz4S56_.js → agent-https-B7IjRRzT.js} +2 -2
  2. package/dist/{agent-https-D6lRxHma.mjs → agent-https-oXsR24R2.mjs} +2 -2
  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/alert-episode-policy.d.ts +39 -0
  8. package/dist/builtins/alerts/alerts.addon.d.ts +25 -38
  9. package/dist/builtins/alerts/alerts.addon.js +178 -423
  10. package/dist/builtins/alerts/alerts.addon.mjs +178 -423
  11. package/dist/builtins/alerts/emit-alert-policy.d.ts +3 -16
  12. package/dist/builtins/alerts/runner-alert.d.ts +0 -13
  13. package/dist/builtins/autotrack/index.js +1 -1
  14. package/dist/builtins/autotrack/index.mjs +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  17. package/dist/builtins/calendars/calendars.addon.js +1 -1
  18. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  19. package/dist/builtins/camera-grid/index.js +1 -1
  20. package/dist/builtins/camera-grid/index.mjs +1 -1
  21. package/dist/builtins/composer/composer.addon.js +2 -2
  22. package/dist/builtins/composer/composer.addon.mjs +2 -2
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-history/device-history.addon.js +1 -1
  28. package/dist/builtins/device-history/device-history.addon.mjs +1 -1
  29. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/keychain/index.js +4 -4
  34. package/dist/builtins/keychain/index.mjs +4 -4
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.js +3 -3
  38. package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
  39. package/dist/builtins/local-network/local-network.addon.js +2 -2
  40. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  41. package/dist/builtins/loki-logging/index.js +1 -1
  42. package/dist/builtins/loki-logging/index.mjs +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.js +15 -2
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +15 -2
  45. package/dist/builtins/platform-probe/index.js +1 -1
  46. package/dist/builtins/platform-probe/index.mjs +1 -1
  47. package/dist/builtins/record-entries/index.js +22 -2
  48. package/dist/builtins/record-entries/index.mjs +22 -2
  49. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  50. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/snapshot/index.js +1 -1
  52. package/dist/builtins/snapshot/index.mjs +1 -1
  53. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  54. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  55. package/dist/builtins/sqlite-storage/index.js +1 -1
  56. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  57. package/dist/builtins/sqlite-storage/records/records-engine.d.ts +13 -0
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  60. package/dist/builtins/storage-orchestrator/location-class-paths.d.ts +13 -0
  61. package/dist/builtins/storage-orchestrator/location-repoint-move.d.ts +6 -1
  62. package/dist/builtins/storage-orchestrator/location-repointer.d.ts +97 -0
  63. package/dist/builtins/storage-orchestrator/location-store.d.ts +13 -2
  64. package/dist/builtins/storage-orchestrator/record-kinds.d.ts +5 -0
  65. package/dist/builtins/storage-orchestrator/storage-location-band-monitor.d.ts +38 -4
  66. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +8 -2
  67. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +486 -143
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +486 -143
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +26 -1
  70. package/dist/builtins/system-config/system-config.addon.js +3 -3
  71. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  72. package/dist/builtins/winston-logging/index.js +1 -1
  73. package/dist/builtins/winston-logging/index.mjs +1 -1
  74. package/dist/{composition-sources-BQmhc3se.js → composition-sources-Cukc78eu.js} +1 -1
  75. package/dist/{composition-sources-DzuruDVY.mjs → composition-sources-bt6IQDLu.mjs} +1 -1
  76. package/dist/{constants-BPWA-r-Y.js → constants-DBGG6Zu5.js} +1 -1
  77. package/dist/{constants-gzGZbnwO.mjs → constants-DQcVrhVI.mjs} +1 -1
  78. package/dist/{device-ca-75rt3U_U.js → device-ca-B3hUVfYk.js} +1 -1
  79. package/dist/{device-ca-D05nqNXE.mjs → device-ca-Utfvsx5x.mjs} +1 -1
  80. package/dist/{dist-DV2ll9Hi.mjs → dist-7ZciHCIp.mjs} +394 -38
  81. package/dist/{dist-Ct88catN.js → dist-Bi4Ioxop.js} +417 -37
  82. package/dist/file-host-worker.js +1 -1
  83. package/dist/file-host-worker.mjs +1 -1
  84. package/dist/{hub-mdns-ciao-DAU1WttG.js → hub-mdns-ciao-2FzLWfbD.js} +2 -2
  85. package/dist/{hub-mdns-ciao-BPOg2iyc.mjs → hub-mdns-ciao-BRs3DUT-.mjs} +2 -2
  86. package/dist/index.js +17 -9
  87. package/dist/index.mjs +17 -9
  88. package/dist/kernel/cap-router-builder.d.ts +9 -0
  89. package/dist/{phase6-declare-gate-BJplW6tX.mjs → phase6-declare-gate-D-2LMw9o.mjs} +1 -1
  90. package/dist/{phase6-declare-gate-C6WqagkC.js → phase6-declare-gate-DwYAep7y.js} +1 -1
  91. package/dist/{restore-archive-Cprdh3P9.js → restore-archive-C0ymYQk7.js} +1 -1
  92. package/dist/{restore-archive-Jiz63mVB.mjs → restore-archive-UVZv_hmr.mjs} +1 -1
  93. package/dist/{wal-maintenance-BspX29jz.js → wal-maintenance-Bo_lo3Dh.js} +0 -0
  94. package/dist/{wal-maintenance-BNIaf3QP.mjs → wal-maintenance-Df89xxrK.mjs} +0 -0
  95. package/package.json +1 -1
  96. package/dist/builtins/alerts/critical-push.d.ts +0 -90
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Ct88catN.js");
6
+ const require_dist = require("../../dist-Bi4Ioxop.js");
7
7
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -31,6 +31,7 @@ var StorageLocationBodySchema = zod.z.object({
31
31
  mode: require_dist.StorageLocationModeSchema.optional(),
32
32
  isDefault: zod.z.boolean().optional()
33
33
  });
34
+ var StorageSeedMarkerBodySchema = zod.z.object({ seededAt: zod.z.number() });
34
35
  var StorageDrainStateBodySchema = zod.z.object({
35
36
  locationId: zod.z.string(),
36
37
  startedAtMs: zod.z.number(),
@@ -55,6 +56,14 @@ var _storageOrchestratorRecordKinds = require_dist.defineRecordKinds({
55
56
  retention: { none: "operator delete or declaration prune only" },
56
57
  adoptsFrom: STORAGE_LOCATIONS
57
58
  },
59
+ "seed-markers": {
60
+ schema: StorageSeedMarkerBodySchema,
61
+ class: "config",
62
+ writeTrigger: "a declared location type was seeded (or found already present on an existing install); the id is the type",
63
+ device: "none",
64
+ at: "none",
65
+ retention: { none: "one row per location type ever seeded — what makes an operator delete stick" }
66
+ },
58
67
  "drain-state": {
59
68
  schema: StorageDrainStateBodySchema,
60
69
  class: "config",
@@ -188,9 +197,27 @@ var SqliteLocationStore = class {
188
197
  store;
189
198
  declared = false;
190
199
  locationsKind;
191
- constructor(store, locationsKind = null) {
200
+ constructor(store, locationsKind = null, seedMarkersKind = null) {
192
201
  this.store = store;
193
202
  this.locationsKind = locationsKind;
203
+ this.seedMarkersKind = seedMarkersKind;
204
+ }
205
+ seedMarkersKind;
206
+ static seedMarkersOf(records) {
207
+ return records.kind(storageOrchestratorRecordKinds["seed-markers"]);
208
+ }
209
+ async loadSeededTypes() {
210
+ const kind = this.seedMarkersKind;
211
+ if (kind === null) return [];
212
+ return (await (0, import_read_all_rows.readAllRows)(kind)).map((r) => r.id);
213
+ }
214
+ async markTypeSeeded(type) {
215
+ const kind = this.seedMarkersKind;
216
+ if (kind === null) return;
217
+ await kind.put({
218
+ id: type,
219
+ data: { seededAt: Date.now() }
220
+ });
194
221
  }
195
222
  static recordStoresOf(records) {
196
223
  return records.kind(storageOrchestratorRecordKinds.locations);
@@ -1232,6 +1259,111 @@ function buildStorageLocationRegistry(perAddon) {
1232
1259
  };
1233
1260
  }
1234
1261
  //#endregion
1262
+ //#region src/builtins/storage-orchestrator/location-occupancy.ts
1263
+ /**
1264
+ * Delete-time occupancy of a location's on-disk root.
1265
+ *
1266
+ * A recordings disk may host several classes at one `basePath`
1267
+ * (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
1268
+ * a class is therefore the class subtree, not the shared root — otherwise a
1269
+ * drained `eventMedia:default` still looks full because high footage is
1270
+ * writing next to it (observed 2026-09-01).
1271
+ *
1272
+ * The walk is shallow: one `readdir` of the root (one dirent per camera)
1273
+ * plus `opendir`+first-entry of each named subtree. No recursive find of
1274
+ * segments — that is the walk that saturates a USB disk.
1275
+ */
1276
+ /**
1277
+ * Class → directories under `<root>/<camera>/`. Same map as the discard
1278
+ * wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
1279
+ * Types not listed keep the legacy "any non-dot entry at the root" rule
1280
+ * (backups, addon-data, …).
1281
+ */
1282
+ var STORAGE_CLASS_SUBTREES = {
1283
+ eventMedia: ["events"],
1284
+ recordingsLow: ["low"],
1285
+ recordings: ["high", "mid"]
1286
+ };
1287
+ function isDotEntry(name) {
1288
+ return name.startsWith(".");
1289
+ }
1290
+ /**
1291
+ * True when `dir` exists and contains at least one dirent. Missing → false.
1292
+ * Any other errno throws so the caller can answer `unknown` (D49).
1293
+ */
1294
+ async function dirHasAnyEntry(dir) {
1295
+ let handle;
1296
+ try {
1297
+ handle = await (0, node_fs_promises.opendir)(dir);
1298
+ } catch (err) {
1299
+ if (err.code === "ENOENT") return false;
1300
+ throw err;
1301
+ }
1302
+ try {
1303
+ return await handle.read() !== null;
1304
+ } finally {
1305
+ await handle.close();
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Occupancy of `basePath` for a location of `type`.
1310
+ *
1311
+ * - missing root → `empty` (nothing this delete can strand)
1312
+ * - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
1313
+ * - other types → occupied iff the root has a non-dot entry
1314
+ */
1315
+ async function probeLocalDirectoryOccupancy(input) {
1316
+ let names;
1317
+ try {
1318
+ names = await (0, node_fs_promises.readdir)(input.basePath);
1319
+ } catch (err) {
1320
+ if (err.code === "ENOENT") return "empty";
1321
+ return "unknown";
1322
+ }
1323
+ const visible = names.filter((name) => !isDotEntry(name));
1324
+ const subtrees = STORAGE_CLASS_SUBTREES[input.type];
1325
+ if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
1326
+ try {
1327
+ for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
1328
+ } catch {
1329
+ return "unknown";
1330
+ }
1331
+ return "empty";
1332
+ }
1333
+ //#endregion
1334
+ //#region src/builtins/storage-orchestrator/location-class-paths.ts
1335
+ /**
1336
+ * Which paths under a shared root BELONG to a storage class (D327).
1337
+ *
1338
+ * A recordings disk hosts several classes at one `basePath`:
1339
+ * `<camera>/high|mid` (recordings), `<camera>/low` (recordingsLow),
1340
+ * `<camera>/events` (eventMedia) and `homekit-clips/…` (homekitClips). A
1341
+ * location may touch only its own subtree; the rest of the root is another
1342
+ * location's data. On 2026-10-20 a repoint of `homekitClips:default` walked the
1343
+ * WHOLE shared root and deleted ~8.6k event-media files that were not its own.
1344
+ *
1345
+ * The per-camera map is {@link STORAGE_CLASS_SUBTREES} (the one source, shared
1346
+ * with occupancy scoring). A class whose subtree sits at the ROOT, not under a
1347
+ * camera, is listed here. A type in neither map owns its whole root (backups,
1348
+ * artifacts, …): there is no subtree to scope to.
1349
+ */
1350
+ /** Class → top-level directories directly under the root (no camera segment). */
1351
+ var STORAGE_CLASS_ROOT_SUBTREES = { homekitClips: ["homekit-clips"] };
1352
+ /** Classify a root-relative path (`/` separators) for a location of `type`. */
1353
+ function classifyPath(type, relativePath) {
1354
+ const rootSubtrees = STORAGE_CLASS_ROOT_SUBTREES[type];
1355
+ const cameraSubtrees = STORAGE_CLASS_SUBTREES[type];
1356
+ if (rootSubtrees === void 0 && cameraSubtrees === void 0) return "own";
1357
+ const segments = relativePath.split("/").filter((segment) => segment.length > 0);
1358
+ if (rootSubtrees !== void 0) {
1359
+ const top = segments[0];
1360
+ return top !== void 0 && rootSubtrees.includes(top) ? "own" : "foreign";
1361
+ }
1362
+ if (segments.length < 2) return "descend";
1363
+ const subtree = segments[1];
1364
+ return subtree !== void 0 && cameraSubtrees?.includes(subtree) === true ? "own" : "foreign";
1365
+ }
1366
+ //#endregion
1235
1367
  //#region src/builtins/storage-orchestrator/location-repoint-move.ts
1236
1368
  /**
1237
1369
  * Walk the source root and collect FILES.
@@ -1242,24 +1374,28 @@ function buildStorageLocationRegistry(perAddon) {
1242
1374
  * (an empty directory) is skipped by the copy loop, which is the only place
1243
1375
  * that can tell — and skipping it is right, since there is nothing to move.
1244
1376
  */
1245
- async function collectFiles(io, prefix) {
1377
+ async function collectFiles(io, scope, prefix) {
1246
1378
  const entries = await io.list(prefix);
1247
1379
  const files = [];
1248
1380
  for (const entry of entries) {
1381
+ const verdict = scope(entry);
1382
+ if (verdict === "foreign") continue;
1249
1383
  if ((await io.list(entry)).length > 0) {
1250
- files.push(...await collectFiles(io, entry));
1384
+ files.push(...await collectFiles(io, scope, entry));
1251
1385
  continue;
1252
1386
  }
1253
- files.push(entry);
1387
+ if (verdict === "own") files.push(entry);
1254
1388
  }
1255
1389
  return files;
1256
1390
  }
1391
+ /** The whole root — only correct for a class that owns all of it. */
1392
+ var WHOLE_ROOT = () => "own";
1257
1393
  /**
1258
1394
  * Copy every file from the source root to the destination, verify each, then
1259
1395
  * delete the originals. Throws with NOTHING deleted if any copy fails.
1260
1396
  */
1261
- async function moveLocationTree(io) {
1262
- const candidates = await collectFiles(io);
1397
+ async function moveLocationTree(io, scope = WHOLE_ROOT) {
1398
+ const candidates = await collectFiles(io, scope);
1263
1399
  const copied = [];
1264
1400
  let bytes = 0;
1265
1401
  for (const relativePath of candidates) {
@@ -1282,6 +1418,121 @@ async function moveLocationTree(io) {
1282
1418
  };
1283
1419
  }
1284
1420
  //#endregion
1421
+ //#region src/builtins/storage-orchestrator/location-repointer.ts
1422
+ var RepointRefusedError = class extends Error {
1423
+ reason;
1424
+ name = "RepointRefusedError";
1425
+ constructor(reason, message) {
1426
+ super(message);
1427
+ this.reason = reason;
1428
+ }
1429
+ };
1430
+ /** Does `a` equal `b`, or sit inside it, or contain it? Segment-wise, never by raw prefix. */
1431
+ function rootsOverlap(a, b) {
1432
+ if (a === b) return true;
1433
+ const within = (inner, outer) => inner.startsWith(outer.endsWith("/") ? outer : `${outer}/`);
1434
+ return within(a, b) || within(b, a);
1435
+ }
1436
+ function basePathOf$1(config) {
1437
+ const raw = config["basePath"];
1438
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
1439
+ }
1440
+ var LocationRepointer = class {
1441
+ deps;
1442
+ claims = /* @__PURE__ */ new Set();
1443
+ constructor(deps) {
1444
+ this.deps = deps;
1445
+ }
1446
+ /**
1447
+ * Move `stored`'s own subtree to `next`'s root, then run `commit` (the row
1448
+ * save) while the claim is still held. Throws {@link RepointRefusedError}
1449
+ * with NOTHING copied or deleted when a mover is on the root.
1450
+ */
1451
+ async run(stored, next, commit) {
1452
+ const claim = await this.claim(stored, next);
1453
+ try {
1454
+ await this.refuseWhileMovers(claim, stored);
1455
+ const moved = await moveLocationTree(await this.deps.ioFor(stored, next), (path) => classifyPath(stored.type, path));
1456
+ this.logMoved(stored, next, moved);
1457
+ return await commit();
1458
+ } finally {
1459
+ this.claims.delete(claim);
1460
+ }
1461
+ }
1462
+ /**
1463
+ * May a mover that touches `locationIds` run? Refuses while a repoint holds
1464
+ * an overlapping root. Called by the owning addon AFTER its job is
1465
+ * registered, so the repoint's mover listing and this check cannot both miss.
1466
+ */
1467
+ async clearance(locationIds) {
1468
+ const keys = await this.keysOf(locationIds);
1469
+ for (const claim of this.claims) if (claim.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) return {
1470
+ clear: false,
1471
+ reason: "repoint-active",
1472
+ repointLocationId: claim.locationId
1473
+ };
1474
+ return { clear: true };
1475
+ }
1476
+ async claim(stored, next) {
1477
+ const paths = [basePathOf$1(stored.config), basePathOf$1(next.config)].filter((path) => path !== null);
1478
+ const keys = await Promise.all(paths.map((path) => this.deps.resolveKey(path)));
1479
+ for (const other of this.claims) if (other.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) this.refuse(stored, "repoint-active", `a repoint of "${other.locationId}" holds the same root`);
1480
+ const claim = {
1481
+ locationId: stored.id,
1482
+ keys
1483
+ };
1484
+ this.claims.add(claim);
1485
+ return claim;
1486
+ }
1487
+ async refuseWhileMovers(claim, stored) {
1488
+ let movers;
1489
+ try {
1490
+ movers = await this.deps.listMovers();
1491
+ } catch (err) {
1492
+ this.claims.delete(claim);
1493
+ this.refuse(stored, "movers-unreadable", `the active movers could not be listed (${err instanceof Error ? err.message : String(err)})`);
1494
+ }
1495
+ for (const mover of movers) {
1496
+ if (mover.job.state !== "queued" && mover.job.state !== "running") continue;
1497
+ const touched = await this.keysOf(this.locationsTouchedBy(mover));
1498
+ if (claim.keys.some((held) => touched.some((key) => rootsOverlap(held, key)))) {
1499
+ this.claims.delete(claim);
1500
+ this.refuse(stored, "mover-active", `${mover.lane} mover ${mover.job.jobId} (${mover.job.fromLocationId} -> ${mover.job.toLocationId}) touches the same physical root`, {
1501
+ jobId: mover.job.jobId,
1502
+ lane: mover.lane
1503
+ });
1504
+ }
1505
+ }
1506
+ }
1507
+ locationsTouchedBy(mover) {
1508
+ return [mover.job.fromLocationId, mover.job.toLocationId];
1509
+ }
1510
+ /** `*` is the media mover's "from wherever the rows are": every eventMedia location. */
1511
+ async keysOf(locationIds) {
1512
+ const paths = locationIds.flatMap((id) => id === "*" ? this.deps.locationIdsOfType("eventMedia") : [id]).map((id) => this.deps.locationBasePath(id)).filter((path) => path !== null);
1513
+ return Promise.all(paths.map((path) => this.deps.resolveKey(path)));
1514
+ }
1515
+ refuse(stored, reason, detail, meta = {}) {
1516
+ this.deps.logger.warn("storage-orchestrator: REFUSING to re-point a location — nothing was moved", { meta: {
1517
+ id: stored.id,
1518
+ type: stored.type,
1519
+ reason,
1520
+ detail,
1521
+ ...meta
1522
+ } });
1523
+ throw new RepointRefusedError(reason, `re-point of "${stored.id}" refused (${reason}): ${detail}`);
1524
+ }
1525
+ logMoved(stored, next, moved) {
1526
+ this.deps.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
1527
+ id: stored.id,
1528
+ type: stored.type,
1529
+ from: basePathOf$1(stored.config),
1530
+ to: basePathOf$1(next.config),
1531
+ ...moved
1532
+ } });
1533
+ }
1534
+ };
1535
+ //#endregion
1285
1536
  //#region src/builtins/storage-orchestrator/access-guards.ts
1286
1537
  /**
1287
1538
  * May `storage.resolve` return a path for this location?
@@ -1781,78 +2032,6 @@ function resolveHubSettingsForward(options) {
1781
2032
  }, { timeout: HUB_SETTINGS_FORWARD_TIMEOUT_MS }));
1782
2033
  }
1783
2034
  //#endregion
1784
- //#region src/builtins/storage-orchestrator/location-occupancy.ts
1785
- /**
1786
- * Delete-time occupancy of a location's on-disk root.
1787
- *
1788
- * A recordings disk may host several classes at one `basePath`
1789
- * (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
1790
- * a class is therefore the class subtree, not the shared root — otherwise a
1791
- * drained `eventMedia:default` still looks full because high footage is
1792
- * writing next to it (observed 2026-09-01).
1793
- *
1794
- * The walk is shallow: one `readdir` of the root (one dirent per camera)
1795
- * plus `opendir`+first-entry of each named subtree. No recursive find of
1796
- * segments — that is the walk that saturates a USB disk.
1797
- */
1798
- /**
1799
- * Class → directories under `<root>/<camera>/`. Same map as the discard
1800
- * wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
1801
- * Types not listed keep the legacy "any non-dot entry at the root" rule
1802
- * (backups, addon-data, …).
1803
- */
1804
- var STORAGE_CLASS_SUBTREES = {
1805
- eventMedia: ["events"],
1806
- recordingsLow: ["low"],
1807
- recordings: ["high", "mid"]
1808
- };
1809
- function isDotEntry(name) {
1810
- return name.startsWith(".");
1811
- }
1812
- /**
1813
- * True when `dir` exists and contains at least one dirent. Missing → false.
1814
- * Any other errno throws so the caller can answer `unknown` (D49).
1815
- */
1816
- async function dirHasAnyEntry(dir) {
1817
- let handle;
1818
- try {
1819
- handle = await (0, node_fs_promises.opendir)(dir);
1820
- } catch (err) {
1821
- if (err.code === "ENOENT") return false;
1822
- throw err;
1823
- }
1824
- try {
1825
- return await handle.read() !== null;
1826
- } finally {
1827
- await handle.close();
1828
- }
1829
- }
1830
- /**
1831
- * Occupancy of `basePath` for a location of `type`.
1832
- *
1833
- * - missing root → `empty` (nothing this delete can strand)
1834
- * - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
1835
- * - other types → occupied iff the root has a non-dot entry
1836
- */
1837
- async function probeLocalDirectoryOccupancy(input) {
1838
- let names;
1839
- try {
1840
- names = await (0, node_fs_promises.readdir)(input.basePath);
1841
- } catch (err) {
1842
- if (err.code === "ENOENT") return "empty";
1843
- return "unknown";
1844
- }
1845
- const visible = names.filter((name) => !isDotEntry(name));
1846
- const subtrees = STORAGE_CLASS_SUBTREES[input.type];
1847
- if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
1848
- try {
1849
- for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
1850
- } catch {
1851
- return "unknown";
1852
- }
1853
- return "empty";
1854
- }
1855
- //#endregion
1856
2035
  //#region src/builtins/storage-orchestrator/location-occupancy-slice.ts
1857
2036
  /** One minute: the same order as the pressure sweep, and far longer than an
1858
2037
  * admin-UI repaint. */
@@ -2588,6 +2767,44 @@ var StorageOrchestratorService = class {
2588
2767
  getLocalNodeId() {
2589
2768
  return this.localNode;
2590
2769
  }
2770
+ /**
2771
+ * Whether a seed must wait for an ANSWERED read of the store (D49). True on a
2772
+ * node whose locations live in a store: seeding before that read lands writes
2773
+ * defaults the store's own rows (not yet adopted, or not yet read) then
2774
+ * contradict. False on a node that has no store by design — it seeds in memory
2775
+ * at every boot.
2776
+ */
2777
+ storeRequiredForSeed = false;
2778
+ setStoreRequiredForSeed(required) {
2779
+ this.storeRequiredForSeed = required;
2780
+ }
2781
+ /**
2782
+ * The location types a default was ever seeded for — null when the store
2783
+ * cannot remember (then a type with no row is seeded, as before). What makes
2784
+ * an operator's delete of a seeded default STICK: zero rows today is not
2785
+ * "fresh install", it is only fresh when nothing was ever seeded.
2786
+ */
2787
+ seededTypes = null;
2788
+ /** The store held locations but no seed memory: an install older than the memory. */
2789
+ installPredatesSeedMemory = false;
2790
+ async loadSeedMemory(store, rowCount) {
2791
+ if (!store.loadSeededTypes) return;
2792
+ const types = await store.loadSeededTypes();
2793
+ this.seededTypes = new Set(types);
2794
+ this.installPredatesSeedMemory = types.length === 0 && rowCount > 0;
2795
+ }
2796
+ rememberSeeded(type) {
2797
+ const seeded = this.seededTypes;
2798
+ const store = this.locationStore;
2799
+ if (seeded === null || seeded.has(type) || !store?.markTypeSeeded) return;
2800
+ seeded.add(type);
2801
+ store.markTypeSeeded(type).catch((err) => {
2802
+ this.logger.error("storage-orchestrator: persisting the seed memory failed", { meta: {
2803
+ type,
2804
+ error: err instanceof Error ? err.message : String(err)
2805
+ } });
2806
+ });
2807
+ }
2591
2808
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
2592
2809
  hasStore() {
2593
2810
  return this.locationStore !== null;
@@ -2621,6 +2838,7 @@ var StorageOrchestratorService = class {
2621
2838
  async attachStore(store) {
2622
2839
  if (this.locationStore) return;
2623
2840
  const rows = await store.loadAll();
2841
+ await this.loadSeedMemory(store, rows.length);
2624
2842
  this.locationStore = store;
2625
2843
  const dbIds = new Set(rows.map((r) => r.id));
2626
2844
  for (const loc of rows) this.locations.set(loc.id, loc);
@@ -2759,6 +2977,7 @@ var StorageOrchestratorService = class {
2759
2977
  async initialize() {
2760
2978
  if (!this.locationStore) return;
2761
2979
  const rows = await this.locationStore.loadAll();
2980
+ await this.loadSeedMemory(this.locationStore, rows.length);
2762
2981
  for (const loc of rows) this.locations.set(loc.id, loc);
2763
2982
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
2764
2983
  this.migrateOntoTheWriteSet(rows);
@@ -3210,14 +3429,35 @@ var StorageOrchestratorService = class {
3210
3429
  * directory for derivative slots like `recordingsLow` → `recordings`).
3211
3430
  */
3212
3431
  seedDefaults(input) {
3432
+ if (this.storeRequiredForSeed && this.locationStore === null) {
3433
+ this.logger.warn("storage-orchestrator: seed deferred — the location store has not answered a read yet (not wired, adoption pending, or the read failed); seeding now could shadow the operator's existing locations");
3434
+ return {
3435
+ added: 0,
3436
+ deferred: "store-not-answered"
3437
+ };
3438
+ }
3213
3439
  let added = 0;
3440
+ let deferredProvider = false;
3214
3441
  const declById = new Map(input.declarations.map((d) => [d.id, d]));
3215
3442
  const rootFor = (d) => {
3216
3443
  const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
3217
3444
  return node_path.join(dedicated ?? input.basePath, d.id);
3218
3445
  };
3219
3446
  for (const d of input.declarations) {
3220
- if (this.hasAnyLocationOfType(d.id)) continue;
3447
+ if (this.hasAnyLocationOfType(d.id)) {
3448
+ this.rememberSeeded(d.id);
3449
+ continue;
3450
+ }
3451
+ if (this.seededTypes?.has(d.id)) continue;
3452
+ if (this.installPredatesSeedMemory) {
3453
+ this.logger.info("storage-orchestrator: existing install — not re-creating a default the operator may have removed", { meta: { type: d.id } });
3454
+ this.rememberSeeded(d.id);
3455
+ continue;
3456
+ }
3457
+ if (this.nodeLocalResolver !== null && this.nodeLocalResolver(input.providerId) === void 0) {
3458
+ deferredProvider = true;
3459
+ continue;
3460
+ }
3221
3461
  const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootFor(declById.get(d.defaultsTo) ?? d) : rootFor(d);
3222
3462
  this.upsertLocation({
3223
3463
  id: `${d.id}:default`,
@@ -3228,12 +3468,18 @@ var StorageOrchestratorService = class {
3228
3468
  enabled: true,
3229
3469
  isSystem: false
3230
3470
  });
3471
+ this.rememberSeeded(d.id);
3231
3472
  added++;
3232
3473
  }
3233
3474
  if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
3234
3475
  added,
3235
3476
  basePath: input.basePath
3236
3477
  } });
3478
+ if (deferredProvider) this.logger.info("storage-orchestrator: seed deferred for some types — provider not classified yet", { meta: { providerId: input.providerId } });
3479
+ return deferredProvider ? {
3480
+ added,
3481
+ deferred: "provider-unclassified"
3482
+ } : { added };
3237
3483
  }
3238
3484
  /**
3239
3485
  * The same-type physical-root refusal (D389).
@@ -3608,29 +3854,72 @@ var DEFAULT_STORAGE_LOCATION_BAND_CONFIG = {
3608
3854
  lowPercent: 15,
3609
3855
  criticalPercent: 5
3610
3856
  };
3611
- function bandOf(freePercent, config) {
3612
- if (freePercent === null) return {
3613
- band: "critical",
3614
- reason: "capacity-unreadable"
3615
- };
3616
- if (freePercent <= config.criticalPercent) return {
3617
- band: "critical",
3618
- reason: "free-below-critical"
3619
- };
3620
- if (freePercent <= config.lowPercent) return {
3621
- band: "low",
3622
- reason: "free-below-warn"
3857
+ /** Margin, in percentage points, between the enter and exit of an unmanaged edge. */
3858
+ var UNMANAGED_HYSTERESIS_POINTS = 2;
3859
+ /** Managed: fractions of the floor. */
3860
+ var MANAGED_LOW_ENTER = .8;
3861
+ var MANAGED_LOW_EXIT = .9;
3862
+ var MANAGED_CRITICAL_ENTER = .5;
3863
+ var MANAGED_CRITICAL_EXIT = .6;
3864
+ /** How long the evictor may sit below the low margin before it counts as behind. */
3865
+ var MANAGED_LOW_GRACE_MS = 10 * 6e4;
3866
+ function boundsOf(floorPercent, config) {
3867
+ if (floorPercent === void 0) return {
3868
+ lowEnter: config.lowPercent,
3869
+ lowExit: config.lowPercent + UNMANAGED_HYSTERESIS_POINTS,
3870
+ criticalEnter: config.criticalPercent,
3871
+ criticalExit: config.criticalPercent + UNMANAGED_HYSTERESIS_POINTS,
3872
+ lowGraceMs: 0,
3873
+ lowReason: "free-below-warn",
3874
+ criticalReason: "free-below-critical"
3623
3875
  };
3624
3876
  return {
3625
- band: "ok",
3626
- reason: "free-ok"
3877
+ lowEnter: floorPercent * MANAGED_LOW_ENTER,
3878
+ lowExit: floorPercent * MANAGED_LOW_EXIT,
3879
+ criticalEnter: floorPercent * MANAGED_CRITICAL_ENTER,
3880
+ criticalExit: floorPercent * MANAGED_CRITICAL_EXIT,
3881
+ lowGraceMs: MANAGED_LOW_GRACE_MS,
3882
+ lowReason: "free-below-floor",
3883
+ criticalReason: "free-below-hard-bound"
3627
3884
  };
3628
3885
  }
3629
3886
  var StorageLocationBandMonitor = class {
3630
3887
  config;
3888
+ now;
3631
3889
  band = /* @__PURE__ */ new Map();
3632
- constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG) {
3890
+ /** When free first went under `lowEnter` and has stayed there. */
3891
+ belowLowSince = /* @__PURE__ */ new Map();
3892
+ constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG, now = Date.now) {
3633
3893
  this.config = config;
3894
+ this.now = now;
3895
+ }
3896
+ nextBand(locationId, previous, free, b) {
3897
+ if (free < b.criticalEnter || previous === "critical" && free < b.criticalExit) return {
3898
+ band: "critical",
3899
+ reason: b.criticalReason
3900
+ };
3901
+ const lowActive = previous === "low" || previous === "critical";
3902
+ if (free < b.lowEnter || lowActive && free < b.lowExit) {
3903
+ if (lowActive) return {
3904
+ band: "low",
3905
+ reason: b.lowReason
3906
+ };
3907
+ const since = this.belowLowSince.get(locationId) ?? this.now();
3908
+ this.belowLowSince.set(locationId, since);
3909
+ if (this.now() - since >= b.lowGraceMs) return {
3910
+ band: "low",
3911
+ reason: b.lowReason
3912
+ };
3913
+ return {
3914
+ band: previous ?? "ok",
3915
+ reason: "free-ok"
3916
+ };
3917
+ }
3918
+ this.belowLowSince.delete(locationId);
3919
+ return {
3920
+ band: "ok",
3921
+ reason: "free-ok"
3922
+ };
3634
3923
  }
3635
3924
  /**
3636
3925
  * Observe one sweep of managed locations. Locations missing from `samples`
@@ -3641,8 +3930,9 @@ var StorageLocationBandMonitor = class {
3641
3930
  const out = [];
3642
3931
  for (const sample of samples) {
3643
3932
  seen.add(sample.locationId);
3644
- const { band, reason } = bandOf(sample.freePercent, this.config);
3933
+ if (sample.freePercent === null) continue;
3645
3934
  const previous = this.band.get(sample.locationId) ?? null;
3935
+ const { band, reason } = this.nextBand(sample.locationId, previous, sample.freePercent, boundsOf(sample.floorPercent, this.config));
3646
3936
  if (previous === band) continue;
3647
3937
  this.band.set(sample.locationId, band);
3648
3938
  if (previous === null && band === "ok") continue;
@@ -3652,11 +3942,17 @@ var StorageLocationBandMonitor = class {
3652
3942
  ...sample.locationName !== void 0 ? { locationName: sample.locationName } : {},
3653
3943
  band,
3654
3944
  freePercent: sample.freePercent,
3945
+ ...sample.floorPercent !== void 0 ? { floorPercent: sample.floorPercent } : {},
3946
+ ...sample.freeBytes !== void 0 ? { freeBytes: sample.freeBytes } : {},
3947
+ ...sample.floorBytes !== void 0 ? { floorBytes: sample.floorBytes } : {},
3655
3948
  reason,
3656
3949
  previous
3657
3950
  });
3658
3951
  }
3659
- for (const id of [...this.band.keys()]) if (!seen.has(id)) this.band.delete(id);
3952
+ for (const id of [...this.band.keys()]) if (!seen.has(id)) {
3953
+ this.band.delete(id);
3954
+ this.belowLowSince.delete(id);
3955
+ }
3660
3956
  return out;
3661
3957
  }
3662
3958
  };
@@ -3704,6 +4000,7 @@ var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
3704
4000
  var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
3705
4001
  var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
3706
4002
  locationsKind = null;
4003
+ seedMarkersKind = null;
3707
4004
  drainStateKind = null;
3708
4005
  service = null;
3709
4006
  pressureManager = null;
@@ -3729,6 +4026,8 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
3729
4026
  pressureTimer = null;
3730
4027
  pressureSweepInFlight = false;
3731
4028
  migration = null;
4029
+ /** Guards every single-location repoint and arbitrates movers per physical root. */
4030
+ repointer = null;
3732
4031
  /**
3733
4032
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
3734
4033
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
@@ -3765,6 +4064,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
3765
4064
  if (records !== void 0) {
3766
4065
  await records.declare(storageOrchestratorRecordKinds);
3767
4066
  this.locationsKind = SqliteLocationStore.recordStoresOf(records);
4067
+ this.seedMarkersKind = SqliteLocationStore.seedMarkersOf(records);
3768
4068
  this.drainStateKind = SqliteDrainStateStore.recordStoresOf(records);
3769
4069
  }
3770
4070
  const getProviders = () => {
@@ -3784,6 +4084,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
3784
4084
  return proxy;
3785
4085
  });
3786
4086
  this.service = service;
4087
+ service.setStoreRequiredForSeed(this.locationsKind !== null);
3787
4088
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
3788
4089
  service.setOccupancyProbe((location) => this.locationOccupancy(location));
3789
4090
  await service.initialize();
@@ -3807,8 +4108,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
3807
4108
  ...input,
3808
4109
  config: keychained
3809
4110
  };
3810
- await this.carryDataOnRepoint(stored, next);
3811
- const saved = service.upsertLocation(next);
4111
+ const saved = await this.upsertCarryingData(stored, next, () => service.upsertLocation(next));
3812
4112
  this.announceLocationsChanged(saved.id, saved.type, "upsert");
3813
4113
  return this.redacted(saved);
3814
4114
  },
@@ -4018,6 +4318,49 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4018
4318
  deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
4019
4319
  });
4020
4320
  this.migration = migration;
4321
+ this.repointer = new LocationRepointer({
4322
+ logger: this.ctx.logger,
4323
+ resolveKey: async (basePath) => {
4324
+ try {
4325
+ return await node_fs_promises.realpath(basePath);
4326
+ } catch {
4327
+ return node_path.resolve(basePath);
4328
+ }
4329
+ },
4330
+ locationBasePath: (id) => this.locationBasePath(id),
4331
+ locationIdsOfType: (type) => service.listLocations({ type }).map((loc) => loc.id),
4332
+ listMovers: () => migration.movers(),
4333
+ ioFor: async (stored, next) => {
4334
+ const target = {
4335
+ ...stored,
4336
+ config: next.config
4337
+ };
4338
+ const [source, destination] = await Promise.all([service.getProviderFor(stored), service.getProviderFor(target)]);
4339
+ return {
4340
+ list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
4341
+ location: stored,
4342
+ prefix
4343
+ }),
4344
+ read: (relativePath) => source.read({
4345
+ location: stored,
4346
+ relativePath
4347
+ }),
4348
+ write: (relativePath, data) => destination.write({
4349
+ location: target,
4350
+ relativePath,
4351
+ data
4352
+ }),
4353
+ readBack: (relativePath) => destination.read({
4354
+ location: target,
4355
+ relativePath
4356
+ }),
4357
+ deleteSource: (relativePath) => source.delete({
4358
+ location: stored,
4359
+ relativePath
4360
+ })
4361
+ };
4362
+ }
4363
+ });
4021
4364
  const cleanup = new StorageCleanupCoordinator({
4022
4365
  locations: service,
4023
4366
  state: this.state("storage-cleanup", require_dist.StorageCleanupJobSchema.nullable(), null),
@@ -4053,6 +4396,13 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4053
4396
  status: ({ jobId }) => migration.status(jobId),
4054
4397
  cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
4055
4398
  movers: () => migration.movers(),
4399
+ moverClearance: async ({ locationIds }) => {
4400
+ const verdict = await this.requireRepointer().clearance(locationIds);
4401
+ return verdict.clear ? { clear: true } : {
4402
+ clear: false,
4403
+ repointLocationId: verdict.repointLocationId
4404
+ };
4405
+ },
4056
4406
  residue: () => migration.residue(),
4057
4407
  drain: async (input) => ({ jobId: await migration.drain(input) }),
4058
4408
  cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
@@ -4179,7 +4529,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4179
4529
  else this.ctx.logger.info("storage-orchestrator: this node boots no data-store engine — storage locations are seeded from addon declarations at every boot, and node-pinned locations live on the hub");
4180
4530
  return null;
4181
4531
  }
4182
- return new SqliteLocationStore(engine, this.locationsKind);
4532
+ return new SqliteLocationStore(engine, this.locationsKind, this.seedMarkersKind);
4183
4533
  }
4184
4534
  /**
4185
4535
  * Late-wire the persistence store once a `data-store-provider` engine
@@ -4191,12 +4541,26 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4191
4541
  * restart — the service never hydrates from / persists to SQLite.
4192
4542
  */
4193
4543
  storeWireInFlight = null;
4544
+ /** True while the locations kind is empty and its legacy table still holds rows. */
4545
+ async adoptionIsPending() {
4546
+ const records = this.ctx.records;
4547
+ if (records === void 0 || this.locationsKind === null) return false;
4548
+ try {
4549
+ await records.declare(storageOrchestratorRecordKinds);
4550
+ return false;
4551
+ } catch (err) {
4552
+ const message = err instanceof Error ? err.message : String(err);
4553
+ this.ctx.logger.warn("storage-orchestrator: persistence NOT wired — the storage-locations kind is not authoritative yet (adoption pending or the check failed); seeding is deferred", { meta: { error: message } });
4554
+ return true;
4555
+ }
4556
+ }
4194
4557
  async ensureStoreWired() {
4195
4558
  const service = this.service;
4196
4559
  if (!service || service.hasStore()) return;
4197
4560
  if (this.storeWireInFlight) return this.storeWireInFlight;
4198
4561
  const store = this.resolveLocationStore(true);
4199
4562
  if (!store) return;
4563
+ if (await this.adoptionIsPending()) return;
4200
4564
  this.ctx.logger.info("storage-orchestrator: data-store engine now available — wiring persistence");
4201
4565
  this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
4202
4566
  this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -4225,7 +4589,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4225
4589
  }
4226
4590
  /**
4227
4591
  * Sample free space for every managed location and emit NC band edges.
4228
- * A failed capacity read stays `null` (critical) — never folded to 0 (D393).
4592
+ * A failed capacity read stays `null` and changes no band — never folded to 0 (D393).
4229
4593
  */
4230
4594
  async emitLocationBandEvents() {
4231
4595
  const svc = this.service;
@@ -4241,11 +4605,18 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4241
4605
  };
4242
4606
  const capacity = await this.volumeCapacityOf(locationId).catch(() => null);
4243
4607
  const freePercent = capacity === null ? null : freePercentOf(capacity);
4608
+ if (capacity === null) ctx.logger.debug("storage: location capacity unreadable — band unchanged", { meta: { locationId } });
4609
+ const floorPercent = thresholds.get(locationId);
4244
4610
  return {
4245
4611
  locationId,
4246
4612
  nodeId: loc.nodeId,
4247
4613
  locationName: loc.displayName,
4248
- freePercent
4614
+ freePercent,
4615
+ ...floorPercent !== void 0 ? { floorPercent } : {},
4616
+ ...capacity !== null ? {
4617
+ freeBytes: capacity.availableBytes,
4618
+ ...floorPercent !== void 0 ? { floorBytes: Math.round(capacity.totalBytes * floorPercent / 100) } : {}
4619
+ } : {}
4249
4620
  };
4250
4621
  }));
4251
4622
  for (const edge of this.locationBandMonitor.observe(samples)) {
@@ -4254,6 +4625,9 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4254
4625
  locationId: edge.locationId,
4255
4626
  band: edge.band,
4256
4627
  freePercent: edge.freePercent,
4628
+ floorPercent: edge.floorPercent ?? null,
4629
+ freeBytes: edge.freeBytes ?? null,
4630
+ floorBytes: edge.floorBytes ?? null,
4257
4631
  reason: edge.reason
4258
4632
  } });
4259
4633
  ctx.eventBus.emit({
@@ -4367,50 +4741,19 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
4367
4741
  * A no-op for everything else — a rename, a mode change, a `multi` kind, or a
4368
4742
  * location being created for the first time.
4369
4743
  */
4370
- async carryDataOnRepoint(stored, next) {
4371
- if (stored === void 0) return;
4744
+ async upsertCarryingData(stored, next, commit) {
4745
+ if (stored === void 0) return commit();
4372
4746
  const service = this.service;
4373
- if (service === null) return;
4374
- if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return;
4747
+ if (service === null) return commit();
4748
+ if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return commit();
4375
4749
  const from = stored.config["basePath"];
4376
4750
  const to = next.config["basePath"];
4377
- if (typeof from !== "string" || typeof to !== "string" || from === to) return;
4378
- const source = await service.getProviderFor(stored);
4379
- const target = {
4380
- ...stored,
4381
- config: next.config
4382
- };
4383
- const destination = await service.getProviderFor(target);
4384
- const moved = await moveLocationTree({
4385
- list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
4386
- location: stored,
4387
- prefix
4388
- }),
4389
- read: (relativePath) => source.read({
4390
- location: stored,
4391
- relativePath
4392
- }),
4393
- write: (relativePath, data) => destination.write({
4394
- location: target,
4395
- relativePath,
4396
- data
4397
- }),
4398
- readBack: (relativePath) => destination.read({
4399
- location: target,
4400
- relativePath
4401
- }),
4402
- deleteSource: (relativePath) => source.delete({
4403
- location: stored,
4404
- relativePath
4405
- })
4406
- });
4407
- this.ctx.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
4408
- id: stored.id,
4409
- type: stored.type,
4410
- from,
4411
- to,
4412
- ...moved
4413
- } });
4751
+ if (typeof from !== "string" || typeof to !== "string" || from === to) return commit();
4752
+ return this.requireRepointer().run(stored, next, commit);
4753
+ }
4754
+ requireRepointer() {
4755
+ if (this.repointer === null) throw new Error("storage-orchestrator: not initialized");
4756
+ return this.repointer;
4414
4757
  }
4415
4758
  secretKeysFor(providerId, config) {
4416
4759
  return secretKeysForConfig(config, this.declaredSecretKeys(providerId));