@camstack/system 1.2.445 → 1.2.447

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 (90) hide show
  1. package/dist/{agent-https-CiOW0-sj.js → agent-https-5DYAOJMO.js} +2 -2
  2. package/dist/{agent-https-D4Q8iFNd.mjs → agent-https-BWUEf6ev.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 +86 -6
  8. package/dist/builtins/alerts/alerts.addon.mjs +86 -6
  9. package/dist/builtins/alerts/critical-push.d.ts +4 -0
  10. package/dist/builtins/alerts/emit-alert-policy.d.ts +28 -0
  11. package/dist/builtins/autotrack/index.js +1 -1
  12. package/dist/builtins/autotrack/index.mjs +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  15. package/dist/builtins/calendars/calendars.addon.js +1 -1
  16. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  17. package/dist/builtins/camera-grid/index.js +1 -1
  18. package/dist/builtins/camera-grid/index.mjs +1 -1
  19. package/dist/builtins/composer/composer.addon.js +2 -2
  20. package/dist/builtins/composer/composer.addon.mjs +2 -2
  21. package/dist/builtins/console-logging/index.js +1 -1
  22. package/dist/builtins/console-logging/index.mjs +1 -1
  23. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  24. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  25. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  26. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  27. package/dist/builtins/hub-forwarder/index.js +1 -1
  28. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  29. package/dist/builtins/keychain/index.js +4 -4
  30. package/dist/builtins/keychain/index.mjs +4 -4
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  32. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  33. package/dist/builtins/local-auth/local-auth.addon.js +3 -3
  34. package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
  35. package/dist/builtins/local-network/local-network.addon.js +2 -2
  36. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  37. package/dist/builtins/loki-logging/index.js +1 -1
  38. package/dist/builtins/loki-logging/index.mjs +1 -1
  39. package/dist/builtins/native-metrics/container-memory-ring.d.ts +18 -0
  40. package/dist/builtins/native-metrics/load-series-config.d.ts +38 -1
  41. package/dist/builtins/native-metrics/load-series-reader.d.ts +3 -3
  42. package/dist/builtins/native-metrics/load-series-types.d.ts +14 -0
  43. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +5 -7
  44. package/dist/builtins/native-metrics/native-metrics.addon.js +220 -974
  45. package/dist/builtins/native-metrics/native-metrics.addon.mjs +220 -974
  46. package/dist/builtins/native-metrics/node-load-ring.d.ts +3 -5
  47. package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +70 -0
  48. package/dist/builtins/platform-probe/index.js +1 -1
  49. package/dist/builtins/platform-probe/index.mjs +1 -1
  50. package/dist/builtins/record-entries/index.js +1 -1
  51. package/dist/builtins/record-entries/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/index.js +1 -1
  59. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  60. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  61. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  62. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  64. package/dist/builtins/system-config/system-config.addon.js +3 -3
  65. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  66. package/dist/builtins/winston-logging/index.js +1 -1
  67. package/dist/builtins/winston-logging/index.mjs +1 -1
  68. package/dist/{composition-sources-BMt5cFYb.mjs → composition-sources-BmC5jAUv.mjs} +1 -1
  69. package/dist/{composition-sources-D3fi9Xns.js → composition-sources-BwIIltnZ.js} +1 -1
  70. package/dist/{constants-BMHCxRzb.mjs → constants-6XHGmEUF.mjs} +1 -1
  71. package/dist/{constants-Br879Axi.js → constants-DA_tjzIk.js} +1 -1
  72. package/dist/{device-ca-T8osSMos.mjs → device-ca-Bh_ClxMO.mjs} +1 -1
  73. package/dist/{device-ca-CnJ_va7i.js → device-ca-CnbrgfmW.js} +1 -1
  74. package/dist/{dist-DgOG8flk.js → dist-BkNfEJKa.js} +292 -11
  75. package/dist/{dist-BNunRyXT.mjs → dist-CkZIrsAP.mjs} +292 -11
  76. package/dist/file-host-worker.js +1 -1
  77. package/dist/file-host-worker.mjs +1 -1
  78. package/dist/{hub-mdns-ciao-D16oRQ9K.mjs → hub-mdns-ciao-CoyIUWHC.mjs} +2 -2
  79. package/dist/{hub-mdns-ciao-BIkTHq-n.js → hub-mdns-ciao-m5LIr4oA.js} +2 -2
  80. package/dist/index.js +9 -8
  81. package/dist/index.mjs +9 -8
  82. package/dist/{phase6-declare-gate-tkKOEitw.mjs → phase6-declare-gate-BDWZuB2n.mjs} +1 -1
  83. package/dist/{phase6-declare-gate-Ci1-8uUn.js → phase6-declare-gate-Bl7KocEs.js} +1 -1
  84. package/dist/{restore-archive-DjJnCgap.js → restore-archive-B57xI5Ue.js} +1 -1
  85. package/dist/{restore-archive-DR9MtO7J.mjs → restore-archive-CMr4V7Fi.mjs} +1 -1
  86. package/dist/{wal-maintenance-CfvgtQ7C.mjs → wal-maintenance-Bqm4zrNA.mjs} +0 -0
  87. package/dist/{wal-maintenance-CODGqhKY.js → wal-maintenance-CuG1DUca.js} +0 -0
  88. package/package.json +1 -1
  89. package/dist/builtins/native-metrics/container-memory-series.d.ts +0 -260
  90. package/dist/builtins/native-metrics/load-series-store.d.ts +0 -293
@@ -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-DgOG8flk.js");
6
+ const require_dist = require("../../dist-BkNfEJKa.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -693,11 +693,9 @@ var NodeLoadRing = class {
693
693
  * rather than a replay of a timestamp this node has already delivered.
694
694
  *
695
695
  * The return value is not diagnostics. It is the idempotence gate the
696
- * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
697
- * appended to the table, which is what lets that table carry an `INTEGER`
698
- * rowid key instead of a composite unique index over two million rows. The
699
- * gate is one monotonic comparison in memory; the index it replaces was
700
- * measured at ~40 bytes per row.
696
+ * DURABLE tier rides on this return value (`node-metrics-history-recorder.ts`):
697
+ * only an accepted sample is sent to device-history. The gate is one monotonic
698
+ * comparison in memory.
701
699
  */
702
700
  record(nodeId, atMs, processes) {
703
701
  if (nodeId.length === 0) return false;
@@ -822,197 +820,7 @@ var NodeLoadRing = class {
822
820
  this.lastWriteSeq.delete(nodeId);
823
821
  }
824
822
  };
825
- //#endregion
826
- //#region src/builtins/native-metrics/container-memory-series.ts
827
- /**
828
- * @durable class=audit owner=metrics-native
829
- * write="one row per NODE per sample, appended by the HUB's `native-metrics`
830
- * off its subscription to `metrics.node-processes-snapshot` — the same
831
- * subscription, the same accepted-sample gate and the same timestamps as
832
- * `metrics:node-load-samples`, so a memory row and the process rows of the
833
- * same instant share an `atMs`. A node with no cgroup writes NOTHING; it
834
- * never writes a zeroed row."
835
- * retention="The operator's `loadSeriesRetentionHours` by age and
836
- * CONTAINER_MEMORY_MAX_ROWS by count, swept in the same pass as the process
837
- * table and through the same bounded page. Losing the table loses the cold
838
- * window only: the ring keeps serving the last hour."
839
- */
840
- var CONTAINER_MEMORY_COLLECTION = "metrics:node-memory-samples";
841
- /**
842
- * Fleet ceiling on the hot ring, in READINGS.
843
- *
844
- * One reading per node per sample, so this is simply the per-node ceiling times
845
- * the partition ceiling the process ring already declares (16). 2 880 readings
846
- * of ten numbers each is well under a megabyte — the bound exists so a runaway
847
- * node id space costs a fixed amount, not because the number is large.
848
- */
849
823
  var CONTAINER_MEMORY_MAX_TOTAL_ROWS = 2880;
850
- /**
851
- * Hard ceiling on the durable table, in ROWS. The guarantee behind the
852
- * retention intention, exactly as `DEFAULT_MAX_ROWS` is for the process table.
853
- *
854
- * | cadence | retention | rows (3 nodes) |
855
- * | --- | --- | --- |
856
- * | 10 s | 6 h (default) | 6 480 |
857
- * | 5 s | 6 h | 12 960 |
858
- * | 5 s | 72 h, 16 nodes | 829 440 — capped here |
859
- *
860
- * 50 000 rows is 7.7x the shipped default. This row is strictly narrower than
861
- * a `metrics:node-load-samples` row — fewer columns, one index, every value an
862
- * integer varint — and that one was MEASURED at 87 B/row, so 50 000 rows is
863
- * **under 4.2 MiB** with the real figure lower still. It is not a knob: a
864
- * guarantee that moves with the hand that raised the intention is not one.
865
- */
866
- var CONTAINER_MEMORY_MAX_ROWS = 5e4;
867
- var CONTAINER_MEMORY_COLUMNS = [
868
- {
869
- name: "id",
870
- type: "INTEGER",
871
- primaryKey: true,
872
- notNull: true
873
- },
874
- {
875
- name: "nodeId",
876
- type: "TEXT",
877
- notNull: true
878
- },
879
- (
880
- /** Shared with the process rows of the same tick — that is the whole point. */
881
- {
882
- name: "atMs",
883
- type: "INTEGER",
884
- notNull: true
885
- }),
886
- {
887
- name: "source",
888
- type: "TEXT",
889
- notNull: true
890
- },
891
- (
892
- /**
893
- * Whole MiB, like `rssMib` on the process row and for the same reason: a
894
- * byte-exact residency on a value in the tens of gigabytes is precision no
895
- * chart can draw, and it costs bytes on every row forever.
896
- */
897
- {
898
- name: "currentMib",
899
- type: "INTEGER",
900
- notNull: true
901
- }),
902
- (
903
- /** `NULL` = NO LIMIT. Never a sentinel. */
904
- {
905
- name: "limitMib",
906
- type: "INTEGER"
907
- }),
908
- (
909
- /** `NULL` = UNKNOWN on all four. Never zero. */
910
- {
911
- name: "anonMib",
912
- type: "INTEGER"
913
- }),
914
- {
915
- name: "fileMib",
916
- type: "INTEGER"
917
- },
918
- {
919
- name: "shmemMib",
920
- type: "INTEGER"
921
- },
922
- {
923
- name: "slabMib",
924
- type: "INTEGER"
925
- },
926
- (
927
- /** Host-wide i915 GEM bytes. `NULL` wherever debugfs is not mounted. */
928
- {
929
- name: "gpuShmemMib",
930
- type: "INTEGER"
931
- })
932
- ];
933
- var CONTAINER_MEMORY_INDEXES = [(
934
- /** The one index, and it serves both the read and the prune — see the
935
- * process table's `idx_load_samples_node_at` for the full reasoning. */
936
- {
937
- name: "idx_node_memory_node_at",
938
- columns: ["nodeId", "atMs"]
939
- })];
940
- var BYTES_PER_MIB$1 = 1048576;
941
- /** Bytes → whole MiB. `null` stays `null`: unknown is not zero. */
942
- function toMib(value) {
943
- return value === null ? null : Math.round(value / BYTES_PER_MIB$1);
944
- }
945
- /** Whole MiB → bytes. `null` stays `null`. */
946
- function fromMib(value) {
947
- return value === null ? null : value * BYTES_PER_MIB$1;
948
- }
949
- /** The ONE encode. */
950
- function snapshotToRow(nodeId, atMs, snapshot) {
951
- return {
952
- nodeId,
953
- atMs,
954
- source: snapshot.source,
955
- currentMib: Math.round(snapshot.currentBytes / BYTES_PER_MIB$1),
956
- limitMib: toMib(snapshot.limitBytes),
957
- anonMib: toMib(snapshot.anonBytes),
958
- fileMib: toMib(snapshot.fileBytes),
959
- shmemMib: toMib(snapshot.shmemBytes),
960
- slabMib: toMib(snapshot.slabBytes),
961
- gpuShmemMib: toMib(snapshot.gpuShmemBytes)
962
- };
963
- }
964
- /**
965
- * The ONE decode. A row whose `source` is not a hierarchy this build knows is
966
- * skipped rather than coerced — a stored string that no longer means anything
967
- * must not be charted as though it did.
968
- */
969
- function rowToPoint(row) {
970
- if (row.source !== "cgroup-v2" && row.source !== "cgroup-v1") return null;
971
- return {
972
- atMs: row.atMs,
973
- source: row.source,
974
- currentBytes: row.currentMib * BYTES_PER_MIB$1,
975
- limitBytes: fromMib(row.limitMib),
976
- anonBytes: fromMib(row.anonMib),
977
- fileBytes: fromMib(row.fileMib),
978
- shmemBytes: fromMib(row.shmemMib),
979
- slabBytes: fromMib(row.slabMib),
980
- gpuShmemBytes: fromMib(row.gpuShmemMib)
981
- };
982
- }
983
- /** Structural validation on read. A malformed row is skipped, never charted. */
984
- function recordToMemoryRow(data) {
985
- const nodeId = data["nodeId"];
986
- const source = data["source"];
987
- if (typeof nodeId !== "string" || typeof source !== "string") return null;
988
- const atMs = Number(data["atMs"]);
989
- const currentMib = Number(data["currentMib"]);
990
- if (!Number.isFinite(atMs) || !Number.isFinite(currentMib)) return null;
991
- const optional = (raw) => {
992
- if (raw === null || raw === void 0) return null;
993
- const n = Number(raw);
994
- return Number.isFinite(n) ? n : null;
995
- };
996
- return {
997
- nodeId,
998
- atMs,
999
- source,
1000
- currentMib,
1001
- limitMib: optional(data["limitMib"]),
1002
- anonMib: optional(data["anonMib"]),
1003
- fileMib: optional(data["fileMib"]),
1004
- shmemMib: optional(data["shmemMib"]),
1005
- slabMib: optional(data["slabMib"]),
1006
- gpuShmemMib: optional(data["gpuShmemMib"])
1007
- };
1008
- }
1009
- /**
1010
- * The HOT tier — a naming adapter over {@link NodeLoadRing}, not a second ring.
1011
- *
1012
- * One reading per sample, so the ring's "rows" and its samples are the same
1013
- * count and its fleet budget is a sample budget. Every bound, the idle sweep
1014
- * and the monotonic replay gate come from there unchanged.
1015
- */
1016
824
  var ContainerMemoryRing = class {
1017
825
  ring;
1018
826
  constructor(options = {}) {
@@ -1022,17 +830,9 @@ var ContainerMemoryRing = class {
1022
830
  ...options.now !== void 0 ? { now: options.now } : {}
1023
831
  });
1024
832
  }
1025
- /** Retain one reading. `false` = a timestamp this node already delivered. */
1026
833
  record(nodeId, atMs, snapshot) {
1027
834
  return this.ring.record(nodeId, atMs, [snapshot]);
1028
835
  }
1029
- /**
1030
- * Read one node's retained readings, oldest-first. `sinceMs` is EXCLUSIVE,
1031
- * matching every other reader in this subsystem.
1032
- *
1033
- * A node nobody has recorded answers EMPTY — which is what "this node has no
1034
- * cgroup" looks like, and it must stay distinguishable from a zero.
1035
- */
1036
836
  read(nodeId, sinceMs) {
1037
837
  const series = this.ring.read(nodeId, sinceMs);
1038
838
  const out = [];
@@ -1047,23 +847,6 @@ var ContainerMemoryRing = class {
1047
847
  return out;
1048
848
  }
1049
849
  };
1050
- /**
1051
- * Merge the two tiers and reduce, oldest-first.
1052
- *
1053
- * Dedupe is on `atMs` with the HOT copy winning, exactly as the process reader
1054
- * does: the ring holds the reading as it arrived, the table holds it after the
1055
- * MiB encode.
1056
- *
1057
- * Reduction keeps, for each bucket, the reading with the largest
1058
- * `currentBytes` — WHOLE, every field from that one sample. A per-field
1059
- * maximum would synthesise a row whose parts never coexisted (an `anon` peak
1060
- * and a `file` peak minutes apart, summed into a total that never happened),
1061
- * and a mean would smear away the spike this series exists to find. `atMs`
1062
- * becomes the bucket START so the points lie on the same x-axis as the folded
1063
- * process series, which is the only reason both travel in one answer.
1064
- *
1065
- * An empty bucket stays ABSENT — never zero, never carried forward.
1066
- */
1067
850
  function reduceContainerMemory(points, bucketMs, origin) {
1068
851
  if (bucketMs <= 0 || points.length === 0) return points;
1069
852
  const buckets = /* @__PURE__ */ new Map();
@@ -1077,727 +860,13 @@ function reduceContainerMemory(points, bucketMs, origin) {
1077
860
  }
1078
861
  return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
1079
862
  }
1080
- /** Merge cold + hot on `atMs`, hot wins. Exported because dedup is the
1081
- * property worth testing on its own. */
1082
863
  function mergeContainerMemory(cold, hot) {
1083
864
  const byAt = /* @__PURE__ */ new Map();
1084
865
  for (const point of cold) byAt.set(point.atMs, point);
1085
866
  for (const point of hot) byAt.set(point.atMs, point);
1086
867
  return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
1087
868
  }
1088
- /**
1089
- * The COLD tier. Append-only, bounded, and never the authority on anything the
1090
- * hot ring can answer.
1091
- *
1092
- * Every method is best-effort and says so in the log rather than throwing at
1093
- * the emitter: a storage hiccup must cost a gap in a chart, never a dropped
1094
- * snapshot on the bus.
1095
- */
1096
- var ContainerMemoryStore = class {
1097
- declared = false;
1098
- store;
1099
- logger;
1100
- nowFn;
1101
- prunePageRows;
1102
- constructor(deps) {
1103
- this.store = deps.store;
1104
- this.logger = deps.logger;
1105
- this.nowFn = deps.now ?? (() => Date.now());
1106
- this.prunePageRows = deps.prunePageRows ?? 2e4;
1107
- }
1108
- /** Idempotently declare the collection. `false` when the store refused. */
1109
- async declare() {
1110
- if (this.declared) return true;
1111
- try {
1112
- await this.store.declareCollection.mutate({
1113
- collection: CONTAINER_MEMORY_COLLECTION,
1114
- columns: [...CONTAINER_MEMORY_COLUMNS],
1115
- indexes: CONTAINER_MEMORY_INDEXES.map((i) => ({
1116
- name: i.name,
1117
- columns: [...i.columns]
1118
- }))
1119
- });
1120
- this.declared = true;
1121
- return true;
1122
- } catch (err) {
1123
- this.logger.warn("container memory declareCollection failed — nothing will be retained on disk", { meta: {
1124
- collection: CONTAINER_MEMORY_COLLECTION,
1125
- error: require_dist.errMsg(err)
1126
- } });
1127
- return false;
1128
- }
1129
- }
1130
- /** Append ONE reading. */
1131
- async append(nodeId, atMs, snapshot) {
1132
- if (!await this.declare()) return 0;
1133
- const record = { ...snapshotToRow(nodeId, atMs, snapshot) };
1134
- try {
1135
- const { inserted } = await this.store.insertMany.mutate({
1136
- collection: CONTAINER_MEMORY_COLLECTION,
1137
- records: [{ data: record }]
1138
- });
1139
- return inserted;
1140
- } catch (err) {
1141
- this.logger.warn("container memory reading not retained — this interval will be missing", { meta: {
1142
- nodeId,
1143
- atMs,
1144
- error: require_dist.errMsg(err)
1145
- } });
1146
- return 0;
1147
- }
1148
- }
1149
- /** Read one node's cold readings, oldest-first. `sinceMs` is EXCLUSIVE. */
1150
- async read(nodeId, sinceMs, limitRows) {
1151
- if (!await this.declare()) return [];
1152
- try {
1153
- const records = await this.store.query.query({
1154
- collection: CONTAINER_MEMORY_COLLECTION,
1155
- filter: {
1156
- where: { nodeId },
1157
- whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
1158
- orderBy: {
1159
- field: "atMs",
1160
- direction: "asc"
1161
- },
1162
- limit: limitRows
1163
- }
1164
- });
1165
- const points = [];
1166
- for (const record of records) {
1167
- const row = recordToMemoryRow(record.data);
1168
- if (row === null) continue;
1169
- const point = rowToPoint(row);
1170
- if (point !== null) points.push(point);
1171
- }
1172
- return points;
1173
- } catch (err) {
1174
- this.logger.warn("container memory cold read failed — answering from the hot window only", { meta: {
1175
- nodeId,
1176
- sinceMs,
1177
- error: require_dist.errMsg(err)
1178
- } });
1179
- return [];
1180
- }
1181
- }
1182
- /**
1183
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
1184
- *
1185
- * Not rate-limited here: it is driven by the process store's own prune
1186
- * cadence, so it runs at most once a minute by construction and a second
1187
- * timer would be a second thing to get wrong.
1188
- */
1189
- async prune(nodeIds, retention) {
1190
- if (!await this.declare()) return null;
1191
- const ageCutoff = this.nowFn() - retention.retentionHours * 36e5;
1192
- let deletedByAge = 0;
1193
- let rowsExamined = 0;
1194
- for (const nodeId of nodeIds) {
1195
- const outcome = await this.deleteOldestPage(nodeId, ageCutoff, this.prunePageRows);
1196
- deletedByAge += outcome.deleted;
1197
- rowsExamined += outcome.examined;
1198
- }
1199
- const total = await this.count();
1200
- const excess = total === null ? 0 : total - CONTAINER_MEMORY_MAX_ROWS;
1201
- let deletedByCap = 0;
1202
- if (excess > 0) {
1203
- let remaining = Math.min(excess, this.prunePageRows);
1204
- for (const nodeId of nodeIds) {
1205
- if (remaining <= 0) break;
1206
- const outcome = await this.deleteOldestPage(nodeId, null, remaining);
1207
- deletedByCap += outcome.deleted;
1208
- remaining -= outcome.deleted;
1209
- rowsExamined += outcome.examined;
1210
- }
1211
- this.logger.warn("container memory ROW CAP bit — evicting the oldest readings", { meta: {
1212
- collection: CONTAINER_MEMORY_COLLECTION,
1213
- rows: total,
1214
- cap: CONTAINER_MEMORY_MAX_ROWS,
1215
- over: excess,
1216
- deleted: deletedByCap,
1217
- hint: "lower the retention or the sampling cadence — the cap is the guarantee, not the intention"
1218
- } });
1219
- }
1220
- return {
1221
- deletedByAge,
1222
- deletedByCap,
1223
- rowsExamined,
1224
- capBit: excess > 0
1225
- };
1226
- }
1227
- /** Total rows, or `null` when the store could not answer. */
1228
- async count() {
1229
- if (!await this.declare()) return null;
1230
- try {
1231
- return await this.store.count.query({ collection: CONTAINER_MEMORY_COLLECTION });
1232
- } catch (err) {
1233
- this.logger.warn("container memory count failed — the row cap is not enforced this pass", { meta: { error: require_dist.errMsg(err) } });
1234
- return null;
1235
- }
1236
- }
1237
- /**
1238
- * Delete this node's oldest rows, at most one page's worth.
1239
- *
1240
- * `cutoff === null` means "the oldest rows, whatever their age" — the
1241
- * row-cap path, which is bounded by `limitRows` instead. Either way the page
1242
- * is READ FIRST, ordered by the one index, and its last `atMs` becomes the
1243
- * effective cutoff for a single bounded delete. An unbounded `deleteWhere`
1244
- * would be one statement and one stalling transaction the first time a
1245
- * retention is lowered.
1246
- */
1247
- async deleteOldestPage(nodeId, cutoff, limitRows) {
1248
- if (limitRows <= 0) return {
1249
- deleted: 0,
1250
- examined: 0
1251
- };
1252
- try {
1253
- const page = await this.store.query.query({
1254
- collection: CONTAINER_MEMORY_COLLECTION,
1255
- filter: {
1256
- where: { nodeId },
1257
- ...cutoff === null ? {} : { whereBetween: { atMs: [0, cutoff] } },
1258
- orderBy: {
1259
- field: "atMs",
1260
- direction: "asc"
1261
- },
1262
- limit: Math.min(limitRows, this.prunePageRows)
1263
- },
1264
- columns: ["atMs"]
1265
- });
1266
- if (page.length === 0) return {
1267
- deleted: 0,
1268
- examined: 0
1269
- };
1270
- const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
1271
- if (!Number.isFinite(effectiveCutoff)) return {
1272
- deleted: 0,
1273
- examined: page.length
1274
- };
1275
- const { deleted } = await this.store.deleteWhere.mutate({
1276
- collection: CONTAINER_MEMORY_COLLECTION,
1277
- filter: {
1278
- where: { nodeId },
1279
- whereBetween: { atMs: [0, effectiveCutoff] }
1280
- }
1281
- });
1282
- return {
1283
- deleted,
1284
- examined: page.length
1285
- };
1286
- } catch (err) {
1287
- this.logger.warn("container memory prune failed — the table keeps growing this pass", { meta: {
1288
- nodeId,
1289
- cutoff,
1290
- error: require_dist.errMsg(err)
1291
- } });
1292
- return {
1293
- deleted: 0,
1294
- examined: 0
1295
- };
1296
- }
1297
- }
1298
- };
1299
- //#endregion
1300
- //#region src/builtins/native-metrics/load-series-store.ts
1301
- /**
1302
- * @durable class=audit owner=metrics-native
1303
- * write="one row per process per sample, appended in ONE transaction per
1304
- * sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
1305
- * subscription to `metrics.node-processes-snapshot`. Emission is
1306
- * fixed-cadence, so the write rate is deterministic: nodes x processes /
1307
- * cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
1308
- * the in-memory ring ACCEPTED is written, which is what makes the append
1309
- * idempotent without a unique key."
1310
- * retention="TWO bounds, swept together at most once a minute. By AGE, the
1311
- * operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
1312
- * DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
1313
- * Both prune oldest-first through a bounded page — never a full scan — and
1314
- * the row cap logs when it bites. Losing the table loses the cold window
1315
- * only: the ring keeps serving the last hour and the charts keep drawing."
1316
- */
1317
- var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
1318
- /**
1319
- * Hard ceiling on the table, in ROWS.
1320
- *
1321
- * The guarantee behind the retention knob. Sized against the shipped default
1322
- * and against the worst configuration the knob can express:
1323
- *
1324
- * | cadence | retention | rows (3 nodes, 76 processes) |
1325
- * | --- | --- | --- |
1326
- * | 10 s | 6 h (default) | 164 160 |
1327
- * | 5 s | 6 h | 328 320 |
1328
- * | 5 s | 72 h | 3 939 840 — capped here |
1329
- *
1330
- * 500 000 rows x the measured 87 B = **41.5 MiB**, roughly 3x what the shipped
1331
- * default needs and far below the 2 M rows a three-day retention would reach.
1332
- * It is deliberately NOT a knob: the point of a guarantee is that it is not
1333
- * subject to the same hand that raised the intention. This is the discipline
1334
- * the in-memory ring already adopted (`DEFAULT_MAX_TOTAL_PROCESS_ROWS`) and
1335
- * the one the RTP pre-roll rings still lack — a maximum per instance and no
1336
- * budget at all for the whole, which reached 898 MB across 47 rings
1337
- * (`docs/architecture/load-ledger.md` #5).
1338
- */
1339
869
  var DEFAULT_MAX_ROWS = 5e5;
1340
- var LOAD_SERIES_COLUMNS = [
1341
- (
1342
- /** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
1343
- * so it costs no separate index and no stored string. */
1344
- {
1345
- name: "id",
1346
- type: "INTEGER",
1347
- primaryKey: true,
1348
- notNull: true
1349
- }),
1350
- {
1351
- name: "nodeId",
1352
- type: "TEXT",
1353
- notNull: true
1354
- },
1355
- (
1356
- /** The EMITTING node's timestamp for the sample this row belongs to. Every
1357
- * row of one sample shares it — that is what makes a sample reassemblable. */
1358
- {
1359
- name: "atMs",
1360
- type: "INTEGER",
1361
- notNull: true
1362
- }),
1363
- {
1364
- name: "pid",
1365
- type: "INTEGER",
1366
- notNull: true
1367
- },
1368
- (
1369
- /** `NULL` for a process no addon owns — `root` and `system` both. */
1370
- {
1371
- name: "addonId",
1372
- type: "TEXT"
1373
- }),
1374
- {
1375
- name: "classification",
1376
- type: "TEXT",
1377
- notNull: true
1378
- },
1379
- (
1380
- /** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
1381
- {
1382
- name: "cpuDeci",
1383
- type: "INTEGER",
1384
- notNull: true
1385
- }),
1386
- {
1387
- name: "rssMib",
1388
- type: "INTEGER",
1389
- notNull: true
1390
- },
1391
- (
1392
- /** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
1393
- {
1394
- name: "cpuMainDeci",
1395
- type: "INTEGER"
1396
- }),
1397
- (
1398
- /** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
1399
- {
1400
- name: "cpuGcDeci",
1401
- type: "INTEGER"
1402
- })
1403
- ];
1404
- var LOAD_SERIES_INDEXES = [(
1405
- /**
1406
- * The ONE index, and it serves both jobs.
1407
- *
1408
- * Reads are always "this node, newer than T" — `nodeId` leads so the
1409
- * equality is a range scan and `atMs` supplies the order without a sort.
1410
- * Prunes are always "this node, oldest first" — the same index, walked from
1411
- * the other end. A second index on `atMs` alone was measured and rejected:
1412
- * it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
1413
- * cross-node prune that a per-node loop over three nodes already serves.
1414
- */
1415
- {
1416
- name: "idx_load_samples_node_at",
1417
- columns: ["nodeId", "atMs"]
1418
- })];
1419
- var BYTES_PER_MIB = 1048576;
1420
- /** Tenths → percent. `null` stays `null`: unknown is not zero. */
1421
- function fromDeci(value) {
1422
- return value === null ? null : Math.round(value) / 10;
1423
- }
1424
- /** Percent → tenths, lossless because both producers already round to tenths. */
1425
- function toDeci(value) {
1426
- return value === null ? null : Math.round(value * 10);
1427
- }
1428
- /** Storage row → the shape a reader gets. The ONE decode. */
1429
- function rowToProcess(row) {
1430
- return {
1431
- pid: row.pid,
1432
- addonId: row.addonId,
1433
- classification: row.classification,
1434
- cpuPercent: fromDeci(row.cpuDeci) ?? 0,
1435
- memoryRssBytes: row.rssMib * BYTES_PER_MIB,
1436
- cpuMainPercent: fromDeci(row.cpuMainDeci),
1437
- cpuGcPercent: fromDeci(row.cpuGcDeci)
1438
- };
1439
- }
1440
- /** The shape a reader gets → the storage row. The ONE encode. */
1441
- function processToRow(nodeId, atMs, process) {
1442
- return {
1443
- nodeId,
1444
- atMs,
1445
- pid: process.pid,
1446
- addonId: process.addonId,
1447
- classification: process.classification,
1448
- cpuDeci: toDeci(process.cpuPercent) ?? 0,
1449
- rssMib: Math.round(process.memoryRssBytes / BYTES_PER_MIB),
1450
- cpuMainDeci: toDeci(process.cpuMainPercent),
1451
- cpuGcDeci: toDeci(process.cpuGcPercent)
1452
- };
1453
- }
1454
- /** Group flat rows into samples, oldest-first. A sample is one `atMs`. */
1455
- function rowsToSamples(rows) {
1456
- const byAt = /* @__PURE__ */ new Map();
1457
- for (const row of rows) {
1458
- const held = byAt.get(row.atMs);
1459
- if (held === void 0) byAt.set(row.atMs, [rowToProcess(row)]);
1460
- else held.push(rowToProcess(row));
1461
- }
1462
- return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, processes]) => ({
1463
- atMs,
1464
- processes
1465
- }));
1466
- }
1467
- /**
1468
- * The cold tier. Append-only, bounded, and never the authority on anything the
1469
- * hot ring can answer.
1470
- *
1471
- * Every method is best-effort and says so in the log rather than throwing at
1472
- * the emitter: a storage hiccup must cost a gap in a chart, never a dropped
1473
- * snapshot on the bus.
1474
- */
1475
- var LoadSeriesStore = class {
1476
- declared = false;
1477
- lastPruneAtMs = 0;
1478
- store;
1479
- logger;
1480
- nowFn;
1481
- prunePageRows;
1482
- pruneIntervalMs;
1483
- constructor(deps) {
1484
- this.store = deps.store;
1485
- this.logger = deps.logger;
1486
- this.nowFn = deps.now ?? (() => Date.now());
1487
- this.prunePageRows = deps.prunePageRows ?? 2e4;
1488
- this.pruneIntervalMs = deps.pruneIntervalMs ?? 6e4;
1489
- }
1490
- /** Idempotently declare the collection. `false` when the store refused. */
1491
- async declare() {
1492
- if (this.declared) return true;
1493
- try {
1494
- await this.store.declareCollection.mutate({
1495
- collection: LOAD_SERIES_COLLECTION,
1496
- columns: [...LOAD_SERIES_COLUMNS],
1497
- indexes: LOAD_SERIES_INDEXES.map((i) => ({
1498
- name: i.name,
1499
- columns: [...i.columns]
1500
- }))
1501
- });
1502
- this.declared = true;
1503
- return true;
1504
- } catch (err) {
1505
- this.logger.warn("load series declareCollection failed — nothing will be retained on disk", { meta: {
1506
- collection: LOAD_SERIES_COLLECTION,
1507
- error: require_dist.errMsg(err)
1508
- } });
1509
- return false;
1510
- }
1511
- }
1512
- /**
1513
- * Append ONE sample — every process row of it — in ONE transaction.
1514
- *
1515
- * Never a write per row. `insertMany` exists for exactly this: at the 10 s
1516
- * default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
1517
- * on the connection that also serves every cluster-wide configuration read
1518
- * is a constant load nobody asked for.
1519
- */
1520
- async append(nodeId, atMs, processes) {
1521
- if (processes.length === 0) return 0;
1522
- if (!await this.declare()) return 0;
1523
- const records = processes.map((p) => ({ data: { ...processToRow(nodeId, atMs, p) } }));
1524
- try {
1525
- const { inserted } = await this.store.insertMany.mutate({
1526
- collection: LOAD_SERIES_COLLECTION,
1527
- records
1528
- });
1529
- return inserted;
1530
- } catch (err) {
1531
- this.logger.warn("load series sample not retained — this interval will be missing", { meta: {
1532
- nodeId,
1533
- atMs,
1534
- rows: processes.length,
1535
- error: require_dist.errMsg(err)
1536
- } });
1537
- return 0;
1538
- }
1539
- }
1540
- /**
1541
- * Read one node's cold samples, oldest-first.
1542
- *
1543
- * `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
1544
- * timestamp it holds gets only what it is missing. `limitRows` bounds the
1545
- * read in ROWS (not samples) because rows are what the query costs.
1546
- */
1547
- async read(nodeId, sinceMs, limitRows) {
1548
- if (!await this.declare()) return [];
1549
- try {
1550
- const records = await this.store.query.query({
1551
- collection: LOAD_SERIES_COLLECTION,
1552
- filter: {
1553
- where: { nodeId },
1554
- whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
1555
- orderBy: {
1556
- field: "atMs",
1557
- direction: "asc"
1558
- },
1559
- limit: limitRows
1560
- }
1561
- });
1562
- const rows = [];
1563
- for (const record of records) {
1564
- const row = recordToRow(record.data);
1565
- if (row !== null) rows.push(row);
1566
- }
1567
- return rowsToSamples(rows);
1568
- } catch (err) {
1569
- this.logger.warn("load series cold read failed — answering from the hot window only", { meta: {
1570
- nodeId,
1571
- sinceMs,
1572
- error: require_dist.errMsg(err)
1573
- } });
1574
- return [];
1575
- }
1576
- }
1577
- /**
1578
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
1579
- *
1580
- * Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
1581
- * append path must not pay a sweep on every sample.
1582
- */
1583
- async prune(nodeIds, retention, force = false) {
1584
- const now = this.nowFn();
1585
- if (!force && now - this.lastPruneAtMs < this.pruneIntervalMs) return null;
1586
- this.lastPruneAtMs = now;
1587
- if (!await this.declare()) return null;
1588
- let deletedByAge = 0;
1589
- let deletedByCap = 0;
1590
- let rowsExamined = 0;
1591
- const ageCutoff = now - retention.retentionHours * 36e5;
1592
- for (const nodeId of nodeIds) {
1593
- const outcome = await this.pruneNodeToCutoff(nodeId, ageCutoff);
1594
- deletedByAge += outcome.deleted;
1595
- rowsExamined += outcome.examined;
1596
- }
1597
- const total = await this.count();
1598
- const excess = total === null ? 0 : total - retention.maxRows;
1599
- if (excess > 0) {
1600
- const outcome = await this.pruneOldestRows(nodeIds, excess);
1601
- deletedByCap = outcome.deleted;
1602
- rowsExamined += outcome.examined;
1603
- this.logger.warn("load series ROW CAP bit — evicting the oldest samples", { meta: {
1604
- collection: LOAD_SERIES_COLLECTION,
1605
- rows: total,
1606
- cap: retention.maxRows,
1607
- over: excess,
1608
- deleted: outcome.deleted,
1609
- retentionHours: retention.retentionHours,
1610
- hint: "lower the retention or the sampling cadence — the cap is the guarantee, not the intention"
1611
- } });
1612
- }
1613
- return {
1614
- deletedByAge,
1615
- deletedByCap,
1616
- rowsExamined,
1617
- capBit: excess > 0
1618
- };
1619
- }
1620
- /** Total rows, or `null` when the store could not answer. */
1621
- async count() {
1622
- if (!await this.declare()) return null;
1623
- try {
1624
- return await this.store.count.query({ collection: LOAD_SERIES_COLLECTION });
1625
- } catch (err) {
1626
- this.logger.warn("load series count failed — the row cap is not enforced this pass", { meta: { error: require_dist.errMsg(err) } });
1627
- return null;
1628
- }
1629
- }
1630
- /**
1631
- * Delete this node's rows older than `cutoff`, at most one page's worth.
1632
- *
1633
- * The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
1634
- * own is one statement but an UNBOUNDED one — a first pass after a retention
1635
- * change would delete millions of rows inside a single stalling transaction.
1636
- * So the page is read first (keys only, ordered by the index), its last
1637
- * `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
1638
- */
1639
- async pruneNodeToCutoff(nodeId, cutoff) {
1640
- try {
1641
- const page = await this.store.query.query({
1642
- collection: LOAD_SERIES_COLLECTION,
1643
- filter: {
1644
- where: { nodeId },
1645
- whereBetween: { atMs: [0, cutoff] },
1646
- orderBy: {
1647
- field: "atMs",
1648
- direction: "asc"
1649
- },
1650
- limit: this.prunePageRows
1651
- },
1652
- columns: ["atMs"]
1653
- });
1654
- if (page.length === 0) return {
1655
- deleted: 0,
1656
- examined: 0
1657
- };
1658
- const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
1659
- if (!Number.isFinite(effectiveCutoff)) return {
1660
- deleted: 0,
1661
- examined: page.length
1662
- };
1663
- const { deleted } = await this.store.deleteWhere.mutate({
1664
- collection: LOAD_SERIES_COLLECTION,
1665
- filter: {
1666
- where: { nodeId },
1667
- whereBetween: { atMs: [0, effectiveCutoff] }
1668
- }
1669
- });
1670
- return {
1671
- deleted,
1672
- examined: page.length
1673
- };
1674
- } catch (err) {
1675
- this.logger.warn("load series age prune failed — the table keeps growing this pass", { meta: {
1676
- nodeId,
1677
- cutoff,
1678
- error: require_dist.errMsg(err)
1679
- } });
1680
- return {
1681
- deleted: 0,
1682
- examined: 0
1683
- };
1684
- }
1685
- }
1686
- /**
1687
- * Drop the oldest rows across the known nodes until `excess` is covered.
1688
- *
1689
- * Same bounded-page technique, walked per node so the one index serves it.
1690
- * The node holding the oldest rows pays first, which is also the node
1691
- * producing the pressure when a runaway process count is the cause.
1692
- */
1693
- async pruneOldestRows(nodeIds, excess) {
1694
- let remaining = Math.min(excess, this.prunePageRows);
1695
- let deleted = 0;
1696
- let examined = 0;
1697
- for (const nodeId of nodeIds) {
1698
- if (remaining <= 0) break;
1699
- try {
1700
- const page = await this.store.query.query({
1701
- collection: LOAD_SERIES_COLLECTION,
1702
- filter: {
1703
- where: { nodeId },
1704
- orderBy: {
1705
- field: "atMs",
1706
- direction: "asc"
1707
- },
1708
- limit: remaining
1709
- },
1710
- columns: ["atMs"]
1711
- });
1712
- examined += page.length;
1713
- if (page.length === 0) continue;
1714
- const cutoff = Number(page.at(-1)?.data["atMs"]);
1715
- if (!Number.isFinite(cutoff)) continue;
1716
- const result = await this.store.deleteWhere.mutate({
1717
- collection: LOAD_SERIES_COLLECTION,
1718
- filter: {
1719
- where: { nodeId },
1720
- whereBetween: { atMs: [0, cutoff] }
1721
- }
1722
- });
1723
- deleted += result.deleted;
1724
- remaining -= result.deleted;
1725
- } catch (err) {
1726
- this.logger.warn("load series cap prune failed — the cap is not enforced this pass", { meta: {
1727
- nodeId,
1728
- error: require_dist.errMsg(err)
1729
- } });
1730
- }
1731
- }
1732
- return {
1733
- deleted,
1734
- examined
1735
- };
1736
- }
1737
- };
1738
- /** Structural validation on read. A malformed row is skipped, never charted. */
1739
- function recordToRow(data) {
1740
- const nodeId = data["nodeId"];
1741
- const classification = data["classification"];
1742
- if (typeof nodeId !== "string" || typeof classification !== "string") return null;
1743
- const atMs = Number(data["atMs"]);
1744
- const pid = Number(data["pid"]);
1745
- const cpuDeci = Number(data["cpuDeci"]);
1746
- const rssMib = Number(data["rssMib"]);
1747
- if (![
1748
- atMs,
1749
- pid,
1750
- cpuDeci,
1751
- rssMib
1752
- ].every((n) => Number.isFinite(n))) return null;
1753
- const rawAddon = data["addonId"];
1754
- const optional = (raw) => {
1755
- if (raw === null || raw === void 0) return null;
1756
- const n = Number(raw);
1757
- return Number.isFinite(n) ? n : null;
1758
- };
1759
- return {
1760
- nodeId,
1761
- atMs,
1762
- pid,
1763
- addonId: typeof rawAddon === "string" ? rawAddon : null,
1764
- classification,
1765
- cpuDeci,
1766
- rssMib,
1767
- cpuMainDeci: optional(data["cpuMainDeci"]),
1768
- cpuGcDeci: optional(data["cpuGcDeci"])
1769
- };
1770
- }
1771
- //#endregion
1772
- //#region src/builtins/native-metrics/load-series-config.ts
1773
- /**
1774
- * The two knobs of the durable load series, and the arithmetic that makes them
1775
- * honest.
1776
- *
1777
- * ## Where they live, and why there
1778
- *
1779
- * In `native-metrics`'s OWN global settings document — the one that already
1780
- * exists and already carries `samplingIntervalMs`. Fields on a document, never
1781
- * a method per knob: `system.getLoggingSettings` / `setLoggingSettings` set
1782
- * that precedent and it holds here.
1783
- *
1784
- * They are NOT in the logging document, and that is a decision rather than an
1785
- * omission. That document is about levels and diagnostic windows; a metrics
1786
- * cadence and a table's retention are neither. Two documents that both claim a
1787
- * knob is how this repo has already shipped a switch nobody read.
1788
- *
1789
- * They are cluster-wide, not per-node. Every node emits on the same cadence or
1790
- * the fleet's series cannot be laid over each other, and the table is single
1791
- * and hub-resident, so a per-node retention would be a promise nothing could
1792
- * keep.
1793
- *
1794
- * ## Refused, not clamped
1795
- *
1796
- * A cadence outside 5-60 s is REFUSED. Clamping would silently store 10 when
1797
- * the operator typed 2 and then report 10 back as though it had been accepted
1798
- * — the failure mode where a knob and its readback agree on a value nobody
1799
- * chose. The schema advertises the bounds; this refuses anything else.
1800
- */
1801
870
  /** Why a value was refused — the message an operator is shown verbatim. */
