@camstack/system 1.2.22 → 1.2.23

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 (60) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +0 -9
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -40
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -40
  12. package/dist/builtins/console-logging/index.js +1 -1
  13. package/dist/builtins/console-logging/index.mjs +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  22. package/dist/builtins/local-network/local-network.addon.d.ts +16 -0
  23. package/dist/builtins/local-network/local-network.addon.js +30 -2
  24. package/dist/builtins/local-network/local-network.addon.mjs +30 -2
  25. package/dist/builtins/loki-logging/index.js +34 -12
  26. package/dist/builtins/loki-logging/index.mjs +34 -12
  27. package/dist/builtins/loki-logging/loki-logging.addon.d.ts +19 -0
  28. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  30. package/dist/builtins/platform-probe/index.js +1 -1
  31. package/dist/builtins/platform-probe/index.mjs +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  34. package/dist/builtins/snapshot/index.js +1 -1
  35. package/dist/builtins/snapshot/index.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  40. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +7 -0
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +108 -4
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +108 -4
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +11 -1
  44. package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +5 -0
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-C9_XdXl5.mjs → dist-CIek6F9X.mjs} +436 -72
  50. package/dist/{dist-j49ewCPY.js → dist-CS0pdC8l.js} +436 -72
  51. package/dist/index.js +58 -8
  52. package/dist/index.mjs +58 -8
  53. package/dist/kernel/capability-registry.d.ts +9 -0
  54. package/dist/kernel/transport/local-child-registry.d.ts +18 -0
  55. package/dist/kernel/transport/parent-unowned-call.d.ts +22 -0
  56. package/dist/logging/log-manager.d.ts +32 -1
  57. package/dist/logging/partitioned-log-buffer.d.ts +15 -3
  58. package/dist/{manifest-python-deps-B30qVAuW.mjs → manifest-python-deps-57PA4rHm.mjs} +15 -6
  59. package/dist/{manifest-python-deps-CnRW2Utv.js → manifest-python-deps-CqDPWqEE.js} +14 -5
  60. package/package.json +1 -1
@@ -1,7 +1,15 @@
1
1
  import { LogLevel, ProviderRegistration, BaseAddon } from '@camstack/types';
2
2
  import { LokiDestination } from './loki-destination.js';
