@camstack/system 1.2.136 → 1.2.138

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 (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-bindings-store.d.ts +10 -4
  14. package/dist/builtins/device-manager/device-manager.addon.js +189 -25
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +189 -25
  16. package/dist/builtins/device-manager/device-row-store.d.ts +20 -1
  17. package/dist/builtins/device-manager/fleet-read-census.d.ts +47 -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 +1 -1
  37. package/dist/builtins/snapshot/index.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-Bh25kEUH.mjs → dist-DMOTtKH8.mjs} +310 -63
  49. package/dist/{dist-D-R0SuP-.js → dist-mQ3BkrJd.js} +310 -63
  50. package/dist/index.js +1 -1
  51. package/dist/index.mjs +1 -1
  52. package/dist/{retired-settings-keys-BZbOXqch.mjs → retired-settings-keys-CPRvyt-E.mjs} +1 -1
  53. package/dist/{retired-settings-keys-CqD6rSQx.js → retired-settings-keys-Dvkcu_zf.js} +1 -1
  54. package/package.json +1 -1
@@ -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-D-R0SuP-.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 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-D-R0SuP-.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -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-D-R0SuP-.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -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-D-R0SuP-.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -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-D-R0SuP-.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D-R0SuP-.js");
3
+ const require_dist = require("../../dist-mQ3BkrJd.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
1
+ import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-DMOTtKH8.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -137,10 +137,16 @@ export declare function getBindings(deps: BindingsDeps, input: {
137
137
  * caller needing seventy-five paid either 75 round-trips (1211 ms / 118 KB) or
138
138
  * `getAllBindings` (511 ms / 679 KB of mostly-unwanted rows).
139
139
  *
140
- * Sequential on purpose, like `getAllBindings`: `getBindings` is a pair of
141
- * store reads on this process's own event loop, so concurrency buys nothing
142
- * and a fan-out of `Promise.all` over hundreds of ids only makes the queue
143
- * less fair to everything else on it.
140
+ * Sequential on purpose, like `getAllBindings`: `resolveBindingsForDevice`'s
141
+ * remaining per-device work (the bindings-store read, the routing rules) runs
142
+ * on this process's own event loop, so concurrency buys nothing there and a
143
+ * fan-out of `Promise.all` over hundreds of ids only makes the queue less fair
144
+ * to everything else on it. The one part that DOES amortise across the whole
145
+ * set — the device row lookup — is fetched ONCE, ahead of the loop
146
+ * ({@link DeviceRowStore.getMany}), rather than once per iteration: measured
147
+ * on the live hub 2026-08-25, `get device-manager:devices` ran 1 656
148
+ * calls/min steady-state, one row per call, which is exactly what looping
149
+ * `getBindings`'s own `rows.get(deviceId)` here produced.
144
150
  */
145
151
  export declare function getBindingsBatch(deps: BindingsDeps, input: {
146
152
  deviceIds: readonly number[];
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D-R0SuP-.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-CqD6rSQx.js");
6
+ const require_dist = require("../../dist-mQ3BkrJd.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-Dvkcu_zf.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -594,16 +594,33 @@ function capAppliesToDeviceType(def, deviceType) {
594
594
  return declared.some((t) => t === deviceType);
595
595
  }
596
596
  async function getBindings(deps, input) {
597
- const storeKey = String(input.deviceId);
598
- const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
599
597
  const row = await deps.rows.get(input.deviceId);
600
- const deviceType = resolveDeviceType(deps, row, input.deviceId);
601
- const presence = await resolveDevicePresence(deps, row, input.deviceId);
598
+ return resolveBindingsForDevice(deps, input.deviceId, row);
599
+ }
600
+ /**
601
+ * The `getBindings` resolver, taking the device's row as a PARAMETER instead
602
+ * of fetching it.
603
+ *
604
+ * `getBindings` (one device) and `getBindingsBatch` / `getAllBindings` (many)
605
+ * share this — the routing rules and the per-device shape stay identical by
606
+ * construction. The split exists so the batch callers can resolve every row
607
+ * they need in ONE store round trip ({@link DeviceRowStore.getMany} /
608
+ * {@link DeviceRowStore.listAll}) instead of looping this function's own
609
+ * `deps.rows.get(deviceId)`, which is the primary-key-lookup-per-loop-
610
+ * iteration shape measured on the live hub 2026-08-25: `get
611
+ * device-manager:devices` at 1 656 calls/min steady-state and 17 687 during
612
+ * boot, one row per call.
613
+ */
614
+ async function resolveBindingsForDevice(deps, deviceId, row) {
615
+ const storeKey = String(deviceId);
616
+ const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
617
+ const deviceType = resolveDeviceType(deps, row, deviceId);
618
+ const presence = await resolveDevicePresence(deps, row, deviceId);
602
619
  const entries = [];
603
620
  const seenCaps = /* @__PURE__ */ new Set();
604
- const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
621
+ const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
605
622
  for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
606
- const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
623
+ const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
607
624
  const remoteNative = resolveRemote(capName);
608
625
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
609
626
  const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
@@ -620,7 +637,7 @@ async function getBindings(deps, input) {
620
637
  });
621
638
  seenCaps.add(capName);
622
639
  }
623
- if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device — returning none", { tags: { deviceId: input.deviceId } });
640
+ if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device — returning none", { tags: { deviceId } });
624
641
  else if (deps.capabilityRegistry) {
625
642
  const skippedForType = [];
626
643
  for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
@@ -631,7 +648,7 @@ async function getBindings(deps, input) {
631
648
  }
632
649
  const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
633
650
  if (!defaultWrapperAddonId) continue;
634
- const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
651
+ const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, deviceId) ?? null;
635
652
  const remoteNative = resolveRemote(capName);
636
653
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
637
654
  entries.push({
@@ -644,16 +661,16 @@ async function getBindings(deps, input) {
644
661
  seenCaps.add(capName);
645
662
  }
646
663
  if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
647
- tags: { deviceId: input.deviceId },
664
+ tags: { deviceId },
648
665
  meta: {
649
666
  deviceType,
650
667
  skipped: skippedForType
651
668
  }
652
669
  });
653
670
  }
654
- if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
671
+ if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(deviceId)) {
655
672
  if (seenCaps.has(capName)) continue;
656
- const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
673
+ const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, deviceId) ?? "";
657
674
  entries.push({
658
675
  capName,
659
676
  kind: "native",
@@ -663,7 +680,7 @@ async function getBindings(deps, input) {
663
680
  });
664
681
  seenCaps.add(capName);
665
682
  }
666
- const pushFed = deps.remoteNativeCaps.get(input.deviceId);
683
+ const pushFed = deps.remoteNativeCaps.get(deviceId);
667
684
  if (pushFed) for (const [capName, info] of pushFed) {
668
685
  if (seenCaps.has(capName)) continue;
669
686
  entries.push({
@@ -675,9 +692,9 @@ async function getBindings(deps, input) {
675
692
  });
676
693
  seenCaps.add(capName);
677
694
  }
678
- const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
695
+ const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
679
696
  if (clusterCaps) for (const entry of clusterCaps) {
680
- if (entry.deviceId !== input.deviceId) continue;
697
+ if (entry.deviceId !== deviceId) continue;
681
698
  if (seenCaps.has(entry.capName)) continue;
682
699
  if (!entry.addonId) continue;
683
700
  const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
@@ -692,7 +709,7 @@ async function getBindings(deps, input) {
692
709
  seenCaps.add(entry.capName);
693
710
  }
694
711
  return {
695
- deviceId: input.deviceId,
712
+ deviceId,
696
713
  entries
697
714
  };
698
715
  }
@@ -705,14 +722,22 @@ async function getBindings(deps, input) {
705
722
  * caller needing seventy-five paid either 75 round-trips (1211 ms / 118 KB) or
706
723
  * `getAllBindings` (511 ms / 679 KB of mostly-unwanted rows).
707
724
  *
708
- * Sequential on purpose, like `getAllBindings`: `getBindings` is a pair of
709
- * store reads on this process's own event loop, so concurrency buys nothing
710
- * and a fan-out of `Promise.all` over hundreds of ids only makes the queue
711
- * less fair to everything else on it.
725
+ * Sequential on purpose, like `getAllBindings`: `resolveBindingsForDevice`'s
726
+ * remaining per-device work (the bindings-store read, the routing rules) runs
727
+ * on this process's own event loop, so concurrency buys nothing there and a
728
+ * fan-out of `Promise.all` over hundreds of ids only makes the queue less fair
729
+ * to everything else on it. The one part that DOES amortise across the whole
730
+ * set — the device row lookup — is fetched ONCE, ahead of the loop
731
+ * ({@link DeviceRowStore.getMany}), rather than once per iteration: measured
732
+ * on the live hub 2026-08-25, `get device-manager:devices` ran 1 656
733
+ * calls/min steady-state, one row per call, which is exactly what looping
734
+ * `getBindings`'s own `rows.get(deviceId)` here produced.
712
735
  */
713
736
  async function getBindingsBatch(deps, input) {
737
+ const ids = [...new Set(input.deviceIds)];
738
+ const rows = await deps.rows.getMany(ids);
714
739
  const out = [];
715
- for (const deviceId of new Set(input.deviceIds)) out.push(await getBindings(deps, { deviceId }));
740
+ for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null));
716
741
  return out;
717
742
  }
718
743
  /**
@@ -727,8 +752,13 @@ async function getBindingsBatch(deps, input) {
727
752
  * payload periodically.
728
753
  */
729
754
  async function getAllBindings(deps) {
755
+ const fleet = await deps.rows.listAll();
756
+ const rowById = /* @__PURE__ */ new Map();
730
757
  const ids = /* @__PURE__ */ new Set();
731
- for (const row of await deps.rows.listAll()) ids.add(row.meta.id);
758
+ for (const row of fleet) {
759
+ rowById.set(row.meta.id, row);
760
+ ids.add(row.meta.id);
761
+ }
732
762
  const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
733
763
  for (const device of registered) ids.add(device.id);
734
764
  if (ids.size === 0) {
@@ -736,7 +766,7 @@ async function getAllBindings(deps) {
736
766
  return [];
737
767
  }
738
768
  const out = [];
739
- for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
769
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null));
740
770
  return out;
741
771
  }
742
772
  /**
@@ -3919,6 +3949,112 @@ var DeviceMetaStore = class {
3919
3949
  };
3920
3950
  };
3921
3951
  //#endregion
3952
+ //#region src/builtins/device-manager/fleet-read-census.ts
3953
+ /** How many stack frames identify a call site. One frame is a method; three are
3954
+ * a cause — `listFleet < listAll < onDeviceRegistered` is the answer, `listAll`
3955
+ * alone is not. Same choice, for the same reason, as `read-census.ts`. */
3956
+ var CENSUS_STACK_FRAMES = 3;
3957
+ function siteFromStack(stack) {
3958
+ if (stack === void 0) return "unknown";
3959
+ const frames = stack.split("\n").slice(1).map((line) => line.trim().replace(/^at\s+/, "")).filter((line) => line.length > 0).slice(0, CENSUS_STACK_FRAMES).map((line) => {
3960
+ const open = line.indexOf(" (");
3961
+ return (open > 0 ? line.slice(0, open) : line).replace(/^async\s+/, "");
3962
+ });
3963
+ return frames.length === 0 ? "unknown" : frames.join(" < ");
3964
+ }
3965
+ var FleetReadCensus = class FleetReadCensus {
3966
+ /**
3967
+ * Longest window the census may be armed for.
3968
+ *
3969
+ * Ten minutes covers a boot several times over and bounds what an operator
3970
+ * pays if they set the variable and forget it. A diagnostic that can be left
3971
+ * on becomes a tax nobody remembers enabling.
3972
+ */
3973
+ static MAX_WINDOW_MS = 10 * 6e4;
3974
+ windowMs;
3975
+ closesAt;
3976
+ counts = /* @__PURE__ */ new Map();
3977
+ reported = false;
3978
+ constructor(rawEnv, nowMs) {
3979
+ const parsed = rawEnv === void 0 ? NaN : Number(rawEnv);
3980
+ const valid = Number.isFinite(parsed) && parsed > 0;
3981
+ this.windowMs = valid ? Math.min(parsed, FleetReadCensus.MAX_WINDOW_MS) : 0;
3982
+ this.closesAt = valid ? nowMs + this.windowMs : 0;
3983
+ }
3984
+ get armed() {
3985
+ return this.windowMs > 0;
3986
+ }
3987
+ /** Test seam: pretend the window closed. Production reads the clock. */
3988
+ closeAt(nowMs) {
3989
+ if (nowMs >= this.closesAt) this.forceClosed = true;
3990
+ }
3991
+ forceClosed = false;
3992
+ isOpen(nowMs) {
3993
+ if (!this.armed || this.forceClosed) return false;
3994
+ return nowMs < this.closesAt;
3995
+ }
3996
+ /**
3997
+ * Capture the caller, or `null` when the window is shut.
3998
+ *
3999
+ * Called at the START of a read. A read that began inside the window belongs
4000
+ * to it even if it lands after the close — otherwise the slow reads at the
4001
+ * edge would be attributed to nobody, and the slow ones are the interesting
4002
+ * ones.
4003
+ */
4004
+ siteOf(nowMs = Date.now()) {
4005
+ if (!this.isOpen(nowMs)) return null;
4006
+ const holder = {};
4007
+ Error.captureStackTrace(holder, this.siteOf);
4008
+ return siteFromStack(holder.stack);
4009
+ }
4010
+ /** Count one read at an already-captured site. A `null` site is not counted. */
4011
+ record(site, rowCount) {
4012
+ if (site === null) return;
4013
+ const existing = this.counts.get(site);
4014
+ if (existing === void 0) {
4015
+ this.counts.set(site, {
4016
+ site,
4017
+ calls: 1,
4018
+ rows: rowCount
4019
+ });
4020
+ return;
4021
+ }
4022
+ existing.calls += 1;
4023
+ existing.rows += rowCount;
4024
+ }
4025
+ /** Attributed sites, busiest first. */
4026
+ rows() {
4027
+ return [...this.counts.values()].map((c) => ({
4028
+ site: c.site,
4029
+ calls: c.calls,
4030
+ rows: c.rows
4031
+ })).sort((a, b) => b.calls - a.calls);
4032
+ }
4033
+ /**
4034
+ * Emit the report ONCE.
4035
+ *
4036
+ * `perMin` is included because the raw count is unreadable without the window
4037
+ * it was taken over — "4 149" means nothing until you know it was 66 seconds.
4038
+ */
4039
+ report(logger, elapsedMs) {
4040
+ if (this.reported || !this.armed) return;
4041
+ this.reported = true;
4042
+ const rows = this.rows();
4043
+ const totalCalls = rows.reduce((acc, r) => acc + r.calls, 0);
4044
+ logger.info("fleet read census", { meta: {
4045
+ elapsedMs,
4046
+ sites: rows.length,
4047
+ totalCalls,
4048
+ rows: rows.map((r) => ({
4049
+ site: r.site,
4050
+ calls: r.calls,
4051
+ rows: r.rows,
4052
+ perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs)
4053
+ }))
4054
+ } });
4055
+ }
4056
+ };
4057
+ //#endregion
3922
4058
  //#region src/builtins/device-manager/device-row-store.ts
