@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
@@ -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-BtDdPdZ7.js");
7
- const require_composition_sources = require("../../composition-sources-DeUSUid6.js");
6
+ const require_dist = require("../../dist-CNU7cdFf.js");
7
+ const require_composition_sources = require("../../composition-sources-BNOP9mFp.js");
8
+ const require_retention = require("../../retention-1QA8e4o0.js");
8
9
  let zod = require("zod");
9
10
  //#region src/builtins/shared/device-state-changed.ts
10
11
  /** Structural narrowing helper for untrusted bus payloads. */
@@ -793,6 +794,41 @@ function wantedClaims(live, claimGate) {
793
794
  for (const owner of held) wanted.push(...claimGate.claimsOf(owner));
794
795
  return wanted;
795
796
  }
797
+ /**
798
+ * Claims an operator has already confirmed are no longer wanted: a deleted
799
+ * block (`retiredBlockIds`), or a cap an edit dropped. Released on this apply,
800
+ * not after a second one — a missing block in a single snapshot is still the
801
+ * two-apply gate (`wantedClaims`). A disabled block is not here: its runtime
802
+ * hands the fields back itself.
803
+ */
804
+ function confirmedReleaseKeys(retiredBlockIds, planned, previous, claimGate) {
805
+ const retired = new Set(retiredBlockIds);
806
+ const seen = /* @__PURE__ */ new Set();
807
+ const keys = [];
808
+ const push = (key) => {
809
+ const id = claimKey(key);
810
+ if (seen.has(id)) return;
811
+ seen.add(id);
812
+ keys.push(key);
813
+ };
814
+ for (const owner of retired) for (const key of claimGate.claimsOf(owner)) push(key);
815
+ const plannedById = new Map(planned.map((block) => [block.block.id, block]));
816
+ for (const [blockId, entry] of previous) {
817
+ if (retired.has(blockId)) continue;
818
+ const next = plannedById.get(blockId);
819
+ if (next === void 0 || !next.block.enabled) continue;
820
+ if (entry.planned.block.updatedAt === next.block.updatedAt) continue;
821
+ for (const key of claimGate.claimsOf(blockId)) if (!planStillWantsClaim(next, key)) push(key);
822
+ }
823
+ return keys;
824
+ }
825
+ /** True when this plan still holds `key` — including while the plan cannot say, so a bad read releases nothing. */
826
+ function planStillWantsClaim(planned, key) {
827
+ const target = planned.target;
828
+ if (target.kind !== "existing") return false;
829
+ if (target.waiting !== null || target.gone !== null || target.deviceId === null || planned.problems.length > 0) return true;
830
+ return target.deviceId === key.deviceId && target.features.some((feature) => feature.cap === key.capName);
831
+ }
796
832
  //#endregion
797
833
  //#region src/builtins/composer/composition-declarations.ts
798
834
  /**
@@ -1166,9 +1202,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
1166
1202
  hasWritten: false,
1167
1203
  written: void 0,
1168
1204
  expressionMemory: require_dist.createStatefulExpressionMemory(),
1169
- eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
1205
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
1206
+ pulseHold: false
1170
1207
  };
1171
1208
  }
1209
+ /** Apply a pulse start seed (D938). `keep` leaves the slice seed in place. */
1210
+ function applyPulseSeed(record, seed) {
1211
+ if (seed === void 0 || seed.kind === "keep") return record;
1212
+ if (seed.kind === "hold") return {
1213
+ ...record,
1214
+ initial: void 0,
1215
+ eventHighWater: null,
1216
+ pulseHold: true
1217
+ };
1218
+ const value = seed.value;
1219
+ return {
1220
+ ...record,
1221
+ initial: value,
1222
+ eventHighWater: typeof value === "number" && Number.isFinite(value) ? value : null,
1223
+ pulseHold: false
1224
+ };
1225
+ }
1226
+ function fieldRecords(fields, byCap, lookupCap, pulseSeeds) {
1227
+ return new Map(fields.map((plan) => {
1228
+ const key = fieldKey(plan);
1229
+ return [key, applyPulseSeed(createFieldRecord(plan, byCap, lookupCap), pulseSeeds?.get(key))];
1230
+ }));
1231
+ }
1172
1232
  function currentOutput(record) {
1173
1233
  return record.hasWritten ? record.written : record.initial;
1174
1234
  }
