@camstack/system 1.2.475 → 1.2.477

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 (92) hide show
  1. package/dist/{agent-https-BsqT0qni.js → agent-https-DkXBXEvz.js} +1 -1
  2. package/dist/{agent-https-wA8gGLc1.mjs → agent-https-vpf_D97z.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/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/composer-live-set.d.ts +9 -1
  18. package/dist/builtins/composer/composer-runtime-factory.d.ts +2 -1
  19. package/dist/builtins/composer/composer.addon.js +581 -15
  20. package/dist/builtins/composer/composer.addon.mjs +581 -15
  21. package/dist/builtins/composer/composer.d.ts +14 -1
  22. package/dist/builtins/composer/composition-runtime.d.ts +5 -0
  23. package/dist/builtins/composer/composition-target.d.ts +6 -0
  24. package/dist/builtins/composer/field-record.d.ts +7 -0
  25. package/dist/builtins/composer/owned-fields-target.d.ts +9 -0
  26. package/dist/builtins/composer/pulse-seed-resolve.d.ts +59 -0
  27. package/dist/builtins/composer/pulse-seed.d.ts +75 -0
  28. package/dist/builtins/console-logging/index.js +1 -1
  29. package/dist/builtins/console-logging/index.mjs +1 -1
  30. package/dist/builtins/core-blocks/block-supervisor.d.ts +19 -1
  31. package/dist/builtins/core-blocks/composition-api.d.ts +7 -1
  32. package/dist/builtins/core-blocks/core-blocks.addon.js +108 -10
  33. package/dist/builtins/core-blocks/core-blocks.addon.mjs +108 -10
  34. package/dist/builtins/device-history/device-history.addon.js +63 -953
  35. package/dist/builtins/device-history/device-history.addon.mjs +2 -892
  36. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  37. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/keychain/index.js +3 -3
  41. package/dist/builtins/keychain/index.mjs +3 -3
  42. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  43. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  44. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  45. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  46. package/dist/builtins/local-network/local-network.addon.js +2 -2
  47. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  48. package/dist/builtins/loki-logging/index.js +1 -1
  49. package/dist/builtins/loki-logging/index.mjs +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  51. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  52. package/dist/builtins/platform-probe/index.js +1 -1
  53. package/dist/builtins/platform-probe/index.mjs +1 -1
  54. package/dist/builtins/record-entries/index.js +1 -1
  55. package/dist/builtins/record-entries/index.mjs +1 -1
  56. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  57. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  58. package/dist/builtins/snapshot/index.js +1 -1
  59. package/dist/builtins/snapshot/index.mjs +1 -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/index.js +1 -1
  63. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  65. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  66. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  67. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  68. package/dist/builtins/system-config/system-config.addon.js +3 -3
  69. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  70. package/dist/builtins/winston-logging/index.js +1 -1
  71. package/dist/builtins/winston-logging/index.mjs +1 -1
  72. package/dist/{composition-sources-CiFkyAcf.mjs → composition-sources-B5nK7pHt.mjs} +1 -1
  73. package/dist/{composition-sources-DeUSUid6.js → composition-sources-BNOP9mFp.js} +1 -1
  74. package/dist/{constants-3Wj5OyEk.js → constants-D4VigrMM.js} +1 -1
  75. package/dist/{constants-DZJAkne8.mjs → constants-jbhoFV7w.mjs} +1 -1
  76. package/dist/{device-ca-DiV5xcMz.mjs → device-ca-CvBZeyVX.mjs} +1 -1
  77. package/dist/{device-ca-ftrfVdc0.js → device-ca-DhyQeXsa.js} +1 -1
  78. package/dist/{dist-DygMs3Qc.mjs → dist-B_V9r61Z.mjs} +21 -2
  79. package/dist/{dist-BtDdPdZ7.js → dist-CNU7cdFf.js} +21 -2
  80. package/dist/file-host-worker.js +1 -1
  81. package/dist/file-host-worker.mjs +1 -1
  82. package/dist/{hub-mdns-ciao-Bc9PnWu3.mjs → hub-mdns-ciao-BuUg0gpI.mjs} +2 -2
  83. package/dist/{hub-mdns-ciao-B1VcdfT1.js → hub-mdns-ciao-DFidf52M.js} +2 -2
  84. package/dist/index.js +7 -7
  85. package/dist/index.mjs +7 -7
  86. package/dist/{restore-archive-TDkQfmLO.js → restore-archive-BtCheeWu.js} +1 -1
  87. package/dist/{restore-archive-D_Pwryre.mjs → restore-archive-CDo4Zlmx.mjs} +1 -1
  88. package/dist/retention-1QA8e4o0.js +1024 -0
  89. package/dist/retention-lPgwLane.mjs +893 -0
  90. package/dist/{wal-maintenance-D3UwIKX5.js → wal-maintenance-B7xBeIFi.js} +0 -0
  91. package/dist/{wal-maintenance-rLv4uOmp.mjs → wal-maintenance-DItlr0Yu.mjs} +0 -0
  92. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-DygMs3Qc.mjs";
2
- import { n as normalizedNativeOwner } from "../../composition-sources-CiFkyAcf.mjs";
1
+ import { $n as planComposition, $r as DeviceType, Gr as compositionItemFieldPath, Hr as composedDeviceRef, It as composerCapability, Jr as createStatefulExpressionMemory, Kr as compositionSliceKey, Lt as compositionFieldKey, N as DeclaredDevices, Oa as EventCategory, Ot as buildCapabilityLookup, Ur as composedDeviceStableId, Vr as cloneStatefulExpressionMemory, Wi as createEvent, Wr as compositionFeatureEnabled, Xr as toExpressionValue, Yr as disarmRoseMemory, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as errMsg, qr as compositionSourceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as BaseAddon, zr as COMPOSER_ADDON_ID } from "../../dist-B_V9r61Z.mjs";
2
+ import { n as normalizedNativeOwner } from "../../composition-sources-B5nK7pHt.mjs";
3
+ import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-lPgwLane.mjs";
3
4
  import { z } from "zod";
4
5
  //#region src/builtins/shared/device-state-changed.ts
5
6
  /** Structural narrowing helper for untrusted bus payloads. */
@@ -788,6 +789,41 @@ function wantedClaims(live, claimGate) {
788
789
  for (const owner of held) wanted.push(...claimGate.claimsOf(owner));
789
790
  return wanted;
790
791
  }
792
+ /**
793
+ * Claims an operator has already confirmed are no longer wanted: a deleted
794
+ * block (`retiredBlockIds`), or a cap an edit dropped. Released on this apply,
795
+ * not after a second one — a missing block in a single snapshot is still the
796
+ * two-apply gate (`wantedClaims`). A disabled block is not here: its runtime
797
+ * hands the fields back itself.
798
+ */
799
+ function confirmedReleaseKeys(retiredBlockIds, planned, previous, claimGate) {
800
+ const retired = new Set(retiredBlockIds);
801
+ const seen = /* @__PURE__ */ new Set();
802
+ const keys = [];
803
+ const push = (key) => {
804
+ const id = claimKey(key);
805
+ if (seen.has(id)) return;
806
+ seen.add(id);
807
+ keys.push(key);
808
+ };
809
+ for (const owner of retired) for (const key of claimGate.claimsOf(owner)) push(key);
810
+ const plannedById = new Map(planned.map((block) => [block.block.id, block]));
811
+ for (const [blockId, entry] of previous) {
812
+ if (retired.has(blockId)) continue;
813
+ const next = plannedById.get(blockId);
814
+ if (next === void 0 || !next.block.enabled) continue;
815
+ if (entry.planned.block.updatedAt === next.block.updatedAt) continue;
816
+ for (const key of claimGate.claimsOf(blockId)) if (!planStillWantsClaim(next, key)) push(key);
817
+ }
818
+ return keys;
819
+ }
820
+ /** True when this plan still holds `key` — including while the plan cannot say, so a bad read releases nothing. */
821
+ function planStillWantsClaim(planned, key) {
822
+ const target = planned.target;
823
+ if (target.kind !== "existing") return false;
824
+ if (target.waiting !== null || target.gone !== null || target.deviceId === null || planned.problems.length > 0) return true;
825
+ return target.deviceId === key.deviceId && target.features.some((feature) => feature.cap === key.capName);
826
+ }
791
827
  //#endregion
792
828
  //#region src/builtins/composer/composition-declarations.ts
793
829
  /**
@@ -1161,9 +1197,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
1161
1197
  hasWritten: false,
1162
1198
  written: void 0,
1163
1199
  expressionMemory: createStatefulExpressionMemory(),
1164
- eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
1200
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
1201
+ pulseHold: false
1165
1202
  };
1166
1203
  }
1204
+ /** Apply a pulse start seed (D938). `keep` leaves the slice seed in place. */
1205
+ function applyPulseSeed(record, seed) {
1206
+ if (seed === void 0 || seed.kind === "keep") return record;
1207
+ if (seed.kind === "hold") return {
1208
+ ...record,
1209
+ initial: void 0,
1210
+ eventHighWater: null,
1211
+ pulseHold: true
1212
+ };
1213
+ const value = seed.value;
1214
+ return {
1215
+ ...record,
1216
+ initial: value,
1217
+ eventHighWater: typeof value === "number" && Number.isFinite(value) ? value : null,
1218
+ pulseHold: false
1219
+ };
1220
+ }
1221
+ function fieldRecords(fields, byCap, lookupCap, pulseSeeds) {
1222
+ return new Map(fields.map((plan) => {
1223
+ const key = fieldKey(plan);
1224
+ return [key, applyPulseSeed(createFieldRecord(plan, byCap, lookupCap), pulseSeeds?.get(key))];
1225
+ }));
1226
+ }
1167
1227
  function currentOutput(record) {
1168
1228
  return record.hasWritten ? record.written : record.initial;
1169
1229
  }
@@ -1217,6 +1277,7 @@ function namedField(r) {
1217
1277
  function writable(record) {
1218
1278
  const outcome = record.outcome;
1219
1279
  if (outcome === null) return void 0;
1280
+ if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
1220
1281
  if (outcome.state !== "unavailable") return outcome.value;
1221
1282
  switch (record.plan.unavailable) {
1222
1283
  case "null": return null;
@@ -1654,7 +1715,7 @@ var CompositionRuntime = class {
1654
1715
  constructor(deps) {
1655
1716
  this.deps = deps;
1656
1717
  this.startedAtMs = deps.now();
1657
- this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
1718
+ this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
1658
1719
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1659
1720
  const eventContext = {
1660
1721
  deviceId: deps.target.id,
@@ -1713,6 +1774,13 @@ var CompositionRuntime = class {
1713
1774
  this.stopped = true;
1714
1775
  this.deps.timer.disarm();
1715
1776
  }
1777
+ /** `stop`, then wait until the target has sent nothing further. */
1778
+ quiesce() {
1779
+ this.stop();
1780
+ const quiesce = this.deps.target.quiesce;
1781
+ if (quiesce === void 0) return Promise.resolve();
1782
+ return quiesce.call(this.deps.target);
1783
+ }
1716
1784
  state() {
1717
1785
  return {
1718
1786
  failure: this.failure,
@@ -2216,6 +2284,10 @@ var OwnedFieldsTarget = class {
2216
2284
  listeners = [];
2217
2285
  /** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
2218
2286
  epoch = /* @__PURE__ */ new Map();
2287
+ /** Set by `quiesce`: nothing further is sent. */
2288
+ closed = false;
2289
+ idle = Promise.resolve();
2290
+ markIdle = null;
2219
2291
  constructor(deps) {
2220
2292
  this.deps = deps;
2221
2293
  this.id = deps.deviceId;
@@ -2240,6 +2312,27 @@ var OwnedFieldsTarget = class {
2240
2312
  }
2241
2313
  /** `device-status` belongs to the native device; availability lives in the block report only. */
2242
2314
  setAvailable(_available) {}
2315
+ /**
2316
+ * The block has stopped. Writes still queued are dropped; the call in flight
2317
+ * is allowed to finish, and then nothing else is sent.
2318
+ */
2319
+ quiesce() {
2320
+ this.closed = true;
2321
+ const dropped = [...this.pending.keys()];
2322
+ this.pending = /* @__PURE__ */ new Map();
2323
+ for (const cap of dropped) this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2324
+ tags: { deviceId: this.deps.deviceId },
2325
+ meta: {
2326
+ cap,
2327
+ owner: this.deps.owner
2328
+ }
2329
+ });
2330
+ if (this.inFlight.size === 0) return Promise.resolve();
2331
+ if (this.markIdle === null) this.idle = new Promise((resolve) => {
2332
+ this.markIdle = resolve;
2333
+ });
2334
+ return this.idle;
2335
+ }
2243
2336
  onAsyncFailure(listener) {
2244
2337
  this.listeners = [...this.listeners, listener];
2245
2338
  }
@@ -2255,6 +2348,17 @@ var OwnedFieldsTarget = class {
2255
2348
  return this.deps.fieldsOf(cap).filter((f) => held === void 0 || !held.has(f));
2256
2349
  }
2257
2350
  enqueue(cap, op) {
2351
+ if (this.closed) {
2352
+ this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2353
+ tags: { deviceId: this.deps.deviceId },
2354
+ meta: {
2355
+ cap,
2356
+ owner: this.deps.owner,
2357
+ op: op.kind
2358
+ }
2359
+ });
2360
+ return;
2361
+ }
2258
2362
  if (!this.inFlight.has(cap)) {
2259
2363
  this.send(cap, op);
2260
2364
  return;
@@ -2277,6 +2381,11 @@ var OwnedFieldsTarget = class {
2277
2381
  }
2278
2382
  settled(cap) {
2279
2383
  this.inFlight = new Set([...this.inFlight].filter((c) => c !== cap));
2384
+ if (this.closed) {
2385
+ this.pending = /* @__PURE__ */ new Map();
2386
+ if (this.inFlight.size === 0) this.markIdle?.();
2387
+ return;
2388
+ }
2280
2389
  const next = this.pending.get(cap);
2281
2390
  if (next === void 0) return;
2282
2391
  this.pending = new Map([...this.pending].filter(([c]) => c !== cap));
@@ -2453,7 +2562,7 @@ function writerFor(deps, p, device, onClaimEvent) {
2453
2562
  onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
2454
2563
  });
2455
2564
  }
2456
- function newCompositionRuntime(deps, p, target, seed) {
2565
+ function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
2457
2566
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2458
2567
  const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
2459
2568
  const nativeCaps = new Set(self?.native ?? []);
@@ -2466,6 +2575,7 @@ function newCompositionRuntime(deps, p, target, seed) {
2466
2575
  target,
2467
2576
  lookupCap: deps.lookupCap,
2468
2577
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2578
+ ...pulseSeeds === void 0 ? {} : { pulseSeeds },
2469
2579
  read: (read) => deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2470
2580
  isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2471
2581
  now: deps.now,
@@ -2671,6 +2781,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2671
2781
  };
2672
2782
  }
2673
2783
  //#endregion
2784
+ //#region src/builtins/composer/pulse-seed.ts
2785
+ /** The last false→true in time order. A series that opens already on is not a transition. */
2786
+ function lastOffToOn(points, beforeOn) {
2787
+ const ordered = [...points].toSorted((a, b) => a.t - b.t);
2788
+ let previous = beforeOn;
2789
+ let rise = null;
2790
+ for (const point of ordered) {
2791
+ if (previous === false && point.on && Number.isFinite(point.t)) rise = point.t;
2792
+ previous = point.on;
2793
+ }
2794
+ return rise;
2795
+ }
2796
+ function finite(value) {
2797
+ return value !== null && Number.isFinite(value) ? value : null;
2798
+ }
2799
+ /**
2800
+ * What the source proves. A failed read is `hold`, never `none`.
2801
+ * When the source records no history and it is off, `lastChangedAt` is the
2802
+ * end of the last ring: it bounds the ring from above (the press was at or
2803
+ * before that instant). It is used only when it is newer than the target's
2804
+ * native stamp — an older bound does not prove the native value.
2805
+ */
2806
+ function proof(facts) {
2807
+ if (facts.history.kind === "unread") return {
2808
+ kind: "hold",
2809
+ reason: "history-unread"
2810
+ };
2811
+ if (facts.history.kind === "series") {
2812
+ const at = lastOffToOn(facts.history.points, facts.history.beforeOn);
2813
+ return at === null ? { kind: "none" } : {
2814
+ kind: "rise",
2815
+ at,
2816
+ via: "history"
2817
+ };
2818
+ }
2819
+ if (facts.level.kind === "unread") return {
2820
+ kind: "hold",
2821
+ reason: "level-unread"
2822
+ };
2823
+ const changedAt = facts.level.kind === "off" ? finite(facts.level.changedAt) : null;
2824
+ const newerThanNative = changedAt !== null && (facts.nativeStamp === null || changedAt > facts.nativeStamp);
2825
+ if (changedAt !== null && newerThanNative) return {
2826
+ kind: "rise",
2827
+ at: changedAt,
2828
+ via: "changed-at-bound"
2829
+ };
2830
+ return { kind: "none" };
2831
+ }
2832
+ function claimed(owned) {
2833
+ return owned.kind === "stamp" || owned.kind === "never";
2834
+ }
2835
+ function decidePulseSeed(facts) {
2836
+ const proved = proof(facts);
2837
+ if (proved.kind === "hold") return claimed(facts.owned) ? { kind: "keep" } : proved;
2838
+ if (proved.kind === "rise") {
2839
+ if (facts.owned.kind === "stamp" && facts.owned.at >= proved.at) return { kind: "keep" };
2840
+ const count = facts.owned.kind === "stamp" ? facts.owned.count : 0;
2841
+ return {
2842
+ kind: "stamp",
2843
+ at: proved.at,
2844
+ count,
2845
+ via: proved.via
2846
+ };
2847
+ }
2848
+ return claimed(facts.owned) ? { kind: "keep" } : {
2849
+ kind: "none",
2850
+ count: 0
2851
+ };
2852
+ }
2853
+ /** One failed source blocks "no ring". A rise from another source still stands. */
2854
+ function mergePulseDecisions(decisions) {
2855
+ if (decisions.length === 0) return {
2856
+ kind: "none",
2857
+ count: 0
2858
+ };
2859
+ const stamps = decisions.flatMap((decision) => decision.kind === "stamp" ? [decision] : []);
2860
+ if (stamps.length > 0) return stamps.reduce((best, next) => next.at > best.at ? next : best);
2861
+ const hold = decisions.find((decision) => decision.kind === "hold");
2862
+ if (hold !== void 0) return hold;
2863
+ if (decisions.some((decision) => decision.kind === "keep")) return { kind: "keep" };
2864
+ return {
2865
+ kind: "none",
2866
+ count: 0
2867
+ };
2868
+ }
2869
+ //#endregion
2870
+ //#region src/builtins/composer/pulse-seed-resolve.ts
2871
+ /**
2872
+ * Gather a pulse override's start seed (D938). The stored expression is not
2873
+ * changed. A failed read holds the field: no seed, and a claimed stamp is
2874
+ * not cleared (D49).
2875
+ */
2876
+ var ROSE_CALL = String.raw`rose\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)`;
2877
+ function historySeriesKey(cap, fieldPath) {
2878
+ return capSeriesKey(cap, fieldPath);
2879
+ }
2880
+ async function resolvePulseSeeds(input) {
2881
+ try {
2882
+ return await resolveNow(input);
2883
+ } catch (err) {
2884
+ input.logger.warn("pulse override seed held — gathering the source facts threw, so nothing is seeded and nothing is cleared", {
2885
+ tags: { deviceId: input.deviceId },
2886
+ meta: {
2887
+ blockId: input.blockId,
2888
+ error: errMsg(err)
2889
+ }
2890
+ });
2891
+ return /* @__PURE__ */ new Map();
2892
+ }
2893
+ }
2894
+ async function resolveNow(input) {
2895
+ const roses = input.fields.filter(isPulseTimestamp);
2896
+ if (roses.length === 0) return /* @__PURE__ */ new Map();
2897
+ const listing = await readClaims(input);
2898
+ if (listing === null) return /* @__PURE__ */ new Map();
2899
+ const seeds = /* @__PURE__ */ new Map();
2900
+ for (const field of roses) {
2901
+ const decision = await decideField(input, field, listing);
2902
+ logDecision(input, field, decision);
2903
+ for (const [key, seed] of seedsFor(input.fields, field, decision)) seeds.set(key, seed);
2904
+ }
2905
+ return seeds;
2906
+ }
2907
+ function isPulseTimestamp(field) {
2908
+ return field.source.kind === "expression" && expressionContainsBuiltinCall(field.source.expr, "rose");
2909
+ }
2910
+ async function readClaims(input) {
2911
+ try {
2912
+ const listing = await input.claims.listClaims({ ownerPrefix: input.blockId });
2913
+ if (listing.state === "loaded") return listing;
2914
+ input.logger.warn("pulse override seed held — the claims index has not loaded, so nothing is seeded and nothing is cleared", {
2915
+ tags: { deviceId: input.deviceId },
2916
+ meta: {
2917
+ blockId: input.blockId,
2918
+ reason: listing.reason
2919
+ }
2920
+ });
2921
+ return null;
2922
+ } catch (err) {
2923
+ input.logger.warn("pulse override seed held — the claims index could not be read, so nothing is seeded and nothing is cleared", {
2924
+ tags: { deviceId: input.deviceId },
2925
+ meta: {
2926
+ blockId: input.blockId,
2927
+ error: errMsg(err)
2928
+ }
2929
+ });
2930
+ return null;
2931
+ }
2932
+ }
2933
+ async function decideField(input, field, listing) {
2934
+ if (listing.state !== "loaded") return {
2935
+ kind: "hold",
2936
+ reason: "claims-unread"
2937
+ };
2938
+ const claim = listing.claims.find((row) => row.owner === input.blockId && row.deviceId === input.deviceId && row.capName === field.cap);
2939
+ const slice = input.targetSeed.byCap.get(field.cap) ?? null;
2940
+ const counter = counterField(input.fields, field);
2941
+ const owned = ownedOf(claim, slice, field.path, counter?.path ?? null);
2942
+ const nativeStamp = stampOf(slice, field.path);
2943
+ const pairs = rosePairs(field);
2944
+ if (pairs.length === 0) return { kind: "keep" };
2945
+ const decisions = [];
2946
+ for (const pair of pairs) decisions.push(await decidePair(input, pair, owned, nativeStamp));
2947
+ return mergePulseDecisions(decisions);
2948
+ }
2949
+ async function decidePair(input, pair, owned, nativeStamp) {
2950
+ const resolution = await input.sources.resolve(pair.source, false);
2951
+ if (resolution.kind === "missing") return { kind: "keep" };
2952
+ if (resolution.kind !== "found") return decidePulseSeed({
2953
+ owned,
2954
+ nativeStamp,
2955
+ history: { kind: "unread" },
2956
+ level: { kind: "unread" }
2957
+ });
2958
+ return decidePulseSeed({
2959
+ owned,
2960
+ nativeStamp,
2961
+ history: await historyOf(input, resolution.deviceId, pair),
2962
+ level: await levelOf(input, resolution.deviceId, pair)
2963
+ });
2964
+ }
2965
+ async function historyOf(input, deviceId, pair) {
2966
+ if (!recordsField(input.lookupCap, pair.cap, pair.activeField)) return { kind: "absent" };
2967
+ const now = input.now();
2968
+ const fromMs = now - DEFAULT_RETENTION.rawDays * DAY_MS;
2969
+ try {
2970
+ const answer = await input.readPulseHistory(deviceId, pair.cap, pair.activeField, fromMs, now);
2971
+ if (answer.kind === "unread") return { kind: "unread" };
2972
+ return {
2973
+ kind: "series",
2974
+ beforeOn: answer.series.beforeOn,
2975
+ points: answer.series.points
2976
+ };
2977
+ } catch (err) {
2978
+ input.logger.warn("pulse override history read threw — treated as unread", {
2979
+ tags: { deviceId: input.deviceId },
2980
+ meta: {
2981
+ blockId: input.blockId,
2982
+ sourceDeviceId: deviceId,
2983
+ cap: pair.cap,
2984
+ field: pair.activeField,
2985
+ error: errMsg(err)
2986
+ }
2987
+ });
2988
+ return { kind: "unread" };
2989
+ }
2990
+ }
2991
+ async function levelOf(input, deviceId, pair) {
2992
+ try {
2993
+ return levelFromSlice(await input.readCap(deviceId, pair.cap), pair);
2994
+ } catch (err) {
2995
+ input.logger.warn("pulse override source slice could not be read — treated as unread", {
2996
+ tags: { deviceId: input.deviceId },
2997
+ meta: {
2998
+ blockId: input.blockId,
2999
+ sourceDeviceId: deviceId,
3000
+ cap: pair.cap,
3001
+ error: errMsg(err)
3002
+ }
3003
+ });
3004
+ return { kind: "unread" };
3005
+ }
3006
+ }
3007
+ function levelFromSlice(slice, pair) {
3008
+ if (slice === null) return {
3009
+ kind: "off",
3010
+ changedAt: null
3011
+ };
3012
+ const active = slice[pair.activeField];
3013
+ if (typeof active !== "boolean") return { kind: "unread" };
3014
+ const changed = slice[pair.changedAtField];
3015
+ const changedAt = typeof changed === "number" && Number.isFinite(changed) ? changed : null;
3016
+ return active ? {
3017
+ kind: "on",
3018
+ changedAt
3019
+ } : {
3020
+ kind: "off",
3021
+ changedAt
3022
+ };
3023
+ }
3024
+ function recordsField(lookup, cap, fieldPath) {
3025
+ const history = lookup(cap)?.history;
3026
+ if (history === void 0 || history === "none") return false;
3027
+ return Object.hasOwn(history.fields, fieldPath);
3028
+ }
3029
+ function ownedOf(claim, slice, timestampPath, counterPath) {
3030
+ if (claim === void 0 || slice === null || !claim.fields.includes(timestampPath)) return { kind: "none" };
3031
+ const at = slice[timestampPath];
3032
+ if (at === null) return { kind: "never" };
3033
+ if (typeof at !== "number" || !Number.isFinite(at)) return { kind: "none" };
3034
+ const rawCount = counterPath === null ? void 0 : slice[counterPath];
3035
+ return {
3036
+ kind: "stamp",
3037
+ at,
3038
+ count: typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount >= 0 ? rawCount : 0
3039
+ };
3040
+ }
3041
+ function stampOf(slice, path) {
3042
+ const at = slice?.[path];
3043
+ return typeof at === "number" && Number.isFinite(at) ? at : null;
3044
+ }
3045
+ function counterField(fields, timestamp) {
3046
+ return fields.find((field) => {
3047
+ if (field.cap !== timestamp.cap || field.path === timestamp.path) return false;
3048
+ return field.source.kind === "expression" && field.source.expr === `count(${timestamp.path})`;
3049
+ });
3050
+ }
3051
+ function rosePairs(field) {
3052
+ if (field.source.kind !== "expression") return [];
3053
+ const found = [];
3054
+ for (const match of field.source.expr.matchAll(new RegExp(ROSE_CALL, "g"))) {
3055
+ const pair = rosePair(field, match[1], match[2]);
3056
+ if (pair !== null) found.push(pair);
3057
+ }
3058
+ return found;
3059
+ }
3060
+ function rosePair(field, activeName, changedName) {
3061
+ if (field.source.kind !== "expression") return null;
3062
+ if (activeName === void 0 || changedName === void 0) return null;
3063
+ const active = field.source.bindings[activeName];
3064
+ const changed = field.source.bindings[changedName];
3065
+ if (active === void 0 || changed === void 0) return null;
3066
+ if (active.kind !== "from" || changed.kind !== "from") return null;
3067
+ if (active.cap !== changed.cap) return null;
3068
+ if (active.source.addonId !== changed.source.addonId) return null;
3069
+ if (active.source.stableId !== changed.source.stableId) return null;
3070
+ return {
3071
+ source: active.source,
3072
+ cap: active.cap,
3073
+ activeField: active.fieldPath,
3074
+ changedAtField: changed.fieldPath
3075
+ };
3076
+ }
3077
+ function seedsFor(fields, timestamp, decision) {
3078
+ const seeds = /* @__PURE__ */ new Map();
3079
+ if (decision.kind === "keep") return seeds;
3080
+ const counter = counterField(fields, timestamp);
3081
+ if (decision.kind === "hold") {
3082
+ seeds.set(fieldKey(timestamp), { kind: "hold" });
3083
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3084
+ kind: "value",
3085
+ value: 0
3086
+ });
3087
+ return seeds;
3088
+ }
3089
+ if (decision.kind === "none") {
3090
+ seeds.set(fieldKey(timestamp), {
3091
+ kind: "value",
3092
+ value: null
3093
+ });
3094
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3095
+ kind: "value",
3096
+ value: 0
3097
+ });
3098
+ return seeds;
3099
+ }
3100
+ seeds.set(fieldKey(timestamp), {
3101
+ kind: "value",
3102
+ value: decision.at
3103
+ });
3104
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3105
+ kind: "value",
3106
+ value: decision.count
3107
+ });
3108
+ return seeds;
3109
+ }
3110
+ /** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
3111
+ function pulseHistoryFromQuery(series) {
3112
+ const row = series.find((item) => item.scope === "");
3113
+ if (row === void 0) return {
3114
+ beforeOn: null,
3115
+ points: []
3116
+ };
3117
+ return {
3118
+ beforeOn: levelBit(row.before?.value ?? null),
3119
+ points: row.points.flatMap((point) => {
3120
+ const on = levelBit(point.last);
3121
+ return on === null ? [] : [{
3122
+ t: point.t,
3123
+ on
3124
+ }];
3125
+ })
3126
+ };
3127
+ }
3128
+ function levelBit(value) {
3129
+ if (value === 1) return true;
3130
+ if (value === 0) return false;
3131
+ return null;
3132
+ }
3133
+ function logDecision(input, field, decision) {
3134
+ const meta = {
3135
+ blockId: input.blockId,
3136
+ cap: field.cap,
3137
+ field: field.path
3138
+ };
3139
+ if (decision.kind === "stamp") {
3140
+ input.logger.info("pulse override seeded lastPressedAt from its source", {
3141
+ tags: { deviceId: input.deviceId },
3142
+ meta: {
3143
+ ...meta,
3144
+ at: decision.at,
3145
+ via: decision.via,
3146
+ count: decision.count
3147
+ }
3148
+ });
3149
+ return;
3150
+ }
3151
+ if (decision.kind === "none") {
3152
+ input.logger.info("pulse override seeded no ring — the source proved none, so the native stamp is not used", {
3153
+ tags: { deviceId: input.deviceId },
3154
+ meta
3155
+ });
3156
+ return;
3157
+ }
3158
+ if (decision.kind === "hold") input.logger.warn("pulse override seed held — a source read failed, so nothing is seeded and nothing is cleared", {
3159
+ tags: { deviceId: input.deviceId },
3160
+ meta: {
3161
+ ...meta,
3162
+ reason: decision.reason
3163
+ }
3164
+ });
3165
+ }
3166
+ //#endregion
2674
3167
  //#region src/builtins/composer/composer.ts
2675
3168
  /**
2676
3169
  * The composer: every declarative composition on this hub, in one runner
@@ -2693,7 +3186,11 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2693
3186
  * filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
2694
3187
  * the hub under the block id (`OwnedFieldsTarget`), and released through the
2695
3188
  * `ClaimGate` — two applies must agree, seeded at boot from the claims index —
2696
- * when the block is gone, disabled, or lost the feature. Its own replaced
3189
+ * when the block merely vanished from one snapshot. An operator delete
3190
+ * (`retiredBlockIds`) or an edit that drops a cap releases those claims on
3191
+ * THIS apply, after the old runtime has quiesced and before any replacement
3192
+ * claims; a release that does not land is logged with `tags: { deviceId }` and
3193
+ * returned on the apply. Its own replaced
2697
3194
  * fields are read in the NATIVE view (rule 6). Its target row gone on two
2698
3195
  * agreeing index reads fails the block by name and ungrafts what it added —
2699
3196
  * whether the loss was seen on `device.unregistered` or on a later plan; its
@@ -2841,12 +3338,16 @@ var Composer = class {
2841
3338
  };
2842
3339
  });
2843
3340
  await this.composed.reconcile(gate.integrationId, declarations, planned);
3341
+ const releasing = confirmedReleaseKeys(input.retiredBlockIds ?? [], planned, this.live, this.claimGate);
2844
3342
  const rebuilt = await this.rebuildRuntimes(planned, declarations, restart, rebuildErrors);
3343
+ await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
3344
+ const unreleasedClaims = await this.releaseConfirmed(releasing);
2845
3345
  await this.syncSources(rebuilt);
2846
3346
  await this.reconcileClaims();
2847
3347
  return {
2848
3348
  blocks: planned.map((p) => this.verdict(p.block.id)),
2849
- acted: true
3349
+ acted: true,
3350
+ unreleasedClaims
2850
3351
  };
2851
3352
  }
2852
3353
  plan(block, restart, claimsWait) {
@@ -2867,7 +3368,13 @@ var Composer = class {
2867
3368
  async rebuildRuntimes(planned, declarations, restart, rebuildErrors) {
2868
3369
  const reset = /* @__PURE__ */ new Set();
2869
3370
  const fresh = /* @__PURE__ */ new Set();
3371
+ const stopped = [];
2870
3372
  const next = /* @__PURE__ */ new Map();
3373
+ const halt = (runtime) => {
3374
+ if (runtime === null) return;
3375
+ runtime.stop();
3376
+ stopped.push(runtime);
3377
+ };
2871
3378
  for (const p of planned) {
2872
3379
  const previous = this.live.get(p.block.id);
2873
3380
  const lost = lostTarget(p, previous?.planned);
@@ -2894,6 +3401,7 @@ var Composer = class {
2894
3401
  continue;
2895
3402
  }
2896
3403
  let targetSeed;
3404
+ let pulseSeeds;
2897
3405
  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 {
2898
3406
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2899
3407
  reportSeedlessRestoredCaps({
@@ -2904,6 +3412,20 @@ var Composer = class {
2904
3412
  seed: targetSeed,
2905
3413
  lookupCap: this.deps.lookupCap
2906
3414
  });
3415
+ const readHistory = this.deps.readPulseHistory;
3416
+ if (readHistory !== void 0) pulseSeeds = await resolvePulseSeeds({
3417
+ blockId: p.block.id,
3418
+ deviceId: p.target.deviceId,
3419
+ fields: p.fields,
3420
+ targetSeed,
3421
+ lookupCap: this.deps.lookupCap,
3422
+ claims: this.deps.claims,
3423
+ sources: this.deps.tracker,
3424
+ readCap: this.deps.readExistingTargetCap,
3425
+ readPulseHistory: readHistory,
3426
+ now: this.deps.now,
3427
+ logger: this.deps.logger
3428
+ });
2907
3429
  } catch (err) {
2908
3430
  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`;
2909
3431
  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", {
@@ -2929,7 +3451,7 @@ var Composer = class {
2929
3451
  const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
2930
3452
  this.onClaimEvent(deviceId, cap, owner, event);
2931
3453
  });
2932
- const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed);
3454
+ const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed, pulseSeeds);
2933
3455
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
2934
3456
  deviceId: p.target.deviceId,
2935
3457
  ref: p.target.ref,
@@ -2938,7 +3460,7 @@ var Composer = class {
2938
3460
  enabled: p.block.enabled,
2939
3461
  settled: p.problems.length === 0
2940
3462
  });
2941
- previous?.runtime?.stop();
3463
+ halt(previous?.runtime ?? null);
2942
3464
  if (runtime !== null) fresh.add(runtime);
2943
3465
  next.set(p.block.id, {
2944
3466
  planned: p,
@@ -2953,12 +3475,15 @@ var Composer = class {
2953
3475
  for (const [blockId, entry] of this.live) {
2954
3476
  if (next.has(blockId)) continue;
2955
3477
  if (held.has(composedDeviceStableId(blockId))) next.set(blockId, entry);
2956
- else entry.runtime?.stop();
3478
+ else halt(entry.runtime);
2957
3479
  }
2958
3480
  this.live = next;
2959
3481
  const liveRuntimes = new Set([...next.values()].map((e) => e.runtime));
2960
3482
  this.unsynced = new Set([...[...this.unsynced].filter((r) => liveRuntimes.has(r)), ...fresh]);
2961
- return { reset };
3483
+ return {
3484
+ reset,
3485
+ stopped
3486
+ };
2962
3487
  }
2963
3488
  ungraftGone(blockId, target, deviceId) {
2964
3489
  this.grafts.gone(deviceId);
@@ -3042,21 +3567,40 @@ var Composer = class {
3042
3567
  if (!this.claimSeed.done) return;
3043
3568
  for (const key of this.claimGate.filter(wantedClaims(this.live, this.claimGate))) await this.release(key);
3044
3569
  }
3570
+ /** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
3571
+ async releaseConfirmed(keys) {
3572
+ const unreleased = [];
3573
+ for (const key of keys) {
3574
+ const error = await this.release(key);
3575
+ if (error === null) continue;
3576
+ unreleased.push({
3577
+ deviceId: key.deviceId,
3578
+ capName: key.capName,
3579
+ owner: key.owner,
3580
+ error
3581
+ });
3582
+ }
3583
+ return unreleased;
3584
+ }
3585
+ /** Null when the hub confirmed the release (or held nothing of ours). */
3045
3586
  async release(key) {
3046
3587
  let outcome;
3047
3588
  try {
3048
3589
  outcome = await this.deps.claims.releaseClaim(key);
3049
3590
  } catch (err) {
3050
- this.claimGate.releaseFailed(key, errMsg(err));
3051
- return;
3591
+ const error = errMsg(err);
3592
+ this.claimGate.releaseFailed(key, error);
3593
+ return error;
3052
3594
  }
3053
3595
  if (!outcome.ok && !RELEASE_SETTLED_CODES.has(outcome.code)) {
3054
- this.claimGate.releaseFailed(key, `${outcome.code}: ${outcome.message}`);
3055
- return;
3596
+ const error = `${outcome.code}: ${outcome.message}`;
3597
+ this.claimGate.releaseFailed(key, error);
3598
+ return error;
3056
3599
  }
3057
3600
  this.claimGate.released(key, outcome.ok ? "released" : outcome.code);
3058
3601
  await this.grafts.released(key.deviceId, key.capName);
3059
3602
  await this.onClaimEvent(key.deviceId, key.capName, key.owner, "free");
3603
+ return null;
3060
3604
  }
3061
3605
  /**
3062
3606
  * Rule 5: a customization whose target row is gone — the tracker's two
@@ -4040,6 +4584,8 @@ var SourceTracker = class {
4040
4584
  */
4041
4585
  /** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
4042
4586
  var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
4587
+ /** The device-history query ceiling. A switch's raw tier is change-only, so this is the whole window. */
4588
+ var PULSE_HISTORY_MAX_POINTS = 5e3;
4043
4589
  /** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
4044
4590
  function parseNativeShadowChanged(data) {
4045
4591
  if (!isRecord$2(data)) return null;
@@ -4188,6 +4734,26 @@ var ComposerAddon = class extends BaseAddon {
4188
4734
  deviceId,
4189
4735
  capName
4190
4736
  }) ?? null,
4737
+ readPulseHistory: async (deviceId, cap, fieldPath, fromMs, toMs) => {
4738
+ try {
4739
+ return {
4740
+ kind: "series",
4741
+ series: pulseHistoryFromQuery((await api.deviceHistory.query.query({
4742
+ deviceId,
4743
+ key: historySeriesKey(cap, fieldPath),
4744
+ from: fromMs,
4745
+ to: toMs,
4746
+ resolution: "raw",
4747
+ maxPoints: PULSE_HISTORY_MAX_POINTS
4748
+ })).series)
4749
+ };
4750
+ } catch (err) {
4751
+ return {
4752
+ kind: "unread",
4753
+ error: errMsg(err)
4754
+ };
4755
+ }
4756
+ },
4191
4757
  describeTarget: readBoundCaps,
4192
4758
  grafts,
4193
4759
  now: () => Date.now(),