@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
@@ -1,6 +1,5 @@
1
- import { D as deviceStateCapability, E as deviceManagerCapability, I as getByPath, M as enumerateSchemaFields, N as evaluateLinkExpression, O as deviceStatusCapability, Rt as sleep, S as buildStreamParamsConfigSchema, St as WELL_KNOWN_TAB_MAP, Y as normalizeUnit, Z as parseStreamParamsFormPatch, _t as DeviceFeature, a as CAP_NAMES_WITH_STATUS, ft as errMsg, j as enumerateItemArrayFields, jt as isDeviceConfigCap, lt as validateExpressionSource, pt as BaseAddon, s as DeviceStatusSchema, st as toExpressionValue, t as ALL_CAPABILITY_DEFINITIONS, tt as setByPath, u as STREAM_PROFILE_META, v as applyTransform, vt as DeviceRole, x as bareAddonId, yt as DeviceType, zt as EventCategory } from "../../dist-L6QPJWsr.mjs";
1
+ import { A as deviceStatusCapability, It as sleep, Lt as EventCategory, N as enumerateItemArrayFields, O as deviceManagerCapability, P as enumerateSchemaFields, Q as parseStreamParamsFormPatch, S as buildStreamParamsConfigSchema, Y as normalizeUnit, _t as DeviceType, a as CAP_NAMES_WITH_STATUS, bt as WELL_KNOWN_TAB_MAP, c as DeviceStatusSchema, d as STREAM_PROFILE_META, dt as BaseAddon, gt as DeviceRole, ht as DeviceFeature, k as deviceStateCapability, kt as isDeviceConfigCap, t as ALL_CAPABILITY_DEFINITIONS, ut as errMsg } from "../../dist-BJ2w3uoN.mjs";
2
2
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
3
- import { z } from "zod";
4
3
  import { randomUUID } from "node:crypto";
5
4
  //#region src/builtins/device-manager/day-night-config-schema.ts
6
5
  var MODE_LABELS = {
@@ -532,13 +531,6 @@ function mergeAggregates(parts) {
532
531
  //#endregion
533
532
  //#region src/builtins/device-manager/device-projection.ts
534
533
  /**
535
- * Pure device-projection + small predicate helpers for the device-manager
536
- * addon. Extracted verbatim from `device-manager.addon.ts`. None of these
537
- * functions close over the addon instance — they take everything they need
538
- * as explicit parameters, so they are safe to share across the addon's
539
- * query / aggregation modules.
540
- */
541
- /**
542
534
  * Return true when `err` is a transient Moleculer error that is worth
543
535
  * retrying — specifically any `MoleculerRetryableError` subclass
544
536
  * (ServiceNotAvailableError, ServiceNotFoundError, BrokerDisconnectedError,
@@ -567,10 +559,6 @@ function shallowEqual(a, b) {
567
559
  for (const k of ak) if (a[k] !== b[k]) return false;
568
560
  return true;
569
561
  }
570
- /** Returns true when `x` is a non-null, non-array plain object. */
571
- function isRecord$1(x) {
572
- return x !== null && typeof x === "object" && !Array.isArray(x);
573
- }
574
562
  function isCameraDevice(device) {
575
563
  return "getStreamSources" in device && typeof device.getStreamSources === "function";
576
564
  }
@@ -655,7 +643,6 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
655
643
  ...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
656
644
  ...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
657
645
  ...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
658
- ...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
659
646
  ...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
660
647
  };
661
648
  }
@@ -671,28 +658,6 @@ function resolveDeviceById(registry, deviceId) {
671
658
  }
672
659
  //#endregion
673
660
  //#region src/builtins/device-manager/device-queries.ts
674
- /**
675
- * Device query + live-runtime + provider/adoption operations for the
676
- * device-manager addon — the read/live half of the `IDeviceManagerProvider`
677
- * surface. Extracted verbatim from the `onInitialize` provider literal as free
678
- * functions taking a `ProviderContext` (DI). Input/return types are pinned to
679
- * the cap contract via `Parameters<IDeviceManagerProvider['x']>[0]` /
680
- * `ReturnType<…>` so the typed surface is identical to the inferred methods.
681
- *
682
- * Covers: persisted/live device queries (listPersistedByAddon, listAll,
683
- * getDevice, getChildren), live runtime methods (getStreamSources,
684
- * getConfigSchema, getSettingsSchema, updateConfig, enable, disable, remove,
685
- * removeByIntegration, getStreamProfileMap, setStreamProfileMap, probeStreams),
686
- * device-provider ops (discoverDevices, adoptDevice, getCreationSchema,
687
- * createDevice, testCreationField), and device-adoption ops
688
- * (adoptionListCandidates/Refresh/Adopt/Release/Resync, testField).
689
- *
690
- * The hub-live-then-persisted-fallback union, the cross-process device-ops
691
- * dispatch, the depth-first remove cascade, and the resetToSource resync purge
692
- * are byte-for-byte preserved. `remove`/`removeByIntegration`/`enable`/`disable`/
693
- * `probeStreams`/`getStreamProfileMap` self-call sibling cap methods via
694
- * `pctx.provider`. Pure refactor — no behavior change.
695
- */
696
661
  async function listPersistedByAddon(pctx, input) {
697
662
  const { addonId } = input;
698
663
  const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
@@ -769,7 +734,6 @@ async function listAll(pctx, input) {
769
734
  ...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
770
735
  ...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
771
736
  ...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
772
- ...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
773
737
  ...m?.display !== void 0 ? { display: m.display } : {},
774
738
  ...(() => {
775
739
  if (slim) return {};
@@ -819,7 +783,6 @@ async function getDevice(pctx, input) {
819
783
  ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
820
784
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
821
785
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
822
- ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
823
786
  ...m.display !== void 0 ? { display: m.display } : {},
824
787
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
825
788
  };
@@ -882,7 +845,6 @@ async function getChildren(pctx, input) {
882
845
  ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
883
846
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
884
847
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
885
- ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
886
848
  ...m.display !== void 0 ? { display: m.display } : {},
887
849
  ...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
888
850
  });
@@ -1438,18 +1400,6 @@ function splitDeviceStoreKeys(patch) {
1438
1400
  //#endregion
1439
1401
  //#region src/builtins/device-manager/device-bindings-store.ts
1440
1402
  /**
1441
- * Persisted wrapper-activation + per-device binding resolution for the
1442
- * device-manager addon. Extracted from `device-manager.addon.ts` as free
1443
- * functions that receive an explicit `BindingsDeps` context (dependency
1444
- * injection) instead of closing over the addon instance — keeps the routing
1445
- * logic cohesive and independently testable while preserving the EXACT call
1446
- * semantics, ordering, and side effects of the original class methods.
1447
- *
1448
- * The addon keeps thin delegator methods (`getBindings`, `getAllBindings`, …)
1449
- * that build `BindingsDeps` from its own state and forward here, so every
1450
- * existing internal + cap-router call site is unchanged.
1451
- */
1452
- /**
1453
1403
  * Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
1454
1404
  * Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
1455
1405
  * removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
@@ -1551,9 +1501,47 @@ function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
1551
1501
  };
1552
1502
  return null;
1553
1503
  }
1504
+ /**
1505
+ * Resolve the device's declared TYPE (`'camera'`, `'event-emitter'`, …), or
1506
+ * `undefined` when it cannot be established.
1507
+ *
1508
+ * The PERSISTED meta is the authority: `ctx.kernel.deviceRegistry` is hub-only
1509
+ * and has been observed empty in the very process that answers `getBindings`
1510
+ * correctly (see the note on `getAllBindings`). The registry is consulted only
1511
+ * as a secondary source, for a device constructed but not yet persisted.
1512
+ *
1513
+ * `undefined` is a first-class answer and callers must treat it as "no
1514
+ * filtering" — an absent row must never be the reason a device loses bindings.
1515
+ */
1516
+ function resolveDeviceType(deps, rawStore, deviceId) {
1517
+ const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
1518
+ if (typeof persisted === "string" && persisted.length > 0) return persisted;
1519
+ const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
1520
+ return typeof live === "string" && live.length > 0 ? live : void 0;
1521
+ }
1522
+ /**
1523
+ * Does a capability apply to a device of type `deviceType`?
1524
+ *
1525
+ * The cap's `deviceTypes` is the ONLY declaration consulted (D4: behavioural
1526
+ * cap metadata lives in the `*.cap.ts`, never in a manifest). Two deliberate
1527
+ * fail-open cases:
1528
+ *
1529
+ * - a cap that declares no `deviceTypes` (or an empty list) applies to every
1530
+ * device — the pre-existing, back-compatible semantics;
1531
+ * - an UNKNOWN `deviceType` never filters, so a missing/failed meta lookup
1532
+ * changes nothing (D49: a read that fails must not destroy work).
1533
+ */
1534
+ function capAppliesToDeviceType(def, deviceType) {
1535
+ if (deviceType === void 0) return true;
1536
+ const declared = def?.deviceTypes;
1537
+ if (!declared || declared.length === 0) return true;
1538
+ return declared.some((t) => t === deviceType);
1539
+ }
1554
1540
  async function getBindings(deps, input) {
1555
1541
  const storeKey = String(input.deviceId);
1556
- const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
1542
+ const rawStore = await deps.ctx.settings.readAddonStore();
1543
+ const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
1544
+ const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
1557
1545
  const entries = [];
1558
1546
  const seenCaps = /* @__PURE__ */ new Set();
1559
1547
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
@@ -1575,21 +1563,35 @@ async function getBindings(deps, input) {
1575
1563
  });
1576
1564
  seenCaps.add(capName);
1577
1565
  }
1578
- if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
1579
- if (seenCaps.has(capName)) continue;
1580
- const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
1581
- if (!defaultWrapperAddonId) continue;
1582
- const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
1583
- const remoteNative = resolveRemote(capName);
1584
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
1585
- entries.push({
1586
- capName,
1587
- kind: "wrapped",
1588
- providerAddonId: defaultWrapperAddonId,
1589
- providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
1590
- nativeAddonId
1566
+ if (deps.capabilityRegistry) {
1567
+ const skippedForType = [];
1568
+ for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
1569
+ if (seenCaps.has(capName)) continue;
1570
+ if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
1571
+ skippedForType.push(capName);
1572
+ continue;
1573
+ }
1574
+ const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
1575
+ if (!defaultWrapperAddonId) continue;
1576
+ const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
1577
+ const remoteNative = resolveRemote(capName);
1578
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
1579
+ entries.push({
1580
+ capName,
1581
+ kind: "wrapped",
1582
+ providerAddonId: defaultWrapperAddonId,
1583
+ providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
1584
+ nativeAddonId
1585
+ });
1586
+ seenCaps.add(capName);
1587
+ }
1588
+ if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
1589
+ tags: { deviceId: input.deviceId },
1590
+ meta: {
1591
+ deviceType,
1592
+ skipped: skippedForType
1593
+ }
1591
1594
  });
1592
- seenCaps.add(capName);
1593
1595
  }
1594
1596
  if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
1595
1597
  if (seenCaps.has(capName)) continue;
@@ -1631,21 +1633,6 @@ async function getBindings(deps, input) {
1631
1633
  });