3923
4059
  /**
3924
4060
  * @durable class=ledger owner=device-manager
@@ -4248,9 +4384,17 @@ var DeviceRowStore = class {
4248
4384
  backend;
4249
4385
  logger;
4250
4386
  declared = null;
4251
- constructor(backend, logger) {
4387
+ /** Call-site attribution for fleet reads. Armed only by
4388
+ * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
4389
+ census;
4390
+ constructor(backend, logger, census) {
4252
4391
  this.backend = backend;
4253
4392
  this.logger = logger;
4393
+ this.census = census ?? new FleetReadCensus(process.env["CAMSTACK_FLEET_READ_CENSUS_MS"], Date.now());
4394
+ if (this.census.armed) {
4395
+ const startedAt = Date.now();
4396
+ setTimeout(() => this.census.report(this.logger, Date.now() - startedAt), this.census.windowMs).unref?.();
4397
+ }
4254
4398
  }
4255
4399
  /**
4256
4400
  * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
@@ -4329,6 +4473,24 @@ var DeviceRowStore = class {
4329
4473
  }, opts.slim === true ? DEVICE_ROWS_SLIM_COLUMNS : void 0);
4330
4474
  }
4331
4475
  /**
4476
+ * Many devices, by numeric id, in ONE query.
4477
+ *
4478
+ * `get(id)` called once per id in a loop is the primary-key lookup this
4479
+ * store exists to make cheap, repeated until it isn't — measured on the
4480
+ * live hub 2026-08-25: `get device-manager:devices` at 1 656 calls/min
4481
+ * steady-state and 17 687 during boot, exactly one row per call. This
4482
+ * collapses that into the same `whereIn` {@link listFleet} already does for
4483
+ * a fleet read, keyed by id.
4484
+ *
4485
+ * A requested id the fleet does not know is simply ABSENT from the map —
4486
+ * never a fabricated row, matching {@link get}'s `null`. An empty `ids`
4487
+ * reads nothing at all, same as `listFleet({ deviceIds: [] })`.
4488
+ */
4489
+ async getMany(ids) {
4490
+ const rows = await this.listFleet({ deviceIds: ids });
4491
+ return new Map(rows.map((row) => [row.meta.id, row]));
4492
+ }
4493
+ /**
4332
4494
  * The device an addon knows as `stableId`, or `null`.
4333
4495
  *
4334
4496
  * `(addonId, stableId)` is the addon-facing identity — unique by
@@ -4401,11 +4563,13 @@ var DeviceRowStore = class {
4401
4563
  }
4402
4564
  async list(filter, columns) {
4403
4565
  await this.declare();
4566
+ const site = this.census.siteOf();
4404
4567
  const records = await this.backend.query({
4405
4568
  collection: DEVICE_ROWS_COLLECTION,
4406
4569
  filter,
4407
4570
  ...columns === void 0 ? {} : { columns }
4408
4571
  });
4572
+ this.census.record(site, records.length);
4409
4573
  const out = [];
4410
4574
  for (const record of records) {
4411
4575
  const decoded = decodeDeviceRow(record.data);