3
3
  interface LokiAddonConfig {
4
+ /**
5
+ * RETIRED (2026-07-30). Enablement is the orchestrator's per-destination
6
+ * toggle now — the ONE truth for every log destination. Kept only so a
7
+ * stored `false` can be migrated ONCE into a registry disable: without that,
8
+ * an operator who had Loki off would silently start shipping logs after this
9
+ * update. `migratedEnabled` marks the migration done; the field is then dead.
10
+ */
4
11
  readonly enabled: boolean;
12
+ readonly migratedEnabled: boolean;
5
13
  readonly url: string;
6
14
  readonly job: string;
7
15
  readonly level: LogLevel;
@@ -17,6 +25,17 @@ export declare class LokiLoggingAddon extends BaseAddon<LokiAddonConfig> {
17
25
  private destination;
18
26
  constructor();
19
27
  protected onInitialize(): Promise<ProviderRegistration[]>;
28
+ /**
29
+ * ONE-SHOT: carry a stored `enabled: false` over to the orchestrator's
30
+ * per-destination toggle, then never look at the flag again.
31
+ *
32
+ * Without this, retiring the private setting would silently RE-ENABLE Loki
33
+ * for every operator who had turned it off — shipping their logs to a host
34
+ * they may no longer control. Best-effort: if the toggle call fails the
35
+ * migration is simply retried next boot (the flag is only cleared on
36
+ * success).
37
+ */
38
+ private migrateRetiredEnabledFlag;
20
39
  protected onShutdown(): Promise<void>;
21
40
  /**
22
41
  * React to a settings change (enable/disable, URL, level, batching) without a
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { H as metricsProviderCapability, at as BaseAddon, jt as EventCategory, vt as createEvent } from "../../dist-C9_XdXl5.mjs";
1
+ import { H as metricsProviderCapability, at as BaseAddon, jt as EventCategory, vt as createEvent } from "../../dist-CIek6F9X.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { J as scoreRuntimes, K as platformProbeCapability, T as enumerateInferenceDevices, at as BaseAddon, bt as emitReadiness, it as errMsg, jt as EventCategory } from "../../dist-C9_XdXl5.mjs";
2
+ import { J as scoreRuntimes, K as platformProbeCapability, T as enumerateInferenceDevices, at as BaseAddon, bt as emitReadiness, it as errMsg, jt as EventCategory } from "../../dist-CIek6F9X.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { at as BaseAddon, jt as EventCategory } from "../../dist-C9_XdXl5.mjs";
1
+ import { at as BaseAddon, jt as EventCategory } from "../../dist-CIek6F9X.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/builtins/snapshot/snapshot-coalescing.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { Z as snapshotCapability, at as BaseAddon, dt as DeviceType, et as streamQualityLabel, i as BatteryStatusSchema, it as errMsg, lt as DeviceFeature, wt as nodePin } from "../../dist-C9_XdXl5.mjs";
1
+ import { Z as snapshotCapability, at as BaseAddon, dt as DeviceType, et as streamQualityLabel, i as BatteryStatusSchema, it as errMsg, lt as DeviceFeature, wt as nodePin } from "../../dist-CIek6F9X.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  //#region src/builtins/snapshot/snapshot-coalescing.ts
4
4
  /**
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { $ as storageProviderCapability, A as filesystemBrowseCapability, at as BaseAddon } from "../../dist-C9_XdXl5.mjs";
1
+ import { $ as storageProviderCapability, A as filesystemBrowseCapability, at as BaseAddon } from "../../dist-CIek6F9X.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import * as path$1 from "node:path";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let better_sqlite3 = require("better-sqlite3");
9
9
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
@@ -1,4 +1,4 @@
1
- import { Et as parseJsonUnknown, X as settingsStoreCapability, at as BaseAddon, c as RUNTIME_DEFAULTS, ht as asJsonObject, it as errMsg } from "../../dist-C9_XdXl5.mjs";
1
+ import { Et as parseJsonUnknown, X as settingsStoreCapability, at as BaseAddon, c as RUNTIME_DEFAULTS, ht as asJsonObject, it as errMsg } from "../../dist-CIek6F9X.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import Database from "better-sqlite3";
4
4
  //#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
@@ -64,6 +64,13 @@ export declare class StorageOrchestratorAddon extends BaseAddon<StorageOrchestra
64
64
  * the recorder so disk-pressure is a single core concern.
65
65
  */
66
66
  private resolveLocationThresholds;
67
+ /** statfs capacity of a location's basePath, walking up to the nearest
68
+ * existing ancestor. Null for remote-node locations or unstattable paths. */
69
+ private localCapacityOf;
70
+ /** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
71
+ * "recordings may use at most N GB in total" — enforced by the pressure
72
+ * manager alongside the free-percent guard. */
73
+ private resolveLocationMaxUsedBytes;
67
74
  private locationBasePath;
68
75
  /**
69
76
  * A stable key for the PHYSICAL storage device behind a location — the
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -14,6 +14,8 @@ var StorageOrchestratorService = class {
14
14
  logger;
15
15
  getProviders;
16
16
  locationStore;
17
+ localNodeId;
18
+ getRemoteProvider;
17
19
  locations = /* @__PURE__ */ new Map();
18
20
  /**
19
21
  * Declaration-driven cardinality source, injected by the addon after
@@ -31,10 +33,12 @@ var StorageOrchestratorService = class {
31
33
  * early-boot path that predates the resolver wiring.
32
34
  */
33
35
  nodeLocalResolver = null;
34
- constructor(logger, getProviders, locationStore = null) {
36
+ constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
35
37
  this.logger = logger;
36
38
  this.getProviders = getProviders;
37
39
  this.locationStore = locationStore;
40
+ this.localNodeId = localNodeId;
41
+ this.getRemoteProvider = getRemoteProvider;
38
42
  }
39
43
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
40
44
  hasStore() {
@@ -349,6 +353,12 @@ var StorageOrchestratorService = class {
349
353
  * the right place to fix the config.
350
354
  */
351
355
  async getProviderFor(location) {
356
+ const nodeId = location.nodeId;
357
+ if (nodeId !== void 0 && nodeId !== this.localNodeId) {
358
+ const remote = this.getRemoteProvider?.(nodeId);
359
+ if (remote) return remote;
360
+ throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
361
+ }
352
362
  const providers = this.getProviders();
353
363
  for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
354
364
  throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
@@ -634,6 +644,53 @@ var StoragePressureManager = class {
634
644
  } });
635
645
  }
636
646
  }
647
+ const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
648
+ for (const [locationId, maxBytes] of caps) {
649
+ if (relievedDevices.size > 0) {
650
+ const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
651
+ if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
652
+ }
653
+ try {
654
+ await this.relieveToUsageCap(locationId, maxBytes);
655
+ } catch (err) {
656
+ this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
657
+ locationId,
658
+ error: err instanceof Error ? err.message : String(err)
659
+ } });
660
+ }
661
+ }
662
+ }
663
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
664
+ async relieveToUsageCap(locationId, maxBytes) {
665
+ const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
666
+ for (let round = 0; round < maxRounds; round++) {
667
+ const providers = this.deps.getEvictableProviders();
668
+ const usages = await Promise.all(providers.map(async (p) => ({
669
+ p,
670
+ bytes: (await p.getEvictableUsage({ locationId })).bytes
671
+ })));
672
+ const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
673
+ const excess = totalUsed - maxBytes;
674
+ if (excess <= 0) return;
675
+ let reclaimed = 0;
676
+ for (const u of usages) {
677
+ if (u.bytes <= 0) continue;
678
+ const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
679
+ const res = await u.p.evict({
680
+ locationId,
681
+ targetBytes: share
682
+ });
683
+ reclaimed += res.reclaimedBytes;
684
+ }
685
+ this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
686
+ locationId,
687
+ round,
688
+ totalUsed,
689
+ maxBytes,
690
+ reclaimedBytes: reclaimed
691
+ } });
692
+ if (reclaimed <= 0) return;
693
+ }
637
694
  }