1802
871
  var LoadSeriesConfigError = class extends Error {
1803
872
  constructor(message) {
@@ -1840,7 +909,7 @@ function resolveLoadSeriesConfig(raw) {
1840
909
  * This is what makes the knob honest: an operator raising retention to three
1841
910
  * days sees the row count and the megabytes BEFORE applying it, rather than
1842
911
  * discovering them afterwards. `MEASURED_BYTES_PER_ROW` is measured, not
1843
- * guessed — see `load-series-store.ts`.
912
+ * guessed — see `node-metrics-history-recorder.ts`.
1844
913
  */
1845
914
  function projectLoadSeriesCost(input) {
1846
915
  const samplesPerNodeWindow = input.config.retentionHours * 3600 / input.config.cadenceSec;
@@ -1866,8 +935,8 @@ function describeLoadSeriesCost(projection) {
1866
935
  /**
1867
936
  * The ONE reader over both retention tiers.
1868
937
  *
1869
- * The hot window is the in-memory ring; the cold one is
1870
- * `metrics:node-load-samples`. A caller never learns which tier a point came
938
+ * The hot window is the in-memory ring; the cold one is device-history raw
939
+ * points. A caller never learns which tier a point came
1871
940
  * from, and that is a hard requirement rather than a nicety: a second reader
1872
941
  * is how two charts start disagreeing about the same node, and it is how "the
1873
942
  * ring says X, the table says Y" becomes a question nobody can answer.
@@ -1955,6 +1024,203 @@ function mergeLoadSeries(input) {
1955
1024
  };
1956
1025
  }
1957
1026
  //#endregion
1027
+ //#region src/builtins/native-metrics/node-metrics-history-recorder.ts
1028
+ var ADDON_ID = "metrics-native";
1029
+ var NODE_PROCESS_CPU_KEY = `addon:${ADDON_ID}/process-cpu-percent`;
1030
+ var NODE_PROCESS_RSS_KEY = `addon:${ADDON_ID}/process-rss-mib`;
1031
+ var NODE_CONTAINER_CURRENT_KEY = `addon:${ADDON_ID}/container-current-mib`;
1032
+ function nodeMetricsHistoryClient(api) {
1033
+ return {
1034
+ declareSeries: (input) => api.declareSeries.mutate(input),
1035
+ record: (input) => api.record.mutate({ points: [...input.points] }),
1036
+ queryNode: async (input) => {
1037
+ return { series: (await api.queryNode.query(input)).series.map((s) => ({
1038
+ scope: s.scope,
1039
+ points: s.points.map((p) => ({
1040
+ t: p.t,
1041
+ samples: p.samples,
1042
+ value: p.last ?? p.mean
1043
+ }))
1044
+ })) };
1045
+ }
1046
+ };
1047
+ }
1048
+ var NodeMetricsHistoryRecorder = class {
1049
+ deps;
1050
+ declared = false;
1051
+ constructor(deps) {
1052
+ this.deps = deps;
1053
+ }
1054
+ async declare() {
1055
+ if (this.declared) return;
1056
+ const { deviceHistory } = this.deps;
1057
+ await deviceHistory.declareSeries({
1058
+ name: "process-cpu-percent",
1059
+ subjectKind: "node",
1060
+ kind: "gauge",
1061
+ displayName: "Process CPU %",
1062
+ ingest: "points",
1063
+ unit: "%"
1064
+ });
1065
+ await deviceHistory.declareSeries({
1066
+ name: "process-rss-mib",
1067
+ subjectKind: "node",
1068
+ kind: "gauge",
1069
+ displayName: "Process RSS (MiB)",
1070
+ ingest: "points",
1071
+ unit: "MiB"
1072
+ });
1073
+ await deviceHistory.declareSeries({
1074
+ name: "container-current-mib",
1075
+ subjectKind: "node",
1076
+ kind: "gauge",
1077
+ displayName: "Container memory current (MiB)",
1078
+ ingest: "points",
1079
+ unit: "MiB"
1080
+ });
1081
+ this.declared = true;
1082
+ }
1083
+ async recordSample(nodeId, atMs, processes, containerMemory) {
1084
+ if (!await this.ensureDeclared()) return;
1085
+ const points = [];
1086
+ for (const p of processes) {
1087
+ const scope = String(p.pid);
1088
+ points.push({
1089
+ key: NODE_PROCESS_CPU_KEY,
1090
+ subject: {
1091
+ kind: "node",
1092
+ nodeId
1093
+ },
1094
+ scope,
1095
+ ts: atMs,
1096
+ value: p.cpuPercent
1097
+ });
1098
+ points.push({
1099
+ key: NODE_PROCESS_RSS_KEY,
1100
+ subject: {
1101
+ kind: "node",
1102
+ nodeId
1103
+ },
1104
+ scope,
1105
+ ts: atMs,
1106
+ value: Math.round(p.memoryRssBytes / 1048576)
1107
+ });
1108
+ }
1109
+ if (containerMemory !== null) points.push({
1110
+ key: NODE_CONTAINER_CURRENT_KEY,
1111
+ subject: {
1112
+ kind: "node",
1113
+ nodeId
1114
+ },
1115
+ scope: "",
1116
+ ts: atMs,
1117
+ value: Math.round(containerMemory.currentBytes / 1048576)
1118
+ });
1119
+ if (points.length === 0) return;
1120
+ try {
1121
+ await this.deps.deviceHistory.record({ points });
1122
+ } catch (err) {
1123
+ this.deps.logger.warn("node metrics history sample not stored", { meta: {
1124
+ nodeId,
1125
+ atMs,
1126
+ error: err instanceof Error ? err.message : String(err)
1127
+ } });
1128
+ }
1129
+ }
1130
+ async readContainerPoints(nodeId, sinceMs, maxPoints) {
1131
+ if (!await this.ensureDeclared()) return [];
1132
+ const result = await this.deps.deviceHistory.queryNode({
1133
+ nodeId,
1134
+ key: NODE_CONTAINER_CURRENT_KEY,
1135
+ from: sinceMs + 1,
1136
+ to: Date.now(),
1137
+ resolution: "raw",
1138
+ maxPoints
1139
+ });
1140
+ const out = [];
1141
+ for (const s of result.series) for (const p of s.points) {
1142
+ if (p.value === null || !Number.isFinite(p.value)) continue;
1143
+ out.push({
1144
+ atMs: p.t,
1145
+ source: "cgroup-v2",
1146
+ currentBytes: p.value * 1048576,
1147
+ limitBytes: null,
1148
+ anonBytes: null,
1149
+ fileBytes: null,
1150
+ shmemBytes: null,
1151
+ slabBytes: null,
1152
+ gpuShmemBytes: null
1153
+ });
1154
+ }
1155
+ return out.toSorted((a, b) => a.atMs - b.atMs);
1156
+ }
1157
+ async readProcessSamples(nodeId, sinceMs, maxPoints) {
1158
+ if (!await this.ensureDeclared()) return [];
1159
+ const to = Date.now();
1160
+ const [cpuResult, rssResult] = await Promise.all([this.deps.deviceHistory.queryNode({
1161
+ nodeId,
1162
+ key: NODE_PROCESS_CPU_KEY,
1163
+ from: sinceMs + 1,
1164
+ to,
1165
+ resolution: "raw",
1166
+ maxPoints
1167
+ }), this.deps.deviceHistory.queryNode({
1168
+ nodeId,
1169
+ key: NODE_PROCESS_RSS_KEY,
1170
+ from: sinceMs + 1,
1171
+ to,
1172
+ resolution: "raw",
1173
+ maxPoints
1174
+ })]);
1175
+ const byAt = /* @__PURE__ */ new Map();
1176
+ const ingest = (series, field) => {
1177
+ for (const s of series) {
1178
+ const pid = Number(s.scope);
1179
+ if (!Number.isFinite(pid)) continue;
1180
+ for (const p of s.points) {
1181
+ if (p.value === null || !Number.isFinite(p.value)) continue;
1182
+ let perPid = byAt.get(p.t);
1183
+ if (perPid === void 0) {
1184
+ perPid = /* @__PURE__ */ new Map();
1185
+ byAt.set(p.t, perPid);
1186
+ }
1187
+ const base = perPid.get(pid) ?? {
1188
+ pid,
1189
+ addonId: null,
1190
+ classification: "managed",
1191
+ cpuPercent: 0,
1192
+ memoryRssBytes: 0,
1193
+ cpuMainPercent: null,
1194
+ cpuGcPercent: null
1195
+ };
1196
+ perPid.set(pid, field === "cpuPercent" ? {
1197
+ ...base,
1198
+ cpuPercent: p.value
1199
+ } : {
1200
+ ...base,
1201
+ memoryRssBytes: p.value * 1048576
1202
+ });
1203
+ }
1204
+ }
1205
+ };
1206
+ ingest(cpuResult.series, "cpuPercent");
1207
+ ingest(rssResult.series, "memoryRssBytes");
1208
+ return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, perPid]) => ({
1209
+ atMs,
1210
+ processes: [...perPid.values()]
1211
+ }));
1212
+ }
1213
+ async ensureDeclared() {
1214
+ try {
1215
+ await this.declare();
1216
+ return true;
1217
+ } catch (err) {
1218
+ this.deps.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1219
+ return false;
1220
+ }
1221
+ }
1222
+ };
1223
+ //#endregion
1958
1224
  //#region src/builtins/native-metrics/gpu-probe.ts
