@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
@@ -1,5 +1,6 @@
1
- import { $ as compositionFieldKey, Bn as BaseAddon, Fn as toExpressionValue, J as buildCapabilityLookup, Mn as compositionItemFieldPath, Nn as compositionSliceKey, On as COMPOSER_ADDON_ID, Pn as compositionSourceKey, Q as composerCapability, Rn as DeviceType, bt as evaluateCompositionField, c as COMPOSED_ITEM_FIXED_FIELDS, en as planComposition, g as DeclaredDevices, i as BaseDevice, jn as composedDeviceStableId, t as ALL_CAPABILITY_DEFINITIONS, ur as EventCategory, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
2
- import { n as parseDeviceStateChanged, t as isRecord$1 } from "../../device-state-changed-DoBbkyEI.mjs";
1
+ import { An as cloneStatefulExpressionMemory, Bn as toExpressionValue, Fn as compositionSliceKey, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Ln as createStatefulExpressionMemory, Mn as composedDeviceStableId, Nn as compositionFeatureEnabled, Pn as compositionItemFieldPath, Un as DeviceType, Wn as errMsg, X as composerCapability, Xt as planComposition, Z as compositionFieldKey, c as COMPOSED_ITEM_FIXED_FIELDS, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, t as ALL_CAPABILITY_DEFINITIONS, tr as createEvent, vt as evaluateCompositionField, wn as COMPOSER_ADDON_ID } from "../../dist-MzYJVCLE.mjs";
2
+ import { n as parseDeviceStateChanged, t as isRecord$2 } from "../../device-state-changed-DoBbkyEI.mjs";
3
+ import { n as normalizedNativeOwner } from "../../composition-sources-yonsRneP.mjs";
3
4
  import { z } from "zod";
4
5
  //#region src/builtins/composer/composed-device.ts
5
6
  /**
@@ -419,6 +420,7 @@ var WithdrawalGate = class {
419
420
  var PLACEHOLDER_SHAPE = "placeholder";
420
421
  var NO_PLAN = {
421
422
  fields: [],
423
+ events: [],
422
424
  problems: [],
423
425
  declaration: null,
424
426
  shape: null,
@@ -465,6 +467,7 @@ function planNew(block, target, lookupCap, rows) {
465
467
  };
466
468
  }
467
469
  const caps = block.composition.features.flatMap((f) => {
470
+ if (!compositionFeatureEnabled(f)) return [];
468
471
  const def = lookupCap(f.cap);
469
472
  return def === null ? [] : [def];
470
473
  });
@@ -477,6 +480,7 @@ function planNew(block, target, lookupCap, rows) {
477
480
  return {
478
481
  block,
479
482
  fields: plan.fields,
483
+ events: plan.events,
480
484
  problems: [],
481
485
  declaration: declarationFor(declarationInput),
482
486
  shape: shapeOf(declarationInput),
@@ -524,6 +528,7 @@ async function planExisting(input) {
524
528
  ...NO_PLAN,
525
529
  block,
526
530
  fields: plan.problems.length > 0 ? [] : plan.fields,
531
+ events: plan.problems.length > 0 ? [] : plan.events,
527
532
  problems: plan.problems.map((p) => `${p.path}: ${p.message}`),
528
533
  target: {
529
534
  ...existing,
@@ -547,6 +552,20 @@ function waitingPlan(block, ref, deviceId, waiting) {
547
552
  }
548
553
  };
549
554
  }
555
+ /**
556
+ * Runtime preparation can still fail after a complete existing-target plan.
557
+ * Preserve its fields and description for retry, but mark it waiting so claim
558
+ * reconciliation cannot treat the unprepared candidate as live.
559
+ */
560
+ function withExistingTargetWait(p, waiting) {
561
+ return p.target.kind === "existing" ? {
562
+ ...p,
563
+ target: {
564
+ ...p.target,
565
+ waiting
566
+ }
567
+ } : p;
568
+ }
550
569
  /** What `logPlanTransition` compares: problems, and for an existing target its wait or its loss. */
551
570
  function planSummary(p) {
552
571
  const target = p.target;
@@ -624,7 +643,7 @@ function logPlanTransition(logger, previous, p, liveTags) {
624
643
  * is thrown by name, and the runtime's write path turns a throw into a named,
625
644
  * retried failure — never a half-built array.
626
645
  */
627
- function isRecord(value) {
646
+ function isRecord$1(value) {
628
647
  return typeof value === "object" && value !== null && !Array.isArray(value);
629
648
  }
630
649
  /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
@@ -638,7 +657,7 @@ function setPath(record, segments, value) {
638
657
  const current = record[head];
639
658
  return {
640
659
  ...record,
641
- [head]: setPath(isRecord(current) ? current : {}, rest, value)
660
+ [head]: setPath(isRecord$1(current) ? current : {}, rest, value)
642
661
  };
643
662
  }
644
663
  function itemArrayOf(group, lookupCap) {
@@ -701,6 +720,38 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
701
720
  function fieldKey(plan) {
702
721
  return compositionFieldKey(plan);
703
722
  }
723
+ function isRecord(value) {
724
+ return typeof value === "object" && value !== null && !Array.isArray(value);
725
+ }
726
+ function valueAtPath$1(value, path) {
727
+ return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
728
+ }
729
+ function initialValue(plan, byCap, lookupCap) {
730
+ const slice = byCap?.get(plan.cap);
731
+ if (slice === void 0 || slice === null) return void 0;
732
+ const item = plan.item;
733
+ if (item === void 0) return valueAtPath$1(slice, plan.path);
734
+ const itemArray = lookupCap(plan.cap)?.status?.itemArray;
735
+ if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
736
+ const entries = valueAtPath$1(slice, item.arrayPath);
737
+ if (!Array.isArray(entries)) return void 0;
738
+ return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
739
+ }
740
+ function createFieldRecord(plan, byCap, lookupCap) {
741
+ const initial = initialValue(plan, byCap, lookupCap);
742
+ return {
743
+ plan,
744
+ outcome: null,
745
+ initial,
746
+ hasWritten: false,
747
+ written: void 0,
748
+ expressionMemory: createStatefulExpressionMemory(),
749
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
750
+ };
751
+ }
752
+ function currentOutput(record) {
753
+ return record.hasWritten ? record.written : record.initial;
754
+ }
704
755
  /** The top-level field a record writes: an item leaf writes its whole array. */
705
756
  function topLevelField(plan) {
706
757
  return plan.item === void 0 ? plan.path : plan.item.arrayPath;
@@ -812,6 +863,11 @@ function pendingReason(cap, pending) {
812
863
  * - A target whose writes are asynchronous (claims on the hub) reports what did
813
864
  * not land through `onAsyncFailure`: the cap is marked write-pending by name
814
865
  * and re-written whole on the next pass — no retry timer (D3).
866
+ *
867
+ * Slice 2b (D670): stateful expression memory commits only when the whole
868
+ * expression succeeds. A declared state-derived event advances its seeded
869
+ * high-water and is emitted before the corresponding target write; neither an
870
+ * event-bus failure nor a refused write can replay the event.
815
871
  */
816
872
  /**
817
873
  * A refusal nothing on this side can retry away: another owner holds the
@@ -824,9 +880,13 @@ var TERMINAL_REFUSALS = new Set([
824
880
  "invalid-slice",
825
881
  "unknown-device"
826
882
  ]);
883
+ function strictFiniteAdvance(previous, next) {
884
+ return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
885
+ }
827
886
  var CompositionRuntime = class {
828
887
  deps;
829
888
  records;
889
+ startedAtMs;
830
890
  seeded = /* @__PURE__ */ new Set();
831
891
  available = null;
832
892
  failure = null;
@@ -848,11 +908,8 @@ var CompositionRuntime = class {
848
908
  terminal = /* @__PURE__ */ new Map();
849
909
  constructor(deps) {
850
910
  this.deps = deps;
851
- this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), {
852
- plan,
853
- outcome: null,
854
- written: void 0
855
- }]));
911
+ this.startedAtMs = deps.now();
912
+ this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
856
913
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
857
914
  }
858
915
  start() {
@@ -1011,15 +1068,18 @@ var CompositionRuntime = class {
1011
1068
  pass(keys, trigger) {
1012
1069
  const initial = trigger === "start";
1013
1070
  const now = this.deps.now();
1014
- const derived = this.evaluate(keys.filter((k) => this.records.get(k)?.plan.role === "derived"), now);
1015
- 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));
1071
+ const calculated = this.evaluate(keys.filter((k) => {
1072
+ const role = this.records.get(k)?.plan.role;
1073
+ return role === "derived" || role === "accumulator";
1074
+ }), now);
1075
+ 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));
1016
1076
  const extra = this.evaluate(followers, now);
1017
1077
  const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
1018
1078
  const cap = this.records.get(k)?.plan.cap;
1019
1079
  return cap === void 0 ? [] : [cap];
1020
1080
  }));
