@camstack/system 1.2.83 → 1.2.85

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 (66) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +32 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +32 -2
  17. package/dist/builtins/device-manager/device-meta-actions.d.ts +12 -0
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  20. package/dist/builtins/hub-forwarder/index.js +1 -1
  21. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  26. package/dist/builtins/local-network/local-network.addon.js +1 -1
  27. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  28. package/dist/builtins/loki-logging/index.js +1 -1
  29. package/dist/builtins/loki-logging/index.mjs +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  32. package/dist/builtins/platform-probe/index.js +1 -1
  33. package/dist/builtins/platform-probe/index.mjs +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  36. package/dist/builtins/snapshot/index.js +66 -11
  37. package/dist/builtins/snapshot/index.mjs +66 -11
  38. package/dist/builtins/snapshot/snapshot.addon.d.ts +39 -3
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +30 -10
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +31 -11
  41. package/dist/builtins/sqlite-storage/path-guard.d.ts +8 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  44. package/dist/builtins/storage-orchestrator/index.d.ts +5 -6
  45. package/dist/builtins/storage-orchestrator/index.mjs +1 -1
  46. package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +1 -1
  47. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +1 -1
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +1 -1
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1252 -1191
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1253 -1192
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{dist-EAvNJD24.mjs → dist-DWuNENzg.mjs} +527 -11
  56. package/dist/{dist-CqiMPPs0.js → dist-DiScsb8j.js} +527 -11
  57. package/dist/index.js +83 -18
  58. package/dist/index.mjs +84 -19
  59. package/dist/kernel/addon-installer.d.ts +58 -6
  60. package/dist/kernel/moleculer/event-bus-core.d.ts +47 -1
  61. package/dist/kernel/transport/child-cap-protocol.d.ts +32 -0
  62. package/dist/kernel/transport/hub-cap-forward.d.ts +8 -0
  63. package/dist/kernel/transport/parent-unowned-call.d.ts +19 -0
  64. package/dist/{manifest-python-deps-DG_LLcZF.js → manifest-python-deps-C0zkl1Ae.js} +133 -53
  65. package/dist/{manifest-python-deps-C1DMs1J9.mjs → manifest-python-deps-fWzdEasI.mjs} +133 -53
  66. package/package.json +6 -1
@@ -3871,14 +3871,69 @@ function matchesPattern(pattern, category) {
3871
3871
  if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
3872
3872
  return pattern === category;
3873
3873
  }
