@camstack/system 1.2.61 → 1.2.63

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 (86) 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/block-package.d.ts +9 -27
  14. package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
  17. package/dist/builtins/device-manager/device-aggregation.d.ts +49 -2
  18. package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
  19. package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
  20. package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
  21. package/dist/builtins/device-manager/device-manager.addon.js +182 -888
  22. package/dist/builtins/device-manager/device-manager.addon.mjs +182 -888
  23. package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
  24. package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
  25. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
  26. package/dist/builtins/device-manager/device-projection.d.ts +1 -2
  27. package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
  28. package/dist/builtins/device-manager/device-queries.d.ts +1 -1
  29. package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
  30. package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
  31. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  32. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  33. package/dist/builtins/hub-forwarder/index.js +1 -1
  34. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
  38. package/dist/builtins/local-auth/index.d.ts +1 -0
  39. package/dist/builtins/local-auth/index.js +1 -0
  40. package/dist/builtins/local-auth/index.mjs +2 -2
  41. package/dist/builtins/local-auth/local-auth.addon.js +561 -497
  42. package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
  43. package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
  44. package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
  45. package/dist/builtins/local-network/local-network.addon.js +1 -1
  46. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  47. package/dist/builtins/loki-logging/index.js +1 -1
  48. package/dist/builtins/loki-logging/index.mjs +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  51. package/dist/builtins/platform-probe/index.js +1 -1
  52. package/dist/builtins/platform-probe/index.mjs +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/snapshot/index.js +319 -20
  56. package/dist/builtins/snapshot/index.mjs +319 -20
  57. package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
  58. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
  59. package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
  63. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +45 -5
  64. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +45 -5
  65. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  66. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  67. package/dist/builtins/system-config/system-config.addon.js +1 -1
  68. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  69. package/dist/builtins/winston-logging/index.js +1 -1
  70. package/dist/builtins/winston-logging/index.mjs +1 -1
  71. package/dist/{dist-L6QPJWsr.mjs → dist-BJ2w3uoN.mjs} +1307 -1313
  72. package/dist/{dist-BN681PgO.js → dist-LDrOFrd-.js} +1255 -1273
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.js +209 -5
  75. package/dist/index.mjs +206 -7
  76. package/dist/kernel/addon-residue.d.ts +49 -0
  77. package/dist/kernel/index.d.ts +2 -0
  78. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
  79. package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-CopL1_5C.mjs} +20 -14
  80. package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-DNfmBu20.js} +20 -14
  81. package/package.json +5 -1
  82. package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
  83. package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
  84. package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
  85. package/dist/main-CVLq8IzA.js +0 -2356
  86. package/dist/main-CzH19GkU.mjs +0 -2353
@@ -3,9 +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-BN681PgO.js");
6
+ const require_dist = require("../../dist-LDrOFrd-.js");
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
- let zod = require("zod");
9
8
  let node_crypto = require("node:crypto");
10
9
  //#region src/builtins/device-manager/day-night-config-schema.ts
11
10
  var MODE_LABELS = {
@@ -537,13 +536,6 @@ function mergeAggregates(parts) {
537
536
  //#endregion
538
537
  //#region src/builtins/device-manager/device-projection.ts
539
538
  /**
540
- * Pure device-projection + small predicate helpers for the device-manager
541
- * addon. Extracted verbatim from `device-manager.addon.ts`. None of these
542
- * functions close over the addon instance — they take everything they need
543
- * as explicit parameters, so they are safe to share across the addon's
544
- * query / aggregation modules.
545
- */
546
- /**
547
539
  * Return true when `err` is a transient Moleculer error that is worth
548
540
  * retrying — specifically any `MoleculerRetryableError` subclass
549
541
  * (ServiceNotAvailableError, ServiceNotFoundError, BrokerDisconnectedError,
@@ -572,10 +564,6 @@ function shallowEqual(a, b) {
572
564
  for (const k of ak) if (a[k] !== b[k]) return false;
573
565
  return true;
574
566
  }
575
- /** Returns true when `x` is a non-null, non-array plain object. */
576
- function isRecord$1(x) {
577
- return x !== null && typeof x === "object" && !Array.isArray(x);
578
- }
579
567
  function isCameraDevice(device) {
580
568
  return "getStreamSources" in device && typeof device.getStreamSources === "function";
581
569
  }
@@ -660,7 +648,6 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
660
648
  ...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
661
649
  ...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
662
650
  ...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
663
- ...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
664
651
  ...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
665
652
  };
666
653
  }
@@ -676,28 +663,6 @@ function resolveDeviceById(registry, deviceId) {
676
663
  }
677
664
  //#endregion
678
665
  //#region src/builtins/device-manager/device-queries.ts
679
- /**
680
- * Device query + live-runtime + provider/adoption operations for the
681
- * device-manager addon — the read/live half of the `IDeviceManagerProvider`
682
- * surface. Extracted verbatim from the `onInitialize` provider literal as free
683
- * functions taking a `ProviderContext` (DI). Input/return types are pinned to
684
- * the cap contract via `Parameters<IDeviceManagerProvider['x']>[0]` /
685
- * `ReturnType<…>` so the typed surface is identical to the inferred methods.
686
- *
687
- * Covers: persisted/live device queries (listPersistedByAddon, listAll,
688
- * getDevice, getChildren), live runtime methods (getStreamSources,
689
- * getConfigSchema, getSettingsSchema, updateConfig, enable, disable, remove,
690
- * removeByIntegration, getStreamProfileMap, setStreamProfileMap, probeStreams),
691
- * device-provider ops (discoverDevices, adoptDevice, getCreationSchema,
692
- * createDevice, testCreationField), and device-adoption ops
693
- * (adoptionListCandidates/Refresh/Adopt/Release/Resync, testField).
694
- *
695
- * The hub-live-then-persisted-fallback union, the cross-process device-ops
696
- * dispatch, the depth-first remove cascade, and the resetToSource resync purge
697
- * are byte-for-byte preserved. `remove`/`removeByIntegration`/`enable`/`disable`/
698
- * `probeStreams`/`getStreamProfileMap` self-call sibling cap methods via
699
- * `pctx.provider`. Pure refactor — no behavior change.
700
- */
701
666
  async function listPersistedByAddon(pctx, input) {
702
667
  const { addonId } = input;
703
668
  const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
@@ -774,7 +739,6 @@ async function listAll(pctx, input) {
774
739
  ...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
775
740
  ...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
776
741
  ...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
777
- ...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
778
742
  ...m?.display !== void 0 ? { display: m.display } : {},
779
743
  ...(() => {
780
744
  if (slim) return {};
@@ -824,7 +788,6 @@ async function getDevice(pctx, input) {
824
788
  ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
825
789
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
826
790
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
827
- ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
828
791
  ...m.display !== void 0 ? { display: m.display } : {},
829
792
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
830
793
  };
@@ -887,7 +850,6 @@ async function getChildren(pctx, input) {
887
850
  ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
888
851
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
889
852
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
890
- ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
891
853
  ...m.display !== void 0 ? { display: m.display } : {},
892
854
  ...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
893
855
  });
@@ -1443,18 +1405,6 @@ function splitDeviceStoreKeys(patch) {
1443
1405
  //#endregion
1444
1406
  //#region src/builtins/device-manager/device-bindings-store.ts
1445
1407
  /**
1446
- * Persisted wrapper-activation + per-device binding resolution for the
1447
- * device-manager addon. Extracted from `device-manager.addon.ts` as free
1448
- * functions that receive an explicit `BindingsDeps` context (dependency
1449
- * injection) instead of closing over the addon instance — keeps the routing
1450
- * logic cohesive and independently testable while preserving the EXACT call
1451
- * semantics, ordering, and side effects of the original class methods.
1452
- *
1453
- * The addon keeps thin delegator methods (`getBindings`, `getAllBindings`, …)
1454
- * that build `BindingsDeps` from its own state and forward here, so every
1455
- * existing internal + cap-router call site is unchanged.
1456
- */
1457
- /**
1458
1408
  * Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
1459
1409
  * Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
1460
1410
  * removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
@@ -1556,9 +1506,47 @@ function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
1556
1506
  };
1557
1507
  return null;
1558
1508
  }
1509
+ /**
1510
+ * Resolve the device's declared TYPE (`'camera'`, `'event-emitter'`, …), or
1511
+ * `undefined` when it cannot be established.
1512
+ *
1513
+ * The PERSISTED meta is the authority: `ctx.kernel.deviceRegistry` is hub-only
1514
+ * and has been observed empty in the very process that answers `getBindings`
1515
+ * correctly (see the note on `getAllBindings`). The registry is consulted only
1516
+ * as a secondary source, for a device constructed but not yet persisted.
1517
+ *
1518
+ * `undefined` is a first-class answer and callers must treat it as "no
1519
+ * filtering" — an absent row must never be the reason a device loses bindings.
1520
+ */
1521
+ function resolveDeviceType(deps, rawStore, deviceId) {
1522
+ const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
1523
+ if (typeof persisted === "string" && persisted.length > 0) return persisted;
1524
+ const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
1525
+ return typeof live === "string" && live.length > 0 ? live : void 0;
1526
+ }
1527
+ /**
1528
+ * Does a capability apply to a device of type `deviceType`?
1529
+ *
1530
+ * The cap's `deviceTypes` is the ONLY declaration consulted (D4: behavioural
1531
+ * cap metadata lives in the `*.cap.ts`, never in a manifest). Two deliberate
1532
+ * fail-open cases:
1533
+ *
1534
+ * - a cap that declares no `deviceTypes` (or an empty list) applies to every
1535
+ * device — the pre-existing, back-compatible semantics;
1536
+ * - an UNKNOWN `deviceType` never filters, so a missing/failed meta lookup
1537
+ * changes nothing (D49: a read that fails must not destroy work).
1538
+ */
1539
+ function capAppliesToDeviceType(def, deviceType) {
1540
+ if (deviceType === void 0) return true;
1541
+ const declared = def?.deviceTypes;
1542
+ if (!declared || declared.length === 0) return true;
1543
+ return declared.some((t) => t === deviceType);
1544
+ }
1559
1545
  async function getBindings(deps, input) {
1560
1546
  const storeKey = String(input.deviceId);
1561
- const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
1547
+ const rawStore = await deps.ctx.settings.readAddonStore();
1548
+ const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
1549
+ const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
1562
1550
  const entries = [];
1563
1551
  const seenCaps = /* @__PURE__ */ new Set();
1564
1552
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
@@ -1580,21 +1568,35 @@ async function getBindings(deps, input) {
1580
1568
  });
1581
1569
  seenCaps.add(capName);
1582
1570
  }
1583
- if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
1584
- if (seenCaps.has(capName)) continue;
1585
- const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
1586
- if (!defaultWrapperAddonId) continue;
1587
- const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
1588
- const remoteNative = resolveRemote(capName);
1589
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
1590
- entries.push({
1591
- capName,
1592
- kind: "wrapped",
1593
- providerAddonId: defaultWrapperAddonId,
1594
- providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
1595
- nativeAddonId
1571
+ if (deps.capabilityRegistry) {
1572
+ const skippedForType = [];
1573
+ for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
1574
+ if (seenCaps.has(capName)) continue;
1575
+ if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
1576
+ skippedForType.push(capName);
1577
+ continue;
1578
+ }
1579
+ const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
1580
+ if (!defaultWrapperAddonId) continue;
1581
+ const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
1582
+ const remoteNative = resolveRemote(capName);
1583
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
1584
+ entries.push({
1585
+ capName,
1586
+ kind: "wrapped",
1587
+ providerAddonId: defaultWrapperAddonId,
1588
+ providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
1589
+ nativeAddonId
1590
+ });
1591
+ seenCaps.add(capName);
1592
+ }
1593
+ if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
1594
+ tags: { deviceId: input.deviceId },
1595
+ meta: {
1596
+ deviceType,
1597
+ skipped: skippedForType
1598
+ }
1596
1599
  });
1597
- seenCaps.add(capName);
1598
1600
  }
1599
1601
  if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
1600
1602
  if (seenCaps.has(capName)) continue;
@@ -1636,21 +1638,6 @@ async function getBindings(deps, input) {
1636
1638
  });