1021
1081
  const retry = new Set([...this.pending.keys()].filter((cap) => trigger === "deadline" || evaluatedCaps.has(cap)));
1022
- this.writeCaps(new Set([...derived.keys, ...extra.keys]), retry);
1082
+ this.writeCaps(new Set([...calculated.keys, ...extra.keys]), retry);
1023
1083
  this.updateFailure();
1024
1084
  this.updateAvailability();
1025
1085
  }
@@ -1031,15 +1091,35 @@ var CompositionRuntime = class {
1031
1091
  for (const key of keys) {
1032
1092
  const record = next.get(key);
1033
1093
  if (record === void 0) continue;
1094
+ const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
1034
1095
  const outcome = evaluateCompositionField({
1035
1096
  source: record.plan.source,
1036
1097
  read: this.deps.read,
1037
- now
1098
+ now,
1099
+ stateful: {
1100
+ currentOutput: currentOutput(record),
1101
+ expressionMemory: candidateMemory,
1102
+ startedAtMs: this.startedAtMs,
1103
+ callPathPrefix: fieldKey(record.plan)
1104
+ }
1038
1105
  });
1039
- next.set(key, {
1106
+ const expressionSucceeded = outcome.state !== "unavailable";
1107
+ let evaluated = {
1040
1108
  ...record,
1041
- outcome
1042
- });
1109
+ outcome,
1110
+ ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
1111
+ };
1112
+ const eventPlans = this.deps.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
1113
+ if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
1114
+ const timestamp = outcome.value;
1115
+ evaluated = {
1116
+ ...evaluated,
1117
+ eventHighWater: timestamp
1118
+ };
1119
+ next.set(key, evaluated);
1120
+ this.records = next;
1121
+ for (const eventPlan of eventPlans) this.emitStateEvent(eventPlan, timestamp);
1122
+ } else next.set(key, evaluated);
1043
1123
  if (sameOutcome(record.outcome, outcome)) continue;
1044
1124
  if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
1045
1125
  if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
@@ -1053,6 +1133,39 @@ var CompositionRuntime = class {
1053
1133
  valueCaps
1054
1134
  };
1055
1135
  }