3874
- function extractCategoryPattern(filter) {
3875
- if (typeof filter === "string") return filter;
3874
+ /**
3875
+ * EVERY category pattern a filter asks for the registration key set.
3876
+ *
3877
+ * `EventFilter.category` has always accepted an array, and until 2026-08-12
3878
+ * the delivery path kept only `cat[0]`. Inside a forked runner that was not
3879
+ * merely a local filter: `createUdsEventBus` reports its handler-map keys to
3880
+ * the parent as this child's forwarding interest, so the categories that never
3881
+ * became keys were never forwarded across the process boundary at all. A
3882
+ * ten-category subscribe received one category, silently.
3883
+ *
3884
+ * Duplicates are collapsed so a filter that names the same category twice does
3885
+ * not put the entry in one key twice.
3886
+ */
3887
+ function extractCategoryPatterns(filter) {
3888
+ if (typeof filter === "string") return [filter];
3876
3889
  if (filter && typeof filter === "object" && "category" in filter) {
3877
3890
  const cat = filter.category;
3878
- if (typeof cat === "string") return cat;
3879
- if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
3891
+ if (typeof cat === "string") return [cat];
3892
+ if (Array.isArray(cat) && cat.length > 0) return [...new Set(cat.map((c) => String(c)))];
3893
+ }
3894
+ return ["*"];
3895
+ }
3896
+ /**
3897
+ * Register one handler under every category pattern its filter names, and
3898
+ * return the unsubscribe that removes it from all of them.
3899
+ *
3900
+ * The ONE place a subscriber enters `state.handlers` — both bus
3901
+ * implementations (broker and UDS) call it, so "which keys does this
3902
+ * subscriber occupy" has a single answer. An emptied key is DELETED rather
3903
+ * than left as an empty `Set`: on a child bus the key set is what the parent
3904
+ * is told to forward, and an empty set left behind pins the parent to forward
3905
+ * a category nobody wants.
3906
+ *
3907
+ * @param onKeysChanged - Called after the key set may have changed (both on
3908
+ * registration and on unsubscribe), for buses that mirror their key set
3909
+ * somewhere — the UDS child's parent report, the broker's local-interest
3910
+ * surface. Receives the patterns this call touched.
3911
+ */
3912
+ function registerSubscriber(state, filter, handler, onKeysChanged) {
3913
+ const patterns = extractCategoryPatterns(filter);
3914
+ const entry = {
3915
+ filter,
3916
+ categoryPattern: patterns[0] ?? "*",
3917
+ handler
3918
+ };
3919
+ for (const pattern of patterns) {
3920
+ const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
3921
+ set.add(entry);
3922
+ state.handlers.set(pattern, set);
3880
3923
  }
3881
- return "*";
3924
+ onKeysChanged?.(patterns, true);
3925
+ let released = false;
3926
+ return () => {
3927
+ if (released) return;
3928
+ released = true;
3929
+ for (const pattern of patterns) {
3930
+ const current = state.handlers.get(pattern);
3931
+ if (current === void 0) continue;
3932
+ current.delete(entry);
3933
+ if (current.size === 0) state.handlers.delete(pattern);
3934
+ }
3935
+ onKeysChanged?.(patterns, false);
3936
+ };
3882
3937
  }
3883
3938
  /**
3884
3939
  * Match an event against the full `EventFilter`. The Map-keyed subscribe path
@@ -3991,10 +4046,13 @@ function deliverShared(state, event) {
3991
4046
  state.recent.push(event);
3992
4047
  if (state.recent.length > 1e3) state.recent.splice(0, state.recent.length - MAX_RECENT_EVENTS);
3993
4048
  }
4049
+ const delivered = /* @__PURE__ */ new Set();
3994
4050
  for (const [pattern, set] of state.handlers) {
3995
4051
  if (!matchesPattern(pattern, event.category)) continue;
3996
4052
  for (const entry of set) {
4053
+ if (delivered.has(entry)) continue;
3997
4054
  if (!matchesEventFilter(event, entry.filter)) continue;
4055
+ delivered.add(entry);
3998
4056
  try {
3999
4057
  entry.handler(event);
4000
4058
  } catch (err) {}
@@ -4842,13 +4900,14 @@ var LocalChildRegistry = class {
4842
4900
  method: out.method,
4843
4901
  args: out.args,
4844
4902
  ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
4845
- ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {}
4903
+ ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
4904
+ ...out.native === true ? { native: true } : {}
4846
4905
  };
4847
4906
  const pinnedNodeId = out.nodeId ?? extractNodeId(out.args);
4848
4907
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
4849
4908
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
4850
4909
  const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
4851
- if ((!aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
4910
+ if ((!(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
4852
4911
  if (!this.egressRoutedCaps.has(out.capName)) {
4853
4912
  this.egressRoutedCaps.add(out.capName);
4854
4913
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
@@ -5154,7 +5213,8 @@ var LocalChildClient = class {
5154
5213
  method: input.method,
5155
5214
  args: input.args,
5156
5215
  ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
5157
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
5216
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
5217
+ ...input.native === true ? { native: true } : {}
5158
5218
  };
5159
5219
  return this.channel.request(msg);
5160
5220
  }
@@ -5305,23 +5365,7 @@ function createUdsEventBus(client, addonId) {
5305
5365
  client.emitEvent(enriched);
5306
5366
  },
5307
5367
  subscribe(filter, handler) {
5308
- const pattern = extractCategoryPattern(filter);
5309
- const entry = {
5310
- filter,
5311
- categoryPattern: pattern,
5312
- handler
5313
- };
5314
- const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
5315
- set.add(entry);
5316
- state.handlers.set(pattern, set);
5317
- reportPatterns();
5318
- return () => {
5319
- const current = state.handlers.get(pattern);
5320
- if (current === void 0) return;
5321
- current.delete(entry);
5322
- if (current.size === 0) state.handlers.delete(pattern);
5323
- reportPatterns();
5324
- };
5368
+ return registerSubscriber(state, filter, handler, () => reportPatterns());
5325
5369
  },
5326
5370
  getRecent(filter, limit) {
5327
5371
  const filtered = filter ? state.recent.filter((e) => {
@@ -5420,6 +5464,26 @@ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5420
5464
  function createParentUnownedCallHandler(deps) {
5421
5465
  return async (input) => {
5422
5466
  const deviceId = input.deviceId ?? extractDeviceId(input.args);
5467
+ if (input.native === true) {
5468
+ if (deviceId === void 0) throw new Error(`no native provider for capability "${input.capName}" — the call carried no deviceId`);
5469
+ const provider = deps.resolveNative?.(input.capName, deviceId) ?? null;
5470
+ if (provider !== null) {
5471
+ const fn = provider[input.method];
5472
+ if (typeof fn !== "function") throw new Error(`no native provider for capability "${input.capName}" implementing "${input.method}" on device ${String(deviceId)}`);
5473
+ return await fn(typeof input.args === "object" && input.args !== null && !Array.isArray(input.args) ? {
5474
+ ...input.args,
5475
+ deviceId
5476
+ } : { deviceId });
5477
+ }
5478
+ if (deps.forwardToHub !== void 0) return await deps.forwardToHub({
5479
+ capName: input.capName,
5480
+ method: input.method,
5481
+ args: input.args,
5482
+ deviceId,
5483
+ native: true
5484
+ });
5485
+ throw new Error(`no native provider for capability "${input.capName}" on device ${String(deviceId)}`);
5486
+ }
5423
5487
  const nodeId = deps.isDataNodeIdCap?.(input.capName) === true ? void 0 : input.nodeId ?? extractNodeId(input.args);
5424
5488
  const pinnedToOtherNode = nodeId !== void 0 && nodeId !== deps.broker.nodeID;
5425
5489
  if (nodeId === void 0) {
@@ -5525,12 +5589,14 @@ function narrowParams(raw) {
5525
5589
  if (typeof capName !== "string" || typeof method !== "string") throw new Error("$hub-cap-fwd.forward: invalid params — capName and method are required strings");
5526
5590
  const deviceId = Reflect.get(raw, "deviceId");
5527
5591
  const nodeId = Reflect.get(raw, "nodeId");
5592
+ const native = Reflect.get(raw, "native");
5528
5593
  return {
5529
5594
  capName,
5530
5595
  method,
5531
5596
  args: Reflect.get(raw, "args"),
5532
5597
  deviceId: typeof deviceId === "number" ? deviceId : void 0,
5533
- nodeId: typeof nodeId === "string" && nodeId.length > 0 ? nodeId : void 0
5598
+ nodeId: typeof nodeId === "string" && nodeId.length > 0 ? nodeId : void 0,
5599
+ native: native === true ? true : void 0
5534
5600
  };
5535
5601
  }
5536
5602
  /**
@@ -5546,13 +5612,14 @@ function createHubCapForwardService(onUnownedCall) {
5546
5612
  return {
5547
5613
  name: HUB_CAP_FWD_SERVICE,
5548
5614
  actions: { forward: { handler: async (ctx) => {
5549
- const { capName, method, args, deviceId, nodeId } = narrowParams(ctx.params);
5615
+ const { capName, method, args, deviceId, nodeId, native } = narrowParams(ctx.params);
5550
5616
  return onUnownedCall({
5551
5617
  capName,
5552
5618
  method,
5553
5619
  args,
5554
5620
  ...deviceId !== void 0 ? { deviceId } : {},
5555
- ...nodeId !== void 0 ? { nodeId } : {}
5621
+ ...nodeId !== void 0 ? { nodeId } : {},
5622
+ ...native === true ? { native: true } : {}
5556
5623
  });
5557
5624
  } } }
5558
5625
  };
@@ -6486,18 +6553,7 @@ function localSubscriberPatterns(broker) {
6486
6553
  * Returns an unsubscribe function.
6487
6554
  */
6488
6555
  function subscribePassthrough(broker, handler) {
6489
- const state = getSharedBusState(broker);
6490
- const entry = {
6491
- filter: {},
6492
- categoryPattern: "*",
6493
- handler
6494
- };
6495
- const set = state.handlers.get("*") ?? /* @__PURE__ */ new Set();
6496
- set.add(entry);
6497
- state.handlers.set("*", set);
6498
- return () => {
6499
- state.handlers.get("*")?.delete(entry);
6500
- };
6556
+ return registerSubscriber(getSharedBusState(broker), {}, handler);
6501
6557
  }
6502
6558
  /** Per-broker cross-node inbound counters (delivered / would-suppress). */
6503
6559
  var brokerCrossNodeStats = /* @__PURE__ */ new WeakMap();
@@ -6672,19 +6728,10 @@ function getBrokerEventBus(broker, options) {
6672
6728
  }
6673
6729
  },
6674
6730
  subscribe(filter, handler) {
6675
- const pattern = extractCategoryPattern(filter);
6676
- const entry = {
6677
- filter,
6678
- categoryPattern: pattern,
6679
- handler
6680
- };
6681
- const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
6682
- set.add(entry);
6683
- state.handlers.set(pattern, set);
6684
- bumpLocalInterest(broker, pattern);
6685
- return () => {
6686
- if (state.handlers.get(pattern)?.delete(entry) === true) dropLocalInterest(broker, pattern);
6687
- };
6731
+ return registerSubscriber(state, filter, handler, (patterns, added) => {
6732
+ for (const pattern of patterns) if (added) bumpLocalInterest(broker, pattern);
6733
+ else dropLocalInterest(broker, pattern);
6734
+ });
6688
6735
  },
6689
6736
  getRecent(filter, limit) {
6690
6737
  const filtered = filter ? state.recent.filter((e) => {
@@ -7227,6 +7274,29 @@ function kebabToCamel(s) {
7227
7274
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7228
7275
  }
7229
7276
  /**
7277
+ * Native-provider proxy for a UDS runner: every cap method dispatches
7278
+ * `native: true` over the child→parent channel, carrying `deviceId` both
7279
+ * out-of-band (for routing) and inside the args (where the provider reads it).
7280
+ *
7281
+ * Built from `cap.methods` rather than as a `Proxy` so an unknown method name
7282
+ * is a local `undefined` — a wrapper that probes for an optional method sees
7283
+ * the same shape it sees on the hub instead of a round-trip that fails.
7284
+ *
7285
+ * Irreducible bridge: the returned object is assembled by name at runtime, so
7286
+ * it is validated structurally against `InferProvider<TCap>` at this one seam
7287
+ * (same boundary `buildRemoteNativeProvider` casts at).
7288
+ */
7289
+ function buildUdsNativeProvider(callOut, cap, deviceId) {
7290
+ const provider = {};
7291
+ for (const methodName of Object.keys(cap.methods)) provider[methodName] = async (input) => {
7292
+ return await callOut(methodName, typeof input === "object" && input !== null && !Array.isArray(input) ? {
7293
+ ...input,
7294
+ deviceId
7295
+ } : { deviceId });
7296
+ };
7297
+ return provider;
7298
+ }
7299
+ /**
7230
7300
  * Broker-mode entry point — UNCHANGED public signature. The hub
7231
7301
  * (`addon-registry.service.ts`), the agent (`agent-bootstrap.ts`), and
7232
7302
  * every broker-mode test call this with a positional `ServiceBroker`.
@@ -7530,6 +7600,16 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7530
7600
  },
7531
7601
  getNativeProvider(cap, deviceId) {
7532
7602
  if (options?.capabilityRegistry) return options.capabilityRegistry.getNativeProvider(cap.name, deviceId);
7603
+ if (runtime.mode === "uds") {
7604
+ const udsClient = runtime.client;
7605
+ return buildUdsNativeProvider((method, args) => udsClient.callOut({
7606
+ capName: cap.name,
7607
+ method,
7608
+ args,
7609
+ deviceId,
7610
+ native: true
7611
+ }), cap, deviceId);
7612
+ }
7533
7613
  return buildRemoteNativeProvider(api, cap, {
7534
7614
  nodeId: "hub",
7535
7615
  bypassWrapper: true
@@ -3869,14 +3869,69 @@ function matchesPattern(pattern, category) {
3869
3869
  if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
3870
3870
  return pattern === category;
3871
3871
  }
3872
- function extractCategoryPattern(filter) {
3873
- if (typeof filter === "string") return filter;
3872
+ /**
3873
+ * EVERY category pattern a filter asks for the registration key set.
3874
+ *
3875
+ * `EventFilter.category` has always accepted an array, and until 2026-08-12
3876
+ * the delivery path kept only `cat[0]`. Inside a forked runner that was not
3877
+ * merely a local filter: `createUdsEventBus` reports its handler-map keys to
3878
+ * the parent as this child's forwarding interest, so the categories that never
3879
+ * became keys were never forwarded across the process boundary at all. A
3880
+ * ten-category subscribe received one category, silently.
3881
+ *
3882
+ * Duplicates are collapsed so a filter that names the same category twice does
3883
+ * not put the entry in one key twice.
3884
+ */
3885
+ function extractCategoryPatterns(filter) {
3886
+ if (typeof filter === "string") return [filter];
3874
3887
  if (filter && typeof filter === "object" && "category" in filter) {
3875
3888
  const cat = filter.category;
3876
- if (typeof cat === "string") return cat;
3877
- if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
3889
+ if (typeof cat === "string") return [cat];
3890
+ if (Array.isArray(cat) && cat.length > 0) return [...new Set(cat.map((c) => String(c)))];
3891
+ }
3892
+ return ["*"];
3893
+ }
3894
+ /**
3895
+ * Register one handler under every category pattern its filter names, and
3896
+ * return the unsubscribe that removes it from all of them.
3897
+ *
3898
+ * The ONE place a subscriber enters `state.handlers` — both bus
3899
+ * implementations (broker and UDS) call it, so "which keys does this
3900
+ * subscriber occupy" has a single answer. An emptied key is DELETED rather
3901
+ * than left as an empty `Set`: on a child bus the key set is what the parent
3902
+ * is told to forward, and an empty set left behind pins the parent to forward
3903
+ * a category nobody wants.
3904
+ *
3905
+ * @param onKeysChanged - Called after the key set may have changed (both on
3906
+ * registration and on unsubscribe), for buses that mirror their key set
3907
+ * somewhere — the UDS child's parent report, the broker's local-interest
3908
+ * surface. Receives the patterns this call touched.
3909
+ */
3910
+ function registerSubscriber(state, filter, handler, onKeysChanged) {
3911
+ const patterns = extractCategoryPatterns(filter);
3912
+ const entry = {
3913
+ filter,
3914
+ categoryPattern: patterns[0] ?? "*",
3915
+ handler
3916
+ };
3917
+ for (const pattern of patterns) {
3918
+ const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
3919
+ set.add(entry);
3920
+ state.handlers.set(pattern, set);
3878
3921
  }
3879
- return "*";
3922
+ onKeysChanged?.(patterns, true);
3923
+ let released = false;
3924
+ return () => {
3925
+ if (released) return;
3926
+ released = true;
3927
+ for (const pattern of patterns) {
3928
+ const current = state.handlers.get(pattern);
3929
+ if (current === void 0) continue;
3930
+ current.delete(entry);
3931
+ if (current.size === 0) state.handlers.delete(pattern);
3932
+ }
3933
+ onKeysChanged?.(patterns, false);
3934
+ };
3880
3935
  }
3881
3936
  /**
3882
3937
  * Match an event against the full `EventFilter`. The Map-keyed subscribe path
@@ -3989,10 +4044,13 @@ function deliverShared(state, event) {
3989
4044
  state.recent.push(event);
3990
4045
  if (state.recent.length > 1e3) state.recent.splice(0, state.recent.length - MAX_RECENT_EVENTS);
3991
4046
  }
4047
+ const delivered = /* @__PURE__ */ new Set();
3992
4048
  for (const [pattern, set] of state.handlers) {
3993
4049
  if (!matchesPattern(pattern, event.category)) continue;
3994
4050
  for (const entry of set) {
4051
+ if (delivered.has(entry)) continue;
3995
4052
  if (!matchesEventFilter(event, entry.filter)) continue;
4053
+ delivered.add(entry);
3996
4054
  try {
3997
4055
  entry.handler(event);
3998
4056
  } catch (err) {}
@@ -4840,13 +4898,14 @@ var LocalChildRegistry = class {
4840
4898
  method: out.method,
4841
4899
  args: out.args,
4842
4900
  ...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
4843
- ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {}
4901
+ ...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
4902
+ ...out.native === true ? { native: true } : {}
4844
4903
  };
4845
4904
  const pinnedNodeId = out.nodeId ?? extractNodeId(out.args);
4846
4905
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
4847
4906
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
4848
4907
  const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
4849
- if ((!aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
4908
+ if ((!(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
4850
4909
  if (!this.egressRoutedCaps.has(out.capName)) {
4851
4910
  this.egressRoutedCaps.add(out.capName);
4852
4911
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
@@ -5152,7 +5211,8 @@ var LocalChildClient = class {
5152
5211
  method: input.method,
5153
5212
  args: input.args,
5154
5213
  ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
5155
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
5214
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
5215
+ ...input.native === true ? { native: true } : {}
5156
5216
  };
5157
5217
  return this.channel.request(msg);
5158
5218
  }
@@ -5303,23 +5363,7 @@ function createUdsEventBus(client, addonId) {
5303
5363
  client.emitEvent(enriched);
5304
5364
  },
5305
5365
  subscribe(filter, handler) {
5306
- const pattern = extractCategoryPattern(filter);
5307
- const entry = {
5308
- filter,
5309
- categoryPattern: pattern,
5310
- handler
5311
- };
5312
- const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
5313
- set.add(entry);
5314
- state.handlers.set(pattern, set);
5315
- reportPatterns();
5316
- return () => {
5317
- const current = state.handlers.get(pattern);
5318
- if (current === void 0) return;
5319
- current.delete(entry);
5320
- if (current.size === 0) state.handlers.delete(pattern);
5321
- reportPatterns();
5322
- };
5366
+ return registerSubscriber(state, filter, handler, () => reportPatterns());
5323
5367
  },
5324
5368
  getRecent(filter, limit) {
5325
5369
  const filtered = filter ? state.recent.filter((e) => {
@@ -5418,6 +5462,26 @@ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5418
5462
  function createParentUnownedCallHandler(deps) {
5419
5463
  return async (input) => {
5420
5464
  const deviceId = input.deviceId ?? extractDeviceId(input.args);
5465
+ if (input.native === true) {
5466
+ if (deviceId === void 0) throw new Error(`no native provider for capability "${input.capName}" — the call carried no deviceId`);
5467
+ const provider = deps.resolveNative?.(input.capName, deviceId) ?? null;
5468
+ if (provider !== null) {
5469
+ const fn = provider[input.method];
5470
+ if (typeof fn !== "function") throw new Error(`no native provider for capability "${input.capName}" implementing "${input.method}" on device ${String(deviceId)}`);
5471
+ return await fn(typeof input.args === "object" && input.args !== null && !Array.isArray(input.args) ? {
5472
+ ...input.args,
5473
+ deviceId
5474
+ } : { deviceId });
5475
+ }
5476
+ if (deps.forwardToHub !== void 0) return await deps.forwardToHub({
5477
+ capName: input.capName,
5478
+ method: input.method,
5479
+ args: input.args,
5480
+ deviceId,
5481
+ native: true
5482
+ });
5483
+ throw new Error(`no native provider for capability "${input.capName}" on device ${String(deviceId)}`);
5484
+ }
5421
5485
  const nodeId = deps.isDataNodeIdCap?.(input.capName) === true ? void 0 : input.nodeId ?? extractNodeId(input.args);
5422
5486
  const pinnedToOtherNode = nodeId !== void 0 && nodeId !== deps.broker.nodeID;
5423
5487
  if (nodeId === void 0) {
@@ -5523,12 +5587,14 @@ function narrowParams(raw) {
5523
5587
  if (typeof capName !== "string" || typeof method !== "string") throw new Error("$hub-cap-fwd.forward: invalid params — capName and method are required strings");
5524
5588
  const deviceId = Reflect.get(raw, "deviceId");
5525
5589
  const nodeId = Reflect.get(raw, "nodeId");
5590
+ const native = Reflect.get(raw, "native");
5526
5591
  return {
5527
5592
  capName,
5528
5593
  method,
5529
5594
  args: Reflect.get(raw, "args"),
5530
5595
  deviceId: typeof deviceId === "number" ? deviceId : void 0,
5531
- nodeId: typeof nodeId === "string" && nodeId.length > 0 ? nodeId : void 0
5596
+ nodeId: typeof nodeId === "string" && nodeId.length > 0 ? nodeId : void 0,
5597
+ native: native === true ? true : void 0
5532
5598
  };
5533
5599
  }
5534
5600
  /**
@@ -5544,13 +5610,14 @@ function createHubCapForwardService(onUnownedCall) {
5544
5610
  return {
5545
5611
  name: HUB_CAP_FWD_SERVICE,
5546
5612
  actions: { forward: { handler: async (ctx) => {
5547
- const { capName, method, args, deviceId, nodeId } = narrowParams(ctx.params);
5613
+ const { capName, method, args, deviceId, nodeId, native } = narrowParams(ctx.params);
5548
5614
  return onUnownedCall({
5549
5615
  capName,
5550
5616
  method,
5551
5617
  args,
5552
5618
  ...deviceId !== void 0 ? { deviceId } : {},
5553
- ...nodeId !== void 0 ? { nodeId } : {}
5619
+ ...nodeId !== void 0 ? { nodeId } : {},
5620
+ ...native === true ? { native: true } : {}
5554
5621
  });
5555
5622
  } } }
5556
5623
  };
@@ -6484,18 +6551,7 @@ function localSubscriberPatterns(broker) {
6484
6551
  * Returns an unsubscribe function.
6485
6552
  */
6486
6553
  function subscribePassthrough(broker, handler) {
6487
- const state = getSharedBusState(broker);
6488
- const entry = {
6489
- filter: {},
6490
- categoryPattern: "*",
6491
- handler
6492
- };
6493
- const set = state.handlers.get("*") ?? /* @__PURE__ */ new Set();
6494
- set.add(entry);
6495
- state.handlers.set("*", set);
6496
- return () => {
6497
- state.handlers.get("*")?.delete(entry);
6498
- };
6554
+ return registerSubscriber(getSharedBusState(broker), {}, handler);
6499
6555
  }
6500
6556
  /** Per-broker cross-node inbound counters (delivered / would-suppress). */
6501
6557
  var brokerCrossNodeStats = /* @__PURE__ */ new WeakMap();
@@ -6670,19 +6726,10 @@ function getBrokerEventBus(broker, options) {
6670
6726
  }
6671
6727
  },
6672
6728
  subscribe(filter, handler) {
6673
- const pattern = extractCategoryPattern(filter);
6674
- const entry = {
6675
- filter,
6676
- categoryPattern: pattern,
6677
- handler
6678
- };
6679
- const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
6680
- set.add(entry);
6681
- state.handlers.set(pattern, set);
6682
- bumpLocalInterest(broker, pattern);
6683
- return () => {
6684
- if (state.handlers.get(pattern)?.delete(entry) === true) dropLocalInterest(broker, pattern);
6685
- };
6729
+ return registerSubscriber(state, filter, handler, (patterns, added) => {
6730
+ for (const pattern of patterns) if (added) bumpLocalInterest(broker, pattern);
6731
+ else dropLocalInterest(broker, pattern);
6732
+ });
6686
6733
  },
6687
6734
  getRecent(filter, limit) {
6688
6735
  const filtered = filter ? state.recent.filter((e) => {
@@ -7225,6 +7272,29 @@ function kebabToCamel(s) {
7225
7272
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7226
7273
  }
7227
7274
  /**
7275
+ * Native-provider proxy for a UDS runner: every cap method dispatches
7276
+ * `native: true` over the child→parent channel, carrying `deviceId` both
7277
+ * out-of-band (for routing) and inside the args (where the provider reads it).
7278
+ *
7279
+ * Built from `cap.methods` rather than as a `Proxy` so an unknown method name
7280
+ * is a local `undefined` — a wrapper that probes for an optional method sees
7281
+ * the same shape it sees on the hub instead of a round-trip that fails.
7282
+ *
7283
+ * Irreducible bridge: the returned object is assembled by name at runtime, so
7284
+ * it is validated structurally against `InferProvider<TCap>` at this one seam
7285
+ * (same boundary `buildRemoteNativeProvider` casts at).
7286
+ */
7287
+ function buildUdsNativeProvider(callOut, cap, deviceId) {
7288
+ const provider = {};
7289
+ for (const methodName of Object.keys(cap.methods)) provider[methodName] = async (input) => {
7290
+ return await callOut(methodName, typeof input === "object" && input !== null && !Array.isArray(input) ? {
7291
+ ...input,
7292
+ deviceId
7293
+ } : { deviceId });
7294
+ };
7295
+ return provider;
7296
+ }
7297
+ /**
7228
7298
  * Broker-mode entry point — UNCHANGED public signature. The hub
7229
7299
  * (`addon-registry.service.ts`), the agent (`agent-bootstrap.ts`), and
7230
7300
  * every broker-mode test call this with a positional `ServiceBroker`.
@@ -7528,6 +7598,16 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7528
7598
  },
7529
7599
  getNativeProvider(cap, deviceId) {
7530
7600
  if (options?.capabilityRegistry) return options.capabilityRegistry.getNativeProvider(cap.name, deviceId);
7601
+ if (runtime.mode === "uds") {
7602
+ const udsClient = runtime.client;
7603
+ return buildUdsNativeProvider((method, args) => udsClient.callOut({
7604
+ capName: cap.name,
7605
+ method,
7606
+ args,
7607
+ deviceId,
7608
+ native: true
7609
+ }), cap, deviceId);
7610
+ }
7531
7611
  return buildRemoteNativeProvider(api, cap, {
7532
7612
  nodeId: "hub",
7533
7613
  bypassWrapper: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.83",
3
+ "version": "1.2.85",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",
@@ -168,7 +168,12 @@
168
168
  "id": "snapshot",
169
169
  "category": "system",
170
170
  "name": "Snapshot",
171
+ "description": "Owns every camera's latest frame: the capture ladder (vendor native → ffmpeg keyframe grab off the stream-broker restream), the sharp resize, the courtesy frame for a camera that cannot answer, and the two HTTP planes the clients actually fetch bytes from. Runs in its OWN process (`execution.isolate`) — capture, transcode and byte caches do not belong on the event loop that serves the API.",
171
172
  "entry": "./dist/builtins/snapshot/index.js",
173
+ "execution": {
174
+ "isolate": true,
175
+ "heapProfile": "heavy"
176
+ },
172
177
  "capabilities": [
173
178
  {
174
179
  "name": "snapshot"