@camstack/system 1.2.476 → 1.2.478

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 (94) hide show
  1. package/assets/display-palette/icon.svg +7 -0
  2. package/dist/{agent-https-vpf_D97z.mjs → agent-https-CXMsY05a.mjs} +1 -1
  3. package/dist/{agent-https-DkXBXEvz.js → agent-https-D6a1IRPK.js} +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer-runtime-factory.d.ts +2 -1
  19. package/dist/builtins/composer/composer.addon.js +453 -6
  20. package/dist/builtins/composer/composer.addon.mjs +453 -6
  21. package/dist/builtins/composer/composer.d.ts +6 -0
  22. package/dist/builtins/composer/composition-runtime.d.ts +3 -0
  23. package/dist/builtins/composer/field-record.d.ts +7 -0
  24. package/dist/builtins/composer/pulse-seed-resolve.d.ts +59 -0
  25. package/dist/builtins/composer/pulse-seed.d.ts +75 -0
  26. package/dist/builtins/console-logging/index.js +1 -1
  27. package/dist/builtins/console-logging/index.mjs +1 -1
  28. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  29. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  30. package/dist/builtins/device-history/device-history.addon.js +63 -953
  31. package/dist/builtins/device-history/device-history.addon.mjs +2 -892
  32. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  33. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  34. package/dist/builtins/display-palette/display-palette.addon.d.ts +12 -0
  35. package/dist/builtins/display-palette/display-palette.addon.js +85 -0
  36. package/dist/builtins/display-palette/display-palette.addon.mjs +79 -0
  37. package/dist/builtins/display-palette/index.d.ts +1 -0
  38. package/dist/builtins/display-palette/index.js +8 -0
  39. package/dist/builtins/display-palette/index.mjs +2 -0
  40. package/dist/builtins/hub-forwarder/index.js +1 -1
  41. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  42. package/dist/builtins/keychain/index.js +3 -3
  43. package/dist/builtins/keychain/index.mjs +3 -3
  44. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  45. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  46. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  47. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  48. package/dist/builtins/local-network/local-network.addon.js +2 -2
  49. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  50. package/dist/builtins/loki-logging/index.js +1 -1
  51. package/dist/builtins/loki-logging/index.mjs +1 -1
  52. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  53. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  54. package/dist/builtins/platform-probe/index.js +1 -1
  55. package/dist/builtins/platform-probe/index.mjs +1 -1
  56. package/dist/builtins/record-entries/index.js +1 -1
  57. package/dist/builtins/record-entries/index.mjs +1 -1
  58. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  59. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  60. package/dist/builtins/snapshot/index.js +1 -1
  61. package/dist/builtins/snapshot/index.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  63. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/index.js +1 -1
  65. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  70. package/dist/builtins/system-config/system-config.addon.js +3 -3
  71. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  72. package/dist/builtins/winston-logging/index.js +1 -1
  73. package/dist/builtins/winston-logging/index.mjs +1 -1
  74. package/dist/{composition-sources-B5nK7pHt.mjs → composition-sources-CQcYYZxL.mjs} +1 -1
  75. package/dist/{composition-sources-BNOP9mFp.js → composition-sources-yfxjp3qV.js} +1 -1
  76. package/dist/{constants-D4VigrMM.js → constants-D--JWd55.js} +1 -1
  77. package/dist/{constants-jbhoFV7w.mjs → constants-lWkcQxVE.mjs} +1 -1
  78. package/dist/{device-ca-DhyQeXsa.js → device-ca-D0KnLHMw.js} +1 -1
  79. package/dist/{device-ca-CvBZeyVX.mjs → device-ca-J9ZsyoFw.mjs} +1 -1
  80. package/dist/{dist-B_V9r61Z.mjs → dist-OC-L50JY.mjs} +751 -70
  81. package/dist/{dist-CNU7cdFf.js → dist-iVUkxljP.js} +792 -69
  82. package/dist/file-host-worker.js +1 -1
  83. package/dist/file-host-worker.mjs +1 -1
  84. package/dist/{hub-mdns-ciao-DFidf52M.js → hub-mdns-ciao-BDKiKV75.js} +2 -2
  85. package/dist/{hub-mdns-ciao-BuUg0gpI.mjs → hub-mdns-ciao-CvXWbJub.mjs} +2 -2
  86. package/dist/index.js +7 -7
  87. package/dist/index.mjs +7 -7
  88. package/dist/{restore-archive-CDo4Zlmx.mjs → restore-archive-D1Uzojhc.mjs} +1 -1
  89. package/dist/{restore-archive-BtCheeWu.js → restore-archive-DxkdJpWd.js} +1 -1
  90. package/dist/retention-B3z_lBP1.js +1024 -0
  91. package/dist/retention-DdVnmXXS.mjs +893 -0
  92. package/dist/{wal-maintenance-DItlr0Yu.mjs → wal-maintenance-DuO0D2H3.mjs} +0 -0
  93. package/dist/{wal-maintenance-B7xBeIFi.js → wal-maintenance-o9TyZeYp.js} +0 -0
  94. package/package.json +19 -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-CNU7cdFf.js");