1136
+ emitStateEvent(plan, timestamp) {
1137
+ const extras = {
1138
+ tags: { deviceId: this.deps.target.id },
1139
+ meta: {
1140
+ blockId: this.deps.blockId,
1141
+ cap: plan.cap,
1142
+ eventName: plan.eventName,
1143
+ category: plan.category,
1144
+ timestamp
1145
+ }
1146
+ };
1147
+ try {
1148
+ if (plan.category !== EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
1149
+ const event = createEvent(EventCategory.DoorbellOnPressed, {
1150
+ type: "device",
1151
+ id: this.deps.target.id,
1152
+ deviceId: this.deps.target.id
1153
+ }, {
1154
+ deviceId: this.deps.target.id,
1155
+ timestamp
1156
+ });
1157
+ this.deps.events.emit(event);
1158
+ this.deps.logger.debug("composition: state-derived event emitted", extras);
1159
+ } catch (error) {
1160
+ this.deps.logger.warn("composition: state-derived event delivery failed", {
1161
+ ...extras,
1162
+ meta: {
1163
+ ...extras.meta,
1164
+ error: errorText(error)
1165
+ }
1166
+ });
1167
+ }
1168
+ }
1056
1169
  recordsOf(cap) {
1057
1170
  return [...this.records.values()].filter((r) => r.plan.cap === cap);
1058
1171
  }
@@ -1081,8 +1194,9 @@ var CompositionRuntime = class {
1081
1194
  const records = this.recordsOf(cap);
1082
1195
  this.logReleaseTransitions(cap, records);
1083
1196
  const releasedRecords = records.filter(isReleased);
1084
- this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.written !== void 0 ? [k, {
1197
+ this.records = new Map([...this.records].map(([k, r]) => releasedRecords.includes(r) && r.hasWritten ? [k, {
1085
1198
  ...r,
1199
+ hasWritten: false,
1086
1200
  written: void 0
1087
1201
  }] : [k, r]));
1088
1202
  const withhold = this.deps.target.withhold;
@@ -1144,7 +1258,7 @@ var CompositionRuntime = class {
1144
1258
  this.markWritten(cap, entries);
1145
1259
  }
1146
1260
  patchCap(cap, touched) {
1147
- const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && writable(r) !== r.written));
1261
+ const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && (!r.hasWritten || writable(r) !== r.written)));
1148
1262
  if (entries.length === 0) return;
1149
1263
  if (this.write(cap, "patch", entries)) this.markWritten(cap, entries);
1150
1264
  }