1637
1639
  seenCaps.add(entry.capName);
1638
1640
  }
1639
- if (deps.devicesWithLinks?.has(input.deviceId)) {
1640
- const row = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(input.deviceId)];
1641
- for (const link of row?.deviceLinks ?? []) {
1642
- const capName = link.target.cap;
1643
- if (seenCaps.has(capName)) continue;
1644
- entries.push({
1645
- capName,
1646
- kind: "linked",
1647
- providerAddonId: require_dist.bareAddonId(deps.ctx.id),
1648
- providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
1649
- nativeAddonId: ""
1650
- });
1651
- seenCaps.add(capName);
1652
- }
1653
- }
1654
1641
  return {
1655
1642
  deviceId: input.deviceId,
1656
1643
  entries
@@ -1668,10 +1655,20 @@ async function getBindings(deps, input) {
1668
1655
  * payload periodically.
1669
1656
  */
1670
1657
  async function getAllBindings(deps) {
1671
- const hubRegistry = deps.ctx.kernel?.deviceRegistry;
1672
- if (!hubRegistry) return [];
1658
+ const store = await deps.ctx.settings.readAddonStore();
1659
+ const ids = /* @__PURE__ */ new Set();
1660
+ for (const key of Object.keys(store.deviceMeta ?? {})) {
1661
+ const id = Number(key);
1662
+ if (Number.isInteger(id)) ids.add(id);
1663
+ }
1664
+ const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
1665
+ for (const device of registered) ids.add(device.id);
1666
+ if (ids.size === 0) {
1667
+ deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
1668
+ return [];
1669
+ }
1673
1670
  const out = [];
1674
- for (const device of hubRegistry.getAll()) out.push(await getBindings(deps, { deviceId: device.id }));
1671
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
1675
1672
  return out;
1676
1673
  }
1677
1674
  /**
@@ -1992,6 +1989,30 @@ function resolveNativeDeviceOwner(deps, deviceId) {
1992
1989
  } : null;
1993
1990
  }
1994
1991
  /**
1992
+ * Race `work` against a deadline WITHOUT swallowing its rejection.
1993
+ *
1994
+ * `Promise.race` attaches a handler to every entrant, so a `work` that rejects
1995
+ * after the deadline already fired is still handled — no unhandled rejection,
1996
+ * and no `.catch(() => {})` that would also hide a genuine throw from the
1997
+ * caller's `try`. A rejection propagates out of here exactly as it would
1998
+ * without the race; only the TIMEOUT is turned into a value.
1999
+ */
2000
+ async function withDeadline(work, deadlineMs) {
2001
+ let timer;
2002
+ const deadline = new Promise((resolve) => {
2003
+ timer = setTimeout(() => resolve({ kind: "timed-out" }), deadlineMs);
2004
+ timer.unref?.();
2005
+ });
2006
+ try {
2007
+ return await Promise.race([work.then((value) => ({
2008
+ kind: "value",
2009
+ value
2010
+ })), deadline]);
2011
+ } finally {
2012
+ if (timer !== void 0) clearTimeout(timer);
2013
+ }
2014
+ }
2015
+ /**
1995
2016
  * Aggregate `status` across every registered cap for a device.
1996
2017
  *
1997
2018
  * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
@@ -2002,11 +2023,16 @@ function resolveNativeDeviceOwner(deps, deviceId) {
2002
2023
  * overall aggregate stays usable — a single misbehaving provider
2003
2024
  * must not blank out a device's entire status view.
2004
2025
  *
2026
+ * A provider that never answers is treated exactly like one that throws: it
2027
+ * costs its own cap and nothing else. See {@link STATUS_CALL_DEADLINE_MS} for
2028
+ * why a bound is needed at all when the transport already has one.
2029
+ *
2005
2030
  * Returned shape is `Record<capName, unknown | null>`; the client-
2006
2031
  * side hook tightens this to `CapStatusTypeMap` via the generated
2007
2032
  * `cap-status-types.ts`.
2008
2033
  */
2009
- async function getDeviceStatusAggregate(deps, input) {
2034
+ async function getDeviceStatusAggregate(deps, input, options) {
2035
+ const deadlineMs = options?.deadlineMs ?? 3e3;
2010
2036
  const capNames = input.caps ?? require_dist.CAP_NAMES_WITH_STATUS;
2011
2037
  const registry = deps.capabilityRegistry;
2012
2038
  const out = {};
@@ -2026,7 +2052,20 @@ async function getDeviceStatusAggregate(deps, input) {
2026
2052
  out[capName] = null;
2027
2053
  return;
2028
2054
  }
2029
- const raw = await provider.getStatus({ deviceId: input.deviceId });
2055
+ const settled = await withDeadline(provider.getStatus({ deviceId: input.deviceId }), deadlineMs);
2056
+ if (settled.kind === "timed-out") {
2057
+ deps.ctx.logger.warn("getDeviceStatusAggregate: cap missed its deadline, dropping", {
2058
+ tags: { deviceId: input.deviceId },
2059
+ meta: {
2060
+ deviceId: input.deviceId,
2061
+ capName,
2062
+ deadlineMs
2063
+ }
2064
+ });
2065
+ out[capName] = null;
2066
+ return;
2067
+ }
2068
+ const raw = settled.value;
2030
2069
  if (raw == null) {
2031
2070
  out[capName] = null;
2032
2071
  return;
@@ -2080,8 +2119,21 @@ var STATUS_BATCH_WINDOW = 10;
2080
2119
  * A device whose whole aggregation fails still gets an ENTRY (all caps null)
2081
2120
  * rather than being omitted: a caller that reads a missing key as "not
2082
2121
  * answered yet" would keep the previous badge on screen forever.
2122
+ *
2123
+ * ## The response is BOUNDED, and that is load-bearing
2124
+ *
2125
+ * The windows are walked SEQUENTIALLY, so before the per-cap deadline existed a
2126
+ * single unanswerable device did not merely cost its own window — it held every
2127
+ * later window behind it, and the call never returned at all. That is the whole
2128
+ * mechanism behind "one node offline and the app loads nothing": this is the
2129
+ * one call a camera list makes for every card, the client has no timeout of its
2130
+ * own, and a tRPC query that never settles is `isLoading` forever.
2131
+ *
2132
+ * The ceiling is now `ceil(devices / STATUS_BATCH_WINDOW) × STATUS_CALL_DEADLINE_MS`
2133
+ * in the worst case (a dead device in every window), and unchanged when every
2134
+ * node is healthy. It is never unbounded.
2083
2135
  */
2084
- async function getDeviceStatusAggregateBatch(deps, input) {
2136
+ async function getDeviceStatusAggregateBatch(deps, input, options) {
2085
2137
  const out = {};
2086
2138
  const ids = [...new Set(input.deviceIds)];
2087
2139
  for (let i = 0; i < ids.length; i += STATUS_BATCH_WINDOW) {
@@ -2090,7 +2142,7 @@ async function getDeviceStatusAggregateBatch(deps, input) {
2090
2142
  try {
2091
2143
  const single = { deviceId };
2092
2144
  if (input.caps !== void 0) single.caps = input.caps;
2093
- out[String(deviceId)] = await getDeviceStatusAggregate(deps, single);
2145
+ out[String(deviceId)] = await getDeviceStatusAggregate(deps, single, options);
2094
2146
  } catch (err) {
2095
2147
  deps.ctx.logger.warn("getDeviceStatusAggregateBatch: device aggregation threw", {
2096
2148
  tags: { deviceId },
@@ -2540,120 +2592,6 @@ var DeviceEventPropagator = class {
2540
2592
  }
2541
2593
  };
2542
2594
  //#endregion
2543
- //#region src/builtins/device-manager/device-link-cycle.ts
2544
- function nodeKey(deviceId, cap) {
2545
- return `${deviceId}:${cap}`;
2546
- }
2547
- /** The target's container stableId — its parent's (falling back to its own for
2548
- * a top-level target). Sibling FIELD sources resolve relative to this, the same
2549
- * rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
2550
- function containerStableId(targetRow, rowById) {
2551
- return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
2552
- }
2553
- /** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
2554
- * null when it is a literal or resolves to no known device. */
2555
- function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
2556
- if (binding.kind === "literal") return null;
2557
- const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
2558
- const srcId = idByStableId.get(wantedStableId);
2559
- if (srcId === void 0) return null;
2560
- return nodeKey(srcId, binding.cap);
2561
- }
2562
- /**
2563
- * Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
2564
- * A literal source depends on nothing; a field/global source depends on exactly
2565
- * one node; an EXPRESSION source depends on one node per resolvable non-literal
2566
- * binding. Unresolvable (dangling) bindings contribute no edge — they cannot
2567
- * loop while dangling; a cycle that only becomes resolvable later is bounded by
2568
- * the resolve-time re-entrancy guard.
2569
- */
2570
- function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
2571
- const src = link.source;
2572
- if (src.kind === "expression") {
2573
- const nodes = [];
2574
- for (const binding of Object.values(src.bindings)) {
2575
- const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
2576
- if (node !== null) nodes.push(node);
2577
- }
2578
- return nodes;
2579
- }
2580
- const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
2581
- return node !== null ? [node] : [];
2582
- }
2583
- /**
2584
- * Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
2585
- * a dependency cycle. Returns the cycle as an ordered list of node keys
2586
- * (first node repeated at the end) or null when the set is safe.
2587
- */
2588
- function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
2589
- const rows = Object.values(allMeta);
2590
- const rowById = /* @__PURE__ */ new Map();
2591
- const idByStableId = /* @__PURE__ */ new Map();
2592
- for (const r of rows) {
2593
- rowById.set(r.id, r);
2594
- if (!idByStableId.has(r.stableId)) idByStableId.set(r.stableId, r.id);
2595
- }
2596
- const dependsOn = /* @__PURE__ */ new Map();
2597
- const addEdges = (row, links) => {
2598
- for (const link of links) {
2599
- const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
2600
- if (srcNodes.length === 0) continue;
2601
- const tNode = nodeKey(row.id, link.target.cap);
2602
- const list = dependsOn.get(tNode) ?? [];
2603
- for (const srcNode of srcNodes) list.push(srcNode);
2604
- dependsOn.set(tNode, list);
2605
- }
2606
- };
2607
- for (const r of rows) {
2608
- const links = r.id === editedDeviceId ? editedLinks : r.deviceLinks ?? [];
2609
- if (links.length > 0) addEdges(r, links);
2610
- }
2611
- const roots = [...new Set(editedLinks.map((l) => nodeKey(editedDeviceId, l.target.cap)))];
2612
- for (const root of roots) {
2613
- const cycle = dfsFindCycle(root, dependsOn);
2614
- if (cycle) return cycle;
2615
- }
2616
- return null;
2617
- }
2618
- /** Iterative DFS from `root` over `dependsOn`; returns the first back-edge
2619
- * cycle path (closed — first node repeated last) or null. */
2620
- function dfsFindCycle(root, dependsOn) {
2621
- const onPath = /* @__PURE__ */ new Set();
2622
- const done = /* @__PURE__ */ new Set();
2623
- const path = [];
2624
- const stack = [{
2625
- node: root,
2626
- nextChild: 0
2627
- }];
2628
- onPath.add(root);
2629
- path.push(root);
2630
- while (stack.length > 0) {
2631
- const frame = stack[stack.length - 1];
2632
- const children = dependsOn.get(frame.node) ?? [];
2633
- if (frame.nextChild < children.length) {
2634
- const child = children[frame.nextChild];
2635
- frame.nextChild += 1;
2636
- if (onPath.has(child)) {
2637
- const start = path.indexOf(child);
2638
- return [...path.slice(start), child];
2639
- }
2640
- if (done.has(child)) continue;
2641
- stack.push({
2642
- node: child,
2643
- nextChild: 0
2644
- });
2645
- onPath.add(child);
2646
- path.push(child);
2647
- continue;
2648
- }
2649
- stack.pop();
2650
- onPath.delete(frame.node);
2651
- path.pop();
2652
- done.add(frame.node);
2653
- }
2654
- return null;
2655
- }
2656
- //#endregion
2657
2595
  //#region src/builtins/device-manager/device-meta-actions.ts
2658
2596
  /**
2659
2597
  * Device meta-mutation + persistence actions for the device-manager addon —
@@ -2667,7 +2605,7 @@ function dfsFindCycle(root, dependsOn) {
2667
2605
  * removeDevice), config persistence (persistConfig, loadConfig), the meta
2668
2606
  * surface load (loadMeta, loadRuntimeState), every meta setter (setName,
2669
2607
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
2670
- * setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
2608
+ * setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
2671
2609
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
2672
2610
  * (listLocations, addLocation, removeLocation).
2673
2611
  *
@@ -2774,7 +2712,6 @@ async function registerDevice(pctx, input) {
2774
2712
  ...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
2775
2713
  ...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
2776
2714
  ...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
2777
- ...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
2778
2715
  ...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
2779
2716
  ...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
2780
2717
  parentDeviceId,
@@ -2836,7 +2773,6 @@ async function registerDevice(pctx, input) {
2836
2773
  generation: Date.now()
2837
2774
  }
2838
2775
  });
2839
- if (pctx.host.devicesWithLinks.size > 0 && pctx.host.expectedSourceStableIds.has(stableId)) await pctx.metaStore.rebuildLinkDependents();
2840
2776
  }
2841
2777
  async function removeDevice(pctx, input) {
2842
2778
  const { deviceId } = input;
@@ -2875,9 +2811,6 @@ async function removeDevice(pctx, input) {
2875
2811
  pctx.host.remoteNativeCaps.delete(deviceId);
2876
2812
  pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
2877
2813
  pctx.metaStore.idToAddonId.delete(deviceId);
2878
- pctx.host.devicesWithLinks.delete(deviceId);
2879
- pctx.host.dropDeviceOverlays(deviceId);
2880
- await pctx.metaStore.rebuildLinkDependents();
2881
2814
  pctx.host.ctx.logger.info("removed device", { tags: {
2882
2815
  deviceId,
2883
2816
  deviceName: deviceName.length > 0 ? deviceName : stableId
@@ -3208,54 +3141,6 @@ async function setChildLayout(pctx, input) {
3208
3141
  });
3209
3142
  }
3210
3143
  /**
3211
- * Set (or replace) the operator-authored cross-device field wirings on
3212
- * a device's meta row. Mirrors `setChildLayout` but persists the
3213
- * `deviceLinks` array. Persisted, projected, and preserved across
3214
- * re-register/restore. Also maintains the in-memory `devicesWithLinks`
3215
- * Set used as an O(1) gate by `resolveLinkedStatus`.
3216
- */
3217
- async function setDeviceLinks(pctx, input) {
3218
- const { deviceId, deviceLinks } = input;
3219
- const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
3220
- if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
3221
- for (const link of deviceLinks) {
3222
- if (link.source.kind !== "expression") continue;
3223
- const err = require_dist.validateExpressionSource(link.source);
3224
- if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
3225
- }
3226
- await pctx.metaStore.withMetaWriteLock(async () => {
3227
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
3228
- if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
3229
- const { meta: m } = persisted;
3230
- const key = String(deviceId);
3231
- const allMeta = await pctx.metaStore.readMeta();
3232
- await pctx.settings.writeAddonStore({ deviceMeta: {
3233
- ...allMeta,
3234
- [key]: {
3235
- ...m,
3236
- deviceLinks
3237
- }
3238
- } });
3239
- });
3240
- if (deviceLinks.length > 0) pctx.host.devicesWithLinks.add(deviceId);
3241
- else pctx.host.devicesWithLinks.delete(deviceId);
3242
- await pctx.metaStore.rebuildLinkDependents();
3243
- pctx.host.ctx.eventBus.emit({
3244
- id: (0, node_crypto.randomUUID)(),
3245
- timestamp: /* @__PURE__ */ new Date(),
3246
- source: {
3247
- type: "device",
3248
- id: deviceId
3249
- },
3250
- category: require_dist.EventCategory.DeviceMetaChanged,
3251
- data: {
3252
- deviceId,
3253
- field: "deviceLinks",
3254
- value: deviceLinks
3255
- }
3256
- });
3257
- }
3258
- /**
3259
3144
  * Stamp (or update) the semantic role on the device's meta row.
3260
3145
  * Called by the kernel's `create()` / `spawnAccessoryChild`
3261
3146
  * pre-seed when `initialMeta.role` is set (analogous to
@@ -3610,69 +3495,10 @@ async function removeLocation(pctx, input) {
3610
3495
  });
3611
3496
  }
3612
3497
  //#endregion
3613
- //#region src/builtins/device-manager/device-link-overlay.ts
3614
- /** Build both lookup maps from resolved link entries. Pure — order-preserving. */
3615
- function buildLinkIndexes(entries) {
3616
- const targets = /* @__PURE__ */ new Map();
3617
- const dependents = /* @__PURE__ */ new Map();
3618
- for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
3619
- const tKey = `${targetDeviceId}:${link.target.cap}`;
3620
- const tList = targets.get(tKey) ?? [];
3621
- tList.push({
3622
- link,
3623
- sourceDeviceId,
3624
- ...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
3625
- });
3626
- targets.set(tKey, tList);
3627
- const addDependent = (sourceId, sourceCap) => {
3628
- const sKey = `${sourceId}:${sourceCap}`;
3629
- const sList = dependents.get(sKey) ?? [];
3630
- sList.push({
3631
- targetDeviceId,
3632
- targetCap: link.target.cap
3633
- });
3634
- dependents.set(sKey, sList);
3635
- };
3636
- if (link.source.kind === "expression") {
3637
- if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
3638
- const binding = link.source.bindings[name];
3639
- if (binding === void 0 || binding.kind === "literal") continue;
3640
- addDependent(bindingDeviceId, binding.cap);
3641
- }
3642
- continue;
3643
- }
3644
- if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
3645
- }
3646
- return {
3647
- targets,
3648
- dependents
3649
- };
3650
- }
3651
- //#endregion
3652
3498
  //#region src/builtins/device-manager/device-meta-store.ts
3653
- /**
3654
- * Meta persistence layer for the device-manager addon.
3655
- *
3656
- * `DeviceMetaStore` encapsulates the read/modify/write helpers that previously
3657
- * lived as closures inside `onInitialize`: the `ctx.settings`-backed reads
3658
- * (`readStore` / `readIndex` / `readMeta` / `readMetadataMap`), the
3659
- * single-writer mutex (`withMetaWriteLock`), id allocation
3660
- * (`allocateNextDeviceId`), ownership/child resolution, link-source resolution,
3661
- * and the cross-device link reverse-index rebuild. Extracting it as a class
3662
- * (constructed once per `onInitialize`) keeps the EXACT semantics — the
3663
- * `withMetaWriteLock` promise-chain is per-instance state, identical to the
3664
- * former closure — while letting the meta-action + query modules depend on a
3665
- * named, injectable surface instead of captured closures.
3666
- *
3667
- * Cross-device link state (the `linkTargets` / `linkDependents` /
3668
- * `expectedSourceStableIds` maps + the `devicesWithLinks` gate) lives on the
3669
- * addon; the store reaches it through the injected `LinkIndexHost` so the
3670
- * single owner of that state stays the addon instance.
3671
- */
3672
3499
  var DeviceMetaStore = class {
3673
3500
  settings;
3674
3501
  registry;
3675
- linkHost;
3676
3502
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
3677
3503
  * The persisted meta store is authoritative but reads are async; hub-side
3678
3504
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
@@ -3683,10 +3509,9 @@ var DeviceMetaStore = class {
3683
3509
  * through one promise chain (see `withMetaWriteLock`). Per-instance state —
3684
3510
  * identical to the former `onInitialize` closure variable. */
3685
3511
  metaWriteChain = Promise.resolve();
3686
- constructor(settings, registry, linkHost) {
3512
+ constructor(settings, registry) {
3687
3513
  this.settings = settings;
3688
3514
  this.registry = registry;
3689
- this.linkHost = linkHost;
3690
3515
  }
3691
3516
  readStore = async () => {
3692
3517
  return await this.settings.readAddonStore();
@@ -3748,279 +3573,31 @@ var DeviceMetaStore = class {
3748
3573
  ids.delete(parentId);
3749
3574
  return [...ids];
3750
3575
  };
3751
- /** Resolve a link's `sourceKey` to a live device id: the sibling accessory
3752
- * whose stableId is `${parentStableId}-${sourceKey}`. Null when absent.
3753
- * Pure over a pre-read meta map so a multi-link resolve reads the store once. */
3754
- resolveSourceDeviceId = (parentStableId, sourceKey, meta) => {
3755
- const wanted = `${parentStableId}-${sourceKey}`;
3756
- for (const m of Object.values(meta)) if (m.stableId === wanted) return m.id;
3757
- return null;
3758
- };
3759
- /** Resolve a GLOBAL link source (P2e) to a live device id: the device whose
3760
- * FULL stableId equals `sourceStableId`, regardless of parent container.
3761
- * First match wins (stableIds are effectively unique cluster-wide — see the
3762
- * `DeviceLinkGlobalSource` docblock). Null when absent. Pure over a
3763
- * pre-read meta map so a multi-link resolve reads the store once. */
3764
- resolveGlobalSourceDeviceId = (sourceStableId, meta) => {
3765
- for (const m of Object.values(meta)) if (m.stableId === sourceStableId) return m.id;
3766
- return null;
3767
- };
3768
3576
  allocateNextDeviceId = async () => {
3769
3577
  const current = (await this.readStore()).nextDeviceId ?? 1;
3770
3578
  await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
3771
3579
  return current;
3772
3580
  };
3773
- /** Rebuild the `linkTargets` / `linkDependents` reverse-index maps from the
3774
- * current persisted meta. Called at boot (once the `devicesWithLinks` seed
3775
- * has run) and whenever the link topology can change: `setDeviceLinks`,
3776
- * `registerDevice` (a new sibling source may resolve previously-dangling
3777
- * links), and `removeDevice` (its entries must be evicted). */
3778
- rebuildLinkDependents = async () => {
3779
- if (this.linkHost.devicesWithLinks.size === 0) {
3780
- this.linkHost.setLinkIndexes(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
3781
- return;
3782
- }
3783
- const allMeta = await this.readMeta();
3784
- const stableIdById = /* @__PURE__ */ new Map();
3785
- const idByStableId = /* @__PURE__ */ new Map();
3786
- for (const m of Object.values(allMeta)) {
3787
- stableIdById.set(m.id, m.stableId);
3788
- idByStableId.set(m.stableId, m.id);
3789
- }
3790
- const entries = [];
3791
- const expectedSources = /* @__PURE__ */ new Set();
3792
- for (const targetId of this.linkHost.devicesWithLinks) {
3793
- const targetMeta = Object.values(allMeta).find((m) => m.id === targetId);
3794
- if (!targetMeta) continue;
3795
- const container = targetMeta.parentDeviceId !== null ? stableIdById.get(targetMeta.parentDeviceId) ?? stableIdById.get(targetId) : stableIdById.get(targetId);
3796
- if (container === void 0) continue;
3797
- for (const link of targetMeta.deviceLinks ?? []) {
3798
- const src = link.source;
3799
- if (src.kind === "expression") {
3800
- const bindingSourceIds = {};
3801
- for (const [name, b] of Object.entries(src.bindings)) {
3802
- if (b.kind === "literal") continue;
3803
- const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
3804
- expectedSources.add(wanted);
3805
- const bid = idByStableId.get(wanted);
3806
- if (bid !== void 0) bindingSourceIds[name] = bid;
3807
- }
3808
- entries.push({
3809
- targetDeviceId: targetId,
3810
- link,
3811
- sourceDeviceId: -1,
3812
- bindingSourceIds
3813
- });
3814
- continue;
3815
- }
3816
- if (src.kind === "literal") {
3817
- entries.push({
3818
- targetDeviceId: targetId,
3819
- link,
3820
- sourceDeviceId: -1
3821
- });
3822
- continue;
3823
- }
3824
- const wantedStableId = src.kind === "global" ? src.sourceStableId : `${container}-${src.sourceKey}`;
3825
- expectedSources.add(wantedStableId);
3826
- const srcId = idByStableId.get(wantedStableId);
3827
- if (srcId === void 0) continue;
3828
- entries.push({
3829
- targetDeviceId: targetId,
3830
- link,
3831
- sourceDeviceId: srcId
3832
- });
3833
- }
3834
- }
3835
- const { targets, dependents } = buildLinkIndexes(entries);
3836
- this.linkHost.setLinkIndexes(targets, dependents, expectedSources);
3837
- };
3838
3581
  };
3839
3582
  //#endregion
3840
- //#region src/builtins/device-manager/device-link-resolver.ts
3841
- /** Returns true when `x` is a non-null, non-array plain object. */
3842
- function isRecord(x) {
3843
- return x !== null && typeof x === "object" && !Array.isArray(x);
3844
- }
3845
- /**
3846
- * Resolve one link's transformed value. Field sources read via `readField`
3847
- * (the caller supplies the sibling lookup — sync, e.g. the in-hub state
3848
- * mirror); literal sources use their per-device constant and never consult
3849
- * the reader. Returns `undefined` to skip the overlay (unreadable field).
3850
- * Pure. Async source reads (`resolveLinkedStatus`'s provider `getStatus`
3851
- * path) cannot use this helper for field sources — they branch on
3852
- * `source.kind` directly.
3853
- */
3854
- function resolveLinkValue(link, readField) {
3855
- const src = link.source;
3856
- if (src.kind === "expression") return void 0;
3857
- return require_dist.applyTransform(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
3858
- }
3859
- /**
3860
- * Sync-channel expression resolution: bindings read from the mirror via
3861
- * `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
3862
- * on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
3863
- * binding id (dangling source) or a non-primitive read resolves to `null`.
3864
- * Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
3865
- * the mirror path is hot and churn-free; the async channel logs. Compile
3866
- * failures cost one parse ever thanks to the negative LRU compile cache. Pure.
3867
- */
3868
- function resolveExpressionLinkValue(rl, readField, now) {
3869
- const src = rl.link.source;
3870
- if (src.kind !== "expression") return void 0;
3871
- const bindingValues = {};
3872
- for (const [name, b] of Object.entries(src.bindings)) {
3873
- if (b.kind === "literal") {
3874
- bindingValues[name] = b.value;
3875
- continue;
3876
- }
3877
- const bId = rl.bindingSourceIds?.[name];
3878
- if (bId === void 0) {
3879
- bindingValues[name] = null;
3880
- continue;
3881
- }
3882
- bindingValues[name] = require_dist.toExpressionValue(readField(bId, b.cap, b.fieldPath)) ?? null;
3883
- }
3884
- const result = require_dist.evaluateLinkExpression(src.expr, bindingValues, now);
3885
- return result.ok ? require_dist.applyTransform(result.value, rl.link.transform) : void 0;
3886
- }
3887
- /** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
3888
- * concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
3889
- * is a true `instanceof` guard rather than a cast. */
3890
- function asZodType(schema) {
3891
- return schema instanceof zod.z.ZodType ? schema : null;
3892
- }
3893
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner
3894
- * type. This lets the repair logic recognise a `TankStatus.nullable()` field
3895
- * as a ZodObject so it can fill in missing nullable keys. */
3896
- function unwrapSchema(schema) {
3897
- if (schema instanceof zod.z.ZodNullable || schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodDefault) {
3898
- const inner = asZodType(schema.unwrap());
3899
- return inner ? unwrapSchema(inner) : schema;
3900
- }
3901
- return schema;
3902
- }
3903
- /** For a ZodObject schema, ensure every nullable key present under `value` is
3904
- * filled — a missing key whose field accepts `null` is set to `null`. Generic
3905
- * repair for the common "set one leaf of a previously-null structured field"
3906
- * case (e.g. TankStatus.level). Recurses into nested object fields. */
3907
- function fillNullableDefaults(schema, value) {
3908
- const inner = unwrapSchema(schema);
3909
- if (!(inner instanceof zod.z.ZodObject) || !isRecord(value)) return value;
3910
- const shape = inner.shape;
3911
- const out = { ...value };
3912
- for (const [key, field] of Object.entries(shape)) if (out[key] === void 0) {
3913
- if (field.safeParse(null).success) out[key] = null;
3914
- } else out[key] = fillNullableDefaults(field, out[key]);
3915
- return out;
3916
- }
3917
- /**
3918
- * Group item-targeted links (`target.itemKey` set) by itemKey and upsert each
3919
- * group as ONE item into the status' item array (see
3920
- * `CapabilityStatusItemArray`). A NEW item is seeded from `emptyItem` with
3921
- * `keyField` (and `labelField`, when declared) set to the itemKey; an
3922
- * EXISTING item (matched by `keyField`) is cloned and overlaid in place, so
3923
- * native fields a link doesn't touch are preserved. Each upserted item is
3924
- * validated against `itemSchema` — an invalid item is skipped WITHOUT
3925
- * discarding its valid siblings. Returns the new draft and whether anything
3926
- * was applied. Pure + immutable (base array/items are never mutated).
3927
- */
3928
- function upsertItemArrayLinks(draft, grouped, itemArray) {
3929
- const arrRaw = require_dist.getByPath(draft, itemArray.path);
3930
- const arr = Array.isArray(arrRaw) ? [...arrRaw] : [];
3931
- let touched = false;
3932
- for (const [itemKey, group] of grouped) {
3933
- const idx = arr.findIndex((el) => isRecord(el) && el[itemArray.keyField] === itemKey);
3934
- const existing = idx >= 0 ? arr[idx] : null;
3935
- let item = isRecord(existing) ? { ...existing } : {
3936
- ...itemArray.emptyItem,
3937
- [itemArray.keyField]: itemKey,
3938
- ...itemArray.labelField !== void 0 ? { [itemArray.labelField]: itemKey } : {}
3939
- };
3940
- let itemTouched = false;
3941
- for (const { link, sourceValue } of group) {
3942
- if (sourceValue === void 0) continue;
3943
- item = require_dist.setByPath(item, link.target.fieldPath, sourceValue);
3944
- itemTouched = true;
3945
- }
3946
- if (!itemTouched) continue;
3947
- const repaired = fillNullableDefaults(itemArray.itemSchema, item);
3948
- const parsed = itemArray.itemSchema.safeParse(repaired);
3949
- if (!parsed.success) continue;
3950
- if (isRecord(parsed.data)) item = parsed.data;
3951
- if (idx >= 0) arr[idx] = item;
3952
- else arr.push(item);
3953
- touched = true;
3954
- }
3955
- if (!touched) return {
3956
- draft,
3957
- touched: false
3958
- };
3959
- return {
3960
- draft: require_dist.setByPath(draft, itemArray.path, arr),
3961
- touched: true
3962
- };
3963
- }
3964
- /**
3965
- * Overlay transformed source values onto `base` by dot-path, then validate the
3966
- * result against the target cap's `statusSchema`. On validation failure the
3967
- * overlay is discarded and `base` is returned unchanged (a misconfigured link
3968
- * must never corrupt a cap response). Pure — all I/O happens in the caller.
3969
- *
3970
- * Item-array grouping (P2b): when the cap declares `status.itemArray`, links
3971
- * carrying a `target.itemKey` are grouped per key and upserted as items into
3972
- * the array (see `upsertItemArrayLinks`); their `fieldPath` is relative to
3973
- * ONE item ('level', 'status', 'label'). Links WITHOUT an itemKey keep the
3974
- * scalar dot-path behavior unchanged. A link with an itemKey on a cap that
3975
- * declares NO `itemArray` is dropped (its item-relative path must never be
3976
- * scalar-applied to the status root).
3977
- */
3978
- function mergeLinkedStatus(base, resolved, statusSchema, itemArray) {
3979
- let draft = base;
3980
- let touched = false;
3981
- const grouped = /* @__PURE__ */ new Map();
3982
- for (const entry of resolved) {
3983
- const itemKey = entry.link.target.itemKey;
3984
- if (itemKey !== void 0) {
3985
- if (!itemArray) continue;
3986
- const group = grouped.get(itemKey);
3987
- if (group) group.push(entry);
3988
- else grouped.set(itemKey, [entry]);
3989
- continue;
3990
- }
3991
- if (entry.sourceValue === void 0) continue;
3992
- draft = require_dist.setByPath(draft, entry.link.target.fieldPath, entry.sourceValue);
3993
- touched = true;
3994
- }
3995
- if (itemArray && grouped.size > 0) {
3996
- const upserted = upsertItemArrayLinks(draft, grouped, itemArray);
3997
- draft = upserted.draft;
3998
- touched = touched || upserted.touched;
3999
- }
4000
- if (!touched) return base;
4001
- if (!statusSchema) return draft;
4002
- const repaired = fillNullableDefaults(statusSchema, draft);
4003
- const parsed = statusSchema.safeParse(repaired);
4004
- if (!parsed.success) return base;
4005
- return isRecord(parsed.data) ? parsed.data : base;
4006
- }
4007
- //#endregion
4008
3583
  //#region src/builtins/device-manager/device-state-mirror.ts
4009
3584
  /**
4010
3585
  * Hub-side runtime-state mirror for the device-manager addon.
4011
3586
  *
4012
3587
  * `DeviceStateMirror` owns the per-device cap-keyed slice mirror and the
4013
- * debounced disk-write coalescer that previously lived directly on the addon
4014
- * class. It mirrors every `deviceState.setCapSlice` write, emits
4015
- * `DeviceStateChanged` (overlaid with cross-device linked values), and coalesces
4016
- * frequent writes into one `writeDeviceRuntimeState` per debounce window. The
4017
- * cross-device link reverse-index lives on the addon; the mirror reads it
4018
- * through the injected `LinkOverlayHost` so the single owner of that state stays
4019
- * the addon. Extracted verbatim behavior unchanged.
3588
+ * debounced disk-write coalescer. It mirrors every `deviceState.setCapSlice`
3589
+ * write, emits `DeviceStateChanged`, and coalesces frequent writes into one
3590
+ * `writeDeviceRuntimeState` per debounce window.
3591
+ *
3592
+ * It used to do one more thing: overlay each emitted slice with cross-device
3593
+ * LINKED values, walk a `linkDependents` reverse-index and re-emit for every
3594
+ * dependent target, guarded by a `lastEmittedOverlay` churn map. Wiring was
3595
+ * deleted on 2026-08-08 (0 links across 302 devices), so the mirror emits what
3596
+ * the provider wrote — one writer per field, which is what D62 asks for
3597
+ * everywhere else.
4020
3598
  */
4021
3599
  var DeviceStateMirror = class DeviceStateMirror {
4022
3600
  ctx;
4023
- linkHost;
4024
3601
  /**
4025
3602
  * Hub-side mirror of every device's cap-keyed runtime state.
4026
3603
  * Key: deviceId. Value: per-cap slice map. Empty by default —
@@ -4034,11 +3611,8 @@ var DeviceStateMirror = class DeviceStateMirror {
4034
3611
  */
4035
3612
  runtimeStateDebounce = /* @__PURE__ */ new Map();
4036
3613
  static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
4037
- /** Loop/churn guard: last overlaid slice emitted per `${deviceId}:${cap}`. */
4038
- lastEmittedOverlay = /* @__PURE__ */ new Map();
4039
- constructor(ctx, linkHost) {
3614
+ constructor(ctx) {
4040
3615
  this.ctx = ctx;
4041
- this.linkHost = linkHost;
4042
3616
  }
4043
3617
  /**
4044
3618
  * Single-cap mirror update — diff against the current mirror,
@@ -4055,9 +3629,7 @@ var DeviceStateMirror = class DeviceStateMirror {
4055
3629
  const prior = perCap.get(capName);
4056
3630
  if (prior && shallowEqual(prior, slice)) return;
4057
3631
  perCap.set(capName, { ...slice });
4058
- this.emitOverlayed(deviceId, capName);
4059
- const deps = this.linkHost.linkDependents.get(`${deviceId}:${capName}`);
4060
- if (deps) for (const d of deps) this.emitOverlayed(d.targetDeviceId, d.targetCap);
3632
+ this.emitStateChanged(deviceId, capName, { ...slice });
4061
3633
  }
4062
3634
  /**
4063
3635
  * Debounced disk writer. Coalesces frequent writes (motion phase
@@ -4189,68 +3761,17 @@ var DeviceStateMirror = class DeviceStateMirror {
4189
3761
  for (const [k, v] of perCap) out[k] = { ...v };
4190
3762
  return out;
4191
3763
  }
4192
- /**
4193
- * Read-time overlay of a cap slice with its cross-device linked values.
4194
- * Returns a cloned raw mirror slice when the (device, cap) pair has no
4195
- * links. Sources are read from the same in-hub stateMirror — sync, no
4196
- * cross-process call. When the raw slice is ABSENT but the cap is a link
4197
- * target, the slice is SYNTHESIZED: the merge base is seeded from the
4198
- * cap's declared `status.empty` default; a failed/empty synthesize
4199
- * returns null (no phantom slices). The disk writer must NOT use this
4200
- * method; it must persist raw provider truth via snapshotForDevice.
4201
- */
4202
- overlayedSlice(deviceId, cap) {
4203
- const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
4204
- const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
4205
- if (!links || links.length === 0) return raw ? { ...raw } : null;
4206
- const now = Date.now();
4207
- const resolved = links.map((rl) => ({
4208
- link: rl.link,
4209
- sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => require_dist.getByPath(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => require_dist.getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
4210
- }));
4211
- const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
4212
- const schema = capStatus?.schema;
4213
- const empty = capStatus?.empty;
4214
- const base = raw ? { ...raw } : empty !== null && typeof empty === "object" && !Array.isArray(empty) ? { ...empty } : {};
4215
- const merged = mergeLinkedStatus(base, resolved, schema, capStatus?.itemArray);
4216
- if (!raw && merged === base) return null;
4217
- return merged;
3764
+ /** One cap's mirrored slice, cloned. Null when the device has never written
3765
+ * it callers read that as "the cap has no state", never as an empty slice. */
3766
+ capSlice(deviceId, cap) {
3767
+ const raw = this.stateMirror.get(deviceId)?.get(cap);
3768
+ return raw ? { ...raw } : null;
4218
3769
  }
4219
- /**
4220
- * Like snapshotForDevice but applies the device-link overlay per cap.
4221
- * Used exclusively by the device-state READ methods (getSnapshot,
4222
- * getAllSnapshots) so callers see overlayed values. SYNTHESIZE-only
4223
- * link-target caps (no base mirror slice) are unioned into the iterated
4224
- * key set so they appear at warm-load — `overlayedSlice` builds them from
4225
- * the cap's `status.empty` (and drops them when nothing resolves). The
4226
- * debounced disk writer must continue to call snapshotForDevice (raw truth).
4227
- */
4228
- snapshotForDeviceOverlayed(deviceId) {
4229
- const perCap = this.stateMirror.get(deviceId);
4230
- const capNames = new Set(perCap ? perCap.keys() : []);
4231
- const prefix = `${deviceId}:`;
4232
- for (const key of this.linkHost.linkTargets.keys()) if (key.startsWith(prefix)) capNames.add(key.slice(prefix.length));
3770
+ /** Whole-system mirror dump. Backs `deviceState.getAllSnapshots` — one
3771
+ * round-trip for a warm boot instead of N per-device calls. */
3772
+ allSnapshots() {
4233
3773
  const out = {};
4234
- for (const capName of capNames) {
4235
- const s = this.overlayedSlice(deviceId, capName);
4236
- if (s) out[capName] = s;
4237
- }
4238
- return out;
4239
- }
4240
- /** Whole-system mirror dump (overlaid). Backs `deviceState.getAllSnapshots`.
4241
- * Devices that only exist as link TARGETS (no mirror slice yet) are
4242
- * included via the link reverse-index so their synthesized caps appear. */
4243
- allSnapshotsOverlayed() {
4244
- const deviceIds = new Set(this.stateMirror.keys());
4245
- for (const key of this.linkHost.linkTargets.keys()) {
4246
- const id = Number(key.slice(0, key.indexOf(":")));
4247
- if (Number.isFinite(id)) deviceIds.add(id);
4248
- }
4249
- const out = {};
4250
- for (const deviceId of deviceIds) {
4251
- const dev = this.snapshotForDeviceOverlayed(deviceId);
4252
- if (this.stateMirror.has(deviceId) || Object.keys(dev).length > 0) out[String(deviceId)] = dev;
4253
- }
3774
+ for (const deviceId of this.stateMirror.keys()) out[String(deviceId)] = this.snapshotForDevice(deviceId);
4254
3775
  return out;
4255
3776
  }
4256
3777
  emitStateChanged(deviceId, capName, slice) {
@@ -4269,24 +3790,6 @@ var DeviceStateMirror = class DeviceStateMirror {
4269
3790
  }
4270
3791
  });
4271
3792
  }
4272
- /** Emit DeviceStateChanged for (deviceId, cap) using the OVERLAID slice,
4273
- * skipping when the overlay is unchanged since the last emit (loop/churn
4274
- * guard). Used for the written pair AND its dependent targets. */
4275
- emitOverlayed(deviceId, cap) {
4276
- const slice = this.overlayedSlice(deviceId, cap);
4277
- if (!slice) return;
4278
- const key = `${deviceId}:${cap}`;
4279
- const prev = this.lastEmittedOverlay.get(key);
4280
- if (prev && shallowEqual(prev, slice)) return;
4281
- this.lastEmittedOverlay.set(key, slice);
4282
- this.emitStateChanged(deviceId, cap, slice);
4283
- }
4284
- /** Drop a removed device's overlay-emit guard entries (keyed
4285
- * `${deviceId}:${cap}`) so the map doesn't retain rows for a removed device.
4286
- * Called from `removeDevice`. */
4287
- dropDeviceOverlays(deviceId) {
4288
- for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
4289
- }
4290
3793
  /** Flush every pending debounced disk write (graceful shutdown). Clears the
4291
3794
  * debounce slots after awaiting in-flight + scheduled writes so shutdown is
4292
3795
  * lossless. */
@@ -4315,11 +3818,15 @@ var DeviceStateMirror = class DeviceStateMirror {
4315
3818
  //#endregion
4316
3819
  //#region src/builtins/device-manager/device-sync-resolvers.ts
4317
3820
  /**
4318
- * Synchronous ownership / native-cap resolvers + the read-time cross-device
4319
- * link-status overlay for the device-manager provider. Extracted verbatim from
4320
- * the `onInitialize` provider literal as free functions taking a
4321
- * `ProviderContext` (DI). These back `CapabilityRegistry`'s native fallback and
4322
- * the cap-mount linked-status overlay. Behavior unchanged.
3821
+ * Synchronous ownership / native-cap resolvers for the device-manager provider.
3822
+ * Free functions over a `ProviderContext` (DI); these back
3823
+ * `CapabilityRegistry`'s native fallback.
3824
+ *
3825
+ * This file also held `resolveLinkedStatus`, the read-time cross-device link
3826
+ * overlay. Wiring was deleted on 2026-08-08 — 0 links across 302 devices, a
3827
+ * month after the tab shipped — so the overlay, its re-entrancy guard and its
3828
+ * per-call source-read memo went with it. A cross-device derivation now
3829
+ * produces a DEVICE, not a field on somebody else's cap.
4323
3830
  */
4324
3831
  /**
4325
3832
  * Sync ownership lookup backing persistence fallbacks (e.g. remove()
@@ -4361,133 +3868,6 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
4361
3868
  };
4362
3869
  return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
4363
3870
  }
4364
- /**
4365
- * Read-time overlay: merge cross-device linked values onto a target cap's
4366
- * status. Returns null when the device has no links for `cap` (caller uses
4367
- * the base status untouched). Reads each source cap via the hub registry's
4368
- * `getProviderForDevice` (routes cross-process); merge is pure.
4369
- */
4370
- async function resolveLinkedStatus(pctx, input) {
4371
- const { deviceId, cap } = input;
4372
- if (!pctx.host.devicesWithLinks.has(deviceId)) return null;
4373
- const inFlightKey = `${deviceId}:${cap}`;
4374
- if (pctx.host.linkResolveInFlight.has(inFlightKey)) return null;
4375
- pctx.host.linkResolveInFlight.add(inFlightKey);
4376
- try {
4377
- return await resolveLinkedStatusInner(pctx, input);
4378
- } finally {
4379
- pctx.host.linkResolveInFlight.delete(inFlightKey);
4380
- }
4381
- }
4382
- /** Body of `resolveLinkedStatus` — see the wrapper for the re-entrancy guard. */
4383
- async function resolveLinkedStatusInner(pctx, input) {
4384
- const { deviceId, cap, baseStatus } = input;
4385
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
4386
- if (!persisted) return null;
4387
- const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
4388
- if (links.length === 0) return null;
4389
- const capRegistry = pctx.host.capabilityRegistry;
4390
- const capStatus = capRegistry?.getDefinition(cap)?.status;
4391
- const schema = capStatus?.schema;
4392
- if (!schema) return null;
4393
- const allMeta = await pctx.metaStore.readMeta();
4394
- let containerStableId = persisted.stableId;
4395
- const parentId = persisted.meta.parentDeviceId;
4396
- if (parentId !== null) {
4397
- const parentMeta = allMeta[String(parentId)];
4398
- if (parentMeta) containerStableId = parentMeta.stableId;
4399
- }
4400
- const now = Date.now();
4401
- const statusMemo = /* @__PURE__ */ new Map();
4402
- const readSourceStatus = async (srcId, srcCap) => {
4403
- const key = `${srcId}:${srcCap}`;
4404
- const cached = statusMemo.get(key);
4405
- if (cached !== void 0) return cached;
4406
- let result;
4407
- try {
4408
- const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
4409
- result = {
4410
- status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
4411
- ok: true
4412
- };
4413
- } catch (err) {
4414
- pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
4415
- tags: {
4416
- deviceId,
4417
- capName: cap
4418
- },
4419
- meta: {
4420
- sourceCap: srcCap,
4421
- sourceId: srcId,
4422
- error: err instanceof Error ? err.message : String(err)
4423
- }
4424
- });
4425
- result = {
4426
- status: void 0,
4427
- ok: false
4428
- };
4429
- }
4430
- statusMemo.set(key, result);
4431
- return result;
4432
- };
4433
- const resolved = [];
4434
- for (const link of links) {
4435
- const src = link.source;
4436
- if (src.kind === "literal") {
4437
- resolved.push({
4438
- link,
4439
- sourceValue: require_dist.applyTransform(src.value, link.transform)
4440
- });
4441
- continue;
4442
- }
4443
- if (src.kind === "expression") {
4444
- const bindingValues = {};
4445
- for (const [name, b] of Object.entries(src.bindings)) {
4446
- if (b.kind === "literal") {
4447
- bindingValues[name] = b.value;
4448
- continue;
4449
- }
4450
- const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
4451
- if (bId === null) {
4452
- bindingValues[name] = null;
4453
- continue;
4454
- }
4455
- bindingValues[name] = require_dist.toExpressionValue(require_dist.getByPath((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
4456
- }
4457
- const result = require_dist.evaluateLinkExpression(src.expr, bindingValues, now);
4458
- if (result.ok) resolved.push({
4459
- link,
4460
- sourceValue: require_dist.applyTransform(result.value, link.transform)
4461
- });
4462
- else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
4463
- tags: {
4464
- deviceId,
4465
- capName: cap
4466
- },
4467
- meta: {
4468
- linkId: link.id,
4469
- error: result.error
4470
- }
4471
- });
4472
- continue;
4473
- }
4474
- const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
4475
- if (srcId === null) continue;
4476
- const read = await readSourceStatus(srcId, src.cap);
4477
- if (!read.ok) continue;
4478
- const raw = require_dist.getByPath(read.status, src.fieldPath);
4479
- resolved.push({
4480
- link,
4481
- sourceValue: require_dist.applyTransform(raw, link.transform)
4482
- });
4483
- }
4484
- if (resolved.length === 0) return null;
4485
- const synthesizing = !isRecord$1(baseStatus);
4486
- const base = isRecord$1(baseStatus) ? baseStatus : isRecord$1(capStatus.empty) ? { ...capStatus.empty } : {};
4487
- const merged = mergeLinkedStatus(base, resolved, schema, capStatus.itemArray);
4488
- if (synthesizing && merged === base) return null;
4489
- return merged;
4490
- }
4491
3871
  //#endregion
4492
3872
  //#region src/builtins/device-manager/device-manager.addon.ts
4493
3873
  var DeviceManagerAddon = class extends require_dist.BaseAddon {
@@ -4540,100 +3920,24 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4540
3920
  * the D3 re-handshake after device restore is the reliable replacement.
4541
3921
  */
4542
3922
  remoteNativeCaps = /* @__PURE__ */ new Map();
4543
- /** Device ids that currently have at least one `deviceLinks` entry. O(1) gate
4544
- * consulted by `resolveLinkedStatus` so the cap-mount overlay is a no-op for
4545
- * the vast majority of devices that have no links. Rebuilt on restore. */
4546
- devicesWithLinks = /* @__PURE__ */ new Set();
4547
- /** Whether a device currently has cross-device links. */
4548
- devicesWithLinksHas(deviceId) {
4549
- return this.devicesWithLinks.has(deviceId);
4550
- }
4551
- /** `${targetDeviceId}:${targetCap}` → resolved links feeding that cap's slice. */
4552
- linkTargets = /* @__PURE__ */ new Map();
4553
- /** `${sourceDeviceId}:${sourceCap}` → targets to recompute when that source changes. */
4554
- linkDependents = /* @__PURE__ */ new Map();
4555
- /** Expected source `stableId`s (`${container}-${sourceKey}` for sibling
4556
- * sources, the full `sourceStableId` for global sources) across all links,
4557
- * resolved or not — gates the `registerDevice` rebuild so only a registering
4558
- * device that IS a link source triggers a reindex (not every boot restore). */
4559
- expectedSourceStableIds = /* @__PURE__ */ new Set();
4560
- /** In-flight `${deviceId}:${cap}` pairs of `resolveLinkedStatus` — the P2e
4561
- * defensive re-entrancy guard bounding link cycles at resolve time. Mutated
4562
- * in place (never reassigned), so the host exposes a direct reference. */
4563
- linkResolveInFlight = /* @__PURE__ */ new Set();
4564
- /** Test/diagnostic accessors. */
4565
- linkTargetKeys() {
4566
- return [...this.linkTargets.keys()];
4567
- }
4568
- linkDependentsKeys() {
4569
- return [...this.linkDependents.keys()];
4570
- }
4571
- /** Replace the cross-device link reverse-index maps in one atomic swap.
4572
- * Sole writer of `linkTargets` / `linkDependents` / `expectedSourceStableIds`;
4573
- * called by `DeviceMetaStore.rebuildLinkDependents` via the `LinkIndexHost`. */
4574
- setLinkIndexes(targets, dependents, expectedSourceStableIds) {
4575
- this.linkTargets = targets;
4576
- this.linkDependents = dependents;
4577
- this.expectedSourceStableIds = expectedSourceStableIds;
4578
- }
4579
- /** Build the `LinkIndexHost` the meta store mutates — exposes this addon's
4580
- * `devicesWithLinks` gate (read) + `setLinkIndexes` (write) without leaking
4581
- * the private maps. */
4582
- get linkIndexHost() {
4583
- return {
4584
- devicesWithLinks: this.devicesWithLinks,
4585
- setLinkIndexes: (targets, dependents, expected) => this.setLinkIndexes(targets, dependents, expected)
4586
- };
4587
- }
4588
3923
  /** The runtime-state mirror, asserted present (constructed in onInitialize). */
4589
3924
  get stateMirror() {
4590
3925
  if (!this.stateMirrorImpl) throw new Error("DeviceManagerAddon: state mirror accessed before onInitialize");
4591
3926
  return this.stateMirrorImpl;
4592
3927
  }
4593
- /** Build the `LinkOverlayHost` the state mirror reads when overlaying linked
4594
- * values — exposes the link reverse-index maps + registry via LIVE thunks
4595
- * (the maps are reassigned on every `rebuildLinkDependents`). Arrow thunks
4596
- * capture `this` lexically so the getters never go stale. */
4597
- get linkOverlayHost() {
4598
- const linkTargets = () => this.linkTargets;
4599
- const linkDependents = () => this.linkDependents;
4600
- const capabilityRegistry = () => this.capabilityRegistry;
4601
- return {
4602
- get linkTargets() {
4603
- return linkTargets();
4604
- },
4605
- get linkDependents() {
4606
- return linkDependents();
4607
- },
4608
- get capabilityRegistry() {
4609
- return capabilityRegistry();
4610
- }
4611
- };
4612
- }
4613
3928
  /** Build the `ProviderHost` the extracted provider-method modules
4614
3929
  * (`device-meta-actions`, `device-queries`) reach — exposes the addon's
4615
3930
  * private state/methods they need without leaking the class internals.
4616
3931
  *
4617
- * `expectedSourceStableIds` is exposed via a LIVE thunk: the field is
4618
- * reassigned to a fresh Set on every `rebuildLinkDependents`, so a captured
4619
- * reference would go stale. `devicesWithLinks` / `remoteNativeCaps` are
4620
- * mutated in place (never reassigned) so a direct reference preserves the
4621
- * original live semantics. The overlay-emit guard now lives in the state
4622
- * mirror — pruning is routed through `dropDeviceOverlays`. */
3932
+ * `remoteNativeCaps` is mutated in place (never reassigned) so a direct
3933
+ * reference preserves the live semantics. */
4623
3934
  get providerHost() {
4624
3935
  const capabilityRegistry = () => this.capabilityRegistry;
4625
- const expectedSourceStableIds = () => this.expectedSourceStableIds;
4626
3936
  return {
4627
3937
  ctx: this.ctx,
4628
3938
  get capabilityRegistry() {
4629
3939
  return capabilityRegistry();
4630
3940
  },
4631
- devicesWithLinks: this.devicesWithLinks,
4632
- get expectedSourceStableIds() {
4633
- return expectedSourceStableIds();
4634
- },
4635
- linkResolveInFlight: this.linkResolveInFlight,
4636
- dropDeviceOverlays: (deviceId) => this.stateMirror.dropDeviceOverlays(deviceId),
4637
3941
  remoteNativeCaps: this.remoteNativeCaps,
4638
3942
  seedMirror: (deviceId, blob) => this.stateMirror.seedMirror(deviceId, blob),
4639
3943
  withResetSessionProbe: (blob) => this.stateMirror.withResetSessionProbe(blob),
@@ -4681,15 +3985,12 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4681
3985
  }
4682
3986
  }))).filter((id) => id !== null);
4683
3987
  }
4684
- /** Build the dependency context the extracted binding resolvers consume.
4685
- * `devicesWithLinks` is mutated in place (never reassigned) so the direct
4686
- * reference stays live — it gates the virtual `linked` binding step. */
3988
+ /** Build the dependency context the extracted binding resolvers consume. */
4687
3989
  get bindingsDeps() {
4688
3990
  return {
4689
3991
  ctx: this.ctx,
4690
3992
  capabilityRegistry: this.capabilityRegistry,
4691
- remoteNativeCaps: this.remoteNativeCaps,
4692
- devicesWithLinks: this.devicesWithLinks
3993
+ remoteNativeCaps: this.remoteNativeCaps
4693
3994
  };
4694
3995
  }
4695
3996
  async getBindings(input) {
@@ -4745,21 +4046,16 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4745
4046
  if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
4746
4047
  return ops;
4747
4048
  };
4748
- const metaStore = new DeviceMetaStore(settings, registry, this.linkIndexHost);
4749
- this.stateMirrorImpl = new DeviceStateMirror(this.ctx, this.linkOverlayHost);
4049
+ const metaStore = new DeviceMetaStore(settings, registry);
4050
+ this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
4750
4051
  const stateMirror = this.stateMirrorImpl;
4751
4052
  const readMeta = metaStore.readMeta;
4752
4053
  const resolvePersistedById = metaStore.resolvePersistedById;
4753
- const rebuildLinkDependents = metaStore.rebuildLinkDependents;
4754
4054
  const idToAddonId = metaStore.idToAddonId;
4755
4055
  {
4756
4056
  const meta = await readMeta();
4757
- for (const m of Object.values(meta)) {
4758
- idToAddonId.set(m.id, m.addonId);
4759
- if (Array.isArray(m.deviceLinks) && m.deviceLinks.length > 0) this.devicesWithLinks.add(m.id);
4760
- }
4057
+ for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
4761
4058
  }
4762
- await rebuildLinkDependents();
4763
4059
  const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, settings, this.ctx, deviceId, integrationId);
4764
4060
  const pctx = {
4765
4061
  host: this.providerHost,
@@ -4776,7 +4072,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4776
4072
  const provider = {
4777
4073
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
4778
4074
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
4779
- resolveLinkedStatus: (input) => resolveLinkedStatus(pctx, input),
4780
4075
  allocateDeviceId: (input) => allocateDeviceId(pctx, input),
4781
4076
  registerDevice: (input) => registerDevice(pctx, input),
4782
4077
  removeDevice: (input) => removeDevice(pctx, input),
@@ -4790,7 +4085,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4790
4085
  setLinkDeviceId: (input) => setLinkDeviceId(pctx, input),
4791
4086
  setPrimaryChildEntityId: (input) => setPrimaryChildEntityId(pctx, input),
4792
4087
  setChildLayout: (input) => setChildLayout(pctx, input),
4793
- setDeviceLinks: (input) => setDeviceLinks(pctx, input),
4794
4088
  setRole: (input) => setRole(pctx, input),
4795
4089
  setDisplay: (input) => setDisplay(pctx, input),
4796
4090
  getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
@@ -4950,13 +4244,13 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4950
4244
  capability: require_dist.deviceStateCapability,
4951
4245
  provider: {
4952
4246
  getSnapshot: async (input) => {
4953
- return stateMirror.snapshotForDeviceOverlayed(input.deviceId);
4247
+ return stateMirror.snapshotForDevice(input.deviceId);
4954
4248
  },
4955
4249
  getCapSlice: async (input) => {
4956
- return stateMirror.overlayedSlice(input.deviceId, input.capName);
4250
+ return stateMirror.capSlice(input.deviceId, input.capName);
4957
4251
  },
4958
4252
  getAllSnapshots: async () => {
4959
- return stateMirror.allSnapshotsOverlayed();
4253
+ return stateMirror.allSnapshots();
4960
4254
  },
4961
4255
  setCapSlice: async (input) => {
4962
4256
  const { deviceId, capName, slice } = input;