@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
@@ -1,4 +1,4 @@
1
- import { $n as resolveLocationMode, Hi as isSecretMarker, Hr as BaseAddon, Ji as normalizeNodeId, Mi as defineRecordKinds, Mn as modeMayWrite, On as mayReadLocation, Tr as withLocationMode, W as LegacyStorageLocationDefaultSchema, Xi as parseJsonObject, ai as REDACTED_SECRET, at as StorageLocationModeSchema, cn as gbToBytes, da as transformSecretWrite, en as evictionPolicyOfLocation, fr as storageCapability, ia as resolveSecretMarkers, it as StorageCleanupJobSchema, kn as mayWriteToLocation, ma as EventCategory, mn as isLocationEnabled, oa as secretTreeFromPaths, or as settingsStoreCapability, ot as StorageLocationTypeSchema, pr as storageMigrationCapability, st as StorageMigrationJobSchema, wi as collectSecretConfigKeys, xn as legacyModeOf } from "../../dist-DV2ll9Hi.mjs";
1
+ import { Ii as defineRecordKinds, In as modeMayWrite, Ki as isSecretMarker, Kr as BaseAddon, Mn as mayReadLocation, Nn as mayWriteToLocation, Oi as collectSecretConfigKeys, Qi as normalizeNodeId, Tn as legacyModeOf, W as LegacyStorageLocationDefaultSchema, _r as storageMigrationCapability, at as StorageLocationModeSchema, ca as resolveSecretMarkers, ea as parseJsonObject, gr as storageCapability, ha as transformSecretWrite, it as StorageCleanupJobSchema, kr as withLocationMode, li as REDACTED_SECRET, nn as evictionPolicyOfLocation, ot as StorageLocationTypeSchema, rr as resolveLocationMode, st as StorageMigrationJobSchema, ua as secretTreeFromPaths, un as gbToBytes, ur as settingsStoreCapability, va as EventCategory, vn as isLocationEnabled } from "../../dist-7ZciHCIp.mjs";
2
2
  import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -26,6 +26,7 @@ var StorageLocationBodySchema = z.object({
26
26
  mode: StorageLocationModeSchema.optional(),
27
27
  isDefault: z.boolean().optional()
28
28
  });