1632
1634
  seenCaps.add(entry.capName);
1633
1635
  }
1634
- if (deps.devicesWithLinks?.has(input.deviceId)) {
1635
- const row = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(input.deviceId)];
1636
- for (const link of row?.deviceLinks ?? []) {
1637
- const capName = link.target.cap;
1638
- if (seenCaps.has(capName)) continue;
1639
- entries.push({
1640
- capName,
1641
- kind: "linked",
1642
- providerAddonId: bareAddonId(deps.ctx.id),
1643
- providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
1644
- nativeAddonId: ""
1645
- });
1646
- seenCaps.add(capName);
1647
- }
1648
- }
1649
1636
  return {
1650
1637
  deviceId: input.deviceId,
1651
1638
  entries
@@ -1663,10 +1650,20 @@ async function getBindings(deps, input) {
1663
1650
  * payload periodically.
1664
1651
  */
1665
1652
  async function getAllBindings(deps) {
1666
- const hubRegistry = deps.ctx.kernel?.deviceRegistry;
1667
- if (!hubRegistry) return [];
1653
+ const store = await deps.ctx.settings.readAddonStore();
1654
+ const ids = /* @__PURE__ */ new Set();
1655
+ for (const key of Object.keys(store.deviceMeta ?? {})) {
1656
+ const id = Number(key);
1657
+ if (Number.isInteger(id)) ids.add(id);
1658
+ }
1659
+ const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
1660
+ for (const device of registered) ids.add(device.id);
1661
+ if (ids.size === 0) {
1662
+ deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
1663
+ return [];
1664
+ }
1668
1665
  const out = [];
1669
- for (const device of hubRegistry.getAll()) out.push(await getBindings(deps, { deviceId: device.id }));
1666
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
1670
1667
  return out;
1671
1668
  }
1672
1669
  /**
@@ -1987,6 +1984,30 @@ function resolveNativeDeviceOwner(deps, deviceId) {
1987
1984
  } : null;
1988
1985
  }
1989
1986
  /**
1987
+ * Race `work` against a deadline WITHOUT swallowing its rejection.
1988
+ *
1989
+ * `Promise.race` attaches a handler to every entrant, so a `work` that rejects
1990
+ * after the deadline already fired is still handled — no unhandled rejection,
1991
+ * and no `.catch(() => {})` that would also hide a genuine throw from the
1992
+ * caller's `try`. A rejection propagates out of here exactly as it would
1993
+ * without the race; only the TIMEOUT is turned into a value.
1994
+ */
1995
+ async function withDeadline(work, deadlineMs) {
1996
+ let timer;
1997
+ const deadline = new Promise((resolve) => {
1998
+ timer = setTimeout(() => resolve({ kind: "timed-out" }), deadlineMs);
1999
+ timer.unref?.();
2000
+ });
2001
+ try {
2002
+ return await Promise.race([work.then((value) => ({
2003
+ kind: "value",
2004
+ value
2005
+ })), deadline]);
2006
+ } finally {
2007
+ if (timer !== void 0) clearTimeout(timer);
2008
+ }
2009
+ }
2010
+ /**
1990
2011
  * Aggregate `status` across every registered cap for a device.
1991
2012
  *
1992
2013
  * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
@@ -1997,11 +2018,16 @@ function resolveNativeDeviceOwner(deps, deviceId) {
1997
2018
  * overall aggregate stays usable — a single misbehaving provider
1998
2019
  * must not blank out a device's entire status view.
1999
2020
  *
2021
+ * A provider that never answers is treated exactly like one that throws: it
2022
+ * costs its own cap and nothing else. See {@link STATUS_CALL_DEADLINE_MS} for
2023
+ * why a bound is needed at all when the transport already has one.
2024
+ *
2000
2025
  * Returned shape is `Record<capName, unknown | null>`; the client-
2001
2026
  * side hook tightens this to `CapStatusTypeMap` via the generated
2002
2027
  * `cap-status-types.ts`.
2003
2028
  */
2004
- async function getDeviceStatusAggregate(deps, input) {
2029
+ async function getDeviceStatusAggregate(deps, input, options) {
2030
+ const deadlineMs = options?.deadlineMs ?? 3e3;
2005
2031
  const capNames = input.caps ?? CAP_NAMES_WITH_STATUS;
2006
2032
  const registry = deps.capabilityRegistry;
2007
2033
  const out = {};
@@ -2021,7 +2047,20 @@ async function getDeviceStatusAggregate(deps, input) {
2021
2047
  out[capName] = null;
2022
2048
  return;
2023
2049
  }
2024
- const raw = await provider.getStatus({ deviceId: input.deviceId });
2050
+ const settled = await withDeadline(provider.getStatus({ deviceId: input.deviceId }), deadlineMs);
2051
+ if (settled.kind === "timed-out") {
2052
+ deps.ctx.logger.warn("getDeviceStatusAggregate: cap missed its deadline, dropping", {
2053
+ tags: { deviceId: input.deviceId },
2054
+ meta: {
2055
+ deviceId: input.deviceId,
2056
+ capName,
2057
+ deadlineMs
2058
+ }
2059
+ });
2060
+ out[capName] = null;
2061
+ return;
2062
+ }
2063
+ const raw = settled.value;
2025
2064
  if (raw == null) {
2026
2065
  out[capName] = null;
2027
2066
  return;
@@ -2075,8 +2114,21 @@ var STATUS_BATCH_WINDOW = 10;
2075
2114
  * A device whose whole aggregation fails still gets an ENTRY (all caps null)
2076
2115
  * rather than being omitted: a caller that reads a missing key as "not
2077
2116
  * answered yet" would keep the previous badge on screen forever.
2117
+ *
2118
+ * ## The response is BOUNDED, and that is load-bearing
2119
+ *
2120
+ * The windows are walked SEQUENTIALLY, so before the per-cap deadline existed a
2121
+ * single unanswerable device did not merely cost its own window — it held every
2122
+ * later window behind it, and the call never returned at all. That is the whole
2123
+ * mechanism behind "one node offline and the app loads nothing": this is the
2124
+ * one call a camera list makes for every card, the client has no timeout of its
2125
+ * own, and a tRPC query that never settles is `isLoading` forever.
2126
+ *
2127
+ * The ceiling is now `ceil(devices / STATUS_BATCH_WINDOW) × STATUS_CALL_DEADLINE_MS`
2128
+ * in the worst case (a dead device in every window), and unchanged when every
2129
+ * node is healthy. It is never unbounded.
2078
2130
  */
2079
- async function getDeviceStatusAggregateBatch(deps, input) {
2131
+ async function getDeviceStatusAggregateBatch(deps, input, options) {
2080
2132
  const out = {};
2081
2133
  const ids = [...new Set(input.deviceIds)];
2082
2134
  for (let i = 0; i < ids.length; i += STATUS_BATCH_WINDOW) {
@@ -2085,7 +2137,7 @@ async function getDeviceStatusAggregateBatch(deps, input) {
2085
2137
  try {
2086
2138
  const single = { deviceId };
2087
2139
  if (input.caps !== void 0) single.caps = input.caps;
2088
- out[String(deviceId)] = await getDeviceStatusAggregate(deps, single);
2140
+ out[String(deviceId)] = await getDeviceStatusAggregate(deps, single, options);
2089
2141
  } catch (err) {
2090
2142
  deps.ctx.logger.warn("getDeviceStatusAggregateBatch: device aggregation threw", {
2091
2143
  tags: { deviceId },
@@ -2535,120 +2587,6 @@ var DeviceEventPropagator = class {
2535
2587
  }
2536
2588
  };
2537
2589
  //#endregion
2538
- //#region src/builtins/device-manager/device-link-cycle.ts
2539
- function nodeKey(deviceId, cap) {
2540
- return `${deviceId}:${cap}`;
2541
- }
2542
- /** The target's container stableId — its parent's (falling back to its own for
2543
- * a top-level target). Sibling FIELD sources resolve relative to this, the same
2544
- * rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
2545
- function containerStableId(targetRow, rowById) {
2546
- return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
2547
- }
2548
- /** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
2549
- * null when it is a literal or resolves to no known device. */
2550
- function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
2551
- if (binding.kind === "literal") return null;
2552
- const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
2553
- const srcId = idByStableId.get(wantedStableId);
2554
- if (srcId === void 0) return null;
2555
- return nodeKey(srcId, binding.cap);
2556
- }
2557
- /**
2558
- * Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
2559
- * A literal source depends on nothing; a field/global source depends on exactly
2560
- * one node; an EXPRESSION source depends on one node per resolvable non-literal
2561
- * binding. Unresolvable (dangling) bindings contribute no edge — they cannot
2562
- * loop while dangling; a cycle that only becomes resolvable later is bounded by
2563
- * the resolve-time re-entrancy guard.
2564
- */
2565
- function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
2566
- const src = link.source;
2567
- if (src.kind === "expression") {
2568
- const nodes = [];
2569
- for (const binding of Object.values(src.bindings)) {
2570
- const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
2571
- if (node !== null) nodes.push(node);
2572
- }
2573
- return nodes;
2574
- }
2575
- const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
2576
- return node !== null ? [node] : [];
2577
- }
2578
- /**
2579
- * Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
2580
- * a dependency cycle. Returns the cycle as an ordered list of node keys
2581
- * (first node repeated at the end) or null when the set is safe.
2582
- */
2583
- function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
2584
- const rows = Object.values(allMeta);
2585
- const rowById = /* @__PURE__ */ new Map();
2586
- const idByStableId = /* @__PURE__ */ new Map();
2587
- for (const r of rows) {
2588
- rowById.set(r.id, r);
2589
- if (!idByStableId.has(r.stableId)) idByStableId.set(r.stableId, r.id);
2590
- }
2591
- const dependsOn = /* @__PURE__ */ new Map();
2592
- const addEdges = (row, links) => {
2593
- for (const link of links) {
2594
- const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
2595
- if (srcNodes.length === 0) continue;
2596
- const tNode = nodeKey(row.id, link.target.cap);
2597
- const list = dependsOn.get(tNode) ?? [];
2598
- for (const srcNode of srcNodes) list.push(srcNode);
2599
- dependsOn.set(tNode, list);
2600
- }
2601
- };
2602
- for (const r of rows) {
2603
- const links = r.id === editedDeviceId ? editedLinks : r.deviceLinks ?? [];
2604
- if (links.length > 0) addEdges(r, links);
2605
- }
2606
- const roots = [...new Set(editedLinks.map((l) => nodeKey(editedDeviceId, l.target.cap)))];
2607
- for (const root of roots) {
2608
- const cycle = dfsFindCycle(root, dependsOn);
2609
- if (cycle) return cycle;
2610
- }
2611
- return null;
2612
- }
2613
- /** Iterative DFS from `root` over `dependsOn`; returns the first back-edge
2614
- * cycle path (closed — first node repeated last) or null. */
2615
- function dfsFindCycle(root, dependsOn) {
2616
- const onPath = /* @__PURE__ */ new Set();
2617
- const done = /* @__PURE__ */ new Set();
2618
- const path = [];
2619
- const stack = [{
2620
- node: root,
2621
- nextChild: 0
2622
- }];
2623
- onPath.add(root);
2624
- path.push(root);
2625
- while (stack.length > 0) {
2626
- const frame = stack[stack.length - 1];
2627
- const children = dependsOn.get(frame.node) ?? [];
2628
- if (frame.nextChild < children.length) {
2629
- const child = children[frame.nextChild];
2630
- frame.nextChild += 1;
2631
- if (onPath.has(child)) {
2632
- const start = path.indexOf(child);
2633
- return [...path.slice(start), child];
2634
- }
2635
- if (done.has(child)) continue;
2636
- stack.push({
2637
- node: child,
2638
- nextChild: 0
2639
- });
2640
- onPath.add(child);
2641
- path.push(child);
2642
- continue;
2643
- }
2644
- stack.pop();
2645
- onPath.delete(frame.node);
2646
- path.pop();
2647
- done.add(frame.node);
2648
- }
2649
- return null;
2650
- }
2651
- //#endregion
2652
2590
  //#region src/builtins/device-manager/device-meta-actions.ts
2653
2591
  /**
2654
2592
  * Device meta-mutation + persistence actions for the device-manager addon —
@@ -2662,7 +2600,7 @@ function dfsFindCycle(root, dependsOn) {
2662
2600
  * removeDevice), config persistence (persistConfig, loadConfig), the meta
2663
2601
  * surface load (loadMeta, loadRuntimeState), every meta setter (setName,
2664
2602
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
2665
- * setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
2603
+ * setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
2666
2604
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
2667
2605
  * (listLocations, addLocation, removeLocation).
2668
2606
  *
@@ -2769,7 +2707,6 @@ async function registerDevice(pctx, input) {
2769
2707
  ...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
2770
2708
  ...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
2771
2709
  ...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
2772
- ...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
2773
2710
  ...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
2774
2711
  ...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
2775
2712
  parentDeviceId,
@@ -2831,7 +2768,6 @@ async function registerDevice(pctx, input) {
2831
2768
  generation: Date.now()
2832
2769
  }
2833
2770
  });
2834
- if (pctx.host.devicesWithLinks.size > 0 && pctx.host.expectedSourceStableIds.has(stableId)) await pctx.metaStore.rebuildLinkDependents();
2835
2771
  }
2836
2772
  async function removeDevice(pctx, input) {
2837
2773
  const { deviceId } = input;
@@ -2870,9 +2806,6 @@ async function removeDevice(pctx, input) {
2870
2806
  pctx.host.remoteNativeCaps.delete(deviceId);
2871
2807
  pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
2872
2808
  pctx.metaStore.idToAddonId.delete(deviceId);
2873
- pctx.host.devicesWithLinks.delete(deviceId);
2874
- pctx.host.dropDeviceOverlays(deviceId);
2875
- await pctx.metaStore.rebuildLinkDependents();
2876
2809
  pctx.host.ctx.logger.info("removed device", { tags: {
2877
2810
  deviceId,
2878
2811
  deviceName: deviceName.length > 0 ? deviceName : stableId
@@ -3203,54 +3136,6 @@ async function setChildLayout(pctx, input) {
3203
3136
  });
3204
3137
  }
3205
3138
  /**
3206
- * Set (or replace) the operator-authored cross-device field wirings on
3207
- * a device's meta row. Mirrors `setChildLayout` but persists the
3208
- * `deviceLinks` array. Persisted, projected, and preserved across
3209
- * re-register/restore. Also maintains the in-memory `devicesWithLinks`
3210
- * Set used as an O(1) gate by `resolveLinkedStatus`.
3211
- */
3212
- async function setDeviceLinks(pctx, input) {
3213
- const { deviceId, deviceLinks } = input;
3214
- const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
3215
- if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
3216
- for (const link of deviceLinks) {
3217
- if (link.source.kind !== "expression") continue;
3218
- const err = validateExpressionSource(link.source);
3219
- if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
3220
- }
3221
- await pctx.metaStore.withMetaWriteLock(async () => {
3222
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
3223
- if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
3224
- const { meta: m } = persisted;
3225
- const key = String(deviceId);
3226
- const allMeta = await pctx.metaStore.readMeta();
3227
- await pctx.settings.writeAddonStore({ deviceMeta: {
3228
- ...allMeta,
3229
- [key]: {
3230
- ...m,
3231
- deviceLinks
3232
- }
3233
- } });
3234
- });
3235
- if (deviceLinks.length > 0) pctx.host.devicesWithLinks.add(deviceId);
3236
- else pctx.host.devicesWithLinks.delete(deviceId);
3237
- await pctx.metaStore.rebuildLinkDependents();
3238
- pctx.host.ctx.eventBus.emit({
3239
- id: randomUUID(),
3240
- timestamp: /* @__PURE__ */ new Date(),
3241
- source: {
3242
- type: "device",
3243
- id: deviceId
3244
- },
3245
- category: EventCategory.DeviceMetaChanged,
3246
- data: {
3247
- deviceId,
3248
- field: "deviceLinks",
3249
- value: deviceLinks
3250
- }
3251
- });
3252
- }
3253
- /**
3254
3139
  * Stamp (or update) the semantic role on the device's meta row.
3255
3140
  * Called by the kernel's `create()` / `spawnAccessoryChild`
3256
3141
  * pre-seed when `initialMeta.role` is set (analogous to
@@ -3605,69 +3490,10 @@ async function removeLocation(pctx, input) {
3605
3490
  });
3606
3491
  }
3607
3492
  //#endregion
3608
- //#region src/builtins/device-manager/device-link-overlay.ts
3609
- /** Build both lookup maps from resolved link entries. Pure — order-preserving. */
3610
- function buildLinkIndexes(entries) {
3611
- const targets = /* @__PURE__ */ new Map();
3612
- const dependents = /* @__PURE__ */ new Map();
3613
- for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
3614
- const tKey = `${targetDeviceId}:${link.target.cap}`;
3615
- const tList = targets.get(tKey) ?? [];
3616
- tList.push({
3617
- link,
3618
- sourceDeviceId,
3619
- ...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
3620
- });
3621
- targets.set(tKey, tList);
3622
- const addDependent = (sourceId, sourceCap) => {
3623
- const sKey = `${sourceId}:${sourceCap}`;
3624
- const sList = dependents.get(sKey) ?? [];
3625
- sList.push({
3626
- targetDeviceId,
3627
- targetCap: link.target.cap
3628
- });
3629
- dependents.set(sKey, sList);
3630
- };
3631
- if (link.source.kind === "expression") {
3632
- if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
3633
- const binding = link.source.bindings[name];
3634
- if (binding === void 0 || binding.kind === "literal") continue;
3635
- addDependent(bindingDeviceId, binding.cap);
3636
- }
3637
- continue;
3638
- }
3639
- if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
3640
- }
3641
- return {
3642
- targets,
3643
- dependents
3644
- };
3645
- }
3646
- //#endregion
3647
3493
  //#region src/builtins/device-manager/device-meta-store.ts
3648
- /**
3649
- * Meta persistence layer for the device-manager addon.
3650
- *
3651
- * `DeviceMetaStore` encapsulates the read/modify/write helpers that previously
3652
- * lived as closures inside `onInitialize`: the `ctx.settings`-backed reads
3653
- * (`readStore` / `readIndex` / `readMeta` / `readMetadataMap`), the
3654
- * single-writer mutex (`withMetaWriteLock`), id allocation
3655
- * (`allocateNextDeviceId`), ownership/child resolution, link-source resolution,
3656
- * and the cross-device link reverse-index rebuild. Extracting it as a class
3657
- * (constructed once per `onInitialize`) keeps the EXACT semantics — the
3658
- * `withMetaWriteLock` promise-chain is per-instance state, identical to the
3659
- * former closure — while letting the meta-action + query modules depend on a
3660
- * named, injectable surface instead of captured closures.
3661
- *
3662
- * Cross-device link state (the `linkTargets` / `linkDependents` /
3663
- * `expectedSourceStableIds` maps + the `devicesWithLinks` gate) lives on the
3664
- * addon; the store reaches it through the injected `LinkIndexHost` so the
3665
- * single owner of that state stays the addon instance.
3666
- */
3667
3494
  var DeviceMetaStore = class {
3668
3495
  settings;
3669
3496
  registry;
3670
- linkHost;
3671
3497
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
3672
3498
  * The persisted meta store is authoritative but reads are async; hub-side
3673
3499
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
@@ -3678,10 +3504,9 @@ var DeviceMetaStore = class {
3678
3504
  * through one promise chain (see `withMetaWriteLock`). Per-instance state —
3679
3505
  * identical to the former `onInitialize` closure variable. */
3680
3506
  metaWriteChain = Promise.resolve();
3681
- constructor(settings, registry, linkHost) {
3507
+ constructor(settings, registry) {
3682
3508
  this.settings = settings;
3683
3509
  this.registry = registry;
3684
- this.linkHost = linkHost;
3685
3510
  }
3686
3511
  readStore = async () => {
3687
3512
  return await this.settings.readAddonStore();
@@ -3743,279 +3568,31 @@ var DeviceMetaStore = class {
3743
3568
  ids.delete(parentId);
3744
3569
  return [...ids];
3745
3570
  };
3746
- /** Resolve a link's `sourceKey` to a live device id: the sibling accessory
3747
- * whose stableId is `${parentStableId}-${sourceKey}`. Null when absent.
3748
- * Pure over a pre-read meta map so a multi-link resolve reads the store once. */
3749
- resolveSourceDeviceId = (parentStableId, sourceKey, meta) => {
3750
- const wanted = `${parentStableId}-${sourceKey}`;
3751
- for (const m of Object.values(meta)) if (m.stableId === wanted) return m.id;
3752
- return null;
3753
- };
3754
- /** Resolve a GLOBAL link source (P2e) to a live device id: the device whose
3755
- * FULL stableId equals `sourceStableId`, regardless of parent container.
3756
- * First match wins (stableIds are effectively unique cluster-wide — see the
3757
- * `DeviceLinkGlobalSource` docblock). Null when absent. Pure over a
3758
- * pre-read meta map so a multi-link resolve reads the store once. */
3759
- resolveGlobalSourceDeviceId = (sourceStableId, meta) => {
3760
- for (const m of Object.values(meta)) if (m.stableId === sourceStableId) return m.id;
3761
- return null;
3762
- };
3763
3571
  allocateNextDeviceId = async () => {
3764
3572
  const current = (await this.readStore()).nextDeviceId ?? 1;
3765
3573
  await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
3766
3574
  return current;
3767
3575
  };
3768
- /** Rebuild the `linkTargets` / `linkDependents` reverse-index maps from the
3769
- * current persisted meta. Called at boot (once the `devicesWithLinks` seed
3770
- * has run) and whenever the link topology can change: `setDeviceLinks`,
3771
- * `registerDevice` (a new sibling source may resolve previously-dangling
3772
- * links), and `removeDevice` (its entries must be evicted). */
3773
- rebuildLinkDependents = async () => {
3774
- if (this.linkHost.devicesWithLinks.size === 0) {
3775
- this.linkHost.setLinkIndexes(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
3776
- return;
3777
- }
3778
- const allMeta = await this.readMeta();
3779
- const stableIdById = /* @__PURE__ */ new Map();
3780
- const idByStableId = /* @__PURE__ */ new Map();
3781
- for (const m of Object.values(allMeta)) {
3782
- stableIdById.set(m.id, m.stableId);
3783
- idByStableId.set(m.stableId, m.id);
3784
- }
3785
- const entries = [];
3786
- const expectedSources = /* @__PURE__ */ new Set();
3787
- for (const targetId of this.linkHost.devicesWithLinks) {
3788
- const targetMeta = Object.values(allMeta).find((m) => m.id === targetId);
3789
- if (!targetMeta) continue;
3790
- const container = targetMeta.parentDeviceId !== null ? stableIdById.get(targetMeta.parentDeviceId) ?? stableIdById.get(targetId) : stableIdById.get(targetId);
3791
- if (container === void 0) continue;
3792
- for (const link of targetMeta.deviceLinks ?? []) {
3793
- const src = link.source;
3794
- if (src.kind === "expression") {
3795
- const bindingSourceIds = {};
3796
- for (const [name, b] of Object.entries(src.bindings)) {
3797
- if (b.kind === "literal") continue;
3798
- const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
3799
- expectedSources.add(wanted);
3800
- const bid = idByStableId.get(wanted);
3801
- if (bid !== void 0) bindingSourceIds[name] = bid;
3802
- }
3803
- entries.push({
3804
- targetDeviceId: targetId,
3805
- link,
3806
- sourceDeviceId: -1,
3807
- bindingSourceIds
3808
- });
3809
- continue;
3810
- }
3811
- if (src.kind === "literal") {
3812
- entries.push({
3813
- targetDeviceId: targetId,
3814
- link,
3815
- sourceDeviceId: -1
3816
- });
3817
- continue;
3818
- }
3819
- const wantedStableId = src.kind === "global" ? src.sourceStableId : `${container}-${src.sourceKey}`;
3820
- expectedSources.add(wantedStableId);
3821
- const srcId = idByStableId.get(wantedStableId);
3822
- if (srcId === void 0) continue;
3823
- entries.push({
3824
- targetDeviceId: targetId,
3825
- link,
3826
- sourceDeviceId: srcId
3827
- });
3828
- }
3829
- }
3830
- const { targets, dependents } = buildLinkIndexes(entries);
3831
- this.linkHost.setLinkIndexes(targets, dependents, expectedSources);
3832
- };
3833
3576
  };
3834
3577
  //#endregion
3835
- //#region src/builtins/device-manager/device-link-resolver.ts
3836
- /** Returns true when `x` is a non-null, non-array plain object. */
3837
- function isRecord(x) {
3838
- return x !== null && typeof x === "object" && !Array.isArray(x);
3839
- }
3840
- /**
3841
- * Resolve one link's transformed value. Field sources read via `readField`
3842
- * (the caller supplies the sibling lookup — sync, e.g. the in-hub state
3843
- * mirror); literal sources use their per-device constant and never consult
3844
- * the reader. Returns `undefined` to skip the overlay (unreadable field).
3845
- * Pure. Async source reads (`resolveLinkedStatus`'s provider `getStatus`
3846
- * path) cannot use this helper for field sources — they branch on
3847
- * `source.kind` directly.
3848
- */
3849
- function resolveLinkValue(link, readField) {
3850
- const src = link.source;
3851
- if (src.kind === "expression") return void 0;
3852
- return applyTransform(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
3853
- }
3854
- /**
3855
- * Sync-channel expression resolution: bindings read from the mirror via
3856
- * `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
3857
- * on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
3858
- * binding id (dangling source) or a non-primitive read resolves to `null`.
3859
- * Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
3860
- * the mirror path is hot and churn-free; the async channel logs. Compile
3861
- * failures cost one parse ever thanks to the negative LRU compile cache. Pure.
3862
- */
3863
- function resolveExpressionLinkValue(rl, readField, now) {
3864
- const src = rl.link.source;
3865
- if (src.kind !== "expression") return void 0;
3866
- const bindingValues = {};
3867
- for (const [name, b] of Object.entries(src.bindings)) {
3868
- if (b.kind === "literal") {
3869
- bindingValues[name] = b.value;
3870
- continue;
3871
- }
3872
- const bId = rl.bindingSourceIds?.[name];
3873
- if (bId === void 0) {
3874
- bindingValues[name] = null;
3875
- continue;
3876
- }
3877
- bindingValues[name] = toExpressionValue(readField(bId, b.cap, b.fieldPath)) ?? null;
3878
- }
3879
- const result = evaluateLinkExpression(src.expr, bindingValues, now);
3880
- return result.ok ? applyTransform(result.value, rl.link.transform) : void 0;
3881
- }
3882
- /** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
3883
- * concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
3884
- * is a true `instanceof` guard rather than a cast. */
3885
- function asZodType(schema) {
3886
- return schema instanceof z.ZodType ? schema : null;
3887
- }
3888
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner
3889
- * type. This lets the repair logic recognise a `TankStatus.nullable()` field
3890
- * as a ZodObject so it can fill in missing nullable keys. */
3891
- function unwrapSchema(schema) {
3892
- if (schema instanceof z.ZodNullable || schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) {
3893
- const inner = asZodType(schema.unwrap());
3894
- return inner ? unwrapSchema(inner) : schema;
3895
- }
3896
- return schema;
3897
- }
3898
- /** For a ZodObject schema, ensure every nullable key present under `value` is
3899
- * filled — a missing key whose field accepts `null` is set to `null`. Generic
3900
- * repair for the common "set one leaf of a previously-null structured field"
3901
- * case (e.g. TankStatus.level). Recurses into nested object fields. */
3902
- function fillNullableDefaults(schema, value) {
3903
- const inner = unwrapSchema(schema);
3904
- if (!(inner instanceof z.ZodObject) || !isRecord(value)) return value;
3905
- const shape = inner.shape;
3906
- const out = { ...value };
3907
- for (const [key, field] of Object.entries(shape)) if (out[key] === void 0) {
3908
- if (field.safeParse(null).success) out[key] = null;
3909
- } else out[key] = fillNullableDefaults(field, out[key]);
3910
- return out;
3911
- }
3912
- /**
3913
- * Group item-targeted links (`target.itemKey` set) by itemKey and upsert each
3914
- * group as ONE item into the status' item array (see
3915
- * `CapabilityStatusItemArray`). A NEW item is seeded from `emptyItem` with
3916
- * `keyField` (and `labelField`, when declared) set to the itemKey; an
3917
- * EXISTING item (matched by `keyField`) is cloned and overlaid in place, so
3918
- * native fields a link doesn't touch are preserved. Each upserted item is
3919
- * validated against `itemSchema` — an invalid item is skipped WITHOUT
3920
- * discarding its valid siblings. Returns the new draft and whether anything
3921
- * was applied. Pure + immutable (base array/items are never mutated).
3922
- */
3923
- function upsertItemArrayLinks(draft, grouped, itemArray) {
3924
- const arrRaw = getByPath(draft, itemArray.path);
3925
- const arr = Array.isArray(arrRaw) ? [...arrRaw] : [];
3926
- let touched = false;
3927
- for (const [itemKey, group] of grouped) {
3928
- const idx = arr.findIndex((el) => isRecord(el) && el[itemArray.keyField] === itemKey);
3929
- const existing = idx >= 0 ? arr[idx] : null;
3930
- let item = isRecord(existing) ? { ...existing } : {
3931
- ...itemArray.emptyItem,
3932
- [itemArray.keyField]: itemKey,
3933
- ...itemArray.labelField !== void 0 ? { [itemArray.labelField]: itemKey } : {}
3934
- };
3935
- let itemTouched = false;
3936
- for (const { link, sourceValue } of group) {
3937
- if (sourceValue === void 0) continue;
3938
- item = setByPath(item, link.target.fieldPath, sourceValue);
3939
- itemTouched = true;
3940
- }
3941
- if (!itemTouched) continue;
3942
- const repaired = fillNullableDefaults(itemArray.itemSchema, item);
3943
- const parsed = itemArray.itemSchema.safeParse(repaired);
3944
- if (!parsed.success) continue;
3945
- if (isRecord(parsed.data)) item = parsed.data;
3946
- if (idx >= 0) arr[idx] = item;
3947
- else arr.push(item);
3948
- touched = true;
3949
- }
3950
- if (!touched) return {
3951
- draft,
3952
- touched: false
3953
- };
3954
- return {
3955
- draft: setByPath(draft, itemArray.path, arr),
3956
- touched: true
3957
- };
3958
- }
3959
- /**
3960
- * Overlay transformed source values onto `base` by dot-path, then validate the
3961
- * result against the target cap's `statusSchema`. On validation failure the
3962
- * overlay is discarded and `base` is returned unchanged (a misconfigured link
3963
- * must never corrupt a cap response). Pure — all I/O happens in the caller.
3964
- *
3965
- * Item-array grouping (P2b): when the cap declares `status.itemArray`, links
3966
- * carrying a `target.itemKey` are grouped per key and upserted as items into
3967
- * the array (see `upsertItemArrayLinks`); their `fieldPath` is relative to
3968
- * ONE item ('level', 'status', 'label'). Links WITHOUT an itemKey keep the
3969
- * scalar dot-path behavior unchanged. A link with an itemKey on a cap that
3970
- * declares NO `itemArray` is dropped (its item-relative path must never be
3971
- * scalar-applied to the status root).
3972
- */
3973
- function mergeLinkedStatus(base, resolved, statusSchema, itemArray) {
3974
- let draft = base;
3975
- let touched = false;
3976
- const grouped = /* @__PURE__ */ new Map();
3977
- for (const entry of resolved) {
3978
- const itemKey = entry.link.target.itemKey;
3979
- if (itemKey !== void 0) {
3980
- if (!itemArray) continue;
3981
- const group = grouped.get(itemKey);
3982
- if (group) group.push(entry);
3983
- else grouped.set(itemKey, [entry]);
3984
- continue;
3985
- }
3986
- if (entry.sourceValue === void 0) continue;
3987
- draft = setByPath(draft, entry.link.target.fieldPath, entry.sourceValue);
3988
- touched = true;
3989
- }
3990
- if (itemArray && grouped.size > 0) {
3991
- const upserted = upsertItemArrayLinks(draft, grouped, itemArray);
3992
- draft = upserted.draft;
3993
- touched = touched || upserted.touched;
3994
- }
3995
- if (!touched) return base;
3996
- if (!statusSchema) return draft;
3997
- const repaired = fillNullableDefaults(statusSchema, draft);
3998
- const parsed = statusSchema.safeParse(repaired);
3999
- if (!parsed.success) return base;
4000
- return isRecord(parsed.data) ? parsed.data : base;
4001
- }
4002
- //#endregion
4003
3578
  //#region src/builtins/device-manager/device-state-mirror.ts
4004
3579
  /**
4005
3580
  * Hub-side runtime-state mirror for the device-manager addon.
4006
3581
  *
4007
3582
  * `DeviceStateMirror` owns the per-device cap-keyed slice mirror and the
4008
- * debounced disk-write coalescer that previously lived directly on the addon
4009
- * class. It mirrors every `deviceState.setCapSlice` write, emits
4010
- * `DeviceStateChanged` (overlaid with cross-device linked values), and coalesces
4011
- * frequent writes into one `writeDeviceRuntimeState` per debounce window. The
4012
- * cross-device link reverse-index lives on the addon; the mirror reads it
4013
- * through the injected `LinkOverlayHost` so the single owner of that state stays
4014
- * the addon. Extracted verbatim behavior unchanged.
3583
+ * debounced disk-write coalescer. It mirrors every `deviceState.setCapSlice`
3584
+ * write, emits `DeviceStateChanged`, and coalesces frequent writes into one
3585
+ * `writeDeviceRuntimeState` per debounce window.
3586
+ *
3587
+ * It used to do one more thing: overlay each emitted slice with cross-device
3588
+ * LINKED values, walk a `linkDependents` reverse-index and re-emit for every
3589
+ * dependent target, guarded by a `lastEmittedOverlay` churn map. Wiring was
3590
+ * deleted on 2026-08-08 (0 links across 302 devices), so the mirror emits what
3591
+ * the provider wrote — one writer per field, which is what D62 asks for
3592
+ * everywhere else.
4015
3593
  */
4016
3594
  var DeviceStateMirror = class DeviceStateMirror {
4017
3595
  ctx;
4018
- linkHost;
4019
3596
  /**
4020
3597
  * Hub-side mirror of every device's cap-keyed runtime state.
4021
3598
  * Key: deviceId. Value: per-cap slice map. Empty by default —
@@ -4029,11 +3606,8 @@ var DeviceStateMirror = class DeviceStateMirror {
4029
3606
  */
4030
3607
  runtimeStateDebounce = /* @__PURE__ */ new Map();
4031
3608
  static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
4032
- /** Loop/churn guard: last overlaid slice emitted per `${deviceId}:${cap}`. */
4033
- lastEmittedOverlay = /* @__PURE__ */ new Map();
4034
- constructor(ctx, linkHost) {
3609
+ constructor(ctx) {
4035
3610
  this.ctx = ctx;
4036
- this.linkHost = linkHost;
4037
3611
  }
4038
3612
  /**
4039
3613
  * Single-cap mirror update — diff against the current mirror,
@@ -4050,9 +3624,7 @@ var DeviceStateMirror = class DeviceStateMirror {
4050
3624
  const prior = perCap.get(capName);
4051
3625
  if (prior && shallowEqual(prior, slice)) return;
4052
3626
  perCap.set(capName, { ...slice });
4053
- this.emitOverlayed(deviceId, capName);
4054
- const deps = this.linkHost.linkDependents.get(`${deviceId}:${capName}`);
4055
- if (deps) for (const d of deps) this.emitOverlayed(d.targetDeviceId, d.targetCap);
3627
+ this.emitStateChanged(deviceId, capName, { ...slice });
4056
3628
  }
4057
3629
  /**
4058
3630
  * Debounced disk writer. Coalesces frequent writes (motion phase
@@ -4184,68 +3756,17 @@ var DeviceStateMirror = class DeviceStateMirror {
4184
3756
  for (const [k, v] of perCap) out[k] = { ...v };
4185
3757
  return out;
4186
3758
  }
4187
- /**
4188
- * Read-time overlay of a cap slice with its cross-device linked values.
4189
- * Returns a cloned raw mirror slice when the (device, cap) pair has no
4190
- * links. Sources are read from the same in-hub stateMirror — sync, no
4191
- * cross-process call. When the raw slice is ABSENT but the cap is a link
4192
- * target, the slice is SYNTHESIZED: the merge base is seeded from the
4193
- * cap's declared `status.empty` default; a failed/empty synthesize
4194
- * returns null (no phantom slices). The disk writer must NOT use this
4195
- * method; it must persist raw provider truth via snapshotForDevice.
4196
- */
4197
- overlayedSlice(deviceId, cap) {
4198
- const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
4199
- const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
4200
- if (!links || links.length === 0) return raw ? { ...raw } : null;
4201
- const now = Date.now();
4202
- const resolved = links.map((rl) => ({
4203
- link: rl.link,
4204
- sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => getByPath(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
4205
- }));
4206
- const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
4207
- const schema = capStatus?.schema;
4208
- const empty = capStatus?.empty;
4209
- const base = raw ? { ...raw } : empty !== null && typeof empty === "object" && !Array.isArray(empty) ? { ...empty } : {};
4210
- const merged = mergeLinkedStatus(base, resolved, schema, capStatus?.itemArray);
4211
- if (!raw && merged === base) return null;
4212
- return merged;
3759
+ /** One cap's mirrored slice, cloned. Null when the device has never written
3760
+ * it callers read that as "the cap has no state", never as an empty slice. */
3761
+ capSlice(deviceId, cap) {
3762
+ const raw = this.stateMirror.get(deviceId)?.get(cap);
3763
+ return raw ? { ...raw } : null;
4213
3764
  }
4214
- /**
4215
- * Like snapshotForDevice but applies the device-link overlay per cap.
4216
- * Used exclusively by the device-state READ methods (getSnapshot,
4217
- * getAllSnapshots) so callers see overlayed values. SYNTHESIZE-only
4218
- * link-target caps (no base mirror slice) are unioned into the iterated
4219
- * key set so they appear at warm-load — `overlayedSlice` builds them from
4220
- * the cap's `status.empty` (and drops them when nothing resolves). The
4221
- * debounced disk writer must continue to call snapshotForDevice (raw truth).
4222
- */
4223
- snapshotForDeviceOverlayed(deviceId) {
4224
- const perCap = this.stateMirror.get(deviceId);
4225
- const capNames = new Set(perCap ? perCap.keys() : []);
4226
- const prefix = `${deviceId}:`;
4227
- for (const key of this.linkHost.linkTargets.keys()) if (key.startsWith(prefix)) capNames.add(key.slice(prefix.length));
3765
+ /** Whole-system mirror dump. Backs `deviceState.getAllSnapshots` — one
3766
+ * round-trip for a warm boot instead of N per-device calls. */
3767
+ allSnapshots() {
4228
3768
  const out = {};
4229
- for (const capName of capNames) {
4230
- const s = this.overlayedSlice(deviceId, capName);
4231
- if (s) out[capName] = s;
4232
- }
4233
- return out;
4234
- }
4235
- /** Whole-system mirror dump (overlaid). Backs `deviceState.getAllSnapshots`.
4236
- * Devices that only exist as link TARGETS (no mirror slice yet) are
4237
- * included via the link reverse-index so their synthesized caps appear. */
4238
- allSnapshotsOverlayed() {
4239
- const deviceIds = new Set(this.stateMirror.keys());
4240
- for (const key of this.linkHost.linkTargets.keys()) {
4241
- const id = Number(key.slice(0, key.indexOf(":")));
4242
- if (Number.isFinite(id)) deviceIds.add(id);
4243
- }
4244
- const out = {};
4245
- for (const deviceId of deviceIds) {
4246
- const dev = this.snapshotForDeviceOverlayed(deviceId);
4247
- if (this.stateMirror.has(deviceId) || Object.keys(dev).length > 0) out[String(deviceId)] = dev;
4248
- }
3769
+ for (const deviceId of this.stateMirror.keys()) out[String(deviceId)] = this.snapshotForDevice(deviceId);
4249
3770
  return out;
4250
3771
  }
4251
3772
  emitStateChanged(deviceId, capName, slice) {
@@ -4264,24 +3785,6 @@ var DeviceStateMirror = class DeviceStateMirror {
4264
3785
  }
4265
3786
  });
4266
3787
  }
4267
- /** Emit DeviceStateChanged for (deviceId, cap) using the OVERLAID slice,
4268
- * skipping when the overlay is unchanged since the last emit (loop/churn
4269
- * guard). Used for the written pair AND its dependent targets. */
4270
- emitOverlayed(deviceId, cap) {
4271
- const slice = this.overlayedSlice(deviceId, cap);
4272
- if (!slice) return;
4273
- const key = `${deviceId}:${cap}`;
4274
- const prev = this.lastEmittedOverlay.get(key);
4275
- if (prev && shallowEqual(prev, slice)) return;
4276
- this.lastEmittedOverlay.set(key, slice);
4277
- this.emitStateChanged(deviceId, cap, slice);
4278
- }
4279
- /** Drop a removed device's overlay-emit guard entries (keyed
4280
- * `${deviceId}:${cap}`) so the map doesn't retain rows for a removed device.
4281
- * Called from `removeDevice`. */
4282
- dropDeviceOverlays(deviceId) {
4283
- for (const overlayKey of this.lastEmittedOverlay.keys()) if (overlayKey.startsWith(`${deviceId}:`)) this.lastEmittedOverlay.delete(overlayKey);
4284
- }
4285
3788
  /** Flush every pending debounced disk write (graceful shutdown). Clears the
4286
3789
  * debounce slots after awaiting in-flight + scheduled writes so shutdown is
4287
3790
  * lossless. */
@@ -4310,11 +3813,15 @@ var DeviceStateMirror = class DeviceStateMirror {
4310
3813
  //#endregion
4311
3814
  //#region src/builtins/device-manager/device-sync-resolvers.ts
4312
3815
  /**
4313
- * Synchronous ownership / native-cap resolvers + the read-time cross-device
4314
- * link-status overlay for the device-manager provider. Extracted verbatim from
4315
- * the `onInitialize` provider literal as free functions taking a
4316
- * `ProviderContext` (DI). These back `CapabilityRegistry`'s native fallback and
4317
- * the cap-mount linked-status overlay. Behavior unchanged.
3816
+ * Synchronous ownership / native-cap resolvers for the device-manager provider.
3817
+ * Free functions over a `ProviderContext` (DI); these back
3818
+ * `CapabilityRegistry`'s native fallback.
3819
+ *
3820
+ * This file also held `resolveLinkedStatus`, the read-time cross-device link
3821
+ * overlay. Wiring was deleted on 2026-08-08 — 0 links across 302 devices, a
3822
+ * month after the tab shipped — so the overlay, its re-entrancy guard and its
3823
+ * per-call source-read memo went with it. A cross-device derivation now
3824
+ * produces a DEVICE, not a field on somebody else's cap.
4318
3825
  */
4319
3826
  /**
4320
3827
  * Sync ownership lookup backing persistence fallbacks (e.g. remove()
@@ -4356,133 +3863,6 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
4356
3863
  };
4357
3864
  return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
4358
3865
  }
4359
- /**
4360
- * Read-time overlay: merge cross-device linked values onto a target cap's
4361
- * status. Returns null when the device has no links for `cap` (caller uses
4362
- * the base status untouched). Reads each source cap via the hub registry's
4363
- * `getProviderForDevice` (routes cross-process); merge is pure.
4364
- */
4365
- async function resolveLinkedStatus(pctx, input) {
4366
- const { deviceId, cap } = input;
4367
- if (!pctx.host.devicesWithLinks.has(deviceId)) return null;
4368
- const inFlightKey = `${deviceId}:${cap}`;
4369
- if (pctx.host.linkResolveInFlight.has(inFlightKey)) return null;
4370
- pctx.host.linkResolveInFlight.add(inFlightKey);
4371
- try {
4372
- return await resolveLinkedStatusInner(pctx, input);
4373
- } finally {
4374
- pctx.host.linkResolveInFlight.delete(inFlightKey);
4375
- }
4376
- }
4377
- /** Body of `resolveLinkedStatus` — see the wrapper for the re-entrancy guard. */
4378
- async function resolveLinkedStatusInner(pctx, input) {
4379
- const { deviceId, cap, baseStatus } = input;
4380
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
4381
- if (!persisted) return null;
4382
- const links = (persisted.meta.deviceLinks ?? []).filter((l) => l.target.cap === cap);
4383
- if (links.length === 0) return null;
4384
- const capRegistry = pctx.host.capabilityRegistry;
4385
- const capStatus = capRegistry?.getDefinition(cap)?.status;
4386
- const schema = capStatus?.schema;
4387
- if (!schema) return null;
4388
- const allMeta = await pctx.metaStore.readMeta();
4389
- let containerStableId = persisted.stableId;
4390
- const parentId = persisted.meta.parentDeviceId;
4391
- if (parentId !== null) {
4392
- const parentMeta = allMeta[String(parentId)];
4393
- if (parentMeta) containerStableId = parentMeta.stableId;
4394
- }
4395
- const now = Date.now();
4396
- const statusMemo = /* @__PURE__ */ new Map();
4397
- const readSourceStatus = async (srcId, srcCap) => {
4398
- const key = `${srcId}:${srcCap}`;
4399
- const cached = statusMemo.get(key);
4400
- if (cached !== void 0) return cached;
4401
- let result;
4402
- try {
4403
- const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
4404
- result = {
4405
- status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
4406
- ok: true
4407
- };
4408
- } catch (err) {
4409
- pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
4410
- tags: {
4411
- deviceId,
4412
- capName: cap
4413
- },
4414
- meta: {
4415
- sourceCap: srcCap,
4416
- sourceId: srcId,
4417
- error: err instanceof Error ? err.message : String(err)
4418
- }
4419
- });
4420
- result = {
4421
- status: void 0,
4422
- ok: false
4423
- };
4424
- }
4425
- statusMemo.set(key, result);
4426
- return result;
4427
- };
4428
- const resolved = [];
4429
- for (const link of links) {
4430
- const src = link.source;
4431
- if (src.kind === "literal") {
4432
- resolved.push({
4433
- link,
4434
- sourceValue: applyTransform(src.value, link.transform)
4435
- });
4436
- continue;
4437
- }
4438
- if (src.kind === "expression") {
4439
- const bindingValues = {};
4440
- for (const [name, b] of Object.entries(src.bindings)) {
4441
- if (b.kind === "literal") {
4442
- bindingValues[name] = b.value;
4443
- continue;
4444
- }
4445
- const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
4446
- if (bId === null) {
4447
- bindingValues[name] = null;
4448
- continue;
4449
- }
4450
- bindingValues[name] = toExpressionValue(getByPath((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
4451
- }
4452
- const result = evaluateLinkExpression(src.expr, bindingValues, now);
4453
- if (result.ok) resolved.push({
4454
- link,
4455
- sourceValue: applyTransform(result.value, link.transform)
4456
- });
4457
- else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
4458
- tags: {
4459
- deviceId,
4460
- capName: cap
4461
- },
4462
- meta: {
4463
- linkId: link.id,
4464
- error: result.error
4465
- }
4466
- });
4467
- continue;
4468
- }
4469
- const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
4470
- if (srcId === null) continue;
4471
- const read = await readSourceStatus(srcId, src.cap);
4472
- if (!read.ok) continue;
4473
- const raw = getByPath(read.status, src.fieldPath);
4474
- resolved.push({
4475
- link,
4476
- sourceValue: applyTransform(raw, link.transform)
4477
- });
4478
- }
4479
- if (resolved.length === 0) return null;
4480
- const synthesizing = !isRecord$1(baseStatus);
4481
- const base = isRecord$1(baseStatus) ? baseStatus : isRecord$1(capStatus.empty) ? { ...capStatus.empty } : {};
4482
- const merged = mergeLinkedStatus(base, resolved, schema, capStatus.itemArray);
4483
- if (synthesizing && merged === base) return null;
4484
- return merged;
4485
- }
4486
3866
  //#endregion
4487
3867
  //#region src/builtins/device-manager/device-manager.addon.ts
4488
3868
  var DeviceManagerAddon = class extends BaseAddon {
@@ -4535,100 +3915,24 @@ var DeviceManagerAddon = class extends BaseAddon {
4535
3915
  * the D3 re-handshake after device restore is the reliable replacement.
4536
3916
  */
4537
3917
  remoteNativeCaps = /* @__PURE__ */ new Map();
4538
- /** Device ids that currently have at least one `deviceLinks` entry. O(1) gate
4539
- * consulted by `resolveLinkedStatus` so the cap-mount overlay is a no-op for
4540
- * the vast majority of devices that have no links. Rebuilt on restore. */
4541
- devicesWithLinks = /* @__PURE__ */ new Set();
4542
- /** Whether a device currently has cross-device links. */
4543
- devicesWithLinksHas(deviceId) {
4544
- return this.devicesWithLinks.has(deviceId);
4545
- }
4546
- /** `${targetDeviceId}:${targetCap}` → resolved links feeding that cap's slice. */
4547
- linkTargets = /* @__PURE__ */ new Map();
4548
- /** `${sourceDeviceId}:${sourceCap}` → targets to recompute when that source changes. */
4549
- linkDependents = /* @__PURE__ */ new Map();
4550
- /** Expected source `stableId`s (`${container}-${sourceKey}` for sibling
4551
- * sources, the full `sourceStableId` for global sources) across all links,
4552
- * resolved or not — gates the `registerDevice` rebuild so only a registering
4553
- * device that IS a link source triggers a reindex (not every boot restore). */
4554
- expectedSourceStableIds = /* @__PURE__ */ new Set();
4555
- /** In-flight `${deviceId}:${cap}` pairs of `resolveLinkedStatus` — the P2e
4556
- * defensive re-entrancy guard bounding link cycles at resolve time. Mutated
4557
- * in place (never reassigned), so the host exposes a direct reference. */
4558
- linkResolveInFlight = /* @__PURE__ */ new Set();
4559
- /** Test/diagnostic accessors. */
4560
- linkTargetKeys() {
4561
- return [...this.linkTargets.keys()];
4562
- }
4563
- linkDependentsKeys() {
4564
- return [...this.linkDependents.keys()];
4565
- }
4566
- /** Replace the cross-device link reverse-index maps in one atomic swap.
4567
- * Sole writer of `linkTargets` / `linkDependents` / `expectedSourceStableIds`;
4568
- * called by `DeviceMetaStore.rebuildLinkDependents` via the `LinkIndexHost`. */
4569
- setLinkIndexes(targets, dependents, expectedSourceStableIds) {
4570
- this.linkTargets = targets;
4571
- this.linkDependents = dependents;
4572
- this.expectedSourceStableIds = expectedSourceStableIds;
4573
- }
4574
- /** Build the `LinkIndexHost` the meta store mutates — exposes this addon's
4575
- * `devicesWithLinks` gate (read) + `setLinkIndexes` (write) without leaking
4576
- * the private maps. */
4577
- get linkIndexHost() {
4578
- return {
4579
- devicesWithLinks: this.devicesWithLinks,
4580
- setLinkIndexes: (targets, dependents, expected) => this.setLinkIndexes(targets, dependents, expected)
4581
- };
4582
- }
4583
3918
  /** The runtime-state mirror, asserted present (constructed in onInitialize). */
4584
3919
  get stateMirror() {
4585
3920
  if (!this.stateMirrorImpl) throw new Error("DeviceManagerAddon: state mirror accessed before onInitialize");
4586
3921
  return this.stateMirrorImpl;
4587
3922
  }
4588
- /** Build the `LinkOverlayHost` the state mirror reads when overlaying linked
4589
- * values — exposes the link reverse-index maps + registry via LIVE thunks
4590
- * (the maps are reassigned on every `rebuildLinkDependents`). Arrow thunks
4591
- * capture `this` lexically so the getters never go stale. */
4592
- get linkOverlayHost() {
4593
- const linkTargets = () => this.linkTargets;
4594
- const linkDependents = () => this.linkDependents;
4595
- const capabilityRegistry = () => this.capabilityRegistry;
4596
- return {
4597
- get linkTargets() {
4598
- return linkTargets();
4599
- },
4600
- get linkDependents() {
4601
- return linkDependents();
4602
- },
4603
- get capabilityRegistry() {
4604
- return capabilityRegistry();
4605
- }
4606
- };
4607
- }
4608
3923
  /** Build the `ProviderHost` the extracted provider-method modules
4609
3924
  * (`device-meta-actions`, `device-queries`) reach — exposes the addon's
4610
3925
  * private state/methods they need without leaking the class internals.
4611
3926
  *
4612
- * `expectedSourceStableIds` is exposed via a LIVE thunk: the field is
4613
- * reassigned to a fresh Set on every `rebuildLinkDependents`, so a captured
4614
- * reference would go stale. `devicesWithLinks` / `remoteNativeCaps` are
4615
- * mutated in place (never reassigned) so a direct reference preserves the
4616
- * original live semantics. The overlay-emit guard now lives in the state
4617
- * mirror — pruning is routed through `dropDeviceOverlays`. */
3927
+ * `remoteNativeCaps` is mutated in place (never reassigned) so a direct
3928
+ * reference preserves the live semantics. */
4618
3929
  get providerHost() {
4619
3930
  const capabilityRegistry = () => this.capabilityRegistry;
4620
- const expectedSourceStableIds = () => this.expectedSourceStableIds;
4621
3931
  return {
4622
3932
  ctx: this.ctx,
4623
3933
  get capabilityRegistry() {
4624
3934
  return capabilityRegistry();
4625
3935
  },
4626
- devicesWithLinks: this.devicesWithLinks,
4627
- get expectedSourceStableIds() {
4628
- return expectedSourceStableIds();
4629
- },
4630
- linkResolveInFlight: this.linkResolveInFlight,
4631
- dropDeviceOverlays: (deviceId) => this.stateMirror.dropDeviceOverlays(deviceId),
4632
3936
  remoteNativeCaps: this.remoteNativeCaps,
4633
3937
  seedMirror: (deviceId, blob) => this.stateMirror.seedMirror(deviceId, blob),
4634
3938
  withResetSessionProbe: (blob) => this.stateMirror.withResetSessionProbe(blob),
@@ -4676,15 +3980,12 @@ var DeviceManagerAddon = class extends BaseAddon {
4676
3980
  }
4677
3981
  }))).filter((id) => id !== null);
4678
3982
  }
4679
- /** Build the dependency context the extracted binding resolvers consume.
4680
- * `devicesWithLinks` is mutated in place (never reassigned) so the direct
4681
- * reference stays live — it gates the virtual `linked` binding step. */
3983
+ /** Build the dependency context the extracted binding resolvers consume. */
4682
3984
  get bindingsDeps() {
4683
3985
  return {
4684
3986
  ctx: this.ctx,
4685
3987
  capabilityRegistry: this.capabilityRegistry,
4686
- remoteNativeCaps: this.remoteNativeCaps,
4687
- devicesWithLinks: this.devicesWithLinks
3988
+ remoteNativeCaps: this.remoteNativeCaps
4688
3989
  };
4689
3990
  }
4690
3991
  async getBindings(input) {
@@ -4740,21 +4041,16 @@ var DeviceManagerAddon = class extends BaseAddon {
4740
4041
  if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
4741
4042
  return ops;
4742
4043
  };
4743
- const metaStore = new DeviceMetaStore(settings, registry, this.linkIndexHost);
4744
- this.stateMirrorImpl = new DeviceStateMirror(this.ctx, this.linkOverlayHost);
4044
+ const metaStore = new DeviceMetaStore(settings, registry);
4045
+ this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
4745
4046
  const stateMirror = this.stateMirrorImpl;
4746
4047
  const readMeta = metaStore.readMeta;
4747
4048
  const resolvePersistedById = metaStore.resolvePersistedById;
4748
- const rebuildLinkDependents = metaStore.rebuildLinkDependents;
4749
4049
  const idToAddonId = metaStore.idToAddonId;
4750
4050
  {
4751
4051
  const meta = await readMeta();
4752
- for (const m of Object.values(meta)) {
4753
- idToAddonId.set(m.id, m.addonId);
4754
- if (Array.isArray(m.deviceLinks) && m.deviceLinks.length > 0) this.devicesWithLinks.add(m.id);
4755
- }
4052
+ for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
4756
4053
  }
4757
- await rebuildLinkDependents();
4758
4054
  const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, settings, this.ctx, deviceId, integrationId);
4759
4055
  const pctx = {
4760
4056
  host: this.providerHost,
@@ -4771,7 +4067,6 @@ var DeviceManagerAddon = class extends BaseAddon {
4771
4067
  const provider = {
4772
4068
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
4773
4069
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
4774
- resolveLinkedStatus: (input) => resolveLinkedStatus(pctx, input),
4775
4070
  allocateDeviceId: (input) => allocateDeviceId(pctx, input),
4776
4071
  registerDevice: (input) => registerDevice(pctx, input),
4777
4072
  removeDevice: (input) => removeDevice(pctx, input),
@@ -4785,7 +4080,6 @@ var DeviceManagerAddon = class extends BaseAddon {
4785
4080
  setLinkDeviceId: (input) => setLinkDeviceId(pctx, input),
4786
4081
  setPrimaryChildEntityId: (input) => setPrimaryChildEntityId(pctx, input),
4787
4082
  setChildLayout: (input) => setChildLayout(pctx, input),
4788
- setDeviceLinks: (input) => setDeviceLinks(pctx, input),
4789
4083
  setRole: (input) => setRole(pctx, input),
4790
4084
  setDisplay: (input) => setDisplay(pctx, input),
4791
4085
  getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
@@ -4945,13 +4239,13 @@ var DeviceManagerAddon = class extends BaseAddon {
4945
4239
  capability: deviceStateCapability,
4946
4240
  provider: {
4947
4241
  getSnapshot: async (input) => {
4948
- return stateMirror.snapshotForDeviceOverlayed(input.deviceId);
4242
+ return stateMirror.snapshotForDevice(input.deviceId);
4949
4243
  },
4950
4244
  getCapSlice: async (input) => {
4951
- return stateMirror.overlayedSlice(input.deviceId, input.capName);
4245
+ return stateMirror.capSlice(input.deviceId, input.capName);
4952
4246
  },
4953
4247
  getAllSnapshots: async () => {
4954
- return stateMirror.allSnapshotsOverlayed();
4248
+ return stateMirror.allSnapshots();
4955
4249
  },
4956
4250
  setCapSlice: async (input) => {
4957
4251
  const { deviceId, capName, slice } = input;