@camstack/system 1.2.24 → 1.2.26

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 (53) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/device-manager/device-aggregation.d.ts +18 -0
  14. package/dist/builtins/device-manager/device-manager.addon.js +47 -1
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +47 -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.js +1 -1
  23. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  24. package/dist/builtins/loki-logging/index.js +1 -1
  25. package/dist/builtins/loki-logging/index.mjs +1 -1
  26. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  28. package/dist/builtins/platform-probe/index.js +1 -1
  29. package/dist/builtins/platform-probe/index.mjs +1 -1
  30. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  32. package/dist/builtins/snapshot/index.js +1 -1
  33. package/dist/builtins/snapshot/index.mjs +1 -1
  34. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  38. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +0 -19
  39. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +143 -67
  40. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +143 -67
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +67 -7
  42. package/dist/builtins/system-config/system-config.addon.js +1 -1
  43. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  44. package/dist/builtins/winston-logging/index.js +1 -1
  45. package/dist/builtins/winston-logging/index.mjs +1 -1
  46. package/dist/{dist-CS0pdC8l.js → dist-Cu72wsz5.js} +132 -3
  47. package/dist/{dist-CIek6F9X.mjs → dist-DimX9eZy.mjs} +132 -3
  48. package/dist/index.js +2 -2
  49. package/dist/index.mjs +2 -2
  50. package/dist/kernel/moleculer/settings-read-guard.d.ts +49 -0
  51. package/dist/{manifest-python-deps-BbOZZbVt.js → manifest-python-deps-COgal7Jy.js} +68 -17
  52. package/dist/{manifest-python-deps-C9qhvMUi.mjs → manifest-python-deps-julrfvyf.mjs} +68 -17
  53. package/package.json +1 -1
@@ -3,18 +3,44 @@ 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-CS0pdC8l.js");
6
+ const require_dist = require("../../dist-Cu72wsz5.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");
10
10
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
11
11
  let _camstack_system = require("@camstack/system");
12
12
  //#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
13
+ var SESSION_VOCABULARY = {
14
+ upload: {
15
+ idLabel: "uploadId",
16
+ openMethod: "beginUpload"
17
+ },
18
+ download: {
19
+ idLabel: "downloadId",
20
+ openMethod: "beginDownload"
21
+ }
22
+ };
23
+ /** Map key for a tracked chunked session — the id spaces never collide. */
24
+ function sessionKey(kind, sessionId) {
25
+ return `${kind}:${sessionId}`;
26
+ }
27
+ /**
28
+ * Bare node id for a raw kernel `localNodeId`. Forked children carry a
29
+ * `<node>/<addon>` suffix; routing is per-NODE, so the addon segment is
30
+ * dropped. (Mirrors `normalizeNodeId` in `@camstack/types/addon`, which is a
31
+ * leaf module not re-exported on any subpath this package can import.)
32
+ */
33
+ function bareNodeId(raw) {
34
+ if (raw === void 0 || raw === "") return "hub";
35
+ const slash = raw.indexOf("/");
36
+ if (slash < 0) return raw;
37
+ const bare = raw.slice(0, slash);
38
+ return bare === "" ? "hub" : bare;
39
+ }
13
40
  var StorageOrchestratorService = class {
14
41
  logger;
15
42
  getProviders;
16
43
  locationStore;
17
- localNodeId;
18
44
  getRemoteProvider;
19
45
  locations = /* @__PURE__ */ new Map();
20
46
  /**
@@ -33,12 +59,29 @@ var StorageOrchestratorService = class {
33
59
  * early-boot path that predates the resolver wiring.
34
60
  */
35
61
  nodeLocalResolver = null;
62
+ /** `localNodeId` with any forked-child `/addon` suffix stripped. */
63
+ localNode;
64
+ /**
65
+ * `<kind>:<sessionId>` → the dispatch target that OPENED the session.
66
+ * Chunked I/O carries no `StorageLocation` after the first hop, so without
67
+ * this the follow-up chunks fall back to a `providerId`-only lookup — which
68
+ * on a multi-node cluster means the hub's filesystem, not the agent's.
69
+ */
70
+ sessionTargets = /* @__PURE__ */ new Map();
36
71
  constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
37
72
  this.logger = logger;
38
73
  this.getProviders = getProviders;
39
74
  this.locationStore = locationStore;
40
- this.localNodeId = localNodeId;
41
75
  this.getRemoteProvider = getRemoteProvider;
76
+ this.localNode = bareNodeId(localNodeId);
77
+ }
78
+ /**
79
+ * The node this orchestrator runs on, with any forked-child `/addon`
80
+ * suffix stripped. A location whose `nodeId` differs is NOT this node's
81
+ * to touch — its bytes, and its free space, live elsewhere.
82
+ */
83
+ getLocalNodeId() {
84
+ return this.localNode;
42
85
  }
