@camstack/system 1.2.314 → 1.2.315

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 (68) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.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/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/composer/composer-plan.d.ts +8 -1
  14. package/dist/builtins/composer/composer.addon.js +191 -29
  15. package/dist/builtins/composer/composer.addon.mjs +195 -33
  16. package/dist/builtins/composer/composer.d.ts +5 -1
  17. package/dist/builtins/composer/composition-runtime.d.ts +10 -1
  18. package/dist/builtins/composer/existing-target.d.ts +9 -0
  19. package/dist/builtins/composer/field-record.d.ts +12 -3
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/composition-sources.d.ts +3 -1
  23. package/dist/builtins/core-blocks/core-blocks.addon.js +3 -103
  24. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -102
  25. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  26. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  27. package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
  28. package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
  29. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -8
  31. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -8
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  38. package/dist/builtins/local-network/local-network.addon.js +1 -1
  39. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  40. package/dist/builtins/loki-logging/index.js +1 -1
  41. package/dist/builtins/loki-logging/index.mjs +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  44. package/dist/builtins/platform-probe/index.js +1 -1
  45. package/dist/builtins/platform-probe/index.mjs +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/snapshot/index.js +1 -1
  49. package/dist/builtins/snapshot/index.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  53. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  55. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/system-config/system-config.addon.js +1 -1
  57. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  58. package/dist/builtins/winston-logging/index.js +1 -1
  59. package/dist/builtins/winston-logging/index.mjs +1 -1
  60. package/dist/composition-sources-DYqQLRzF.js +122 -0
  61. package/dist/composition-sources-yonsRneP.mjs +111 -0
  62. package/dist/{dist-BtNkk3Qz.js → dist-DVKv5i-e.js} +611 -299
  63. package/dist/{dist-cH4t1ZEm.mjs → dist-MzYJVCLE.mjs} +582 -300
  64. package/dist/index.js +1 -1
  65. package/dist/index.mjs +1 -1
  66. package/dist/{retired-settings-keys-DQYHfZ0S.mjs → retired-settings-keys-8yY2sTc-.mjs} +1 -1
  67. package/dist/{retired-settings-keys-B7imb-qH.js → retired-settings-keys-iY5I0nKM.js} +1 -1
  68. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { B as addonPagesCapability, Bn as BaseAddon, ur as EventCategory, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
1
+ import { Gn as BaseAddon, R as addonPagesCapability, Wn as errMsg, hr as EventCategory } from "../../dist-MzYJVCLE.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, H as addonWidgetsCapability, ur as EventCategory, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
1
+ import { B as addonWidgetsCapability, Gn as BaseAddon, Wn as errMsg, hr as EventCategory } from "../../dist-MzYJVCLE.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, U as alertsCapability, Xn as createEvent, n as AlertSchema, ur as EventCategory, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
1
+ import { Gn as BaseAddon, V as alertsCapability, Wn as errMsg, hr as EventCategory, n as AlertSchema, tr as createEvent } from "../../dist-MzYJVCLE.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  //#region src/builtins/autotrack/autotrack-config.ts
8
8
  var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
9
9
  deadZoneFraction: .5,
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, K as bareAddonId, nr as isEvent, rn as ptzAutotrackCapability, ur as EventCategory, v as FIRST_LEVEL_MACRO_CLASSES, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
1
+ import { $t as ptzAutotrackCapability, Gn as BaseAddon, W as bareAddonId, Wn as errMsg, _ as FIRST_LEVEL_MACRO_CLASSES, hr as EventCategory, sr as isEvent } from "../../dist-MzYJVCLE.mjs";
2
2
  //#region src/builtins/autotrack/autotrack-config.ts
3
3
  var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
4
4
  deadZoneFraction: .5,
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, G as backupCapability, Kt as mayWriteToLocation, ur as EventCategory, vn as storageOccupancyCapability } from "../../dist-cH4t1ZEm.mjs";
1
+ import { Gn as BaseAddon, Ht as mayWriteToLocation, U as backupCapability, hr as EventCategory, mn as storageOccupancyCapability } from "../../dist-MzYJVCLE.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  let zod = require("zod");
8
8
  let _camstack_types_node = require("@camstack/types/node");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, E as MOTION_CLOSE_AFTER_MS, In as DeviceFeature, Rn as DeviceType, X as cameraGridLayoutCapability, Xn as createEvent, Yt as motionCapability, bn as streamCatalogCapability, ct as declarationOwnerNodeId, er as hydrateSchema, g as DeclaredDevices, h as DERIVED_CAMERA_SILENCED_CAP_NAMES, hn as snapshotCapability, i as BaseDevice, kn as buildFfmpegArgs, ur as EventCategory, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
