@camstack/system 1.1.52 → 1.1.53

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 (51) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  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/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/device-manager/device-linked-devices.d.ts +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.js +1595 -1585
  15. package/dist/builtins/device-manager/device-manager.addon.mjs +1595 -1585
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  18. package/dist/builtins/hub-forwarder/index.js +1 -1
  19. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  22. package/dist/builtins/local-network/local-network.addon.js +1 -1
  23. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  24. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  25. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  26. package/dist/builtins/platform-probe/index.js +1 -1
  27. package/dist/builtins/platform-probe/index.mjs +1 -1
  28. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  29. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  30. package/dist/builtins/snapshot/index.js +1 -1
  31. package/dist/builtins/snapshot/index.mjs +1 -1
  32. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  33. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  34. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +11 -3
  35. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +11 -3
  36. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  37. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/system-config/system-config.addon.js +1 -1
  39. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  40. package/dist/builtins/winston-logging/index.js +1 -1
  41. package/dist/builtins/winston-logging/index.mjs +1 -1
  42. package/dist/{dist-BduBhv6N.js → dist-BNro5CRu.js} +92 -16
  43. package/dist/{dist-B_PSvr-m.mjs → dist-D7_69kDt.mjs} +92 -16
  44. package/dist/index.js +22 -5
  45. package/dist/index.mjs +22 -6
  46. package/dist/kernel/index.d.ts +1 -1
  47. package/dist/kernel/moleculer/readiness-context.d.ts +12 -8
  48. package/dist/kernel/moleculer/readiness-service.d.ts +16 -1
  49. package/dist/{manifest-python-deps-CPAMH5gV.js → manifest-python-deps-DszE7bYn.js} +35 -9
  50. package/dist/{manifest-python-deps-CIyvuHAB.mjs → manifest-python-deps-EM2H-jjS.mjs} +35 -9
  51. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  var DOORBELL_TRIGGERS = [
9
9
  "contact-open",
@@ -1,4 +1,4 @@
1
- import { C as doorbellCapability, _t as createEvent, it as BaseAddon, kt as EventCategory, rt as errMsg, ut as DeviceType } from "../../dist-B_PSvr-m.mjs";
1
+ import { C as doorbellCapability, _t as createEvent, it as BaseAddon, kt as EventCategory, rt as errMsg, ut as DeviceType } from "../../dist-D7_69kDt.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  var DOORBELL_TRIGGERS = [
4
4
  "contact-open",
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { L as logDestinationCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
1
+ import { L as logDestinationCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,4 +1,4 @@
1
- import { _ as authProviderCapability, d as UserRecordSchema, it as BaseAddon, l as ScopedTokenSchema, r as ApiKeyRecordSchema, tt as userManagementCapability } from "../../dist-B_PSvr-m.mjs";
1
+ import { _ as authProviderCapability, d as UserRecordSchema, it as BaseAddon, l as ScopedTokenSchema, r as ApiKeyRecordSchema, tt as userManagementCapability } from "../../dist-D7_69kDt.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as crypto$1 from "node:crypto";
4
4
  import { randomUUID } from "node:crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-BduBhv6N.js");
3
+ const require_dist = require("../../dist-BNro5CRu.js");
4
4
  let node_os = require("node:os");
5
5
  node_os = require_chunk.__toESM(node_os);
6
6
  //#region src/builtins/local-network/local-network.addon.ts
@@ -1,4 +1,4 @@
1
- import { I as localNetworkCapability, it as BaseAddon, kt as EventCategory } from "../../dist-B_PSvr-m.mjs";
1
+ import { I as localNetworkCapability, it as BaseAddon, kt as EventCategory } from "../../dist-D7_69kDt.mjs";
2
2
  import * as os from "node:os";
3
3
  //#region src/builtins/local-network/local-network.addon.ts
4
4
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { _t as createEvent, it as BaseAddon, kt as EventCategory, z as metricsProviderCapability } from "../../dist-B_PSvr-m.mjs";
1
+ import { _t as createEvent, it as BaseAddon, kt as EventCategory, z as metricsProviderCapability } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { W as platformProbeCapability, it as BaseAddon, kt as EventCategory, q as scoreRuntimes, rt as errMsg, yt as emitReadiness } from "../../dist-B_PSvr-m.mjs";
1
+ import { W as platformProbeCapability, it as BaseAddon, kt as EventCategory, q as scoreRuntimes, rt as errMsg, yt as emitReadiness } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { it as BaseAddon, kt as EventCategory } from "../../dist-B_PSvr-m.mjs";
1
+ import { it as BaseAddon, kt as EventCategory } from "../../dist-D7_69kDt.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/builtins/snapshot/snapshot-coalescing.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { $ as streamQualityLabel, B as nodePin, X as snapshotCapability, ct as DeviceFeature, it as BaseAddon, rt as errMsg, ut as DeviceType } from "../../dist-B_PSvr-m.mjs";
1
+ import { $ as streamQualityLabel, B as nodePin, X as snapshotCapability, ct as DeviceFeature, it as BaseAddon, rt as errMsg, ut as DeviceType } from "../../dist-D7_69kDt.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  //#region src/builtins/snapshot/snapshot-coalescing.ts
4
4
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
1
+ import { O as filesystemBrowseCapability, Q as storageProviderCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import * as path$1 from "node:path";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let better_sqlite3 = require("better-sqlite3");
9
9
  better_sqlite3 = require_chunk.__toESM(better_sqlite3);
@@ -510,10 +510,18 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
510
510
  if (existingCols.length > 0) {
511
511
  const existingNames = new Set(existingCols.map((c) => c.name));
512
512
  const declaredNames = new Set(schema.columns.map((c) => c.name));
513
- const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
514
- if (existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data") || missingDeclared) try {
513
+ const missingCols = schema.columns.filter((c) => !existingNames.has(c.name));
514
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
515
+ const addableInPlace = (col) => !col.primaryKey && !col.unique && (!col.notNull || col.defaultValue !== void 0);
516
+ if (isLegacyKv || missingCols.some((c) => !addableInPlace(c))) try {
515
517
  this.getDb().exec(`DROP TABLE "${table}"`);
516
518
  } catch {}
519
+ else if (missingCols.length > 0) for (const col of missingCols) {
520
+ const parts = [`"${col.name}" ${col.type}`];
521
+ if (col.notNull) parts.push("NOT NULL");
522
+ if (col.defaultValue !== void 0) parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
523
+ this.getDb().exec(`ALTER TABLE "${table}" ADD COLUMN ${parts.join(" ")}`);
524
+ }
517
525
  }
518
526
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
519
527
  if (schema.indexes) for (const idx of schema.indexes) {
@@ -1,4 +1,4 @@
1
- import { Y as settingsStoreCapability, it as BaseAddon, mt as asJsonObject, rt as errMsg, s as RUNTIME_DEFAULTS, wt as parseJsonUnknown } from "../../dist-B_PSvr-m.mjs";
1
+ import { Y as settingsStoreCapability, it as BaseAddon, mt as asJsonObject, rt as errMsg, s as RUNTIME_DEFAULTS, wt as parseJsonUnknown } from "../../dist-D7_69kDt.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import Database from "better-sqlite3";
4
4
  //#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
@@ -504,10 +504,18 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
504
504
  if (existingCols.length > 0) {
505
505
  const existingNames = new Set(existingCols.map((c) => c.name));
506
506
  const declaredNames = new Set(schema.columns.map((c) => c.name));
507
- const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
508
- if (existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data") || missingDeclared) try {
507
+ const missingCols = schema.columns.filter((c) => !existingNames.has(c.name));
508
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
509
+ const addableInPlace = (col) => !col.primaryKey && !col.unique && (!col.notNull || col.defaultValue !== void 0);
510
+ if (isLegacyKv || missingCols.some((c) => !addableInPlace(c))) try {
509
511
  this.getDb().exec(`DROP TABLE "${table}"`);
510
512
  } catch {}
513
+ else if (missingCols.length > 0) for (const col of missingCols) {
514
+ const parts = [`"${col.name}" ${col.type}`];
515
+ if (col.notNull) parts.push("NOT NULL");
516
+ if (col.defaultValue !== void 0) parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
517
+ this.getDb().exec(`ALTER TABLE "${table}" ADD COLUMN ${parts.join(" ")}`);
518
+ }
511
519
  }
512
520
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
513
521
  if (schema.indexes) for (const idx of schema.indexes) {
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { Ct as parseJsonObject, Z as storageCapability, it as BaseAddon, u as StorageLocationTypeSchema } from "../../dist-B_PSvr-m.mjs";
1
+ import { Ct as parseJsonObject, Z as storageCapability, it as BaseAddon, u as StorageLocationTypeSchema } from "../../dist-D7_69kDt.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { buildStorageLocationRegistry } from "@camstack/system";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { it as BaseAddon, rt as errMsg, xt as hydrateSchema } from "../../dist-B_PSvr-m.mjs";
1
+ import { it as BaseAddon, rt as errMsg, xt as hydrateSchema } from "../../dist-D7_69kDt.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BduBhv6N.js");
6
+ const require_dist = require("../../dist-BNro5CRu.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { L as logDestinationCapability, it as BaseAddon } from "../../dist-B_PSvr-m.mjs";
1
+ import { L as logDestinationCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -1802,6 +1802,19 @@ function scopeKey(scope) {
1802
1802
  case "device": return `device:${scope.deviceId}`;
1803
1803
  }
1804
1804
  }
1805
+ /**
1806
+ * Producer transition order within ONE generation: `starting → ready →
1807
+ * down`. Used by `hydrate` to accept only forward moves for
1808
+ * same-generation snapshot records (a snapshot can be ahead of a lost
1809
+ * delta, never behind it, within one producer generation).
1810
+ */
1811
+ function stateRank(state) {
1812
+ switch (state) {
1813
+ case "starting": return 0;
1814
+ case "ready": return 1;
1815
+ case "down": return 2;
1816
+ }
1817
+ }
1805
1818
  function scopesEqual(a, b) {
1806
1819
  if (a.type !== b.type) return false;
1807
1820
  if (a.type === "global" || b.type === "global") return true;
@@ -1852,43 +1865,70 @@ var ReadinessRegistry = class {
1852
1865
  return Array.from(this.snapshot.values());
1853
1866
  }
1854
1867
  /**
1855
- * Hydrate the snapshot from an authoritative source. Entries already
1856
- * present locally are skipped live deltas (received via the event
1857
- * bus subscription) always take precedence over the snapshot. For
1858
- * each newly hydrated entry, a one-shot transition is dispatched to
1859
- * matching subscriptions so pending `awaitReady` callers unblock
1860
- * without having to wait for a fresh event.
1868
+ * Hydrate the snapshot from an authoritative source a RECONCILE, not
1869
+ * an add-only merge (D8: readiness events are telemetry and may be
1870
+ * lost; the on-reconnect/periodic snapshot re-pull is the repair path,
1871
+ * so it MUST be able to advance a stale record the 2026-07-17
1872
+ * "still awaiting platform-probe forever" wedge was this method
1873
+ * skipping every already-seen key).
1874
+ *
1875
+ * - **Unseen keys** are added (epoch 1 — mirrors the value the
1876
+ * consumer would have derived from the first observed `ready`).
1877
+ * - **Existing keys** are UPDATED when the authoritative record
1878
+ * carries a different generation (producer restart / synthetic-down
1879
+ * superseded — epoch bumps on a new-generation `ready`, mirroring
1880
+ * `ingest`), or a FORWARD state move within the same generation
1881
+ * (`starting → ready → down`; within one producer generation a
1882
+ * snapshot can only be ahead of a lost delta, never behind it).
1883
+ * - **Never a same-generation regress** — a snapshot pulled just
1884
+ * before a live delta landed must not un-ready a record (the next
1885
+ * reconcile round converges it anyway).
1886
+ * - **Never a record this process is authoritative for** — node-scoped
1887
+ * records for our own nodeId, and device/global records we emitted,
1888
+ * keep local truth (an authority's stale copy of OUR record must
1889
+ * not regress us).
1861
1890
  *
1862
- * Local `epoch` is reset to 1 per entry — consumer-side epoch is
1863
- * derived from observed generation transitions, so this mirrors the
1864
- * value that would have been assigned had the consumer observed the
1865
- * first `ready` event directly.
1891
+ * Every applied entry dispatches a one-shot transition to matching
1892
+ * subscriptions so pending `awaitReady` callers unblock without having
1893
+ * to wait for a fresh event.
1866
1894
  */
1867
1895
  hydrate(records) {
1868
1896
  const now = this.now();
1869
1897
  for (const record of records) {
1870
1898
  const key = readinessKey(record.capName, record.scope);
1871
- if (this.snapshot.has(key)) continue;
1899
+ const prev = this.snapshot.get(key) ?? null;
1900
+ let epoch;
1901
+ let durationInPrevState;
1902
+ if (prev !== null) {
1903
+ if (this.isLocallyAuthoritative(prev)) continue;
1904
+ const sameGeneration = prev.generation === record.generation;
1905
+ if (sameGeneration && stateRank(record.state) <= stateRank(prev.state)) continue;
1906
+ epoch = !sameGeneration && record.state === "ready" ? prev.epoch + 1 : prev.epoch;
1907
+ durationInPrevState = Math.max(0, now - prev.lastChange);
1908
+ } else {
1909
+ epoch = 1;
1910
+ durationInPrevState = 0;
1911
+ }
1872
1912
  const hydrated = {
1873
1913
  capName: record.capName,
1874
1914
  scope: record.scope,
1875
1915
  state: record.state,
1876
1916
  generation: record.generation,
1877
- epoch: 1,
1917
+ epoch,
1878
1918
  lastChange: now,
1879
1919
  sourceNodeId: record.sourceNodeId
1880
1920
  };
1881
1921
  this.snapshot.set(key, hydrated);
1882
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
1922
+ if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
1883
1923
  const transition = {
1884
1924
  capName: record.capName,
1885
1925
  scope: record.scope,
1886
1926
  state: record.state,
1887
- epoch: 1,
1927
+ epoch,
1888
1928
  generation: record.generation,
1889
1929
  sourceNodeId: "hydrated",
1890
1930
  ts: now,
1891
- durationInPrevState: 0
1931
+ durationInPrevState
1892
1932
  };
1893
1933
  for (const sub of this.subscriptions) {
1894
1934
  if (sub.capName !== record.capName) continue;
@@ -1901,6 +1941,17 @@ var ReadinessRegistry = class {
1901
1941
  }
1902
1942
  }
1903
1943
  }
1944
+ /**
1945
+ * True when THIS process is the origin authority for `record` — a
1946
+ * snapshot pulled from elsewhere must never overwrite it:
1947
+ * - node-scoped records whose `scope.nodeId` is our own node id
1948
+ * (this process — or a child bridged onto our bus — emits them);
1949
+ * - device/global-scoped records whose latest transition WE emitted.
1950
+ */
1951
+ isLocallyAuthoritative(record) {
1952
+ if (record.scope.type === "node") return record.scope.nodeId === this.sourceNodeId;
1953
+ return record.sourceNodeId === this.sourceNodeId;
1954
+ }
1904
1955
  /** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
1905
1956
  getAll() {
1906
1957
  return new Map(this.snapshot);
@@ -3204,6 +3255,24 @@ var RecordingRetentionSchema = zod.z.object({
3204
3255
  maxSizeGb: zod.z.number().min(0).optional()
3205
3256
  });
3206
3257
  /**
3258
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
3259
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
3260
+ * previews at. Five graduated steps; absent on a config = `standard` (the
3261
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
3262
+ *
3263
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
3264
+ * Each window's index sidecar carries its own tile dims, so a camera whose
3265
+ * preset changed over time renders every historical window at the dims it was
3266
+ * written with.
3267
+ */
3268
+ var ScrubThumbnailPresetSchema = zod.z.enum([
3269
+ "minimal",
3270
+ "low",
3271
+ "standard",
3272
+ "high",
3273
+ "max"
3274
+ ]);
3275
+ /**
3207
3276
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
3208
3277
  *
3209
3278
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -3240,7 +3309,14 @@ var RecordingConfigSchema = zod.z.object({
3240
3309
  * derived into bands once via `migrateConfigToBands`.
3241
3310
  */
3242
3311
  bands: zod.z.array(RecordingBandSchema).optional(),
3243
- retention: RecordingRetentionSchema.optional()
3312
+ retention: RecordingRetentionSchema.optional(),
3313
+ /**
3314
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
3315
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
3316
+ * windows only — existing sheets are immutable, and each window's index
3317
+ * carries its own tile dims so mixed-preset history renders correctly.
3318
+ */
3319
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
3244
3320
  });
3245
3321
  /**
3246
3322
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -1802,6 +1802,19 @@ function scopeKey(scope) {
1802
1802
  case "device": return `device:${scope.deviceId}`;
1803
1803
  }
1804
1804
  }
1805
+ /**
1806
+ * Producer transition order within ONE generation: `starting → ready →
1807
+ * down`. Used by `hydrate` to accept only forward moves for
1808
+ * same-generation snapshot records (a snapshot can be ahead of a lost
1809
+ * delta, never behind it, within one producer generation).
1810
+ */
1811
+ function stateRank(state) {
1812
+ switch (state) {
1813
+ case "starting": return 0;
1814
+ case "ready": return 1;
1815
+ case "down": return 2;
1816
+ }
1817
+ }
1805
1818
  function scopesEqual(a, b) {
1806
1819
  if (a.type !== b.type) return false;
1807
1820
  if (a.type === "global" || b.type === "global") return true;
@@ -1852,43 +1865,70 @@ var ReadinessRegistry = class {
1852
1865
  return Array.from(this.snapshot.values());
1853
1866
  }
1854
1867
  /**
1855
- * Hydrate the snapshot from an authoritative source. Entries already
1856
- * present locally are skipped live deltas (received via the event
1857
- * bus subscription) always take precedence over the snapshot. For
1858
- * each newly hydrated entry, a one-shot transition is dispatched to
1859
- * matching subscriptions so pending `awaitReady` callers unblock
1860
- * without having to wait for a fresh event.
1868
+ * Hydrate the snapshot from an authoritative source a RECONCILE, not
1869
+ * an add-only merge (D8: readiness events are telemetry and may be
1870
+ * lost; the on-reconnect/periodic snapshot re-pull is the repair path,
1871
+ * so it MUST be able to advance a stale record the 2026-07-17
1872
+ * "still awaiting platform-probe forever" wedge was this method
1873
+ * skipping every already-seen key).
1874
+ *
1875
+ * - **Unseen keys** are added (epoch 1 — mirrors the value the
1876
+ * consumer would have derived from the first observed `ready`).
1877
+ * - **Existing keys** are UPDATED when the authoritative record
1878
+ * carries a different generation (producer restart / synthetic-down
1879
+ * superseded — epoch bumps on a new-generation `ready`, mirroring
1880
+ * `ingest`), or a FORWARD state move within the same generation
1881
+ * (`starting → ready → down`; within one producer generation a
1882
+ * snapshot can only be ahead of a lost delta, never behind it).
1883
+ * - **Never a same-generation regress** — a snapshot pulled just
1884
+ * before a live delta landed must not un-ready a record (the next
1885
+ * reconcile round converges it anyway).
1886
+ * - **Never a record this process is authoritative for** — node-scoped
1887
+ * records for our own nodeId, and device/global records we emitted,
1888
+ * keep local truth (an authority's stale copy of OUR record must
1889
+ * not regress us).
1861
1890
  *
1862
- * Local `epoch` is reset to 1 per entry — consumer-side epoch is
1863
- * derived from observed generation transitions, so this mirrors the
1864
- * value that would have been assigned had the consumer observed the
1865
- * first `ready` event directly.
1891
+ * Every applied entry dispatches a one-shot transition to matching
1892
+ * subscriptions so pending `awaitReady` callers unblock without having
1893
+ * to wait for a fresh event.
1866
1894
  */
1867
1895
  hydrate(records) {
1868
1896
  const now = this.now();
1869
1897
  for (const record of records) {
1870
1898
  const key = readinessKey(record.capName, record.scope);
1871
- if (this.snapshot.has(key)) continue;
1899
+ const prev = this.snapshot.get(key) ?? null;
1900
+ let epoch;
1901
+ let durationInPrevState;
1902
+ if (prev !== null) {
1903
+ if (this.isLocallyAuthoritative(prev)) continue;
1904
+ const sameGeneration = prev.generation === record.generation;
1905
+ if (sameGeneration && stateRank(record.state) <= stateRank(prev.state)) continue;
1906
+ epoch = !sameGeneration && record.state === "ready" ? prev.epoch + 1 : prev.epoch;
1907
+ durationInPrevState = Math.max(0, now - prev.lastChange);
1908
+ } else {
1909
+ epoch = 1;
1910
+ durationInPrevState = 0;
1911
+ }
1872
1912
  const hydrated = {
1873
1913
  capName: record.capName,
1874
1914
  scope: record.scope,
1875
1915
  state: record.state,
1876
1916
  generation: record.generation,
1877
- epoch: 1,
1917
+ epoch,
1878
1918
  lastChange: now,
1879
1919
  sourceNodeId: record.sourceNodeId
1880
1920
  };
1881
1921
  this.snapshot.set(key, hydrated);
1882
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
1922
+ if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
1883
1923
  const transition = {
1884
1924
  capName: record.capName,
1885
1925
  scope: record.scope,
1886
1926
  state: record.state,
1887
- epoch: 1,
1927
+ epoch,
1888
1928
  generation: record.generation,
1889
1929
  sourceNodeId: "hydrated",
1890
1930
  ts: now,
1891
- durationInPrevState: 0
1931
+ durationInPrevState
1892
1932
  };
1893
1933
  for (const sub of this.subscriptions) {
1894
1934
  if (sub.capName !== record.capName) continue;
@@ -1901,6 +1941,17 @@ var ReadinessRegistry = class {
1901
1941
  }
1902
1942
  }
1903
1943
  }
1944
+ /**
1945
+ * True when THIS process is the origin authority for `record` — a
1946
+ * snapshot pulled from elsewhere must never overwrite it:
1947
+ * - node-scoped records whose `scope.nodeId` is our own node id
1948
+ * (this process — or a child bridged onto our bus — emits them);
1949
+ * - device/global-scoped records whose latest transition WE emitted.
1950
+ */
1951
+ isLocallyAuthoritative(record) {
1952
+ if (record.scope.type === "node") return record.scope.nodeId === this.sourceNodeId;
1953
+ return record.sourceNodeId === this.sourceNodeId;
1954
+ }
1904
1955
  /** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
1905
1956
  getAll() {
1906
1957
  return new Map(this.snapshot);
@@ -3204,6 +3255,24 @@ var RecordingRetentionSchema = z.object({
3204
3255
  maxSizeGb: z.number().min(0).optional()
3205
3256
  });
3206
3257
  /**
3258
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
3259
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
3260
+ * previews at. Five graduated steps; absent on a config = `standard` (the
3261
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
3262
+ *
3263
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
3264
+ * Each window's index sidecar carries its own tile dims, so a camera whose
3265
+ * preset changed over time renders every historical window at the dims it was
3266
+ * written with.
3267
+ */
3268
+ var ScrubThumbnailPresetSchema = z.enum([
3269
+ "minimal",
3270
+ "low",
3271
+ "standard",
3272
+ "high",
3273
+ "max"
3274
+ ]);
3275
+ /**
3207
3276
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
3208
3277
  *
3209
3278
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -3240,7 +3309,14 @@ var RecordingConfigSchema = z.object({
3240
3309
  * derived into bands once via `migrateConfigToBands`.
3241
3310
  */
3242
3311
  bands: z.array(RecordingBandSchema).optional(),
3243
- retention: RecordingRetentionSchema.optional()
3312
+ retention: RecordingRetentionSchema.optional(),
3313
+ /**
3314
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
3315
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
3316
+ * windows only — existing sheets are immutable, and each window's index
3317
+ * carries its own tile dims so mixed-preset history renders correctly.
3318
+ */
3319
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
3244
3320
  });
3245
3321
  /**
3246
3322
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of