1959
1225
  /**
1960
1226
  * The Linux GPU probe, forking only when there is something to fork.
@@ -3003,7 +2269,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3003
2269
  * the `settings-store` singleton would ship its rows over Moleculer, once
3004
2270
  * per sample, to reach that very same table.
3005
2271
  */
3006
- loadStore = null;
2272
+ metricsHistory = null;
3007
2273
  /**
3008
2274
  * The container's OWN memory, sampled on the process-snapshot tick.
3009
2275
  *
@@ -3015,10 +2281,8 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3015
2281
  */
3016
2282
  memoryProbe = null;
3017
2283
  pidsProbe = null;
3018
- /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
2284
+ /** Hot tier for the container reading. Bounds in `container-memory-ring.ts`. */
3019
2285
  memoryRing = new ContainerMemoryRing();
3020
- /** Cold tier, HUB only — same reasoning as `loadStore`. */
3021
- memoryStore = null;
3022
2286
  /** The resolved knobs. Re-resolved on every settings write. */
3023
2287
  loadConfig = {
3024
2288
  cadenceSec: 10,
@@ -3091,13 +2355,12 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3091
2355
  };
3092
2356
  this.applyLoadSeriesConfig();
3093
2357
  if (this.isHub) {
3094
- this.loadStore = new LoadSeriesStore({
3095
- store: this.ctx.api.settingsStore,
3096
- logger: this.ctx.logger.child("LoadSeries")
2358
+ this.metricsHistory = new NodeMetricsHistoryRecorder({
2359
+ deviceHistory: nodeMetricsHistoryClient(this.ctx.api.deviceHistory),
2360
+ logger: this.ctx.logger.child("NodeMetricsHistory")
3097
2361
  });
3098
- this.memoryStore = new ContainerMemoryStore({
3099
- store: this.ctx.api.settingsStore,
3100
- logger: this.ctx.logger.child("ContainerMemory")
2362
+ this.metricsHistory.declare().catch((err) => {
2363
+ this.ctx.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
3101
2364
  });
3102
2365
  }
3103
2366
  this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
@@ -3118,7 +2381,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3118
2381
  * this node has already delivered, and only an accepted sample reaches the
3119
2382
  * table. That is what makes the append idempotent without a unique index
3120
2383
  * over two million rows — the composite key it replaces was measured at ~40
3121
- * bytes per row (`load-series-store.ts`).
2384
+ * bytes per row (measured on the old SQLite tier; see D850).
3122
2385
  *
3123
2386
  * The durable append is fire-and-forget: a storage stall must cost a gap in
3124
2387
  * the cold window, never a blocked event-bus handler. Every failure logs.
@@ -3128,26 +2391,9 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3128
2391
  if (!this.loadRing.record(nodeId, atMs, retained)) return;
3129
2392
  this.observedRowsByNode.set(nodeId, retained.length);
3130
2393
  if (containerMemory !== null) this.memoryRing.record(nodeId, atMs, containerMemory);
3131
- const store = this.loadStore;
3132
- if (store === null) return;
3133
- store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).then(async (outcome) => {
3134
- if (outcome === null) return;
3135
- await this.memoryStore?.prune([...this.observedRowsByNode.keys()], this.loadConfig);
3136
- }).catch((err) => {
3137
- this.ctx.logger.warn("durable load series write failed", { meta: {
3138
- nodeId,
3139
- atMs,
3140
- error: err instanceof Error ? err.message : String(err)
3141
- } });
3142
- });
3143
- if (containerMemory === null) return;
3144
- this.memoryStore?.append(nodeId, atMs, containerMemory).catch((err) => {
3145
- this.ctx.logger.warn("durable container memory write failed", { meta: {
3146
- nodeId,
3147
- atMs,
3148
- error: err instanceof Error ? err.message : String(err)
3149
- } });
3150
- });
2394
+ const history = this.metricsHistory;
2395
+ if (history === null) return;
2396
+ history.recordSample(nodeId, atMs, retained, containerMemory);
3151
2397
  }
3152
2398
  /**
3153
2399
  * The ONE reader, over both tiers.
@@ -3166,9 +2412,9 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3166
2412
  atMs: sample.atMs,
3167
2413
  processes: sample.processes
3168
2414
  }));
3169
- const store = this.loadStore;
2415
+ const history = this.metricsHistory;
3170
2416
  const merged = mergeLoadSeries({
3171
- cold: store === null ? [] : await store.read(params.forNodeId, sinceMs, this.loadConfig.maxRows),
2417
+ cold: history === null ? [] : await history.readProcessSamples(params.forNodeId, sinceMs, this.loadConfig.maxRows),
3172
2418
  hot: hotSamples,
3173
2419
  cadenceMs,
3174
2420
  ...params.maxPoints !== void 0 ? { maxPoints: params.maxPoints } : {}
@@ -3188,7 +2434,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3188
2434
  retainedSamples: merged.retainedSamples,
3189
2435
  oldestAtMs: merged.oldestAtMs,
3190
2436
  cadenceMs,
3191
- durable: store !== null
2437
+ durable: history !== null
3192
2438
  };
3193
2439
  }
3194
2440
  /**
@@ -3204,8 +2450,8 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3204
2450
  */
3205
2451
  async readContainerMemory(params) {
3206
2452
  const hot = this.memoryRing.read(params.forNodeId, params.exclusiveSinceMs);
3207
- const store = this.memoryStore;
3208
- const merged = mergeContainerMemory(store === null ? [] : await store.read(params.forNodeId, params.sinceMs, CONTAINER_MEMORY_MAX_ROWS), hot);
2453
+ const history = this.metricsHistory;
2454
+ const merged = mergeContainerMemory(history === null ? [] : await history.readContainerPoints(params.forNodeId, params.sinceMs, 5e3), hot);
3209
2455
  if (params.bucketMs <= params.cadenceMs) return merged;
3210
2456
  return reduceContainerMemory(merged, params.bucketMs, params.origin ?? 0);
3211
2457
  }