1
+ import { Gn as BaseAddon, Gt as motionCapability, J as cameraGridLayoutCapability, T as MOTION_CLOSE_AFTER_MS, Un as DeviceType, Vn as DeviceFeature, Wn as errMsg, ar as hydrateSchema, dn as snapshotCapability, gn as streamCatalogCapability, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, kn as buildFfmpegArgs, m as DERIVED_CAMERA_SILENCED_CAP_NAMES, ot as declarationOwnerNodeId, tr as createEvent } from "../../dist-MzYJVCLE.mjs";
2
2
  import { z } from "zod";
3
3
  import { FfmpegProcess } from "@camstack/types/node";
4
4
  import { join } from "node:path";
@@ -1,4 +1,4 @@
1
- import { CapabilityLookup, ComposerBlock, ComposerUnreadableBlock, CompositionExistingTarget, CompositionFeaturePlan, CompositionFieldPlan, CompositionNewTarget, CompositionSourceRef, DeviceDeclaration, DeviceType, IScopedLogger, LogTags } from '@camstack/types';
1
+ import { CapabilityLookup, ComposerBlock, ComposerUnreadableBlock, CompositionExistingTarget, CompositionEventPlan, CompositionFeaturePlan, CompositionFieldPlan, CompositionNewTarget, CompositionSourceRef, DeviceDeclaration, DeviceType, IScopedLogger, LogTags } from '@camstack/types';
2
2
  import { TargetDescriptions } from './existing-target.js';
3
3
  import { SourceResolution } from './source-tracker.js';
4
4
  /** What the apply knows of a block besides its plan. `name` is null for an unreadable row whose name is unreadable too. */