638
695
  /**
639
696
  * Relieve one location until it recovers, nothing is evictable, or a round
@@ -772,13 +829,21 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
772
829
  return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
773
830
  };
774
831
  const locationStore = this.resolveLocationStore();
775
- const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
832
+ const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
833
+ const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
834
+ if (!proxy) return null;
835
+ return proxy;
836
+ });
776
837
  this.service = service;
777
838
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
778
839
  await service.initialize();
779
840
  const provider = {
780
841
  listLocations: async ({ type }) => {
781
- return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
842
+ const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
843
+ return Promise.all(rows.map(async (loc) => ({
844
+ ...loc,
845
+ capacity: await this.localCapacityOf(loc)
846
+ })));
782
847
  },
783
848
  getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
784
849
  listLocationDeclarations: async () => service.listDeclarations(),
@@ -944,6 +1009,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
944
1009
  getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
945
1010
  getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
946
1011
  getLocationThresholds: () => this.resolveLocationThresholds(),
1012
+ getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
947
1013
  getLocationDeviceKey: (id) => this.locationDeviceKey(id)
948
1014
  });
949
1015
  this.pressureTimer = setInterval(() => {
@@ -1053,6 +1119,44 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1053
1119
  }
1054
1120
  return out;
1055
1121
  }
1122
+ /** statfs capacity of a location's basePath, walking up to the nearest
1123
+ * existing ancestor. Null for remote-node locations or unstattable paths. */
1124
+ async localCapacityOf(loc) {
1125
+ if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1126
+ const bp = loc.config["basePath"];
1127
+ if (typeof bp !== "string" || bp.length === 0) return null;
1128
+ try {
1129
+ let target = node_path.resolve(bp);
1130
+ for (;;) try {
1131
+ await node_fs_promises.access(target);
1132
+ break;
1133
+ } catch {
1134
+ const parent = node_path.dirname(target);
1135
+ if (!parent || parent === target) return null;
1136
+ target = parent;
1137
+ }
1138
+ const st = await node_fs_promises.statfs(target);
1139
+ return {
1140
+ totalBytes: st.blocks * st.bsize,
1141
+ availableBytes: st.bavail * st.bsize
1142
+ };
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ /** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
1148
+ * "recordings may use at most N GB in total" — enforced by the pressure
1149
+ * manager alongside the free-percent guard. */
1150
+ async resolveLocationMaxUsedBytes() {
1151
+ const out = /* @__PURE__ */ new Map();
1152
+ const svc = this.service;
1153
+ if (!svc) return out;
1154
+ for (const loc of svc.listLocations()) {
1155
+ const raw = loc.config["maxUsedGb"];
1156
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
1157
+ }
1158
+ return out;
1159
+ }
1056
1160
  locationBasePath(locationId) {
1057
1161
  const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
1058
1162
  return typeof bp === "string" && bp.length > 0 ? bp : null;
@@ -1,4 +1,4 @@
1
- import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-C9_XdXl5.mjs";
1
+ import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-CIek6F9X.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { buildStorageLocationRegistry } from "@camstack/system";
@@ -7,6 +7,8 @@ var StorageOrchestratorService = class {
7
7
  logger;
8
8
  getProviders;
9
9
  locationStore;
10
+ localNodeId;
11
+ getRemoteProvider;
10
12
  locations = /* @__PURE__ */ new Map();
11
13
  /**
12
14
  * Declaration-driven cardinality source, injected by the addon after
@@ -24,10 +26,12 @@ var StorageOrchestratorService = class {
24
26
  * early-boot path that predates the resolver wiring.
25
27
  */
26
28
  nodeLocalResolver = null;
27
- constructor(logger, getProviders, locationStore = null) {
29
+ constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
28
30
  this.logger = logger;
29
31
  this.getProviders = getProviders;
30
32
  this.locationStore = locationStore;
33
+ this.localNodeId = localNodeId;
34
+ this.getRemoteProvider = getRemoteProvider;
31
35
  }
32
36
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
33
37
  hasStore() {
@@ -342,6 +346,12 @@ var StorageOrchestratorService = class {
342
346
  * the right place to fix the config.
343
347
  */
344
348
  async getProviderFor(location) {
349
+ const nodeId = location.nodeId;
350
+ if (nodeId !== void 0 && nodeId !== this.localNodeId) {
351
+ const remote = this.getRemoteProvider?.(nodeId);
352
+ if (remote) return remote;
353
+ throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
354
+ }
345
355
  const providers = this.getProviders();
346
356
  for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
347
357
  throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
@@ -627,6 +637,53 @@ var StoragePressureManager = class {
627
637
  } });
628
638
  }
629
639
  }
640
+ const caps = await this.deps.getLocationMaxUsedBytes?.() ?? /* @__PURE__ */ new Map();
641
+ for (const [locationId, maxBytes] of caps) {
642
+ if (relievedDevices.size > 0) {
643
+ const deviceKey = await this.deps.getLocationDeviceKey?.(locationId) ?? null;
644
+ if (deviceKey !== null && relievedDevices.has(deviceKey)) continue;
645
+ }
646
+ try {
647
+ await this.relieveToUsageCap(locationId, maxBytes);
648
+ } catch (err) {
649
+ this.deps.logger.warn("storage pressure: usage-cap relief failed", { meta: {
650
+ locationId,
651
+ error: err instanceof Error ? err.message : String(err)
652
+ } });
653
+ }
654
+ }
655
+ }
656
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
657
+ async relieveToUsageCap(locationId, maxBytes) {
658
+ const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
659
+ for (let round = 0; round < maxRounds; round++) {
660
+ const providers = this.deps.getEvictableProviders();
661
+ const usages = await Promise.all(providers.map(async (p) => ({
662
+ p,
663
+ bytes: (await p.getEvictableUsage({ locationId })).bytes
664
+ })));
665
+ const totalUsed = usages.reduce((s, u) => s + u.bytes, 0);
666
+ const excess = totalUsed - maxBytes;
667
+ if (excess <= 0) return;
668
+ let reclaimed = 0;
669
+ for (const u of usages) {
670
+ if (u.bytes <= 0) continue;
671
+ const share = Math.max(1, Math.ceil(excess * u.bytes / totalUsed));
672
+ const res = await u.p.evict({
673
+ locationId,
674
+ targetBytes: share
675
+ });
676
+ reclaimed += res.reclaimedBytes;
677
+ }
678
+ this.deps.logger.info("storage pressure: usage-cap relief round", { meta: {
679
+ locationId,
680
+ round,
681
+ totalUsed,
682
+ maxBytes,
683
+ reclaimedBytes: reclaimed
684
+ } });
685
+ if (reclaimed <= 0) return;
686
+ }
630
687
  }
631
688
  /**
632
689
  * Relieve one location until it recovers, nothing is evictable, or a round
@@ -765,13 +822,21 @@ var StorageOrchestratorAddon = class extends BaseAddon {
765
822
  return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
766
823
  };
767
824
  const locationStore = this.resolveLocationStore();
768
- const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
825
+ const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore, this.ctx.kernel.localNodeId ?? "hub", (nodeId) => {
826
+ const proxy = this.ctx.kernel.resolveNodeCapability?.("storage-provider", nodeId);
827
+ if (!proxy) return null;
828
+ return proxy;
829
+ });
769
830
  this.service = service;
770
831
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
771
832
  await service.initialize();
772
833
  const provider = {
773
834
  listLocations: async ({ type }) => {
774
- return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
835
+ const rows = type !== void 0 ? service.listLocations({ type }) : service.listLocations();
836
+ return Promise.all(rows.map(async (loc) => ({
837
+ ...loc,
838
+ capacity: await this.localCapacityOf(loc)
839
+ })));
775
840
  },
776
841
  getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
777
842
  listLocationDeclarations: async () => service.listDeclarations(),
@@ -937,6 +1002,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
937
1002
  getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
938
1003
  getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
939
1004
  getLocationThresholds: () => this.resolveLocationThresholds(),
1005
+ getLocationMaxUsedBytes: () => this.resolveLocationMaxUsedBytes(),
940
1006
  getLocationDeviceKey: (id) => this.locationDeviceKey(id)
941
1007
  });
942
1008
  this.pressureTimer = setInterval(() => {
@@ -1046,6 +1112,44 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1046
1112
  }
1047
1113
  return out;
1048
1114
  }
1115
+ /** statfs capacity of a location's basePath, walking up to the nearest
1116
+ * existing ancestor. Null for remote-node locations or unstattable paths. */
1117
+ async localCapacityOf(loc) {
1118
+ if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1119
+ const bp = loc.config["basePath"];
1120
+ if (typeof bp !== "string" || bp.length === 0) return null;
1121
+ try {
1122
+ let target = path$1.resolve(bp);
1123
+ for (;;) try {
1124
+ await fs.access(target);
1125
+ break;
1126
+ } catch {
1127
+ const parent = path$1.dirname(target);
1128
+ if (!parent || parent === target) return null;
1129
+ target = parent;
1130
+ }
1131
+ const st = await fs.statfs(target);
1132
+ return {
1133
+ totalBytes: st.blocks * st.bsize,
1134
+ availableBytes: st.bavail * st.bsize
1135
+ };
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ /** Per-location cap on EVICTABLE bytes (`config.maxUsedGb`): the operator's
1141
+ * "recordings may use at most N GB in total" — enforced by the pressure
1142
+ * manager alongside the free-percent guard. */
1143
+ async resolveLocationMaxUsedBytes() {
1144
+ const out = /* @__PURE__ */ new Map();
1145
+ const svc = this.service;
1146
+ if (!svc) return out;
1147
+ for (const loc of svc.listLocations()) {
1148
+ const raw = loc.config["maxUsedGb"];
1149
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) out.set(loc.id, Math.floor(raw * 1024 ** 3));
1150
+ }
1151
+ return out;
1152
+ }
1049
1153
  locationBasePath(locationId) {
1050
1154
  const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
1051
1155
  return typeof bp === "string" && bp.length > 0 ? bp : null;
@@ -54,6 +54,11 @@ export declare class StorageOrchestratorService {
54
54
  * the provider appears. See {@link attachStore}.
55
55
  */
56
56
  private locationStore;
57
+ /** This node's id — locations pinned to OTHER nodes dispatch remotely. */
58
+ private readonly localNodeId;
59
+ /** Cross-node provider resolver (hub wiring: a node-routed capability
60
+ * proxy over `storage-provider`). Absent → remote locations error. */
61
+ private readonly getRemoteProvider?;
57
62
  private readonly locations;
58
63
  /**
59
64
  * Declaration-driven cardinality source, injected by the addon after
@@ -85,7 +90,12 @@ export declare class StorageOrchestratorService {
85
90
  * service starts store-less and the addon calls {@link attachStore} once
86
91
  * the provider appears. See {@link attachStore}.
87
92
  */
88
- locationStore?: ILocationStore | null);
93
+ locationStore?: ILocationStore | null,
94
+ /** This node's id — locations pinned to OTHER nodes dispatch remotely. */
95
+ localNodeId?: string,
96
+ /** Cross-node provider resolver (hub wiring: a node-routed capability
97
+ * proxy over `storage-provider`). Absent → remote locations error. */
98
+ getRemoteProvider?: ((nodeId: string) => IStorageProviderImpl | null) | undefined);
89
99
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
90
100
  hasStore(): boolean;
91
101
  /**
@@ -24,6 +24,9 @@ export interface PressureManagerDeps {
24
24
  readonly getEvictableProviders: () => readonly IStorageEvictableProvider[];
25
25
  /** locationId → minFreePercent threshold; entries with `<= 0` have the guard off. */
26
26
  readonly getLocationThresholds: () => Promise<ReadonlyMap<string, number>>;
27
+ /** locationId → hard cap on EVICTABLE bytes (`config.maxUsedGb`, operator's
28
+ * "recordings may use at most N GB total"). Optional — absent = no caps. */
29
+ readonly getLocationMaxUsedBytes?: () => Promise<ReadonlyMap<string, number>>;
27
30
  /** Max relief rounds per location per sweep (backstop, default 8). */
28
31
  readonly maxRounds?: number;
29
32
  /** Extra headroom % freed beyond the threshold to avoid re-breaching immediately (default 1). */
@@ -53,6 +56,8 @@ export declare class StoragePressureManager {
53
56
  constructor(deps: PressureManagerDeps);
54
57
  /** Relieve every managed location currently under its threshold. */
55
58
  sweep(): Promise<void>;
59
+ /** Evict oldest-first until the location's evictable usage fits `maxBytes`. */
60
+ private relieveToUsageCap;
56
61
  /**
57
62
  * Relieve one location until it recovers, nothing is evictable, or a round
58
63
  * makes no progress. Returns `true` when at least one relief round actually
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-C9_XdXl5.mjs";
1
+ import { St as hydrateSchema, at as BaseAddon, it as errMsg } from "../../dist-CIek6F9X.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -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-j49ewCPY.js");
6
+ const require_dist = require("../../dist-CS0pdC8l.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { at as BaseAddon, z as logDestinationCapability } from "../../dist-C9_XdXl5.mjs";
1
+ import { at as BaseAddon, z as logDestinationCapability } from "../../dist-CIek6F9X.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";