@@ -1276,6 +1390,7 @@ var CompositionRuntime = class {
1276
1390
  const written = new Map(entries.map((e) => [fieldKey(e.plan), e.value]));
1277
1391
  this.records = new Map([...this.records].map(([k, r]) => r.plan.cap === cap && written.has(k) ? [k, {
1278
1392
  ...r,
1393
+ hasWritten: true,
1279
1394
  written: written.get(k)
1280
1395
  }] : [k, r]));
1281
1396
  }
@@ -1413,6 +1528,16 @@ var ConfirmedSeed = class {
1413
1528
  * is NOT decided here from the plan: it follows whether the claim has landed
1414
1529
  * (`HeldClaims`, C-1).
1415
1530
  */
1531
+ /**
1532
+ * Read every affected effective capability exactly once, before the composer
1533
+ * grafts or claims any of them. A null slice is a successful empty seed; a
1534
+ * rejection is deliberately left to the caller so ownership cannot begin.
1535
+ */
1536
+ async function readExistingTargetSeeds(targetDeviceId, caps, read) {
1537
+ const byCap = /* @__PURE__ */ new Map();
1538
+ for (const cap of new Set(caps)) byCap.set(cap, await read(targetDeviceId, cap));
1539
+ return { byCap };
1540
+ }
1416
1541
  /** The reason every ungraft of a gone target carries (Task 9 rule 5 / Task 10 rule 2). */
1417
1542
  var TARGET_GONE_UNGRAFT_REASON = "target device gone";
1418
1543
  /**
@@ -1746,14 +1871,17 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
1746
1871
  reason: "the composed device has not been constructed yet"
1747
1872
  };
1748
1873
  const runtime = entry.runtime;
1749
- if (runtime === null) return target.kind === "existing" ? {
1750
- blockId,
1751
- status: "starting",
1752
- reason: NOT_STARTED
1753
- } : {
1754
- blockId,
1755
- status: "running"
1756
- };
1874
+ if (runtime === null) {
1875
+ if (target.kind !== "existing" || target.features.length === 0) return {
1876
+ blockId,
1877
+ status: "running"
1878
+ };
1879
+ return {
1880
+ blockId,
1881
+ status: "starting",
1882
+ reason: NOT_STARTED
1883
+ };
1884
+ }
1757
1885
  if (unsynced) return {
1758
1886
  blockId,
1759
1887
  status: "starting",
@@ -2361,8 +2489,30 @@ var Composer = class {
2361
2489
  });
2362
2490
  continue;
2363
2491
  }
2492
+ let targetSeed;
2493
+ 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 {
2494
+ targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2495
+ } catch (err) {
2496
+ const reason = `the target device state could not be read before ownership (${errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
2497
+ 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", {
2498
+ tags: { deviceId: p.target.deviceId },
2499
+ meta: {
2500
+ blockId: p.block.id,
2501
+ error: errMsg(err)
2502
+ }
2503
+ });
2504
+ if (previous !== void 0) next.set(p.block.id, previous);
2505
+ else next.set(p.block.id, {
2506
+ planned: withExistingTargetWait(p, reason),
2507
+ device: null,
2508
+ runtime: null,
2509
+ appliedName: null,
2510
+ rebuildError: null,
2511
+ heldFields: []
2512
+ });
2513
+ continue;
2514
+ }
2364
2515
  const heldFields = this.heldFieldsFor(p, previous);
2365
- previous?.runtime?.stop();
2366
2516
  const operatorAction = previous === void 0 || restart.has(p.block.id) || previous.planned.block.updatedAt !== p.block.updatedAt;
2367
2517
  const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
2368
2518
  if (operatorAction) for (const f of p.fields) for (const r of f.reads) {
@@ -2370,6 +2520,7 @@ var Composer = class {
2370
2520
  if (key !== targetKey) reset.add(key);
2371
2521
  }
2372
2522
  const writer = this.writerFor(p, device);
2523
+ const runtime = writer === null || p.fields.length === 0 ? null : this.newRuntime(p, writer, targetSeed);
2373
2524
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
2374
2525
  deviceId: p.target.deviceId,
2375
2526
  ref: p.target.ref,
@@ -2378,7 +2529,7 @@ var Composer = class {
2378
2529
  enabled: p.block.enabled,
2379
2530
  settled: p.problems.length === 0
2380
2531
  });
2381
- const runtime = writer === null || p.fields.length === 0 ? null : this.newRuntime(p, writer);
2532
+ previous?.runtime?.stop();
2382
2533
  if (runtime !== null) fresh.add(runtime);
2383
2534
  next.set(p.block.id, {
2384
2535
  planned: p,
@@ -2449,15 +2600,18 @@ var Composer = class {
2449
2600
  if (previous.runtime === null) return previous.heldFields;
2450
2601
  return heldFieldsOf(previous.runtime.state().fields, `the composition no longer plans — the field keeps its last value on the device: ${p.problems.join("; ")}`);
2451
2602
  }
2452
- newRuntime(p, target) {
2603
+ newRuntime(p, target, seed) {
2453
2604
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2454
2605
  const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
2455
2606
  const nativeCaps = new Set(self?.native ?? []);
2456
2607
  return new CompositionRuntime({
2457
2608
  blockId: p.block.id,
2458
2609
  fields: p.fields,
2610
+ eventPlans: p.events,
2611
+ events: this.deps.events,
2459
2612
  target,
2460
2613
  lookupCap: this.deps.lookupCap,
2614
+ ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2461
2615
  read: (read) => this.deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2462
2616
  now: this.deps.now,
2463
2617
  timer: this.deps.newTimer(),
@@ -2890,7 +3044,7 @@ function entriesOf(request) {
2890
3044
  }))];
2891
3045
  }
2892
3046
  function valueAtPath(slice, path) {
2893
- return path.split(".").reduce((node, segment) => isRecord$1(node) ? node[segment] : void 0, slice);
3047
+ return path.split(".").reduce((node, segment) => isRecord$2(node) ? node[segment] : void 0, slice);
2894
3048
  }
2895
3049
  function onlineOf(slice) {
2896
3050
  const online = slice["online"];
@@ -3555,12 +3709,12 @@ var SourceTracker = class {
3555
3709
  var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
3556
3710
  /** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
3557
3711
  function parseNativeShadowChanged(data) {
3558
- if (!isRecord$1(data)) return null;
3712
+ if (!isRecord$2(data)) return null;
3559
3713
  const deviceId = data["deviceId"];
3560
3714
  const capName = data["capName"];
3561
3715
  const native = data["native"];
3562
3716
  if (typeof deviceId !== "number" || typeof capName !== "string") return null;
3563
- if (!isRecord$1(native)) return null;
3717
+ if (!isRecord$2(native)) return null;
3564
3718
  return {
3565
3719
  deviceId,
3566
3720
  capName,
@@ -3581,7 +3735,10 @@ var ComposerAddon = class extends BaseAddon {
3581
3735
  const bindings = await api.deviceManager.getBindings.query({ deviceId });
3582
3736
  return {
3583
3737
  kind: "caps",
3584
- caps: new Map(bindings.entries.map((e) => [e.capName, e.nativeAddonId]))
3738
+ caps: new Map(bindings.entries.flatMap((entry) => {
3739
+ const owner = normalizedNativeOwner(entry.nativeAddonId);
3740
+ return owner === null ? [] : [[entry.capName, owner]];
3741
+ }))
3585
3742
  };
3586
3743
  } catch (err) {
3587
3744
  return {
@@ -3693,6 +3850,11 @@ var ComposerAddon = class extends BaseAddon {
3693
3850
  releaseClaim: (input) => api.deviceState.releaseClaim.mutate(input),
3694
3851
  listClaims: (input) => api.deviceState.listClaims.query(input)
3695
3852
  },
3853
+ events: this.ctx.eventBus,
3854
+ readExistingTargetCap: async (deviceId, capName) => await api.deviceState.getCapSlice.query({
3855
+ deviceId,
3856
+ capName
3857
+ }) ?? null,
3696
3858
  describeTarget: readBoundCaps,
3697
3859
  grafts,
3698
3860
  now: () => Date.now(),
@@ -2,7 +2,7 @@ import { CapabilityLookup, ComposerApplyInput, ComposerApplyResult, CompositionB
2
2
  import { DeviceStateChangedData } from '../shared/device-state-changed.js';
3
3
  import { ComposerDeclare, ComposerDevicePorts } from './composed-device-set.js';
4
4
  import { PersistedComposedRow } from './composition-declarations.js';
5
- import { DeadlineTimer } from './composition-runtime.js';
5
+ import { CompositionEventEmitter, DeadlineTimer } from './composition-runtime.js';
6
6
  import { ComposerClaimsApi, ComposerGraftPorts, DescribeTarget } from './existing-target.js';
7
7
  import { SourceBindingsChangeReason, SourceTracker } from './source-tracker.js';
8
8
  export type { ComposerDerivedSpec } from './composer-plan.js';
@@ -31,6 +31,10 @@ export interface ComposerDeps {
31
31
  readonly devices: ComposerDevicePorts;
32
32
  /** The hub's per-field claims (D663): the write target's methods and the boot seed. */
33
33
  readonly claims: ComposerClaimsApi;
34
+ /** Typed state-derived events emitted by composed capability fields. */
35
+ readonly events: CompositionEventEmitter;
36
+ /** Effective target state, read before an existing-target runtime owns any field. */
37
+ readonly readExistingTargetCap: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
34
38
  /** ONE `getBindings` per target description read (rule 1). */
35
39
  readonly describeTarget: DescribeTarget;
36
40
  /** `GraftHost` (Task 10): a method-serving native for each ADDED cap. Absent, nothing is grafted. */
@@ -1,4 +1,4 @@
1
- import { CapabilityLookup, ClaimOutcome, CompositionFieldPlan, CompositionFieldState, IScopedLogger, SourceReader } from '@camstack/types';
1
+ import { CapabilityLookup, ClaimOutcome, CompositionEventPlan, CompositionFieldPlan, CompositionFieldState, IScopedLogger, SourceReader, SystemEvent } from '@camstack/types';
2
2
  import { SlicePatch } from './slice-assembler.js';
3
3
  import { ReleasedField } from './field-record.js';
4
4
  export type { ReleasedField } from './field-record.js';
@@ -47,11 +47,18 @@ export interface DeadlineTimer {
47
47
  arm(at: number, fire: () => void): void;
48
48
  disarm(): void;
49
49
  }
50
+ export interface CompositionEventEmitter {
51
+ emit(event: SystemEvent): void;
52
+ }
50
53
  export interface CompositionRuntimeDeps {
51
54
  readonly blockId: string;
52
55
  readonly fields: readonly CompositionFieldPlan[];
56
+ readonly eventPlans: readonly CompositionEventPlan[];
57
+ readonly events: CompositionEventEmitter;
53
58
  readonly target: ComposedWriteTarget;
54
59
  readonly lookupCap: CapabilityLookup;
60
+ /** Effective target slices captured before an existing-target runtime takes ownership. */
61
+ readonly initialByCap?: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
55
62
  readonly read: SourceReader;
56
63
  readonly now: () => number;
57
64
  readonly timer: DeadlineTimer;
@@ -68,6 +75,7 @@ export interface CompositionRuntimeState {
68
75
  export declare class CompositionRuntime {
69
76
  private readonly deps;
70
77
  private records;
78
+ private readonly startedAtMs;
71
79
  private seeded;
72
80
  private available;
73
81
  private failure;
@@ -117,6 +125,7 @@ export declare class CompositionRuntime {
117
125
  private logPassError;
118
126
  private pass;
119
127
  private evaluate;
128
+ private emitStateEvent;
120
129
  private recordsOf;
121
130
  /** `retry`: write-pending caps this pass re-writes WHOLE, whether or not a value changed. */
122
131
  private writeCaps;
@@ -9,6 +9,15 @@ export type ListClaimsInput = z.infer<typeof deviceStateCapability.methods.listC
9
9
  export interface ComposerClaimsApi extends OwnedFieldsApi {
10
10
  listClaims(input: ListClaimsInput): Promise<ClaimsListing>;
11
11
  }
12
+ export interface ExistingTargetSeedRead {
13
+ readonly byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null>;
14
+ }
15
+ /**
16
+ * Read every affected effective capability exactly once, before the composer
17
+ * grafts or claims any of them. A null slice is a successful empty seed; a
18
+ * rejection is deliberately left to the caller so ownership cannot begin.
19
+ */
20
+ export declare function readExistingTargetSeeds(targetDeviceId: number, caps: readonly string[], read: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>): Promise<ExistingTargetSeedRead>;
12
21
  /**
13
22
  * The composer's grafts (`GraftHost`, Task 10): a method-serving native for each
14
23
  * cap a customization ADDS. Grafted at PLAN time only; removed when a foreign
@@ -1,12 +1,21 @@
1
- import { CompositionFieldItem, CompositionFieldPlan, CompositionFieldState, ExpressionValue, FieldOutcome } from '@camstack/types';
1
+ import { CapabilityLookup, CompositionFieldItem, CompositionFieldPlan, CompositionFieldState, ExpressionValue, FieldOutcome, StatefulExpressionMemory } from '@camstack/types';
2
2
  export declare const NO_FIELDS: ReadonlySet<string>;
3
3
  export interface FieldRecord {
4
4
  readonly plan: CompositionFieldPlan;
5
5
  readonly outcome: FieldOutcome | null;
6
- /** What the target last ACCEPTED; `undefined` = never written. */
7
- readonly written: ExpressionValue | undefined;
6
+ /** The effective target value observed before this runtime took ownership. Never changes. */
7
+ readonly initial: unknown;
8
+ /** Whether `written` is an accepted runtime answer; kept separate because every falsey value is valid. */
9
+ readonly hasWritten: boolean;
10
+ /** What the target last accepted from this runtime. */
11
+ readonly written: unknown;
12
+ readonly expressionMemory: StatefulExpressionMemory;
13
+ /** Last finite state value already represented by a derived event. */
14
+ readonly eventHighWater: number | null;
8
15
  }
9
16
  export declare function fieldKey(plan: CompositionFieldPlan): string;
17
+ export declare function createFieldRecord(plan: CompositionFieldPlan, byCap: ReadonlyMap<string, Readonly<Record<string, unknown>> | null> | undefined, lookupCap: CapabilityLookup): FieldRecord;
18
+ export declare function currentOutput(record: FieldRecord): unknown;
10
19
  /** The top-level field a record writes: an item leaf writes its whole array. */
11
20
  export declare function topLevelField(plan: CompositionFieldPlan): string;
12
21
  /** The field as a report or a log line names it: `items[desiccant].status` for an item leaf. */
@@ -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
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Bn as BaseAddon, Ht as logDestinationCapability } from "../../dist-cH4t1ZEm.mjs";
1
+ import { Gn as BaseAddon, Rt as logDestinationCapability } from "../../dist-MzYJVCLE.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -7,7 +7,7 @@ export interface SourceRowView {
7
7
  /** One `getBindings` entry: the cap, and the addon serving it natively. */
8
8
  export interface BoundCapView {
9
9
  readonly capName: string;
10
- readonly nativeAddonId: string;
10
+ readonly nativeAddonId?: string | null;
11
11
  }
12
12
  export interface CompositionSourcePorts {
13
13
  readonly listByAddon: (addonId: string) => Promise<readonly SourceRowView[]>;
@@ -18,4 +18,6 @@ export interface CompositionDescriptions {
18
18
  readonly sources: ReadonlyMap<string, CompositionSourceDescription>;
19
19
  readonly targets: ReadonlyMap<string, CompositionTargetDescription>;
20
20
  }
21
+ /** Blank binding owners are not native providers and must not turn ADD into REPLACE. */
22
+ export declare function normalizedNativeOwner(owner: string | null | undefined): string | null;
21
23
  export declare function describeCompositionSources(ports: CompositionSourcePorts, sources: readonly CompositionSourceRef[], targets?: readonly CompositionSourceRef[]): Promise<CompositionDescriptions>;