@@ -36,6 +36,7 @@ export type PlannedTarget = PlannedNewTarget | PlannedUnreadableTarget | Planned
36
36
  export interface PlannedBlock {
37
37
  readonly block: PlannedBlockMeta;
38
38
  readonly fields: readonly CompositionFieldPlan[];
39
+ readonly events: readonly CompositionEventPlan[];
39
40
  readonly problems: readonly string[];
40
41
  readonly declaration: DeviceDeclaration | null;
41
42
  /** What forces a rebuild of the live device; null when nothing is declared. */
@@ -71,6 +72,12 @@ export interface ExistingPlanInput {
71
72
  readonly lookupCap: CapabilityLookup;
72
73
  }
73
74
  export declare function planExisting(input: ExistingPlanInput): Promise<PlannedBlock>;
75
+ /**
76
+ * Runtime preparation can still fail after a complete existing-target plan.
77
+ * Preserve its fields and description for retry, but mark it waiting so claim
78
+ * reconciliation cannot treat the unprepared candidate as live.
79
+ */
80
+ export declare function withExistingTargetWait(p: PlannedBlock, waiting: string): PlannedBlock;
74
81
  /** A target the previous plan held live on `deviceId`, that this plan finds gone. */
75
82
  export interface LostTarget {
76
83
  readonly target: PlannedExistingTarget;
@@ -3,8 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BtNkk3Qz.js");
6
+ const require_dist = require("../../dist-DVKv5i-e.js");
7
7
  const require_device_state_changed = require("../../device-state-changed-Bk7cvTnz.js");
8
+ const require_composition_sources = require("../../composition-sources-DYqQLRzF.js");
8
9
  //#region src/builtins/composer/composed-device.ts
9
10
  /**
10
11
  * A composed device: an ordinary `BaseDevice` whose caps are served by the
@@ -423,6 +424,7 @@ var WithdrawalGate = class {
423
424
  var PLACEHOLDER_SHAPE = "placeholder";
424
425
  var NO_PLAN = {
425
426
  fields: [],
427
+ events: [],
426
428
  problems: [],
427
429
  declaration: null,
428
430
  shape: null,
@@ -469,6 +471,7 @@ function planNew(block, target, lookupCap, rows) {
469
471
  };
470
472
  }
471
473
  const caps = block.composition.features.flatMap((f) => {
474
+ if (!require_dist.compositionFeatureEnabled(f)) return [];
472
475
  const def = lookupCap(f.cap);
473
476
  return def === null ? [] : [def];
474
477
  });
@@ -481,6 +484,7 @@ function planNew(block, target, lookupCap, rows) {
481
484
  return {
482
485
  block,
483
486
  fields: plan.fields,
487
+ events: plan.events,
484
488
  problems: [],
485
489
  declaration: declarationFor(declarationInput),
486
490
  shape: shapeOf(declarationInput),
@@ -528,6 +532,7 @@ async function planExisting(input) {
528
532
  ...NO_PLAN,
529
533
  block,
530
534
  fields: plan.problems.length > 0 ? [] : plan.fields,
535
+ events: plan.problems.length > 0 ? [] : plan.events,
531
536
  problems: plan.problems.map((p) => `${p.path}: ${p.message}`),
532
537
  target: {
533
538
  ...existing,
@@ -551,6 +556,20 @@ function waitingPlan(block, ref, deviceId, waiting) {
551
556
  }
552
557
  };
553
558
  }
559
+ /**
560
+ * Runtime preparation can still fail after a complete existing-target plan.
561
+ * Preserve its fields and description for retry, but mark it waiting so claim
562
+ * reconciliation cannot treat the unprepared candidate as live.
563
+ */
564
+ function withExistingTargetWait(p, waiting) {
565
+ return p.target.kind === "existing" ? {
566
+ ...p,
567
+ target: {
568
+ ...p.target,
569
+ waiting
570
+ }
571
+ } : p;
572
+ }
554
573
  /** What `logPlanTransition` compares: problems, and for an existing target its wait or its loss. */
555
574
  function planSummary(p) {
556
575
  const target = p.target;
@@ -628,7 +647,7 @@ function logPlanTransition(logger, previous, p, liveTags) {
628
647
  * is thrown by name, and the runtime's write path turns a throw into a named,
629
648
  * retried failure — never a half-built array.
630
649
  */
631
- function isRecord(value) {
650
+ function isRecord$1(value) {
632
651
  return typeof value === "object" && value !== null && !Array.isArray(value);
633
652
  }
634
653
  /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
@@ -642,7 +661,7 @@ function setPath(record, segments, value) {
642
661
  const current = record[head];
643
662
  return {
644
663
  ...record,
645
- [head]: setPath(isRecord(current) ? current : {}, rest, value)
664
+ [head]: setPath(isRecord$1(current) ? current : {}, rest, value)
646
665
  };
647
666
  }
648
667
  function itemArrayOf(group, lookupCap) {
@@ -705,6 +724,38 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
705
724
  function fieldKey(plan) {
706
725
  return require_dist.compositionFieldKey(plan);
707
726
  }
727
+ function isRecord(value) {
728
+ return typeof value === "object" && value !== null && !Array.isArray(value);
729
+ }
730
+ function valueAtPath$1(value, path) {
731
+ return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
732
+ }
733
+ function initialValue(plan, byCap, lookupCap) {
734
+ const slice = byCap?.get(plan.cap);
735
+ if (slice === void 0 || slice === null) return void 0;
736
+ const item = plan.item;
737
+ if (item === void 0) return valueAtPath$1(slice, plan.path);
738
+ const itemArray = lookupCap(plan.cap)?.status?.itemArray;
739
+ if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
740
+ const entries = valueAtPath$1(slice, item.arrayPath);
741
+ if (!Array.isArray(entries)) return void 0;
742
+ return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
743
+ }
744
+ function createFieldRecord(plan, byCap, lookupCap) {
745
+ const initial = initialValue(plan, byCap, lookupCap);
746
+ return {
747
+ plan,
748
+ outcome: null,
749
+ initial,
750
+ hasWritten: false,
751
+ written: void 0,
752
+ expressionMemory: require_dist.createStatefulExpressionMemory(),
753
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
754
+ };
755
+ }
756
+ function currentOutput(record) {
757
+ return record.hasWritten ? record.written : record.initial;
758
+ }
708
759
  /** The top-level field a record writes: an item leaf writes its whole array. */
709
760
  function topLevelField(plan) {
710
761
  return plan.item === void 0 ? plan.path : plan.item.arrayPath;
@@ -816,6 +867,11 @@ function pendingReason(cap, pending) {
816
867
  * - A target whose writes are asynchronous (claims on the hub) reports what did
817
868
  * not land through `onAsyncFailure`: the cap is marked write-pending by name
818
869
  * and re-written whole on the next pass — no retry timer (D3).
870
+ *
871
+ * Slice 2b (D670): stateful expression memory commits only when the whole
872
+ * expression succeeds. A declared state-derived event advances its seeded
873
+ * high-water and is emitted before the corresponding target write; neither an
874
+ * event-bus failure nor a refused write can replay the event.
819
875
  */
820
876
  /**
821
877
  * A refusal nothing on this side can retry away: another owner holds the
@@ -828,9 +884,13 @@ var TERMINAL_REFUSALS = new Set([
828
884
  "invalid-slice",
829
885
  "unknown-device"
830
886
  ]);
887
+ function strictFiniteAdvance(previous, next) {
888
+ return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
889
+ }
831
890
  var CompositionRuntime = class {
832
891
  deps;
833
892
  records;
893
+ startedAtMs;
834
894
  seeded = /* @__PURE__ */ new Set();
835
895
  available = null;
836
896
  failure = null;
@@ -852,11 +912,8 @@ var CompositionRuntime = class {
852
912
  terminal = /* @__PURE__ */ new Map();
853
913
  constructor(deps) {
854
914
  this.deps = deps;
855
- this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), {
856
- plan,
857
- outcome: null,
858
- written: void 0
859
- }]));
915
+ this.startedAtMs = deps.now();
916
+ this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
860
917
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
861
918
  }
862
919
  start() {
@@ -1015,15 +1072,18 @@ var CompositionRuntime = class {
1015
1072
  pass(keys, trigger) {
1016
1073
  const initial = trigger === "start";
1017
1074
  const now = this.deps.now();
1018
- const derived = this.evaluate(keys.filter((k) => this.records.get(k)?.plan.role === "derived"), now);
1019
- const followers = [...this.records.values()].filter((r) => r.plan.role === "clock" ? initial || derived.valueCaps.has(r.plan.cap) : r.plan.role === "constant" && initial).map((r) => fieldKey(r.plan));
1075
+ const calculated = this.evaluate(keys.filter((k) => {
1076
+ const role = this.records.get(k)?.plan.role;
1077
+ return role === "derived" || role === "accumulator";
1078
+ }), now);
1079
+ const followers = [...this.records.values()].filter((r) => r.plan.role === "clock" ? initial || calculated.valueCaps.has(r.plan.cap) : r.plan.role === "constant" && initial).map((r) => fieldKey(r.plan));
1020
1080
  const extra = this.evaluate(followers, now);
1021
1081
  const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
1022
1082
  const cap = this.records.get(k)?.plan.cap;
1023
1083
  return cap === void 0 ? [] : [cap];
1024
1084
  }));
1025
1085
  const retry = new Set([...this.pending.keys()].filter((cap) => trigger === "deadline" || evaluatedCaps.has(cap)));
1026
- this.writeCaps(new Set([...derived.keys, ...extra.keys]), retry);
1086
+ this.writeCaps(new Set([...calculated.keys, ...extra.keys]), retry);
1027
1087
  this.updateFailure();
1028
1088
  this.updateAvailability();
1029
1089
  }
@@ -1035,15 +1095,35 @@ var CompositionRuntime = class {
1035
1095
  for (const key of keys) {
1036
1096
  const record = next.get(key);
1037
1097
  if (record === void 0) continue;
1098
+ const candidateMemory = require_dist.cloneStatefulExpressionMemory(record.expressionMemory);
1038
1099
  const outcome = require_dist.evaluateCompositionField({
1039
1100
  source: record.plan.source,
1040
1101
  read: this.deps.read,
1041
- now
1102
+ now,
1103
+ stateful: {
1104
+ currentOutput: currentOutput(record),
1105
+ expressionMemory: candidateMemory,
1106
+ startedAtMs: this.startedAtMs,
1107
+ callPathPrefix: fieldKey(record.plan)
1108
+ }
1042
1109
  });
1043
- next.set(key, {
1110
+ const expressionSucceeded = outcome.state !== "unavailable";
1111
+ let evaluated = {
1044
1112
  ...record,
1045
- outcome
1046
- });
1113
+ outcome,
1114
+ ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
1115
+ };
1116
+ const eventPlans = this.deps.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
1117
+ if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
1118
+ const timestamp = outcome.value;
1119
+ evaluated = {
1120
+ ...evaluated,
1121
+ eventHighWater: timestamp
1122
+ };
1123
+ next.set(key, evaluated);
1124
+ this.records = next;
1125
+ for (const eventPlan of eventPlans) this.emitStateEvent(eventPlan, timestamp);
1126
+ } else next.set(key, evaluated);
1047
1127
  if (sameOutcome(record.outcome, outcome)) continue;
1048
1128
  if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
1049
1129
  if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
@@ -1057,6 +1137,39 @@ var CompositionRuntime = class {
1057
1137
  valueCaps
1058
1138
  };
1059
1139
  }
1140
+ emitStateEvent(plan, timestamp) {
1141
+ const extras = {
1142
+ tags: { deviceId: this.deps.target.id },
1143
+ meta: {
1144
+ blockId: this.deps.blockId,
1145
+ cap: plan.cap,
1146
+ eventName: plan.eventName,
1147
+ category: plan.category,
1148
+ timestamp
1149
+ }
1150
+ };
1151
+ try {
1152
+ if (plan.category !== require_dist.EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
1153
+ const event = require_dist.createEvent(require_dist.EventCategory.DoorbellOnPressed, {
1154
+ type: "device",
1155
+ id: this.deps.target.id,
1156
+ deviceId: this.deps.target.id
1157
+ }, {
1158
+ deviceId: this.deps.target.id,
1159
+ timestamp
1160
+ });
1161
+ this.deps.events.emit(event);
1162
+ this.deps.logger.debug("composition: state-derived event emitted", extras);
1163
+ } catch (error) {
1164
+ this.deps.logger.warn("composition: state-derived event delivery failed", {
1165
+ ...extras,
1166
+ meta: {
1167
+ ...extras.meta,
1168
+ error: errorText(error)
1169
+ }
1170
+ });
1171
+ }
1172
+ }
1060
1173
  recordsOf(cap) {
1061
1174
  return [...this.records.values()].filter((r) => r.plan.cap === cap);
1062
1175
  }
@@ -1085,8 +1198,9 @@ var CompositionRuntime = class {
1085
1198
  const records = this.recordsOf(cap);
1086
1199
  this.logReleaseTransitions(cap, records);
1087
1200
  const releasedRecords = records.filter(isReleased);
1088
- this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.written !== void 0 ? [k, {
1201
+ this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.hasWritten ? [k, {
1089
1202
  ...r,
1203
+ hasWritten: false,
1090
1204
  written: void 0
1091
1205
  }] : [k, r]));
1092
1206
  const withhold = this.deps.target.withhold;
@@ -1148,7 +1262,7 @@ var CompositionRuntime = class {
1148
1262
  this.markWritten(cap, entries);
1149
1263
  }
1150
1264
  patchCap(cap, touched) {
1151
- const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && writable(r) !== r.written));
1265
+ const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && (!r.hasWritten || writable(r) !== r.written)));
1152
1266
  if (entries.length === 0) return;
1153
1267
  if (this.write(cap, "patch", entries)) this.markWritten(cap, entries);
1154
1268
  }
@@ -1280,6 +1394,7 @@ var CompositionRuntime = class {
1280
1394
  const written = new Map(entries.map((e) => [fieldKey(e.plan), e.value]));
1281
1395
  this.records = new Map([...this.records].map(([k, r]) => r.plan.cap === cap && written.has(k) ? [k, {
1282
1396
  ...r,
1397
+ hasWritten: true,
1283
1398
  written: written.get(k)
1284
1399
  }] : [k, r]));
1285
1400
  }
@@ -1417,6 +1532,16 @@ var ConfirmedSeed = class {
1417
1532
  * is NOT decided here from the plan: it follows whether the claim has landed
1418
1533
  * (`HeldClaims`, C-1).
1419
1534
  */
1535
+ /**
1536
+ * Read every affected effective capability exactly once, before the composer
1537
+ * grafts or claims any of them. A null slice is a successful empty seed; a
1538
+ * rejection is deliberately left to the caller so ownership cannot begin.
1539
+ */
1540
+ async function readExistingTargetSeeds(targetDeviceId, caps, read) {
1541
+ const byCap = /* @__PURE__ */ new Map();
1542
+ for (const cap of new Set(caps)) byCap.set(cap, await read(targetDeviceId, cap));
1543
+ return { byCap };
1544
+ }
1420
1545
  /** The reason every ungraft of a gone target carries (Task 9 rule 5 / Task 10 rule 2). */
1421
1546
  var TARGET_GONE_UNGRAFT_REASON = "target device gone";
1422
1547
  /**
@@ -1750,14 +1875,17 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
1750
1875
  reason: "the composed device has not been constructed yet"
1751
1876
  };
1752
1877
  const runtime = entry.runtime;
1753
- if (runtime === null) return target.kind === "existing" ? {
1754
- blockId,
1755
- status: "starting",
1756
- reason: NOT_STARTED
1757
- } : {
1758
- blockId,
1759
- status: "running"
1760
- };
1878
+ if (runtime === null) {
1879
+ if (target.kind !== "existing" || target.features.length === 0) return {
1880
+ blockId,
1881
+ status: "running"
1882
+ };
1883
+ return {
1884
+ blockId,
1885
+ status: "starting",
1886
+ reason: NOT_STARTED
1887
+ };
1888
+ }
1761
1889
  if (unsynced) return {
1762
1890
  blockId,
1763
1891
  status: "starting",
@@ -2365,8 +2493,30 @@ var Composer = class {
2365
2493
  });
2366
2494
  continue;
2367
2495
  }
2496
+ let targetSeed;
2497
+ if (p.target.kind === "existing" && p.target.deviceId !== null && p.target.waiting === null && p.target.gone === null && p.block.enabled && p.fields.length > 0) try {
2498
+ targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2499
+ } catch (err) {
2500
+ const reason = `the target device state could not be read before ownership (${require_dist.errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
2501
+ this.deps.logger.warn(previous?.runtime === null || previous === void 0 ? "customization target state could not be read — ownership is held" : "customization edit target state could not be read — the previous runtime stays installed", {
2502
+ tags: { deviceId: p.target.deviceId },
2503
+ meta: {
2504
+ blockId: p.block.id,
2505
+ error: require_dist.errMsg(err)
2506
+ }
2507
+ });
2508
+ if (previous !== void 0) next.set(p.block.id, previous);
2509
+ else next.set(p.block.id, {
2510
+ planned: withExistingTargetWait(p, reason),
2511
+ device: null,
2512
+ runtime: null,
2513
+ appliedName: null,
2514
+ rebuildError: null,
2515
+ heldFields: []
2516
+ });
2517
+ continue;
2518
+ }
2368
2519
  const heldFields = this.heldFieldsFor(p, previous);
2369
- previous?.runtime?.stop();
2370
2520
  const operatorAction = previous === void 0 || restart.has(p.block.id) || previous.planned.block.updatedAt !== p.block.updatedAt;
2371
2521
  const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
2372
2522
  if (operatorAction) for (const f of p.fields) for (const r of f.reads) {
@@ -2374,6 +2524,7 @@ var Composer = class {
2374
2524
  if (key !== targetKey) reset.add(key);
2375
2525
  }
2376
2526
  const writer = this.writerFor(p, device);
2527
+ const runtime = writer === null || p.fields.length === 0 ? null : this.newRuntime(p, writer, targetSeed);
2377
2528
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
2378
2529
  deviceId: p.target.deviceId,
2379
2530
  ref: p.target.ref,
@@ -2382,7 +2533,7 @@ var Composer = class {
2382
2533
  enabled: p.block.enabled,
2383
2534
  settled: p.problems.length === 0
2384
2535
  });
2385
- const runtime = writer === null || p.fields.length === 0 ? null : this.newRuntime(p, writer);
2536
+ previous?.runtime?.stop();
2386
2537
  if (runtime !== null) fresh.add(runtime);
2387
2538
  next.set(p.block.id, {
2388
2539
  planned: p,
@@ -2453,15 +2604,18 @@ var Composer = class {
2453
2604
  if (previous.runtime === null) return previous.heldFields;
2454
2605
  return heldFieldsOf(previous.runtime.state().fields, `the composition no longer plans — the field keeps its last value on the device: ${p.problems.join("; ")}`);
2455
2606
  }
2456
- newRuntime(p, target) {
2607
+ newRuntime(p, target, seed) {
2457
2608
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2458
2609
  const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
2459
2610
  const nativeCaps = new Set(self?.native ?? []);
2460
2611
  return new CompositionRuntime({
2461
2612
  blockId: p.block.id,
2462
2613
  fields: p.fields,
2614
+ eventPlans: p.events,
2615
+ events: this.deps.events,
2463
2616
  target,
2464
2617
  lookupCap: this.deps.lookupCap,
2618
+ ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2465
2619
  read: (read) => this.deps.tracker.read(read, require_dist.compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2466
2620
  now: this.deps.now,
2467
2621
  timer: this.deps.newTimer(),
@@ -3585,7 +3739,10 @@ var ComposerAddon = class extends require_dist.BaseAddon {
3585
3739
  const bindings = await api.deviceManager.getBindings.query({ deviceId });
3586
3740
  return {
3587
3741
  kind: "caps",
3588
- caps: new Map(bindings.entries.map((e) => [e.capName, e.nativeAddonId]))
3742
+ caps: new Map(bindings.entries.flatMap((entry) => {
3743
+ const owner = require_composition_sources.normalizedNativeOwner(entry.nativeAddonId);
3744
+ return owner === null ? [] : [[entry.capName, owner]];
3745
+ }))
3589
3746
  };
3590
3747
  } catch (err) {
3591
3748
  return {
@@ -3697,6 +3854,11 @@ var ComposerAddon = class extends require_dist.BaseAddon {
3697
3854
  releaseClaim: (input) => api.deviceState.releaseClaim.mutate(input),
3698
3855
  listClaims: (input) => api.deviceState.listClaims.query(input)
3699
3856
  },
3857
+ events: this.ctx.eventBus,
3858
+ readExistingTargetCap: async (deviceId, capName) => await api.deviceState.getCapSlice.query({
3859
+ deviceId,
3860
+ capName
3861
+ }) ?? null,
3700
3862
  describeTarget: readBoundCaps,
3701
3863
  grafts,
3702
3864
  now: () => Date.now(),