@@ -1222,6 +1282,7 @@ function namedField(r) {
1222
1282
  function writable(record) {
1223
1283
  const outcome = record.outcome;
1224
1284
  if (outcome === null) return void 0;
1285
+ if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
1225
1286
  if (outcome.state !== "unavailable") return outcome.value;
1226
1287
  switch (record.plan.unavailable) {
1227
1288
  case "null": return null;
@@ -1659,7 +1720,7 @@ var CompositionRuntime = class {
1659
1720
  constructor(deps) {
1660
1721
  this.deps = deps;
1661
1722
  this.startedAtMs = deps.now();
1662
- this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
1723
+ this.records = fieldRecords(deps.fields, deps.initialByCap, deps.lookupCap, deps.pulseSeeds);
1663
1724
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1664
1725
  const eventContext = {
1665
1726
  deviceId: deps.target.id,
@@ -1718,6 +1779,13 @@ var CompositionRuntime = class {
1718
1779
  this.stopped = true;
1719
1780
  this.deps.timer.disarm();
1720
1781
  }
1782
+ /** `stop`, then wait until the target has sent nothing further. */
1783
+ quiesce() {
1784
+ this.stop();
1785
+ const quiesce = this.deps.target.quiesce;
1786
+ if (quiesce === void 0) return Promise.resolve();
1787
+ return quiesce.call(this.deps.target);
1788
+ }
1721
1789
  state() {
1722
1790
  return {
1723
1791
  failure: this.failure,
@@ -2221,6 +2289,10 @@ var OwnedFieldsTarget = class {
2221
2289
  listeners = [];
2222
2290
  /** Per cap, bumped by every `withhold`: a claim that settles under another epoch claimed a stale field set and does not land (I-2). */
2223
2291
  epoch = /* @__PURE__ */ new Map();
2292
+ /** Set by `quiesce`: nothing further is sent. */
2293
+ closed = false;
2294
+ idle = Promise.resolve();
2295
+ markIdle = null;
2224
2296
  constructor(deps) {
2225
2297
  this.deps = deps;
2226
2298
  this.id = deps.deviceId;
@@ -2245,6 +2317,27 @@ var OwnedFieldsTarget = class {
2245
2317
  }
2246
2318
  /** `device-status` belongs to the native device; availability lives in the block report only. */
2247
2319
  setAvailable(_available) {}
2320
+ /**
2321
+ * The block has stopped. Writes still queued are dropped; the call in flight
2322
+ * is allowed to finish, and then nothing else is sent.
2323
+ */
2324
+ quiesce() {
2325
+ this.closed = true;
2326
+ const dropped = [...this.pending.keys()];
2327
+ this.pending = /* @__PURE__ */ new Map();
2328
+ for (const cap of dropped) this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2329
+ tags: { deviceId: this.deps.deviceId },
2330
+ meta: {
2331
+ cap,
2332
+ owner: this.deps.owner
2333
+ }
2334
+ });
2335
+ if (this.inFlight.size === 0) return Promise.resolve();
2336
+ if (this.markIdle === null) this.idle = new Promise((resolve) => {
2337
+ this.markIdle = resolve;
2338
+ });
2339
+ return this.idle;
2340
+ }
2248
2341
  onAsyncFailure(listener) {
2249
2342
  this.listeners = [...this.listeners, listener];
2250
2343
  }
@@ -2260,6 +2353,17 @@ var OwnedFieldsTarget = class {
2260
2353
  return this.deps.fieldsOf(cap).filter((f) => held === void 0 || !held.has(f));
2261
2354
  }
2262
2355
  enqueue(cap, op) {
2356
+ if (this.closed) {
2357
+ this.deps.logger.info("composed write dropped — the block stopped before it was sent", {
2358
+ tags: { deviceId: this.deps.deviceId },
2359
+ meta: {
2360
+ cap,
2361
+ owner: this.deps.owner,
2362
+ op: op.kind
2363
+ }
2364
+ });
2365
+ return;
2366
+ }
2263
2367
  if (!this.inFlight.has(cap)) {
2264
2368
  this.send(cap, op);
2265
2369
  return;
@@ -2282,6 +2386,11 @@ var OwnedFieldsTarget = class {
2282
2386
  }
2283
2387
  settled(cap) {
2284
2388
  this.inFlight = new Set([...this.inFlight].filter((c) => c !== cap));
2389
+ if (this.closed) {
2390
+ this.pending = /* @__PURE__ */ new Map();
2391
+ if (this.inFlight.size === 0) this.markIdle?.();
2392
+ return;
2393
+ }
2285
2394
  const next = this.pending.get(cap);
2286
2395
  if (next === void 0) return;
2287
2396
  this.pending = new Map([...this.pending].filter(([c]) => c !== cap));
@@ -2458,7 +2567,7 @@ function writerFor(deps, p, device, onClaimEvent) {
2458
2567
  onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
2459
2568
  });
2460
2569
  }
2461
- function newCompositionRuntime(deps, p, target, seed) {
2570
+ function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
2462
2571
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2463
2572
  const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
2464
2573
  const nativeCaps = new Set(self?.native ?? []);
@@ -2471,6 +2580,7 @@ function newCompositionRuntime(deps, p, target, seed) {
2471
2580
  target,
2472
2581
  lookupCap: deps.lookupCap,
2473
2582
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2583
+ ...pulseSeeds === void 0 ? {} : { pulseSeeds },
2474
2584
  read: (read) => deps.tracker.read(read, require_dist.compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2475
2585
  isOwnRead: (read) => require_dist.compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2476
2586
  now: deps.now,
@@ -2676,6 +2786,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2676
2786
  };
2677
2787
  }
2678
2788
  //#endregion
2789
+ //#region src/builtins/composer/pulse-seed.ts
2790
+ /** The last false→true in time order. A series that opens already on is not a transition. */
2791
+ function lastOffToOn(points, beforeOn) {
2792
+ const ordered = [...points].toSorted((a, b) => a.t - b.t);
2793
+ let previous = beforeOn;
2794
+ let rise = null;
2795
+ for (const point of ordered) {
2796
+ if (previous === false && point.on && Number.isFinite(point.t)) rise = point.t;
2797
+ previous = point.on;
2798
+ }
2799
+ return rise;
2800
+ }
2801
+ function finite(value) {
2802
+ return value !== null && Number.isFinite(value) ? value : null;
2803
+ }
2804
+ /**
2805
+ * What the source proves. A failed read is `hold`, never `none`.
2806
+ * When the source records no history and it is off, `lastChangedAt` is the
2807
+ * end of the last ring: it bounds the ring from above (the press was at or
2808
+ * before that instant). It is used only when it is newer than the target's
2809
+ * native stamp — an older bound does not prove the native value.
2810
+ */
2811
+ function proof(facts) {
2812
+ if (facts.history.kind === "unread") return {
2813
+ kind: "hold",
2814
+ reason: "history-unread"
2815
+ };
2816
+ if (facts.history.kind === "series") {
2817
+ const at = lastOffToOn(facts.history.points, facts.history.beforeOn);
2818
+ return at === null ? { kind: "none" } : {
2819
+ kind: "rise",
2820
+ at,
2821
+ via: "history"
2822
+ };
2823
+ }
2824
+ if (facts.level.kind === "unread") return {
2825
+ kind: "hold",
2826
+ reason: "level-unread"
2827
+ };
2828
+ const changedAt = facts.level.kind === "off" ? finite(facts.level.changedAt) : null;
2829
+ const newerThanNative = changedAt !== null && (facts.nativeStamp === null || changedAt > facts.nativeStamp);
2830
+ if (changedAt !== null && newerThanNative) return {
2831
+ kind: "rise",
2832
+ at: changedAt,
2833
+ via: "changed-at-bound"
2834
+ };
2835
+ return { kind: "none" };
2836
+ }
2837
+ function claimed(owned) {
2838
+ return owned.kind === "stamp" || owned.kind === "never";
2839
+ }
2840
+ function decidePulseSeed(facts) {
2841
+ const proved = proof(facts);
2842
+ if (proved.kind === "hold") return claimed(facts.owned) ? { kind: "keep" } : proved;
2843
+ if (proved.kind === "rise") {
2844
+ if (facts.owned.kind === "stamp" && facts.owned.at >= proved.at) return { kind: "keep" };
2845
+ const count = facts.owned.kind === "stamp" ? facts.owned.count : 0;
2846
+ return {
2847
+ kind: "stamp",
2848
+ at: proved.at,
2849
+ count,
2850
+ via: proved.via
2851
+ };
2852
+ }
2853
+ return claimed(facts.owned) ? { kind: "keep" } : {
2854
+ kind: "none",
2855
+ count: 0
2856
+ };
2857
+ }
2858
+ /** One failed source blocks "no ring". A rise from another source still stands. */
2859
+ function mergePulseDecisions(decisions) {
2860
+ if (decisions.length === 0) return {
2861
+ kind: "none",
2862
+ count: 0
2863
+ };
2864
+ const stamps = decisions.flatMap((decision) => decision.kind === "stamp" ? [decision] : []);
2865
+ if (stamps.length > 0) return stamps.reduce((best, next) => next.at > best.at ? next : best);
2866
+ const hold = decisions.find((decision) => decision.kind === "hold");
2867
+ if (hold !== void 0) return hold;
2868
+ if (decisions.some((decision) => decision.kind === "keep")) return { kind: "keep" };
2869
+ return {
2870
+ kind: "none",
2871
+ count: 0
2872
+ };
2873
+ }
2874
+ //#endregion
2875
+ //#region src/builtins/composer/pulse-seed-resolve.ts
2876
+ /**
2877
+ * Gather a pulse override's start seed (D938). The stored expression is not
2878
+ * changed. A failed read holds the field: no seed, and a claimed stamp is
2879
+ * not cleared (D49).
2880
+ */
2881
+ var ROSE_CALL = String.raw`rose\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)`;
2882
+ function historySeriesKey(cap, fieldPath) {
2883
+ return require_retention.capSeriesKey(cap, fieldPath);
2884
+ }
2885
+ async function resolvePulseSeeds(input) {
2886
+ try {
2887
+ return await resolveNow(input);
2888
+ } catch (err) {
2889
+ input.logger.warn("pulse override seed held — gathering the source facts threw, so nothing is seeded and nothing is cleared", {
2890
+ tags: { deviceId: input.deviceId },
2891
+ meta: {
2892
+ blockId: input.blockId,
2893
+ error: require_dist.errMsg(err)
2894
+ }
2895
+ });
2896
+ return /* @__PURE__ */ new Map();
2897
+ }
2898
+ }
2899
+ async function resolveNow(input) {
2900
+ const roses = input.fields.filter(isPulseTimestamp);
2901
+ if (roses.length === 0) return /* @__PURE__ */ new Map();
2902
+ const listing = await readClaims(input);
2903
+ if (listing === null) return /* @__PURE__ */ new Map();
2904
+ const seeds = /* @__PURE__ */ new Map();
2905
+ for (const field of roses) {
2906
+ const decision = await decideField(input, field, listing);
2907
+ logDecision(input, field, decision);
2908
+ for (const [key, seed] of seedsFor(input.fields, field, decision)) seeds.set(key, seed);
2909
+ }
2910
+ return seeds;
2911
+ }
2912
+ function isPulseTimestamp(field) {
2913
+ return field.source.kind === "expression" && require_dist.expressionContainsBuiltinCall(field.source.expr, "rose");
2914
+ }
2915
+ async function readClaims(input) {
2916
+ try {
2917
+ const listing = await input.claims.listClaims({ ownerPrefix: input.blockId });
2918
+ if (listing.state === "loaded") return listing;
2919
+ input.logger.warn("pulse override seed held — the claims index has not loaded, so nothing is seeded and nothing is cleared", {
2920
+ tags: { deviceId: input.deviceId },
2921
+ meta: {
2922
+ blockId: input.blockId,
2923
+ reason: listing.reason
2924
+ }
2925
+ });
2926
+ return null;
2927
+ } catch (err) {
2928
+ input.logger.warn("pulse override seed held — the claims index could not be read, so nothing is seeded and nothing is cleared", {
2929
+ tags: { deviceId: input.deviceId },
2930
+ meta: {
2931
+ blockId: input.blockId,
2932
+ error: require_dist.errMsg(err)
2933
+ }
2934
+ });
2935
+ return null;
2936
+ }
2937
+ }
2938
+ async function decideField(input, field, listing) {
2939
+ if (listing.state !== "loaded") return {
2940
+ kind: "hold",
2941
+ reason: "claims-unread"
2942
+ };
2943
+ const claim = listing.claims.find((row) => row.owner === input.blockId && row.deviceId === input.deviceId && row.capName === field.cap);
2944
+ const slice = input.targetSeed.byCap.get(field.cap) ?? null;
2945
+ const counter = counterField(input.fields, field);
2946
+ const owned = ownedOf(claim, slice, field.path, counter?.path ?? null);
2947
+ const nativeStamp = stampOf(slice, field.path);
2948
+ const pairs = rosePairs(field);
2949
+ if (pairs.length === 0) return { kind: "keep" };
2950
+ const decisions = [];
2951
+ for (const pair of pairs) decisions.push(await decidePair(input, pair, owned, nativeStamp));
2952
+ return mergePulseDecisions(decisions);
2953
+ }
2954
+ async function decidePair(input, pair, owned, nativeStamp) {
2955
+ const resolution = await input.sources.resolve(pair.source, false);
2956
+ if (resolution.kind === "missing") return { kind: "keep" };
2957
+ if (resolution.kind !== "found") return decidePulseSeed({
2958
+ owned,
2959
+ nativeStamp,
2960
+ history: { kind: "unread" },
2961
+ level: { kind: "unread" }
2962
+ });
2963
+ return decidePulseSeed({
2964
+ owned,
2965
+ nativeStamp,
2966
+ history: await historyOf(input, resolution.deviceId, pair),
2967
+ level: await levelOf(input, resolution.deviceId, pair)
2968
+ });
2969
+ }
2970
+ async function historyOf(input, deviceId, pair) {
2971
+ if (!recordsField(input.lookupCap, pair.cap, pair.activeField)) return { kind: "absent" };
2972
+ const now = input.now();
2973
+ const fromMs = now - require_retention.DEFAULT_RETENTION.rawDays * require_retention.DAY_MS;
2974
+ try {
2975
+ const answer = await input.readPulseHistory(deviceId, pair.cap, pair.activeField, fromMs, now);
2976
+ if (answer.kind === "unread") return { kind: "unread" };
2977
+ return {
2978
+ kind: "series",
2979
+ beforeOn: answer.series.beforeOn,
2980
+ points: answer.series.points
2981
+ };
2982
+ } catch (err) {
2983
+ input.logger.warn("pulse override history read threw — treated as unread", {
2984
+ tags: { deviceId: input.deviceId },
2985
+ meta: {
2986
+ blockId: input.blockId,
2987
+ sourceDeviceId: deviceId,
2988
+ cap: pair.cap,
2989
+ field: pair.activeField,
2990
+ error: require_dist.errMsg(err)
2991
+ }
2992
+ });
2993
+ return { kind: "unread" };
2994
+ }
2995
+ }
2996
+ async function levelOf(input, deviceId, pair) {
2997
+ try {
2998
+ return levelFromSlice(await input.readCap(deviceId, pair.cap), pair);
2999
+ } catch (err) {
3000
+ input.logger.warn("pulse override source slice could not be read — treated as unread", {
3001
+ tags: { deviceId: input.deviceId },
3002
+ meta: {
3003
+ blockId: input.blockId,
3004
+ sourceDeviceId: deviceId,
3005
+ cap: pair.cap,
3006
+ error: require_dist.errMsg(err)
3007
+ }
3008
+ });
3009
+ return { kind: "unread" };
3010
+ }
3011
+ }
3012
+ function levelFromSlice(slice, pair) {
3013
+ if (slice === null) return {
3014
+ kind: "off",
3015
+ changedAt: null
3016
+ };
3017
+ const active = slice[pair.activeField];
3018
+ if (typeof active !== "boolean") return { kind: "unread" };
3019
+ const changed = slice[pair.changedAtField];
3020
+ const changedAt = typeof changed === "number" && Number.isFinite(changed) ? changed : null;
3021
+ return active ? {
3022
+ kind: "on",
3023
+ changedAt
3024
+ } : {
3025
+ kind: "off",
3026
+ changedAt
3027
+ };
3028
+ }
3029
+ function recordsField(lookup, cap, fieldPath) {
3030
+ const history = lookup(cap)?.history;
3031
+ if (history === void 0 || history === "none") return false;
3032
+ return Object.hasOwn(history.fields, fieldPath);
3033
+ }
3034
+ function ownedOf(claim, slice, timestampPath, counterPath) {
3035
+ if (claim === void 0 || slice === null || !claim.fields.includes(timestampPath)) return { kind: "none" };
3036
+ const at = slice[timestampPath];
3037
+ if (at === null) return { kind: "never" };
3038
+ if (typeof at !== "number" || !Number.isFinite(at)) return { kind: "none" };
3039
+ const rawCount = counterPath === null ? void 0 : slice[counterPath];
3040
+ return {
3041
+ kind: "stamp",
3042
+ at,
3043
+ count: typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount >= 0 ? rawCount : 0
3044
+ };
3045
+ }
3046
+ function stampOf(slice, path) {
3047
+ const at = slice?.[path];
3048
+ return typeof at === "number" && Number.isFinite(at) ? at : null;
3049
+ }
3050
+ function counterField(fields, timestamp) {
3051
+ return fields.find((field) => {
3052
+ if (field.cap !== timestamp.cap || field.path === timestamp.path) return false;
3053
+ return field.source.kind === "expression" && field.source.expr === `count(${timestamp.path})`;
3054
+ });
3055
+ }
3056
+ function rosePairs(field) {
3057
+ if (field.source.kind !== "expression") return [];
3058
+ const found = [];
3059
+ for (const match of field.source.expr.matchAll(new RegExp(ROSE_CALL, "g"))) {
3060
+ const pair = rosePair(field, match[1], match[2]);
3061
+ if (pair !== null) found.push(pair);
3062
+ }
3063
+ return found;
3064
+ }
3065
+ function rosePair(field, activeName, changedName) {
3066
+ if (field.source.kind !== "expression") return null;
3067
+ if (activeName === void 0 || changedName === void 0) return null;
3068
+ const active = field.source.bindings[activeName];
3069
+ const changed = field.source.bindings[changedName];
3070
+ if (active === void 0 || changed === void 0) return null;
3071
+ if (active.kind !== "from" || changed.kind !== "from") return null;
3072
+ if (active.cap !== changed.cap) return null;
3073
+ if (active.source.addonId !== changed.source.addonId) return null;
3074
+ if (active.source.stableId !== changed.source.stableId) return null;
3075
+ return {
3076
+ source: active.source,
3077
+ cap: active.cap,
3078
+ activeField: active.fieldPath,
3079
+ changedAtField: changed.fieldPath
3080
+ };
3081
+ }
3082
+ function seedsFor(fields, timestamp, decision) {
3083
+ const seeds = /* @__PURE__ */ new Map();
3084
+ if (decision.kind === "keep") return seeds;
3085
+ const counter = counterField(fields, timestamp);
3086
+ if (decision.kind === "hold") {
3087
+ seeds.set(fieldKey(timestamp), { kind: "hold" });
3088
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3089
+ kind: "value",
3090
+ value: 0
3091
+ });
3092
+ return seeds;
3093
+ }
3094
+ if (decision.kind === "none") {
3095
+ seeds.set(fieldKey(timestamp), {
3096
+ kind: "value",
3097
+ value: null
3098
+ });
3099
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3100
+ kind: "value",
3101
+ value: 0
3102
+ });
3103
+ return seeds;
3104
+ }
3105
+ seeds.set(fieldKey(timestamp), {
3106
+ kind: "value",
3107
+ value: decision.at
3108
+ });
3109
+ if (counter !== void 0) seeds.set(fieldKey(counter), {
3110
+ kind: "value",
3111
+ value: decision.count
3112
+ });
3113
+ return seeds;
3114
+ }
3115
+ /** Raw boolean points of the device-level series (`scope` empty). 1 is on. */
3116
+ function pulseHistoryFromQuery(series) {
3117
+ const row = series.find((item) => item.scope === "");
3118
+ if (row === void 0) return {
3119
+ beforeOn: null,
3120
+ points: []
3121
+ };
3122
+ return {
3123
+ beforeOn: levelBit(row.before?.value ?? null),
3124
+ points: row.points.flatMap((point) => {
3125
+ const on = levelBit(point.last);
3126
+ return on === null ? [] : [{
3127
+ t: point.t,
3128
+ on
3129
+ }];
3130
+ })
3131
+ };
3132
+ }
3133
+ function levelBit(value) {
3134
+ if (value === 1) return true;
3135
+ if (value === 0) return false;
3136
+ return null;
3137
+ }
3138
+ function logDecision(input, field, decision) {
3139
+ const meta = {
3140
+ blockId: input.blockId,
3141
+ cap: field.cap,
3142
+ field: field.path
3143
+ };
3144
+ if (decision.kind === "stamp") {
3145
+ input.logger.info("pulse override seeded lastPressedAt from its source", {
3146
+ tags: { deviceId: input.deviceId },
3147
+ meta: {
3148
+ ...meta,
3149
+ at: decision.at,
3150
+ via: decision.via,
3151
+ count: decision.count
3152
+ }
3153
+ });
3154
+ return;
3155
+ }
3156
+ if (decision.kind === "none") {
3157
+ input.logger.info("pulse override seeded no ring — the source proved none, so the native stamp is not used", {
3158
+ tags: { deviceId: input.deviceId },
3159
+ meta
3160
+ });
3161
+ return;
3162
+ }
3163
+ if (decision.kind === "hold") input.logger.warn("pulse override seed held — a source read failed, so nothing is seeded and nothing is cleared", {
3164
+ tags: { deviceId: input.deviceId },
3165
+ meta: {
3166
+ ...meta,
3167
+ reason: decision.reason
3168
+ }
3169
+ });
3170
+ }
3171
+ //#endregion
2679
3172
  //#region src/builtins/composer/composer.ts
2680
3173
  /**
2681
3174
  * The composer: every declarative composition on this hub, in one runner
@@ -2698,7 +3191,11 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2698
3191
  * filter in `composer.addon.ts` stays load-bearing. Its fields are CLAIMED on
2699
3192
  * the hub under the block id (`OwnedFieldsTarget`), and released through the
2700
3193
  * `ClaimGate` — two applies must agree, seeded at boot from the claims index —
2701
- * when the block is gone, disabled, or lost the feature. Its own replaced
3194
+ * when the block merely vanished from one snapshot. An operator delete
3195
+ * (`retiredBlockIds`) or an edit that drops a cap releases those claims on
3196
+ * THIS apply, after the old runtime has quiesced and before any replacement
3197
+ * claims; a release that does not land is logged with `tags: { deviceId }` and
3198
+ * returned on the apply. Its own replaced
2702
3199
  * fields are read in the NATIVE view (rule 6). Its target row gone on two
2703
3200
  * agreeing index reads fails the block by name and ungrafts what it added —
2704
3201
  * whether the loss was seen on `device.unregistered` or on a later plan; its
@@ -2846,12 +3343,16 @@ var Composer = class {
2846
3343
  };
2847
3344
  });
2848
3345
  await this.composed.reconcile(gate.integrationId, declarations, planned);
3346
+ const releasing = confirmedReleaseKeys(input.retiredBlockIds ?? [], planned, this.live, this.claimGate);
2849
3347
  const rebuilt = await this.rebuildRuntimes(planned, declarations, restart, rebuildErrors);
3348
+ await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
3349
+ const unreleasedClaims = await this.releaseConfirmed(releasing);
2850
3350
  await this.syncSources(rebuilt);
2851
3351
  await this.reconcileClaims();
2852
3352
  return {
2853
3353
  blocks: planned.map((p) => this.verdict(p.block.id)),
2854
- acted: true
3354
+ acted: true,
3355
+ unreleasedClaims
2855
3356
  };
2856
3357
  }
2857
3358
  plan(block, restart, claimsWait) {
@@ -2872,7 +3373,13 @@ var Composer = class {
2872
3373
  async rebuildRuntimes(planned, declarations, restart, rebuildErrors) {
2873
3374
  const reset = /* @__PURE__ */ new Set();
2874
3375
  const fresh = /* @__PURE__ */ new Set();
3376
+ const stopped = [];
2875
3377
  const next = /* @__PURE__ */ new Map();
3378
+ const halt = (runtime) => {
3379
+ if (runtime === null) return;
3380
+ runtime.stop();
3381
+ stopped.push(runtime);
3382
+ };
2876
3383
  for (const p of planned) {
2877
3384
  const previous = this.live.get(p.block.id);
2878
3385
  const lost = lostTarget(p, previous?.planned);
@@ -2899,6 +3406,7 @@ var Composer = class {
2899
3406
  continue;
2900
3407
  }
2901
3408
  let targetSeed;
3409
+ let pulseSeeds;
2902
3410
  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 {
2903
3411
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2904
3412
  reportSeedlessRestoredCaps({
@@ -2909,6 +3417,20 @@ var Composer = class {
2909
3417
  seed: targetSeed,
2910
3418
  lookupCap: this.deps.lookupCap
2911
3419
  });
3420
+ const readHistory = this.deps.readPulseHistory;
3421
+ if (readHistory !== void 0) pulseSeeds = await resolvePulseSeeds({
3422
+ blockId: p.block.id,
3423
+ deviceId: p.target.deviceId,
3424
+ fields: p.fields,
3425
+ targetSeed,
3426
+ lookupCap: this.deps.lookupCap,
3427
+ claims: this.deps.claims,
3428
+ sources: this.deps.tracker,
3429
+ readCap: this.deps.readExistingTargetCap,
3430
+ readPulseHistory: readHistory,
3431
+ now: this.deps.now,
3432
+ logger: this.deps.logger
3433
+ });
2912
3434
  } catch (err) {
2913
3435
  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`;
2914
3436
  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", {
@@ -2934,7 +3456,7 @@ var Composer = class {
2934
3456
  const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
2935
3457
  this.onClaimEvent(deviceId, cap, owner, event);
2936
3458
  });
2937
- const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed);
3459
+ const runtime = writer === null || p.fields.length === 0 ? null : newCompositionRuntime(this.deps, p, writer, targetSeed, pulseSeeds);
2938
3460
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
2939
3461
  deviceId: p.target.deviceId,
2940
3462
  ref: p.target.ref,
@@ -2943,7 +3465,7 @@ var Composer = class {
2943
3465
  enabled: p.block.enabled,
2944
3466
  settled: p.problems.length === 0
2945
3467
  });
2946
- previous?.runtime?.stop();
3468
+ halt(previous?.runtime ?? null);
2947
3469
  if (runtime !== null) fresh.add(runtime);
2948
3470
  next.set(p.block.id, {
2949
3471
  planned: p,
@@ -2958,12 +3480,15 @@ var Composer = class {
2958
3480
  for (const [blockId, entry] of this.live) {
2959
3481
  if (next.has(blockId)) continue;
2960
3482
  if (held.has(require_dist.composedDeviceStableId(blockId))) next.set(blockId, entry);
2961
- else entry.runtime?.stop();
3483
+ else halt(entry.runtime);
2962
3484
  }
2963
3485
  this.live = next;
2964
3486
  const liveRuntimes = new Set([...next.values()].map((e) => e.runtime));
2965
3487
  this.unsynced = new Set([...[...this.unsynced].filter((r) => liveRuntimes.has(r)), ...fresh]);
2966
- return { reset };
3488
+ return {
3489
+ reset,
3490
+ stopped
3491
+ };
2967
3492
  }
2968
3493
  ungraftGone(blockId, target, deviceId) {
2969
3494
  this.grafts.gone(deviceId);
@@ -3047,21 +3572,40 @@ var Composer = class {
3047
3572
  if (!this.claimSeed.done) return;
3048
3573
  for (const key of this.claimGate.filter(wantedClaims(this.live, this.claimGate))) await this.release(key);
3049
3574
  }
3575
+ /** Confirmed releases, awaited. A failure is the string the hub gave; the gate retries it later. */
3576
+ async releaseConfirmed(keys) {
3577
+ const unreleased = [];
3578
+ for (const key of keys) {
3579
+ const error = await this.release(key);
3580
+ if (error === null) continue;
3581
+ unreleased.push({
3582
+ deviceId: key.deviceId,
3583
+ capName: key.capName,
3584
+ owner: key.owner,
3585
+ error
3586
+ });
3587
+ }
3588
+ return unreleased;
3589
+ }
3590
+ /** Null when the hub confirmed the release (or held nothing of ours). */
3050
3591
  async release(key) {
3051
3592
  let outcome;
3052
3593
  try {
3053
3594
  outcome = await this.deps.claims.releaseClaim(key);
3054
3595
  } catch (err) {
3055
- this.claimGate.releaseFailed(key, require_dist.errMsg(err));
3056
- return;
3596
+ const error = require_dist.errMsg(err);
3597
+ this.claimGate.releaseFailed(key, error);
3598
+ return error;
3057
3599
  }
3058
3600
  if (!outcome.ok && !RELEASE_SETTLED_CODES.has(outcome.code)) {
3059
- this.claimGate.releaseFailed(key, `${outcome.code}: ${outcome.message}`);
3060
- return;
3601
+ const error = `${outcome.code}: ${outcome.message}`;
3602
+ this.claimGate.releaseFailed(key, error);
3603
+ return error;
3061
3604
  }
3062
3605
  this.claimGate.released(key, outcome.ok ? "released" : outcome.code);
3063
3606
  await this.grafts.released(key.deviceId, key.capName);
3064
3607
  await this.onClaimEvent(key.deviceId, key.capName, key.owner, "free");
3608
+ return null;
3065
3609
  }
3066
3610
  /**
3067
3611
  * Rule 5: a customization whose target row is gone — the tracker's two
@@ -4045,6 +4589,8 @@ var SourceTracker = class {
4045
4589
  */
4046
4590
  /** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
4047
4591
  var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
4592
+ /** The device-history query ceiling. A switch's raw tier is change-only, so this is the whole window. */
4593
+ var PULSE_HISTORY_MAX_POINTS = 5e3;
4048
4594
  /** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
4049
4595
  function parseNativeShadowChanged(data) {
4050
4596
  if (!isRecord$2(data)) return null;
@@ -4193,6 +4739,26 @@ var ComposerAddon = class extends require_dist.BaseAddon {
4193
4739
  deviceId,
4194
4740
  capName
4195
4741
  }) ?? null,
4742
+ readPulseHistory: async (deviceId, cap, fieldPath, fromMs, toMs) => {
4743
+ try {
4744
+ return {
4745
+ kind: "series",
4746
+ series: pulseHistoryFromQuery((await api.deviceHistory.query.query({
4747
+ deviceId,
4748
+ key: historySeriesKey(cap, fieldPath),
4749
+ from: fromMs,
4750
+ to: toMs,
4751
+ resolution: "raw",
4752
+ maxPoints: PULSE_HISTORY_MAX_POINTS
4753
+ })).series)
4754
+ };
4755
+ } catch (err) {
4756
+ return {
4757
+ kind: "unread",
4758
+ error: require_dist.errMsg(err)
4759
+ };
4760
+ }
4761
+ },
4196
4762
  describeTarget: readBoundCaps,
4197
4763
  grafts,
4198
4764
  now: () => Date.now(),