43
86
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
44
87
  hasStore() {
@@ -346,22 +389,96 @@ var StorageOrchestratorService = class {
346
389
  return def;
347
390
  }
348
391
  /**
349
- * Find the storage-provider that backs a given location. Lookup is by
350
- * `location.providerId` against `getProviderInfo().providerId` from
351
- * each registered collection provider. Throws with both the missing
352
- * providerId and the offending location id so the operator can pick
353
- * the right place to fix the config.
392
+ * Find the storage-provider that backs a given location the entry point
393
+ * every consumer-facing `storage` cap method funnels through.
394
+ *
395
+ * Routing key is `(providerId, nodeId)`, never `providerId` alone: every
396
+ * node's FilesystemStorageProvider registers under the SAME providerId, so
397
+ * a providerId-only match resolves to whichever node asked. That is how a
398
+ * location created on an agent ended up writing to the HUB's filesystem
399
+ * (root-caused 2026-07-29).
354
400
  */
355
401
  async getProviderFor(location) {
356
- const nodeId = location.nodeId;
357
- if (nodeId !== void 0 && nodeId !== this.localNodeId) {
402
+ return this.getProviderForTarget({
403
+ providerId: location.providerId,
404
+ ...location.nodeId !== void 0 && { nodeId: location.nodeId }
405
+ }, `location "${location.id}"`, location.id);
406
+ }
407
+ /**
408
+ * Resolve one dispatch target to the provider that must execute it.
409
+ *
410
+ * `descriptor` is the operator-facing subject of the error message
411
+ * (`location "recordings:disk"`, `uploadId "u-42"`); `locationId` is the
412
+ * structured tag for the log line when there is a location behind the
413
+ * dispatch (chunked sessions carry only the session id).
414
+ *
415
+ * Both failure branches LOG before throwing, and NEITHER falls back to the
416
+ * local provider — a silent fallback is the bug this method exists to fix.
417
+ */
418
+ async getProviderForTarget(target, descriptor, locationId) {
419
+ const nodeId = target.nodeId;
420
+ if (nodeId !== void 0 && bareNodeId(nodeId) !== this.localNode) {
358
421
  const remote = this.getRemoteProvider?.(nodeId);
359
422
  if (remote) return remote;
360
- throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
423
+ this.logger.error("storage-orchestrator: target node has no reachable storage-provider — dispatch refused", { meta: {
424
+ ...locationId !== void 0 && { locationId },
425
+ descriptor,
426
+ nodeId,
427
+ providerId: target.providerId,
428
+ localNodeId: this.localNode,
429
+ crossNodeResolverWired: this.getRemoteProvider !== void 0
430
+ } });
431
+ throw new Error(`storage-provider for ${descriptor} lives on node "${nodeId}" which is not reachable`);
361
432
  }
362
433
  const providers = this.getProviders();
363
- for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
364
- throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
434
+ for (const p of providers) if ((await p.getProviderInfo()).providerId === target.providerId) return p;
435
+ this.logger.error("storage-orchestrator: no local storage-provider for providerId", { meta: {
436
+ ...locationId !== void 0 && { locationId },
437
+ descriptor,
438
+ providerId: target.providerId,
439
+ localNodeId: this.localNode,
440
+ registered: providers.length
441
+ } });
442
+ throw new Error(`No storage-provider registered for providerId "${target.providerId}" (${descriptor})`);
443
+ }
444
+ /**
445
+ * Remember which provider — and on which NODE — opened a chunked session.
446
+ * `beginUpload` / `beginDownload` carry a `StorageLocation`; every later
447
+ * chunk carries only the session id, and a session id issued by an agent
448
+ * means nothing to the hub's provider.
449
+ */
450
+ rememberSession(kind, sessionId, location) {
451
+ this.sessionTargets.set(sessionKey(kind, sessionId), {
452
+ providerId: location.providerId,
453
+ ...location.nodeId !== void 0 && { nodeId: location.nodeId }
454
+ });
455
+ }
456
+ /** Drop a session's routing entry (finalize / abort / end). */
457
+ forgetSession(kind, sessionId) {
458
+ this.sessionTargets.delete(sessionKey(kind, sessionId));
459
+ }
460
+ /** Drop every session entry (addon shutdown). */
461
+ clearSessions() {
462
+ this.sessionTargets.clear();
463
+ }
464
+ /**
465
+ * Resolve the provider that owns an in-flight chunked session, on the node
466
+ * that opened it. Throws when the orchestrator has no record of the open
467
+ * call (provider gone, or a chunk for a session it never issued).
468
+ */
469
+ async getProviderForSession(kind, sessionId) {
470
+ const vocabulary = SESSION_VOCABULARY[kind];
471
+ const descriptor = `${vocabulary.idLabel} "${sessionId}"`;
472
+ const target = this.sessionTargets.get(sessionKey(kind, sessionId));
473
+ if (!target) {
474
+ this.logger.error("storage-orchestrator: chunk for an unknown session — dispatch refused", { meta: {
475
+ descriptor,
476
+ kind,
477
+ sessionId
478
+ } });
479
+ throw new Error(`Unknown ${vocabulary.idLabel} "${sessionId}" — orchestrator has no record of ${vocabulary.openMethod}`);
480
+ }
481
+ return this.getProviderForTarget(target, descriptor);
365
482
  }
366
483
  /** Convenience: lookup by id (returns `undefined` if not found). */
367
484
  getLocationById(id) {
@@ -795,16 +912,6 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
795
912
  pressureTimer = null;
796
913
  pressureSweepInFlight = false;
797
914
  /**
798
- * `uploadId` → providerId. Populated on `beginUpload`, consulted on
799
- * every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
800
- * cleared on terminal calls. The provider holds the actual session
801
- * state (open file descriptor, buffered offsets, …) — we just
802
- * remember which provider owns it.
803
- */
804
- uploadOwners = /* @__PURE__ */ new Map();
805
- /** Symmetric to `uploadOwners` for download sessions. */
806
- downloadOwners = /* @__PURE__ */ new Map();
807
- /**
808
915
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
809
916
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
810
917
  * keep a sync cache and refresh it whenever the provider collection
@@ -949,26 +1056,26 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
949
1056
  location: loc,
950
1057
  relativePath
951
1058
  });
952
- this.uploadOwners.set(result.uploadId, loc.providerId);
1059
+ service.rememberSession("upload", result.uploadId, loc);
953
1060
  return result;
954
1061
  },
955
1062
  writeChunk: async (input) => {
956
- return (await this.resolveByUploadId(input.uploadId)).writeChunk(input);
1063
+ return (await service.getProviderForSession("upload", input.uploadId)).writeChunk(input);
957
1064
  },
958
1065
  finalizeUpload: async (input) => {
959
- const p = await this.resolveByUploadId(input.uploadId);
1066
+ const p = await service.getProviderForSession("upload", input.uploadId);
960
1067
  try {
961
1068
  return await p.finalizeUpload(input);
962
1069
  } finally {
963
- this.uploadOwners.delete(input.uploadId);
1070
+ service.forgetSession("upload", input.uploadId);
964
1071
  }
965
1072
  },
966
1073
  abortUpload: async (input) => {
967
- const p = await this.resolveByUploadId(input.uploadId);
1074
+ const p = await service.getProviderForSession("upload", input.uploadId);
968
1075
  try {
969
1076
  return await p.abortUpload(input);
970
1077
  } finally {
971
- this.uploadOwners.delete(input.uploadId);
1078
+ service.forgetSession("upload", input.uploadId);
972
1079
  }
973
1080
  },
974
1081
  beginDownload: async ({ location, relativePath }) => {
@@ -977,18 +1084,18 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
977
1084
  location: loc,
978
1085
  relativePath
979
1086
  });
980
- this.downloadOwners.set(result.downloadId, loc.providerId);
1087
+ service.rememberSession("download", result.downloadId, loc);
981
1088
  return result;
982
1089
  },
983
1090
  readChunk: async (input) => {
984
- return (await this.resolveByDownloadId(input.downloadId)).readChunk(input);
1091
+ return (await service.getProviderForSession("download", input.downloadId)).readChunk(input);
985
1092
  },
986
1093
  endDownload: async (input) => {
987
- const p = await this.resolveByDownloadId(input.downloadId);
1094
+ const p = await service.getProviderForSession("download", input.downloadId);
988
1095
  try {
989
1096
  return await p.endDownload(input);
990
1097
  } finally {
991
- this.downloadOwners.delete(input.downloadId);
1098
+ service.forgetSession("download", input.downloadId);
992
1099
  }
993
1100
  }
994
1101
  };
@@ -1031,8 +1138,7 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1031
1138
  this.pressureTimer = null;
1032
1139
  }
1033
1140
  this.pressureManager = null;
1034
- this.uploadOwners.clear();
1035
- this.downloadOwners.clear();
1141
+ this.service?.clearSessions();
1036
1142
  this.service = null;
1037
1143
  }
1038
1144
  /**
@@ -1122,7 +1228,8 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1122
1228
  /** statfs capacity of a location's basePath, walking up to the nearest
1123
1229
  * existing ancestor. Null for remote-node locations or unstattable paths. */
1124
1230
  async localCapacityOf(loc) {
1125
- if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1231
+ const localNode = this.service?.getLocalNodeId() ?? "hub";
1232
+ if (loc.nodeId !== void 0 && loc.nodeId !== localNode) return null;
1126
1233
  const bp = loc.config["basePath"];
1127
1234
  if (typeof bp !== "string" || bp.length === 0) return null;
1128
1235
  try {
@@ -1251,37 +1358,6 @@ var StorageOrchestratorAddon = class extends require_dist.BaseAddon {
1251
1358
  });
1252
1359
  for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
1253
1360
  }
1254
- async resolveByUploadId(uploadId) {
1255
- const providerId = this.uploadOwners.get(uploadId);
1256
- if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
1257
- return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
1258
- }
1259
- async resolveByDownloadId(downloadId) {
1260
- const providerId = this.downloadOwners.get(downloadId);
1261
- if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
1262
- return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
1263
- }
1264
- /**
1265
- * Look up a `storage-provider` by `providerId` only — used for
1266
- * upload / download dispatch where the original `StorageLocation` is
1267
- * no longer in scope. Throws if the provider has gone away (e.g.
1268
- * addon unloaded mid-session).
1269
- */
1270
- async providerByProviderId(providerId, sessionDescriptor) {
1271
- if (!this.service) throw new Error("storage-orchestrator: service not initialized");
1272
- const probe = {
1273
- id: `__session:${sessionDescriptor}`,
1274
- type: "data",
1275
- displayName: sessionDescriptor,
1276
- providerId,
1277
- config: {},
1278
- isDefault: false,
1279
- isSystem: false,
1280
- createdAt: 0,
1281
- updatedAt: 0
1282
- };
1283
- return this.service.getProviderFor(probe);
1284
- }
1285
1361
  };
1286
1362
  //#endregion
1287
1363
  exports.SqliteLocationStore = SqliteLocationStore;
@@ -1,13 +1,39 @@
1
- import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-CIek6F9X.mjs";
1
+ import { Q as storageCapability, Tt as parseJsonObject, at as BaseAddon, d as StorageLocationTypeSchema } from "../../dist-DimX9eZy.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";
5
5
  //#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
6
+ var SESSION_VOCABULARY = {
7
+ upload: {
8
+ idLabel: "uploadId",
9
+ openMethod: "beginUpload"
10
+ },
11
+ download: {
12
+ idLabel: "downloadId",
13
+ openMethod: "beginDownload"
14
+ }
15
+ };
16
+ /** Map key for a tracked chunked session — the id spaces never collide. */
17
+ function sessionKey(kind, sessionId) {
18
+ return `${kind}:${sessionId}`;
19
+ }
20
+ /**
21
+ * Bare node id for a raw kernel `localNodeId`. Forked children carry a
22
+ * `<node>/<addon>` suffix; routing is per-NODE, so the addon segment is
23
+ * dropped. (Mirrors `normalizeNodeId` in `@camstack/types/addon`, which is a
24
+ * leaf module not re-exported on any subpath this package can import.)
25
+ */
26
+ function bareNodeId(raw) {
27
+ if (raw === void 0 || raw === "") return "hub";
28
+ const slash = raw.indexOf("/");
29
+ if (slash < 0) return raw;
30
+ const bare = raw.slice(0, slash);
31
+ return bare === "" ? "hub" : bare;
32
+ }
6
33
  var StorageOrchestratorService = class {
7
34
  logger;
8
35
  getProviders;
9
36
  locationStore;
10
- localNodeId;
11
37
  getRemoteProvider;
12
38
  locations = /* @__PURE__ */ new Map();
13
39
  /**
@@ -26,12 +52,29 @@ var StorageOrchestratorService = class {
26
52
  * early-boot path that predates the resolver wiring.
27
53
  */
28
54
  nodeLocalResolver = null;
55
+ /** `localNodeId` with any forked-child `/addon` suffix stripped. */
56
+ localNode;
57
+ /**
58
+ * `<kind>:<sessionId>` → the dispatch target that OPENED the session.
59
+ * Chunked I/O carries no `StorageLocation` after the first hop, so without
60
+ * this the follow-up chunks fall back to a `providerId`-only lookup — which
61
+ * on a multi-node cluster means the hub's filesystem, not the agent's.
62
+ */
63
+ sessionTargets = /* @__PURE__ */ new Map();
29
64
  constructor(logger, getProviders, locationStore = null, localNodeId = "hub", getRemoteProvider) {
30
65
  this.logger = logger;
31
66
  this.getProviders = getProviders;
32
67
  this.locationStore = locationStore;
33
- this.localNodeId = localNodeId;
34
68
  this.getRemoteProvider = getRemoteProvider;
69
+ this.localNode = bareNodeId(localNodeId);
70
+ }
71
+ /**
72
+ * The node this orchestrator runs on, with any forked-child `/addon`
73
+ * suffix stripped. A location whose `nodeId` differs is NOT this node's
74
+ * to touch — its bytes, and its free space, live elsewhere.
75
+ */
76
+ getLocalNodeId() {
77
+ return this.localNode;
35
78
  }
36
79
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
37
80
  hasStore() {
@@ -339,22 +382,96 @@ var StorageOrchestratorService = class {
339
382
  return def;
340
383
  }
341
384
  /**
342
- * Find the storage-provider that backs a given location. Lookup is by
343
- * `location.providerId` against `getProviderInfo().providerId` from
344
- * each registered collection provider. Throws with both the missing
345
- * providerId and the offending location id so the operator can pick
346
- * the right place to fix the config.
385
+ * Find the storage-provider that backs a given location the entry point
386
+ * every consumer-facing `storage` cap method funnels through.
387
+ *
388
+ * Routing key is `(providerId, nodeId)`, never `providerId` alone: every
389
+ * node's FilesystemStorageProvider registers under the SAME providerId, so
390
+ * a providerId-only match resolves to whichever node asked. That is how a
391
+ * location created on an agent ended up writing to the HUB's filesystem
392
+ * (root-caused 2026-07-29).
347
393
  */
348
394
  async getProviderFor(location) {
349
- const nodeId = location.nodeId;
350
- if (nodeId !== void 0 && nodeId !== this.localNodeId) {
395
+ return this.getProviderForTarget({
396
+ providerId: location.providerId,
397
+ ...location.nodeId !== void 0 && { nodeId: location.nodeId }
398
+ }, `location "${location.id}"`, location.id);
399
+ }
400
+ /**
401
+ * Resolve one dispatch target to the provider that must execute it.
402
+ *
403
+ * `descriptor` is the operator-facing subject of the error message
404
+ * (`location "recordings:disk"`, `uploadId "u-42"`); `locationId` is the
405
+ * structured tag for the log line when there is a location behind the
406
+ * dispatch (chunked sessions carry only the session id).
407
+ *
408
+ * Both failure branches LOG before throwing, and NEITHER falls back to the
409
+ * local provider — a silent fallback is the bug this method exists to fix.
410
+ */
411
+ async getProviderForTarget(target, descriptor, locationId) {
412
+ const nodeId = target.nodeId;
413
+ if (nodeId !== void 0 && bareNodeId(nodeId) !== this.localNode) {
351
414
  const remote = this.getRemoteProvider?.(nodeId);
352
415
  if (remote) return remote;
353
- throw new Error(`storage-provider for location "${location.id}" lives on node "${nodeId}" which is not reachable`);
416
+ this.logger.error("storage-orchestrator: target node has no reachable storage-provider — dispatch refused", { meta: {
417
+ ...locationId !== void 0 && { locationId },
418
+ descriptor,
419
+ nodeId,
420
+ providerId: target.providerId,
421
+ localNodeId: this.localNode,
422
+ crossNodeResolverWired: this.getRemoteProvider !== void 0
423
+ } });
424
+ throw new Error(`storage-provider for ${descriptor} lives on node "${nodeId}" which is not reachable`);
354
425
  }
355
426
  const providers = this.getProviders();
356
- for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
357
- throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
427
+ for (const p of providers) if ((await p.getProviderInfo()).providerId === target.providerId) return p;
428
+ this.logger.error("storage-orchestrator: no local storage-provider for providerId", { meta: {
429
+ ...locationId !== void 0 && { locationId },
430
+ descriptor,
431
+ providerId: target.providerId,
432
+ localNodeId: this.localNode,
433
+ registered: providers.length
434
+ } });
435
+ throw new Error(`No storage-provider registered for providerId "${target.providerId}" (${descriptor})`);
436
+ }
437
+ /**
438
+ * Remember which provider — and on which NODE — opened a chunked session.
439
+ * `beginUpload` / `beginDownload` carry a `StorageLocation`; every later
440
+ * chunk carries only the session id, and a session id issued by an agent
441
+ * means nothing to the hub's provider.
442
+ */
443
+ rememberSession(kind, sessionId, location) {
444
+ this.sessionTargets.set(sessionKey(kind, sessionId), {
445
+ providerId: location.providerId,
446
+ ...location.nodeId !== void 0 && { nodeId: location.nodeId }
447
+ });
448
+ }
449
+ /** Drop a session's routing entry (finalize / abort / end). */
450
+ forgetSession(kind, sessionId) {
451
+ this.sessionTargets.delete(sessionKey(kind, sessionId));
452
+ }
453
+ /** Drop every session entry (addon shutdown). */
454
+ clearSessions() {
455
+ this.sessionTargets.clear();
456
+ }
457
+ /**
458
+ * Resolve the provider that owns an in-flight chunked session, on the node
459
+ * that opened it. Throws when the orchestrator has no record of the open
460
+ * call (provider gone, or a chunk for a session it never issued).
461
+ */
462
+ async getProviderForSession(kind, sessionId) {
463
+ const vocabulary = SESSION_VOCABULARY[kind];
464
+ const descriptor = `${vocabulary.idLabel} "${sessionId}"`;
465
+ const target = this.sessionTargets.get(sessionKey(kind, sessionId));
466
+ if (!target) {
467
+ this.logger.error("storage-orchestrator: chunk for an unknown session — dispatch refused", { meta: {
468
+ descriptor,
469
+ kind,
470
+ sessionId
471
+ } });
472
+ throw new Error(`Unknown ${vocabulary.idLabel} "${sessionId}" — orchestrator has no record of ${vocabulary.openMethod}`);
473
+ }
474
+ return this.getProviderForTarget(target, descriptor);
358
475
  }
359
476
  /** Convenience: lookup by id (returns `undefined` if not found). */
360
477
  getLocationById(id) {
@@ -788,16 +905,6 @@ var StorageOrchestratorAddon = class extends BaseAddon {
788
905
  pressureTimer = null;
789
906
  pressureSweepInFlight = false;
790
907
  /**
791
- * `uploadId` → providerId. Populated on `beginUpload`, consulted on
792
- * every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
793
- * cleared on terminal calls. The provider holds the actual session
794
- * state (open file descriptor, buffered offsets, …) — we just
795
- * remember which provider owns it.
796
- */
797
- uploadOwners = /* @__PURE__ */ new Map();
798
- /** Symmetric to `uploadOwners` for download sessions. */
799
- downloadOwners = /* @__PURE__ */ new Map();
800
- /**
801
908
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
802
909
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
803
910
  * keep a sync cache and refresh it whenever the provider collection
@@ -942,26 +1049,26 @@ var StorageOrchestratorAddon = class extends BaseAddon {
942
1049
  location: loc,
943
1050
  relativePath
944
1051
  });
945
- this.uploadOwners.set(result.uploadId, loc.providerId);
1052
+ service.rememberSession("upload", result.uploadId, loc);
946
1053
  return result;
947
1054
  },
948
1055
  writeChunk: async (input) => {
949
- return (await this.resolveByUploadId(input.uploadId)).writeChunk(input);
1056
+ return (await service.getProviderForSession("upload", input.uploadId)).writeChunk(input);
950
1057
  },
951
1058
  finalizeUpload: async (input) => {
952
- const p = await this.resolveByUploadId(input.uploadId);
1059
+ const p = await service.getProviderForSession("upload", input.uploadId);
953
1060
  try {
954
1061
  return await p.finalizeUpload(input);
955
1062
  } finally {
956
- this.uploadOwners.delete(input.uploadId);
1063
+ service.forgetSession("upload", input.uploadId);
957
1064
  }
958
1065
  },
959
1066
  abortUpload: async (input) => {
960
- const p = await this.resolveByUploadId(input.uploadId);
1067
+ const p = await service.getProviderForSession("upload", input.uploadId);
961
1068
  try {
962
1069
  return await p.abortUpload(input);
963
1070
  } finally {
964
- this.uploadOwners.delete(input.uploadId);
1071
+ service.forgetSession("upload", input.uploadId);
965
1072
  }
966
1073
  },
967
1074
  beginDownload: async ({ location, relativePath }) => {
@@ -970,18 +1077,18 @@ var StorageOrchestratorAddon = class extends BaseAddon {
970
1077
  location: loc,
971
1078
  relativePath
972
1079
  });
973
- this.downloadOwners.set(result.downloadId, loc.providerId);
1080
+ service.rememberSession("download", result.downloadId, loc);
974
1081
  return result;
975
1082
  },
976
1083
  readChunk: async (input) => {
977
- return (await this.resolveByDownloadId(input.downloadId)).readChunk(input);
1084
+ return (await service.getProviderForSession("download", input.downloadId)).readChunk(input);
978
1085
  },
979
1086
  endDownload: async (input) => {
980
- const p = await this.resolveByDownloadId(input.downloadId);
1087
+ const p = await service.getProviderForSession("download", input.downloadId);
981
1088
  try {
982
1089
  return await p.endDownload(input);
983
1090
  } finally {
984
- this.downloadOwners.delete(input.downloadId);
1091
+ service.forgetSession("download", input.downloadId);
985
1092
  }
986
1093
  }
987
1094
  };
@@ -1024,8 +1131,7 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1024
1131
  this.pressureTimer = null;
1025
1132
  }
1026
1133
  this.pressureManager = null;
1027
- this.uploadOwners.clear();
1028
- this.downloadOwners.clear();
1134
+ this.service?.clearSessions();
1029
1135
  this.service = null;
1030
1136
  }
1031
1137
  /**
@@ -1115,7 +1221,8 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1115
1221
  /** statfs capacity of a location's basePath, walking up to the nearest
1116
1222
  * existing ancestor. Null for remote-node locations or unstattable paths. */
1117
1223
  async localCapacityOf(loc) {
1118
- if (loc.nodeId !== void 0 && loc.nodeId !== "hub" && loc.nodeId !== this.ctx.nodeId) return null;
1224
+ const localNode = this.service?.getLocalNodeId() ?? "hub";
1225
+ if (loc.nodeId !== void 0 && loc.nodeId !== localNode) return null;
1119
1226
  const bp = loc.config["basePath"];
1120
1227
  if (typeof bp !== "string" || bp.length === 0) return null;
1121
1228
  try {
@@ -1244,37 +1351,6 @@ var StorageOrchestratorAddon = class extends BaseAddon {
1244
1351
  });
1245
1352
  for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
1246
1353
  }
1247
- async resolveByUploadId(uploadId) {
1248
- const providerId = this.uploadOwners.get(uploadId);
1249
- if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
1250
- return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
1251
- }
1252
- async resolveByDownloadId(downloadId) {
1253
- const providerId = this.downloadOwners.get(downloadId);
1254
- if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
1255
- return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
1256
- }
1257
- /**
1258
- * Look up a `storage-provider` by `providerId` only — used for
1259
- * upload / download dispatch where the original `StorageLocation` is
1260
- * no longer in scope. Throws if the provider has gone away (e.g.
1261
- * addon unloaded mid-session).
1262
- */
1263
- async providerByProviderId(providerId, sessionDescriptor) {
1264
- if (!this.service) throw new Error("storage-orchestrator: service not initialized");
1265
- const probe = {
1266
- id: `__session:${sessionDescriptor}`,
1267
- type: "data",
1268
- displayName: sessionDescriptor,
1269
- providerId,
1270
- config: {},
1271
- isDefault: false,
1272
- isSystem: false,
1273
- createdAt: 0,
1274
- updatedAt: 0
1275
- };
1276
- return this.service.getProviderFor(probe);
1277
- }
1278
1354
  };
1279
1355
  //#endregion
1280
1356
  export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, StorageOrchestratorService as n, SqliteLocationStore as t };