7
- const require_composition_sources = require("../../composition-sources-BNOP9mFp.js");
6
+ const require_dist = require("../../dist-iVUkxljP.js");
7
+ const require_composition_sources = require("../../composition-sources-yfxjp3qV.js");
8
+ const require_retention = require("../../retention-B3z_lBP1.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. */
@@ -1201,9 +1202,33 @@ function createFieldRecord(plan, byCap, lookupCap) {
1201
1202
  hasWritten: false,
1202
1203
  written: void 0,
1203
1204
  expressionMemory: require_dist.createStatefulExpressionMemory(),
1204
- eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null
1205
+ eventHighWater: typeof initial === "number" && Number.isFinite(initial) ? initial : null,
1206
+ pulseHold: false
1205
1207
  };
1206
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
+ }
1207
1232
  function currentOutput(record) {
1208
1233
  return record.hasWritten ? record.written : record.initial;
1209
1234
  }
@@ -1257,6 +1282,7 @@ function namedField(r) {
1257
1282
  function writable(record) {
1258
1283
  const outcome = record.outcome;
1259
1284
  if (outcome === null) return void 0;
1285
+ if (record.pulseHold && !record.hasWritten && (outcome.state === "unavailable" || outcome.value === null)) return;
1260
1286
  if (outcome.state !== "unavailable") return outcome.value;
1261
1287
  switch (record.plan.unavailable) {
1262
1288
  case "null": return null;
@@ -1694,7 +1720,7 @@ var CompositionRuntime = class {
1694
1720
  constructor(deps) {
1695
1721
  this.deps = deps;
1696
1722
  this.startedAtMs = deps.now();
1697
- 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);
1698
1724
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1699
1725
  const eventContext = {
1700
1726
  deviceId: deps.target.id,
@@ -2541,7 +2567,7 @@ function writerFor(deps, p, device, onClaimEvent) {
2541
2567
  onClaim: (cap, event) => onClaimEvent(deviceId, cap, owner, event)
2542
2568
  });
2543
2569
  }
2544
- function newCompositionRuntime(deps, p, target, seed) {
2570
+ function newCompositionRuntime(deps, p, target, seed, pulseSeeds) {
2545
2571
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2546
2572
  const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
2547
2573
  const nativeCaps = new Set(self?.native ?? []);
@@ -2554,6 +2580,7 @@ function newCompositionRuntime(deps, p, target, seed) {
2554
2580
  target,
2555
2581
  lookupCap: deps.lookupCap,
2556
2582
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2583
+ ...pulseSeeds === void 0 ? {} : { pulseSeeds },
2557
2584
  read: (read) => deps.tracker.read(read, require_dist.compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2558
2585
  isOwnRead: (read) => require_dist.compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2559
2586
  now: deps.now,
@@ -2759,6 +2786,389 @@ function verdictOf(blockId, entry, unsynced, grafts = null) {
2759
2786
  };
2760
2787
  }
2761
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
2762
3172
  //#region src/builtins/composer/composer.ts
2763
3173
  /**
2764
3174
  * The composer: every declarative composition on this hub, in one runner
@@ -2996,6 +3406,7 @@ var Composer = class {
2996
3406
  continue;
2997
3407
  }
2998
3408
  let targetSeed;
3409
+ let pulseSeeds;
2999
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 {
3000
3411
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
3001
3412
  reportSeedlessRestoredCaps({
@@ -3006,6 +3417,20 @@ var Composer = class {
3006
3417
  seed: targetSeed,
3007
3418
  lookupCap: this.deps.lookupCap
3008
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
+ });
3009
3434
  } catch (err) {
3010
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`;
3011
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", {
@@ -3031,7 +3456,7 @@ var Composer = class {
3031
3456
  const writer = writerFor(this.deps, p, device, (deviceId, cap, owner, event) => {
3032
3457
  this.onClaimEvent(deviceId, cap, owner, event);
3033
3458
  });
3034
- 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);
3035
3460
  if (writer !== null && p.target.kind === "existing" && p.target.deviceId !== null) this.grafts.plan({
3036
3461
  deviceId: p.target.deviceId,
3037
3462
  ref: p.target.ref,
@@ -4164,6 +4589,8 @@ var SourceTracker = class {
4164
4589
  */
4165
4590
  /** Ignored by `DeclaredDevices` whenever `integrationId` is supplied, which is always here. */
4166
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;
4167
4594
  /** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
4168
4595
  function parseNativeShadowChanged(data) {
4169
4596
  if (!isRecord$2(data)) return null;
@@ -4312,6 +4739,26 @@ var ComposerAddon = class extends require_dist.BaseAddon {
4312
4739
  deviceId,
4313
4740
  capName
4314
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
+ },
4315
4762
  describeTarget: readBoundCaps,
4316
4763
  grafts,
4317
4764
  now: () => Date.now(),