29
+ var StorageSeedMarkerBodySchema = z.object({ seededAt: z.number() });
29
30
  var StorageDrainStateBodySchema = z.object({
30
31
  locationId: z.string(),
31
32
  startedAtMs: z.number(),
@@ -50,6 +51,14 @@ var _storageOrchestratorRecordKinds = defineRecordKinds({
50
51
  retention: { none: "operator delete or declaration prune only" },
51
52
  adoptsFrom: STORAGE_LOCATIONS
52
53
  },
54
+ "seed-markers": {
55
+ schema: StorageSeedMarkerBodySchema,
56
+ class: "config",
57
+ writeTrigger: "a declared location type was seeded (or found already present on an existing install); the id is the type",
58
+ device: "none",
59
+ at: "none",
60
+ retention: { none: "one row per location type ever seeded — what makes an operator delete stick" }
61
+ },
53
62
  "drain-state": {
54
63
  schema: StorageDrainStateBodySchema,
55
64
  class: "config",
@@ -183,9 +192,27 @@ var SqliteLocationStore = class {
183
192
  store;
184
193
  declared = false;
185
194
  locationsKind;
186
- constructor(store, locationsKind = null) {
195
+ constructor(store, locationsKind = null, seedMarkersKind = null) {
187
196
  this.store = store;
188
197
  this.locationsKind = locationsKind;
198
+ this.seedMarkersKind = seedMarkersKind;
199
+ }
200
+ seedMarkersKind;
201
+ static seedMarkersOf(records) {
202
+ return records.kind(storageOrchestratorRecordKinds["seed-markers"]);
203
+ }
204
+ async loadSeededTypes() {
205
+ const kind = this.seedMarkersKind;
206
+ if (kind === null) return [];
207
+ return (await (0, import_read_all_rows.readAllRows)(kind)).map((r) => r.id);
208
+ }
209
+ async markTypeSeeded(type) {
210
+ const kind = this.seedMarkersKind;
211
+ if (kind === null) return;
212
+ await kind.put({
213
+ id: type,
214
+ data: { seededAt: Date.now() }
215
+ });
189
216
  }
190
217
  static recordStoresOf(records) {
191
218
  return records.kind(storageOrchestratorRecordKinds.locations);
@@ -1227,6 +1254,111 @@ function buildStorageLocationRegistry(perAddon) {
1227
1254
  };
1228
1255
  }
1229
1256
  //#endregion
1257
+ //#region src/builtins/storage-orchestrator/location-occupancy.ts
1258
+ /**
1259
+ * Delete-time occupancy of a location's on-disk root.
1260
+ *
1261
+ * A recordings disk may host several classes at one `basePath`
1262
+ * (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
1263
+ * a class is therefore the class subtree, not the shared root — otherwise a
1264
+ * drained `eventMedia:default` still looks full because high footage is
1265
+ * writing next to it (observed 2026-09-01).
1266
+ *
1267
+ * The walk is shallow: one `readdir` of the root (one dirent per camera)
1268
+ * plus `opendir`+first-entry of each named subtree. No recursive find of
1269
+ * segments — that is the walk that saturates a USB disk.
1270
+ */
1271
+ /**
1272
+ * Class → directories under `<root>/<camera>/`. Same map as the discard
1273
+ * wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
1274
+ * Types not listed keep the legacy "any non-dot entry at the root" rule
1275
+ * (backups, addon-data, …).
1276
+ */
1277
+ var STORAGE_CLASS_SUBTREES = {
1278
+ eventMedia: ["events"],
1279
+ recordingsLow: ["low"],
1280
+ recordings: ["high", "mid"]
1281
+ };
1282
+ function isDotEntry(name) {
1283
+ return name.startsWith(".");
1284
+ }
1285
+ /**
1286
+ * True when `dir` exists and contains at least one dirent. Missing → false.
1287
+ * Any other errno throws so the caller can answer `unknown` (D49).
1288
+ */
1289
+ async function dirHasAnyEntry(dir) {
1290
+ let handle;
1291
+ try {
1292
+ handle = await opendir(dir);
1293
+ } catch (err) {
1294
+ if (err.code === "ENOENT") return false;
1295
+ throw err;
1296
+ }
1297
+ try {
1298
+ return await handle.read() !== null;
1299
+ } finally {
1300
+ await handle.close();
1301
+ }
1302
+ }
1303
+ /**
1304
+ * Occupancy of `basePath` for a location of `type`.
1305
+ *
1306
+ * - missing root → `empty` (nothing this delete can strand)
1307
+ * - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
1308
+ * - other types → occupied iff the root has a non-dot entry
1309
+ */
1310
+ async function probeLocalDirectoryOccupancy(input) {
1311
+ let names;
1312
+ try {
1313
+ names = await readdir(input.basePath);
1314
+ } catch (err) {
1315
+ if (err.code === "ENOENT") return "empty";
1316
+ return "unknown";
1317
+ }
1318
+ const visible = names.filter((name) => !isDotEntry(name));
1319
+ const subtrees = STORAGE_CLASS_SUBTREES[input.type];
1320
+ if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
1321
+ try {
1322
+ for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(path.join(input.basePath, camera, subtree))) return "occupied";
1323
+ } catch {
1324
+ return "unknown";
1325
+ }
1326
+ return "empty";
1327
+ }
1328
+ //#endregion
1329
+ //#region src/builtins/storage-orchestrator/location-class-paths.ts
1330
+ /**
1331
+ * Which paths under a shared root BELONG to a storage class (D327).
1332
+ *
1333
+ * A recordings disk hosts several classes at one `basePath`:
1334
+ * `<camera>/high|mid` (recordings), `<camera>/low` (recordingsLow),
1335
+ * `<camera>/events` (eventMedia) and `homekit-clips/…` (homekitClips). A
1336
+ * location may touch only its own subtree; the rest of the root is another
1337
+ * location's data. On 2026-10-20 a repoint of `homekitClips:default` walked the
1338
+ * WHOLE shared root and deleted ~8.6k event-media files that were not its own.
1339
+ *
1340
+ * The per-camera map is {@link STORAGE_CLASS_SUBTREES} (the one source, shared
1341
+ * with occupancy scoring). A class whose subtree sits at the ROOT, not under a
1342
+ * camera, is listed here. A type in neither map owns its whole root (backups,
1343
+ * artifacts, …): there is no subtree to scope to.
1344
+ */
1345
+ /** Class → top-level directories directly under the root (no camera segment). */
1346
+ var STORAGE_CLASS_ROOT_SUBTREES = { homekitClips: ["homekit-clips"] };
1347
+ /** Classify a root-relative path (`/` separators) for a location of `type`. */
1348
+ function classifyPath(type, relativePath) {
1349
+ const rootSubtrees = STORAGE_CLASS_ROOT_SUBTREES[type];
1350
+ const cameraSubtrees = STORAGE_CLASS_SUBTREES[type];
1351
+ if (rootSubtrees === void 0 && cameraSubtrees === void 0) return "own";
1352
+ const segments = relativePath.split("/").filter((segment) => segment.length > 0);
1353
+ if (rootSubtrees !== void 0) {
1354
+ const top = segments[0];
1355
+ return top !== void 0 && rootSubtrees.includes(top) ? "own" : "foreign";
1356
+ }
1357
+ if (segments.length < 2) return "descend";
1358
+ const subtree = segments[1];
1359
+ return subtree !== void 0 && cameraSubtrees?.includes(subtree) === true ? "own" : "foreign";
1360
+ }
1361
+ //#endregion
1230
1362
  //#region src/builtins/storage-orchestrator/location-repoint-move.ts
1231
1363
  /**
1232
1364
  * Walk the source root and collect FILES.
@@ -1237,24 +1369,28 @@ function buildStorageLocationRegistry(perAddon) {
1237
1369
  * (an empty directory) is skipped by the copy loop, which is the only place
1238
1370
  * that can tell — and skipping it is right, since there is nothing to move.
1239
1371
  */
1240
- async function collectFiles(io, prefix) {
1372
+ async function collectFiles(io, scope, prefix) {
1241
1373
  const entries = await io.list(prefix);
1242
1374
  const files = [];
1243
1375
  for (const entry of entries) {
1376
+ const verdict = scope(entry);
1377
+ if (verdict === "foreign") continue;
1244
1378
  if ((await io.list(entry)).length > 0) {
1245
- files.push(...await collectFiles(io, entry));
1379
+ files.push(...await collectFiles(io, scope, entry));
1246
1380
  continue;
1247
1381
  }
1248
- files.push(entry);
1382
+ if (verdict === "own") files.push(entry);
1249
1383
  }
1250
1384
  return files;
1251
1385
  }
1386
+ /** The whole root — only correct for a class that owns all of it. */
1387
+ var WHOLE_ROOT = () => "own";
1252
1388
  /**
1253
1389
  * Copy every file from the source root to the destination, verify each, then
1254
1390
  * delete the originals. Throws with NOTHING deleted if any copy fails.
1255
1391
  */
1256
- async function moveLocationTree(io) {
1257
- const candidates = await collectFiles(io);
1392
+ async function moveLocationTree(io, scope = WHOLE_ROOT) {
1393
+ const candidates = await collectFiles(io, scope);
1258
1394
  const copied = [];
1259
1395
  let bytes = 0;
1260
1396
  for (const relativePath of candidates) {
@@ -1277,6 +1413,121 @@ async function moveLocationTree(io) {
1277
1413
  };
1278
1414
  }
1279
1415
  //#endregion
1416
+ //#region src/builtins/storage-orchestrator/location-repointer.ts
1417
+ var RepointRefusedError = class extends Error {
1418
+ reason;
1419
+ name = "RepointRefusedError";
1420
+ constructor(reason, message) {
1421
+ super(message);
1422
+ this.reason = reason;
1423
+ }
1424
+ };
1425
+ /** Does `a` equal `b`, or sit inside it, or contain it? Segment-wise, never by raw prefix. */
1426
+ function rootsOverlap(a, b) {
1427
+ if (a === b) return true;
1428
+ const within = (inner, outer) => inner.startsWith(outer.endsWith("/") ? outer : `${outer}/`);
1429
+ return within(a, b) || within(b, a);
1430
+ }
1431
+ function basePathOf$1(config) {
1432
+ const raw = config["basePath"];
1433
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
1434
+ }
1435
+ var LocationRepointer = class {
1436
+ deps;
1437
+ claims = /* @__PURE__ */ new Set();
1438
+ constructor(deps) {
1439
+ this.deps = deps;
1440
+ }
1441
+ /**
1442
+ * Move `stored`'s own subtree to `next`'s root, then run `commit` (the row
1443
+ * save) while the claim is still held. Throws {@link RepointRefusedError}
1444
+ * with NOTHING copied or deleted when a mover is on the root.
1445
+ */
1446
+ async run(stored, next, commit) {
1447
+ const claim = await this.claim(stored, next);
1448
+ try {
1449
+ await this.refuseWhileMovers(claim, stored);
1450
+ const moved = await moveLocationTree(await this.deps.ioFor(stored, next), (path) => classifyPath(stored.type, path));
1451
+ this.logMoved(stored, next, moved);
1452
+ return await commit();
1453
+ } finally {
1454
+ this.claims.delete(claim);
1455
+ }
1456
+ }
1457
+ /**
1458
+ * May a mover that touches `locationIds` run? Refuses while a repoint holds
1459
+ * an overlapping root. Called by the owning addon AFTER its job is
1460
+ * registered, so the repoint's mover listing and this check cannot both miss.
1461
+ */
1462
+ async clearance(locationIds) {
1463
+ const keys = await this.keysOf(locationIds);
1464
+ for (const claim of this.claims) if (claim.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) return {
1465
+ clear: false,
1466
+ reason: "repoint-active",
1467
+ repointLocationId: claim.locationId
1468
+ };
1469
+ return { clear: true };
1470
+ }
1471
+ async claim(stored, next) {
1472
+ const paths = [basePathOf$1(stored.config), basePathOf$1(next.config)].filter((path) => path !== null);
1473
+ const keys = await Promise.all(paths.map((path) => this.deps.resolveKey(path)));
1474
+ 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`);
1475
+ const claim = {
1476
+ locationId: stored.id,
1477
+ keys
1478
+ };
1479
+ this.claims.add(claim);
1480
+ return claim;
1481
+ }
1482
+ async refuseWhileMovers(claim, stored) {
1483
+ let movers;
1484
+ try {
1485
+ movers = await this.deps.listMovers();
1486
+ } catch (err) {
1487
+ this.claims.delete(claim);
1488
+ this.refuse(stored, "movers-unreadable", `the active movers could not be listed (${err instanceof Error ? err.message : String(err)})`);
1489
+ }
1490
+ for (const mover of movers) {
1491
+ if (mover.job.state !== "queued" && mover.job.state !== "running") continue;
1492
+ const touched = await this.keysOf(this.locationsTouchedBy(mover));
1493
+ if (claim.keys.some((held) => touched.some((key) => rootsOverlap(held, key)))) {
1494
+ this.claims.delete(claim);
1495
+ this.refuse(stored, "mover-active", `${mover.lane} mover ${mover.job.jobId} (${mover.job.fromLocationId} -> ${mover.job.toLocationId}) touches the same physical root`, {
1496
+ jobId: mover.job.jobId,
1497
+ lane: mover.lane
1498
+ });
1499
+ }
1500
+ }
1501
+ }
1502
+ locationsTouchedBy(mover) {
1503
+ return [mover.job.fromLocationId, mover.job.toLocationId];
1504
+ }
1505
+ /** `*` is the media mover's "from wherever the rows are": every eventMedia location. */
1506
+ async keysOf(locationIds) {
1507
+ const paths = locationIds.flatMap((id) => id === "*" ? this.deps.locationIdsOfType("eventMedia") : [id]).map((id) => this.deps.locationBasePath(id)).filter((path) => path !== null);
1508
+ return Promise.all(paths.map((path) => this.deps.resolveKey(path)));
1509
+ }
1510
+ refuse(stored, reason, detail, meta = {}) {
1511
+ this.deps.logger.warn("storage-orchestrator: REFUSING to re-point a location — nothing was moved", { meta: {
1512
+ id: stored.id,
1513
+ type: stored.type,
1514
+ reason,
1515
+ detail,
1516
+ ...meta
1517
+ } });
1518
+ throw new RepointRefusedError(reason, `re-point of "${stored.id}" refused (${reason}): ${detail}`);
1519
+ }
1520
+ logMoved(stored, next, moved) {
1521
+ this.deps.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
1522
+ id: stored.id,
1523
+ type: stored.type,
1524
+ from: basePathOf$1(stored.config),
1525
+ to: basePathOf$1(next.config),
1526
+ ...moved
1527
+ } });
1528
+ }
1529
+ };
1530
+ //#endregion
1280
1531
  //#region src/builtins/storage-orchestrator/access-guards.ts
1281
1532
  /**
1282
1533
  * May `storage.resolve` return a path for this location?
@@ -1776,78 +2027,6 @@ function resolveHubSettingsForward(options) {
1776
2027
  }, { timeout: HUB_SETTINGS_FORWARD_TIMEOUT_MS }));
1777
2028
  }
1778
2029
  //#endregion
1779
- //#region src/builtins/storage-orchestrator/location-occupancy.ts
1780
- /**
1781
- * Delete-time occupancy of a location's on-disk root.
1782
- *
1783
- * A recordings disk may host several classes at one `basePath`
1784
- * (`<camera>/high|mid`, `<camera>/low`, `<camera>/events`). Occupancy for
1785
- * a class is therefore the class subtree, not the shared root — otherwise a
1786
- * drained `eventMedia:default` still looks full because high footage is
1787
- * writing next to it (observed 2026-09-01).
1788
- *
1789
- * The walk is shallow: one `readdir` of the root (one dirent per camera)
1790
- * plus `opendir`+first-entry of each named subtree. No recursive find of
1791
- * segments — that is the walk that saturates a USB disk.
1792
- */
1793
- /**
1794
- * Class → directories under `<root>/<camera>/`. Same map as the discard
1795
- * wipe (`DISCARD_SUBTREES` in the recorder / pipeline-analytics addons).
1796
- * Types not listed keep the legacy "any non-dot entry at the root" rule
1797
- * (backups, addon-data, …).
1798
- */
1799
- var STORAGE_CLASS_SUBTREES = {
1800
- eventMedia: ["events"],
1801
- recordingsLow: ["low"],
1802
- recordings: ["high", "mid"]
1803
- };
1804
- function isDotEntry(name) {
1805
- return name.startsWith(".");
1806
- }
1807
- /**
1808
- * True when `dir` exists and contains at least one dirent. Missing → false.
1809
- * Any other errno throws so the caller can answer `unknown` (D49).
1810
- */
1811
- async function dirHasAnyEntry(dir) {
1812
- let handle;
1813
- try {
1814
- handle = await opendir(dir);
1815
- } catch (err) {
1816
- if (err.code === "ENOENT") return false;
1817
- throw err;
1818
- }
1819
- try {
1820
- return await handle.read() !== null;
1821
- } finally {
1822
- await handle.close();
1823
- }
1824
- }
1825
- /**
1826
- * Occupancy of `basePath` for a location of `type`.
1827
- *
1828
- * - missing root → `empty` (nothing this delete can strand)
1829
- * - class-mapped type → occupied iff any `<camera>/<subtree>` has an entry
1830
- * - other types → occupied iff the root has a non-dot entry
1831
- */
1832
- async function probeLocalDirectoryOccupancy(input) {
1833
- let names;
1834
- try {
1835
- names = await readdir(input.basePath);
1836
- } catch (err) {
1837
- if (err.code === "ENOENT") return "empty";
1838
- return "unknown";
1839
- }
1840
- const visible = names.filter((name) => !isDotEntry(name));
1841
- const subtrees = STORAGE_CLASS_SUBTREES[input.type];
1842
- if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
1843
- try {
1844
- for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(path.join(input.basePath, camera, subtree))) return "occupied";
1845
- } catch {
1846
- return "unknown";
1847
- }
1848
- return "empty";
1849
- }
1850
- //#endregion
1851
2030
  //#region src/builtins/storage-orchestrator/location-occupancy-slice.ts
1852
2031
  /** One minute: the same order as the pressure sweep, and far longer than an
1853
2032
  * admin-UI repaint. */
@@ -2583,6 +2762,44 @@ var StorageOrchestratorService = class {
2583
2762
  getLocalNodeId() {
2584
2763
  return this.localNode;
2585
2764
  }
2765
+ /**
2766
+ * Whether a seed must wait for an ANSWERED read of the store (D49). True on a
2767
+ * node whose locations live in a store: seeding before that read lands writes
2768
+ * defaults the store's own rows (not yet adopted, or not yet read) then
2769
+ * contradict. False on a node that has no store by design — it seeds in memory
2770
+ * at every boot.
2771
+ */
2772
+ storeRequiredForSeed = false;
2773
+ setStoreRequiredForSeed(required) {
2774
+ this.storeRequiredForSeed = required;
2775
+ }
2776
+ /**
2777
+ * The location types a default was ever seeded for — null when the store
2778
+ * cannot remember (then a type with no row is seeded, as before). What makes
2779
+ * an operator's delete of a seeded default STICK: zero rows today is not
2780
+ * "fresh install", it is only fresh when nothing was ever seeded.
2781
+ */
2782
+ seededTypes = null;
2783
+ /** The store held locations but no seed memory: an install older than the memory. */
2784
+ installPredatesSeedMemory = false;
2785
+ async loadSeedMemory(store, rowCount) {
2786
+ if (!store.loadSeededTypes) return;
2787
+ const types = await store.loadSeededTypes();
2788
+ this.seededTypes = new Set(types);
2789
+ this.installPredatesSeedMemory = types.length === 0 && rowCount > 0;
2790
+ }
2791
+ rememberSeeded(type) {
2792
+ const seeded = this.seededTypes;
2793
+ const store = this.locationStore;
2794
+ if (seeded === null || seeded.has(type) || !store?.markTypeSeeded) return;
2795
+ seeded.add(type);
2796
+ store.markTypeSeeded(type).catch((err) => {
2797
+ this.logger.error("storage-orchestrator: persisting the seed memory failed", { meta: {
2798
+ type,
2799
+ error: err instanceof Error ? err.message : String(err)
2800
+ } });
2801
+ });
2802
+ }
2586
2803
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
2587
2804
  hasStore() {
2588
2805
  return this.locationStore !== null;
@@ -2616,6 +2833,7 @@ var StorageOrchestratorService = class {
2616
2833
  async attachStore(store) {
2617
2834
  if (this.locationStore) return;
2618
2835
  const rows = await store.loadAll();
2836
+ await this.loadSeedMemory(store, rows.length);
2619
2837
  this.locationStore = store;
2620
2838
  const dbIds = new Set(rows.map((r) => r.id));
2621
2839
  for (const loc of rows) this.locations.set(loc.id, loc);
@@ -2754,6 +2972,7 @@ var StorageOrchestratorService = class {
2754
2972
  async initialize() {
2755
2973
  if (!this.locationStore) return;
2756
2974
  const rows = await this.locationStore.loadAll();
2975
+ await this.loadSeedMemory(this.locationStore, rows.length);
2757
2976
  for (const loc of rows) this.locations.set(loc.id, loc);
2758
2977
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
2759
2978
  this.migrateOntoTheWriteSet(rows);
@@ -3205,14 +3424,35 @@ var StorageOrchestratorService = class {
3205
3424
  * directory for derivative slots like `recordingsLow` → `recordings`).
3206
3425
  */
3207
3426
  seedDefaults(input) {
3427
+ if (this.storeRequiredForSeed && this.locationStore === null) {
3428
+ 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");
3429
+ return {
3430
+ added: 0,
3431
+ deferred: "store-not-answered"
3432
+ };
3433
+ }
3208
3434
  let added = 0;
3435
+ let deferredProvider = false;
3209
3436
  const declById = new Map(input.declarations.map((d) => [d.id, d]));
3210
3437
  const rootFor = (d) => {
3211
3438
  const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
3212
3439
  return path$1.join(dedicated ?? input.basePath, d.id);
3213
3440
  };
3214
3441
  for (const d of input.declarations) {
3215
- if (this.hasAnyLocationOfType(d.id)) continue;
3442
+ if (this.hasAnyLocationOfType(d.id)) {
3443
+ this.rememberSeeded(d.id);
3444
+ continue;
3445
+ }
3446
+ if (this.seededTypes?.has(d.id)) continue;
3447
+ if (this.installPredatesSeedMemory) {
3448
+ this.logger.info("storage-orchestrator: existing install — not re-creating a default the operator may have removed", { meta: { type: d.id } });
3449
+ this.rememberSeeded(d.id);
3450
+ continue;
3451
+ }
3452
+ if (this.nodeLocalResolver !== null && this.nodeLocalResolver(input.providerId) === void 0) {
3453
+ deferredProvider = true;
3454
+ continue;
3455
+ }
3216
3456
  const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootFor(declById.get(d.defaultsTo) ?? d) : rootFor(d);
3217
3457
  this.upsertLocation({
3218
3458
  id: `${d.id}:default`,
@@ -3223,12 +3463,18 @@ var StorageOrchestratorService = class {
3223
3463
  enabled: true,
3224
3464
  isSystem: false
3225
3465
  });
3466
+ this.rememberSeeded(d.id);
3226
3467
  added++;
3227
3468
  }
3228
3469
  if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
3229
3470
  added,
3230
3471
  basePath: input.basePath
3231
3472
  } });
3473
+ if (deferredProvider) this.logger.info("storage-orchestrator: seed deferred for some types — provider not classified yet", { meta: { providerId: input.providerId } });
3474
+ return deferredProvider ? {
3475
+ added,
3476
+ deferred: "provider-unclassified"
3477
+ } : { added };
3232
3478
  }
3233
3479
  /**
3234
3480
  * The same-type physical-root refusal (D389).
@@ -3603,29 +3849,72 @@ var DEFAULT_STORAGE_LOCATION_BAND_CONFIG = {
3603
3849
  lowPercent: 15,
3604
3850
  criticalPercent: 5
3605
3851
  };
3606
- function bandOf(freePercent, config) {
3607
- if (freePercent === null) return {
3608
- band: "critical",
3609
- reason: "capacity-unreadable"
3610
- };
3611
- if (freePercent <= config.criticalPercent) return {
3612
- band: "critical",
3613
- reason: "free-below-critical"
3614
- };
3615
- if (freePercent <= config.lowPercent) return {
3616
- band: "low",
3617
- reason: "free-below-warn"
3852
+ /** Margin, in percentage points, between the enter and exit of an unmanaged edge. */
3853
+ var UNMANAGED_HYSTERESIS_POINTS = 2;
3854
+ /** Managed: fractions of the floor. */
3855
+ var MANAGED_LOW_ENTER = .8;
3856
+ var MANAGED_LOW_EXIT = .9;
3857
+ var MANAGED_CRITICAL_ENTER = .5;
3858
+ var MANAGED_CRITICAL_EXIT = .6;
3859
+ /** How long the evictor may sit below the low margin before it counts as behind. */
3860
+ var MANAGED_LOW_GRACE_MS = 10 * 6e4;
3861
+ function boundsOf(floorPercent, config) {
3862
+ if (floorPercent === void 0) return {
3863
+ lowEnter: config.lowPercent,
3864
+ lowExit: config.lowPercent + UNMANAGED_HYSTERESIS_POINTS,
3865
+ criticalEnter: config.criticalPercent,
3866
+ criticalExit: config.criticalPercent + UNMANAGED_HYSTERESIS_POINTS,
3867
+ lowGraceMs: 0,
3868
+ lowReason: "free-below-warn",
3869
+ criticalReason: "free-below-critical"
3618
3870
  };
3619
3871
  return {
3620
- band: "ok",
3621
- reason: "free-ok"
3872
+ lowEnter: floorPercent * MANAGED_LOW_ENTER,
3873
+ lowExit: floorPercent * MANAGED_LOW_EXIT,
3874
+ criticalEnter: floorPercent * MANAGED_CRITICAL_ENTER,
3875
+ criticalExit: floorPercent * MANAGED_CRITICAL_EXIT,
3876
+ lowGraceMs: MANAGED_LOW_GRACE_MS,
3877
+ lowReason: "free-below-floor",
3878
+ criticalReason: "free-below-hard-bound"
3622
3879
  };
3623
3880
  }
3624
3881
  var StorageLocationBandMonitor = class {
3625
3882
  config;
3883
+ now;
3626
3884
  band = /* @__PURE__ */ new Map();
3627
- constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG) {
3885
+ /** When free first went under `lowEnter` and has stayed there. */
3886
+ belowLowSince = /* @__PURE__ */ new Map();
3887
+ constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG, now = Date.now) {
3628
3888
  this.config = config;
3889
+ this.now = now;
3890
+ }
3891
+ nextBand(locationId, previous, free, b) {
3892
+ if (free < b.criticalEnter || previous === "critical" && free < b.criticalExit) return {
3893
+ band: "critical",
3894
+ reason: b.criticalReason
3895
+ };
3896
+ const lowActive = previous === "low" || previous === "critical";
3897
+ if (free < b.lowEnter || lowActive && free < b.lowExit) {
3898
+ if (lowActive) return {
3899
+ band: "low",
3900
+ reason: b.lowReason
3901
+ };
3902
+ const since = this.belowLowSince.get(locationId) ?? this.now();
3903
+ this.belowLowSince.set(locationId, since);
3904
+ if (this.now() - since >= b.lowGraceMs) return {
3905
+ band: "low",
3906
+ reason: b.lowReason
3907
+ };
3908
+ return {
3909
+ band: previous ?? "ok",
3910
+ reason: "free-ok"
3911
+ };
3912
+ }
3913
+ this.belowLowSince.delete(locationId);
3914
+ return {
3915
+ band: "ok",
3916
+ reason: "free-ok"
3917
+ };
3629
3918
  }
3630
3919
  /**
3631
3920
  * Observe one sweep of managed locations. Locations missing from `samples`
@@ -3636,8 +3925,9 @@ var StorageLocationBandMonitor = class {
3636
3925
  const out = [];
3637
3926
  for (const sample of samples) {
3638
3927
  seen.add(sample.locationId);
3639
- const { band, reason } = bandOf(sample.freePercent, this.config);
3928
+ if (sample.freePercent === null) continue;
3640
3929
  const previous = this.band.get(sample.locationId) ?? null;
3930
+ const { band, reason } = this.nextBand(sample.locationId, previous, sample.freePercent, boundsOf(sample.floorPercent, this.config));
3641
3931
  if (previous === band) continue;
3642
3932
  this.band.set(sample.locationId, band);
3643
3933
  if (previous === null && band === "ok") continue;
@@ -3647,11 +3937,17 @@ var StorageLocationBandMonitor = class {
3647
3937
  ...sample.locationName !== void 0 ? { locationName: sample.locationName } : {},
3648
3938
  band,
3649
3939
  freePercent: sample.freePercent,
3940
+ ...sample.floorPercent !== void 0 ? { floorPercent: sample.floorPercent } : {},
3941
+ ...sample.freeBytes !== void 0 ? { freeBytes: sample.freeBytes } : {},
3942
+ ...sample.floorBytes !== void 0 ? { floorBytes: sample.floorBytes } : {},
3650
3943
  reason,
3651
3944
  previous
3652
3945
  });
3653
3946
  }
3654
- for (const id of [...this.band.keys()]) if (!seen.has(id)) this.band.delete(id);
3947
+ for (const id of [...this.band.keys()]) if (!seen.has(id)) {
3948
+ this.band.delete(id);
3949
+ this.belowLowSince.delete(id);
3950
+ }
3655
3951
  return out;
3656
3952
  }
3657
3953
  };
@@ -3699,6 +3995,7 @@ var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
3699
3995
  var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
3700
3996
  var StorageOrchestratorAddon = class extends BaseAddon {
3701
3997
  locationsKind = null;
3998
+ seedMarkersKind = null;
3702
3999
  drainStateKind = null;
3703
4000
  service = null;
3704
4001
  pressureManager = null;
@@ -3724,6 +4021,8 @@ var StorageOrchestratorAddon = class extends BaseAddon {
3724
4021
  pressureTimer = null;
3725
4022
  pressureSweepInFlight = false;
3726
4023
  migration = null;
4024
+ /** Guards every single-location repoint and arbitrates movers per physical root. */
4025
+ repointer = null;
3727
4026
  /**
3728
4027
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
3729
4028
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
@@ -3760,6 +4059,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
3760
4059
  if (records !== void 0) {
3761
4060
  await records.declare(storageOrchestratorRecordKinds);
3762
4061
  this.locationsKind = SqliteLocationStore.recordStoresOf(records);
4062
+ this.seedMarkersKind = SqliteLocationStore.seedMarkersOf(records);
3763
4063
  this.drainStateKind = SqliteDrainStateStore.recordStoresOf(records);
3764
4064
  }
3765
4065
  const getProviders = () => {
@@ -3779,6 +4079,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
3779
4079
  return proxy;
3780
4080
  });
3781
4081
  this.service = service;
4082
+ service.setStoreRequiredForSeed(this.locationsKind !== null);
3782
4083
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
3783
4084
  service.setOccupancyProbe((location) => this.locationOccupancy(location));
3784
4085
  await service.initialize();
@@ -3802,8 +4103,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
3802
4103
  ...input,
3803
4104
  config: keychained
3804
4105
  };
3805
- await this.carryDataOnRepoint(stored, next);
3806
- const saved = service.upsertLocation(next);
4106
+ const saved = await this.upsertCarryingData(stored, next, () => service.upsertLocation(next));
3807
4107
  this.announceLocationsChanged(saved.id, saved.type, "upsert");
3808
4108
  return this.redacted(saved);
3809
4109
  },
@@ -4013,6 +4313,49 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4013
4313
  deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
4014
4314
  });
4015
4315
  this.migration = migration;
4316
+ this.repointer = new LocationRepointer({
4317
+ logger: this.ctx.logger,
4318
+ resolveKey: async (basePath) => {
4319
+ try {
4320
+ return await fs.realpath(basePath);
4321
+ } catch {
4322
+ return path$1.resolve(basePath);
4323
+ }
4324
+ },
4325
+ locationBasePath: (id) => this.locationBasePath(id),
4326
+ locationIdsOfType: (type) => service.listLocations({ type }).map((loc) => loc.id),
4327
+ listMovers: () => migration.movers(),
4328
+ ioFor: async (stored, next) => {
4329
+ const target = {
4330
+ ...stored,
4331
+ config: next.config
4332
+ };
4333
+ const [source, destination] = await Promise.all([service.getProviderFor(stored), service.getProviderFor(target)]);
4334
+ return {
4335
+ list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
4336
+ location: stored,
4337
+ prefix
4338
+ }),
4339
+ read: (relativePath) => source.read({
4340
+ location: stored,
4341
+ relativePath
4342
+ }),
4343
+ write: (relativePath, data) => destination.write({
4344
+ location: target,
4345
+ relativePath,
4346
+ data
4347
+ }),
4348
+ readBack: (relativePath) => destination.read({
4349
+ location: target,
4350
+ relativePath
4351
+ }),
4352
+ deleteSource: (relativePath) => source.delete({
4353
+ location: stored,
4354
+ relativePath
4355
+ })
4356
+ };
4357
+ }
4358
+ });
4016
4359
  const cleanup = new StorageCleanupCoordinator({
4017
4360
  locations: service,
4018
4361
  state: this.state("storage-cleanup", StorageCleanupJobSchema.nullable(), null),
@@ -4048,6 +4391,13 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4048
4391
  status: ({ jobId }) => migration.status(jobId),
4049
4392
  cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
4050
4393
  movers: () => migration.movers(),
4394
+ moverClearance: async ({ locationIds }) => {
4395
+ const verdict = await this.requireRepointer().clearance(locationIds);
4396
+ return verdict.clear ? { clear: true } : {
4397
+ clear: false,
4398
+ repointLocationId: verdict.repointLocationId
4399
+ };
4400
+ },
4051
4401
  residue: () => migration.residue(),
4052
4402
  drain: async (input) => ({ jobId: await migration.drain(input) }),
4053
4403
  cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
@@ -4174,7 +4524,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4174
4524
  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");
4175
4525
  return null;
4176
4526
  }
4177
- return new SqliteLocationStore(engine, this.locationsKind);
4527
+ return new SqliteLocationStore(engine, this.locationsKind, this.seedMarkersKind);
4178
4528
  }
4179
4529
  /**
4180
4530
  * Late-wire the persistence store once a `data-store-provider` engine
@@ -4186,12 +4536,26 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4186
4536
  * restart — the service never hydrates from / persists to SQLite.
4187
4537
  */
4188
4538
  storeWireInFlight = null;
4539
+ /** True while the locations kind is empty and its legacy table still holds rows. */
4540
+ async adoptionIsPending() {
4541
+ const records = this.ctx.records;
4542
+ if (records === void 0 || this.locationsKind === null) return false;
4543
+ try {
4544
+ await records.declare(storageOrchestratorRecordKinds);
4545
+ return false;
4546
+ } catch (err) {
4547
+ const message = err instanceof Error ? err.message : String(err);
4548
+ 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 } });
4549
+ return true;
4550
+ }
4551
+ }
4189
4552
  async ensureStoreWired() {
4190
4553
  const service = this.service;
4191
4554
  if (!service || service.hasStore()) return;
4192
4555
  if (this.storeWireInFlight) return this.storeWireInFlight;
4193
4556
  const store = this.resolveLocationStore(true);
4194
4557
  if (!store) return;
4558
+ if (await this.adoptionIsPending()) return;
4195
4559
  this.ctx.logger.info("storage-orchestrator: data-store engine now available — wiring persistence");
4196
4560
  this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
4197
4561
  this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -4220,7 +4584,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4220
4584
  }
4221
4585
  /**
4222
4586
  * Sample free space for every managed location and emit NC band edges.
4223
- * A failed capacity read stays `null` (critical) — never folded to 0 (D393).
4587
+ * A failed capacity read stays `null` and changes no band — never folded to 0 (D393).
4224
4588
  */
4225
4589
  async emitLocationBandEvents() {
4226
4590
  const svc = this.service;
@@ -4236,11 +4600,18 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4236
4600
  };
4237
4601
  const capacity = await this.volumeCapacityOf(locationId).catch(() => null);
4238
4602
  const freePercent = capacity === null ? null : freePercentOf(capacity);
4603
+ if (capacity === null) ctx.logger.debug("storage: location capacity unreadable — band unchanged", { meta: { locationId } });
4604
+ const floorPercent = thresholds.get(locationId);
4239
4605
  return {
4240
4606
  locationId,
4241
4607
  nodeId: loc.nodeId,
4242
4608
  locationName: loc.displayName,
4243
- freePercent
4609
+ freePercent,
4610
+ ...floorPercent !== void 0 ? { floorPercent } : {},
4611
+ ...capacity !== null ? {
4612
+ freeBytes: capacity.availableBytes,
4613
+ ...floorPercent !== void 0 ? { floorBytes: Math.round(capacity.totalBytes * floorPercent / 100) } : {}
4614
+ } : {}
4244
4615
  };
4245
4616
  }));
4246
4617
  for (const edge of this.locationBandMonitor.observe(samples)) {
@@ -4249,6 +4620,9 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4249
4620
  locationId: edge.locationId,
4250
4621
  band: edge.band,
4251
4622
  freePercent: edge.freePercent,
4623
+ floorPercent: edge.floorPercent ?? null,
4624
+ freeBytes: edge.freeBytes ?? null,
4625
+ floorBytes: edge.floorBytes ?? null,
4252
4626
  reason: edge.reason
4253
4627
  } });
4254
4628
  ctx.eventBus.emit({
@@ -4362,50 +4736,19 @@ var StorageOrchestratorAddon = class extends BaseAddon {
4362
4736
  * A no-op for everything else — a rename, a mode change, a `multi` kind, or a
4363
4737
  * location being created for the first time.
4364
4738
  */
4365
- async carryDataOnRepoint(stored, next) {
4366
- if (stored === void 0) return;
4739
+ async upsertCarryingData(stored, next, commit) {
4740
+ if (stored === void 0) return commit();
4367
4741
  const service = this.service;
4368
- if (service === null) return;
4369
- if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return;
4742
+ if (service === null) return commit();
4743
+ if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return commit();
4370
4744
  const from = stored.config["basePath"];
4371
4745
  const to = next.config["basePath"];
4372
- if (typeof from !== "string" || typeof to !== "string" || from === to) return;
4373
- const source = await service.getProviderFor(stored);
4374
- const target = {
4375
- ...stored,
4376
- config: next.config
4377
- };
4378
- const destination = await service.getProviderFor(target);
4379
- const moved = await moveLocationTree({
4380
- list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
4381
- location: stored,
4382
- prefix
4383
- }),
4384
- read: (relativePath) => source.read({
4385
- location: stored,
4386
- relativePath
4387
- }),
4388
- write: (relativePath, data) => destination.write({
4389
- location: target,
4390
- relativePath,
4391
- data
4392
- }),
4393
- readBack: (relativePath) => destination.read({
4394
- location: target,
4395
- relativePath
4396
- }),
4397
- deleteSource: (relativePath) => source.delete({
4398
- location: stored,
4399
- relativePath
4400
- })
4401
- });
4402
- this.ctx.logger.info("storage-orchestrator: singleton location re-pointed — its data was carried over", { meta: {
4403
- id: stored.id,
4404
- type: stored.type,
4405
- from,
4406
- to,
4407
- ...moved
4408
- } });
4746
+ if (typeof from !== "string" || typeof to !== "string" || from === to) return commit();
4747
+ return this.requireRepointer().run(stored, next, commit);
4748
+ }
4749
+ requireRepointer() {
4750
+ if (this.repointer === null) throw new Error("storage-orchestrator: not initialized");
4751
+ return this.repointer;
4409
4752
  }
4410
4753
  secretKeysFor(providerId, config) {
4411
4754
  return secretKeysForConfig(config, this.declaredSecretKeys(providerId));