@camstack/system 1.2.455 → 1.2.457

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 (133) hide show
  1. package/dist/addon-runner.js +96 -10
  2. package/dist/addon-runner.mjs +96 -10
  3. package/dist/{agent-https-CLWrOO3o.mjs → agent-https-CK9Z4sSR.mjs} +32 -5
  4. package/dist/{agent-https-CCvlRQKV.js → agent-https-CoyOZ3pC.js} +37 -4
  5. package/dist/auth/auth-manager.d.ts +9 -0
  6. package/dist/auth/legacy-token-tables.d.ts +16 -0
  7. package/dist/auth/user-management-internal.d.ts +9 -28
  8. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  11. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  12. package/dist/builtins/alerts/alerts.addon.js +2 -2
  13. package/dist/builtins/alerts/alerts.addon.mjs +2 -2
  14. package/dist/builtins/autotrack/index.js +1 -1
  15. package/dist/builtins/autotrack/index.mjs +1 -1
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  17. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  18. package/dist/builtins/calendars/calendars.addon.js +1 -1
  19. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  20. package/dist/builtins/camera-grid/index.js +1 -1
  21. package/dist/builtins/camera-grid/index.mjs +1 -1
  22. package/dist/builtins/composer/composer.addon.js +2 -2
  23. package/dist/builtins/composer/composer.addon.mjs +2 -2
  24. package/dist/builtins/console-logging/console-destination.d.ts +1 -2
  25. package/dist/builtins/console-logging/index.js +1 -4
  26. package/dist/builtins/console-logging/index.mjs +1 -4
  27. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  28. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  29. package/dist/builtins/device-history/capture/capture-engine.d.ts +6 -0
  30. package/dist/builtins/device-history/device-history.addon.js +441 -83
  31. package/dist/builtins/device-history/device-history.addon.mjs +441 -83
  32. package/dist/builtins/device-history/query/history-query.d.ts +35 -9
  33. package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
  34. package/dist/builtins/device-history/retention/retention.d.ts +1 -1
  35. package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
  36. package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
  37. package/dist/builtins/device-history/store/history-store.d.ts +7 -0
  38. package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
  39. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  40. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  41. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
  42. package/dist/builtins/hub-forwarder/index.js +1 -4
  43. package/dist/builtins/hub-forwarder/index.mjs +1 -4
  44. package/dist/builtins/keychain/index.js +4 -4
  45. package/dist/builtins/keychain/index.mjs +4 -4
  46. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  47. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  48. package/dist/builtins/local-auth/auth-schema.d.ts +4 -7
  49. package/dist/builtins/local-auth/local-auth.addon.d.ts +7 -8
  50. package/dist/builtins/local-auth/local-auth.addon.js +1086 -1329
  51. package/dist/builtins/local-auth/local-auth.addon.mjs +1085 -1326
  52. package/dist/builtins/local-auth/preferences-record-kinds.d.ts +27 -0
  53. package/dist/builtins/local-auth/record-kinds.d.ts +4 -3
  54. package/dist/builtins/local-auth/user-preferences-store.d.ts +56 -0
  55. package/dist/builtins/local-network/local-network.addon.d.ts +8 -0
  56. package/dist/builtins/local-network/local-network.addon.js +30 -5
  57. package/dist/builtins/local-network/local-network.addon.mjs +30 -5
  58. package/dist/builtins/loki-logging/index.js +323 -7
  59. package/dist/builtins/loki-logging/index.mjs +323 -7
  60. package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
  61. package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
  62. package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
  63. package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
  64. package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
  65. package/dist/builtins/platform-probe/index.js +1 -1
  66. package/dist/builtins/platform-probe/index.mjs +1 -1
  67. package/dist/builtins/record-entries/index.js +1 -1
  68. package/dist/builtins/record-entries/index.mjs +1 -1
  69. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  70. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  71. package/dist/builtins/snapshot/index.js +1 -1
  72. package/dist/builtins/snapshot/index.mjs +1 -1
  73. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  74. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  75. package/dist/builtins/sqlite-storage/index.js +1 -1
  76. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  77. package/dist/builtins/sqlite-storage/records/records-engine-types.d.ts +1 -0
  78. package/dist/builtins/sqlite-storage/records/records-provider.d.ts +13 -0
  79. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +52 -6
  80. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +52 -6
  81. package/dist/builtins/storage-orchestrator/location-history-recorder.d.ts +76 -0
  82. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
  83. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
  84. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -1
  85. package/dist/builtins/system-config/system-config.addon.js +3 -3
  86. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  87. package/dist/builtins/winston-logging/index.js +165 -3
  88. package/dist/builtins/winston-logging/index.mjs +165 -3
  89. package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
  90. package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
  91. package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
  92. package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
  93. package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
  94. package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
  95. package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
  96. package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
  97. package/dist/{composition-sources-CSzD5_qX.mjs → composition-sources-B_jeZDRJ.mjs} +1 -1
  98. package/dist/{composition-sources-BxUkLNSo.js → composition-sources-qKyvMAvc.js} +1 -1
  99. package/dist/{constants-DruwCkfp.js → constants-B_X9cWEK.js} +1 -1
  100. package/dist/{constants-DOlYt1t1.mjs → constants-cxKcZRMi.mjs} +1 -1
  101. package/dist/{device-ca-uFbTyY_o.mjs → device-ca-BB6nHAL5.mjs} +1 -1
  102. package/dist/{device-ca-QPBgfqVY.js → device-ca-CVK3YcR_.js} +1 -1
  103. package/dist/{dist-DjQ8C-ku.js → dist-C89nqNnn.js} +721 -154
  104. package/dist/{dist-BpSENkRJ.mjs → dist-Dhux90yB.mjs} +668 -155
  105. package/dist/file-host-worker.js +1 -1
  106. package/dist/file-host-worker.mjs +1 -1
  107. package/dist/{hub-mdns-ciao-DlgkFJVY.mjs → hub-mdns-ciao-B8olZqqd.mjs} +20 -13
  108. package/dist/{hub-mdns-ciao-Bkrvgnma.js → hub-mdns-ciao-DB1Y9agt.js} +20 -13
  109. package/dist/index.d.ts +3 -4
  110. package/dist/index.js +145 -24
  111. package/dist/index.mjs +138 -22
  112. package/dist/kernel/config-schema.d.ts +5 -0
  113. package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
  114. package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
  115. package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
  116. package/dist/log-file-read-worker.js +64 -0
  117. package/dist/log-file-read-worker.mjs +64 -0
  118. package/dist/log-read-filter-Bi066jwU.js +134 -0
  119. package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
  120. package/dist/logging/log-manager.d.ts +10 -6
  121. package/dist/logging/log-read-filter.d.ts +43 -0
  122. package/dist/{phase6-declare-gate-DrmTtz7F.js → phase6-declare-gate-BO-_jLh_.js} +1 -1
  123. package/dist/{phase6-declare-gate-JQUqgQgq.mjs → phase6-declare-gate-x9ZCbQJP.mjs} +1 -1
  124. package/dist/{restore-archive-4OP0EYg6.js → restore-archive-4DOUR5Gu.js} +1 -1
  125. package/dist/{restore-archive-YFSJRtKy.mjs → restore-archive-DyOaPeAr.mjs} +1 -1
  126. package/dist/tls/cert-issue.d.ts +16 -1
  127. package/dist/tls/cert-manager.d.ts +6 -0
  128. package/dist/tls/index.d.ts +1 -1
  129. package/dist/{wal-maintenance-TyuOEVy1.mjs → wal-maintenance-BMiX6UIa.mjs} +0 -0
  130. package/dist/{wal-maintenance-Bl0ZKjVR.js → wal-maintenance-DVN-DiVP.js} +0 -0
  131. package/package.json +1 -1
  132. package/dist/auth/admin-token-manager.d.ts +0 -51
  133. package/dist/auth/scoped-token-manager.d.ts +0 -25
@@ -1,4 +1,4 @@
1
- import { In as metricsProviderCapability, Kr as errMsg, Ni as createEvent, Zn as reducePoints, cn as foldSnapshotByFunction, er as resolveBucketMs, qr as BaseAddon, ya as EventCategory } from "../../dist-BpSENkRJ.mjs";
1
+ import { Gn as metricsProviderCapability, Hi as createEvent, Oa as EventCategory, _t as UNATTRIBUTED_BUCKET_KEY, gn as foldSnapshotByFunction, lr as resolveBucketMs, ni as BaseAddon, or as reducePoints, rt as ROOT_BUCKET_KEY, ti as errMsg } from "../../dist-Dhux90yB.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
4
  import * as os$1 from "node:os";
@@ -1020,16 +1020,107 @@ function mergeLoadSeries(input) {
1020
1020
  }
1021
1021
  //#endregion
1022
1022
  //#region src/builtins/native-metrics/node-metrics-history-recorder.ts
1023
+ /**
1024
+ * Node load history via device-history (D850, LTS P4) — hub only: the hub's
1025
+ * bus carries every node's snapshots, so the hub's writer is the cluster's.
1026
+ *
1027
+ * Per RUNNER, never per PID. A series was once scoped by process id, and a pid
1028
+ * changes on every runner restart: the hub held ~11.8k node series, most of
1029
+ * them one restart long, and no chart could follow a runner across a respawn.
1030
+ * The scope is now the fold's own function key (`load-series-fold.ts`): the
1031
+ * runner's addon id, or `__root__` / `__unattributed__`, so a runner keeps ONE
1032
+ * series for life and the cold tier folds exactly like the hot ring. The old
1033
+ * `process-*` keys are no longer written; their rows age out by retention.
1034
+ *
1035
+ * Node TOTALS ride the resources snapshot: CPU %, memory %, CPU temperature
1036
+ * and, when the node has one, GPU utilisation and temperature. A reading the
1037
+ * node did not take (`null`) is no point — never 0 (D393).
1038
+ *
1039
+ * The in-memory rings still serve the last hour for charts.
1040
+ */
1023
1041
  var ADDON_ID = "metrics-native";
1024
- var NODE_PROCESS_CPU_KEY = `addon:${ADDON_ID}/process-cpu-percent`;
1025
- var NODE_PROCESS_RSS_KEY = `addon:${ADDON_ID}/process-rss-mib`;
1026
- var NODE_CONTAINER_CURRENT_KEY = `addon:${ADDON_ID}/container-current-mib`;
1042
+ var MIB = 1048576;
1043
+ function keyOf(name) {
1044
+ return `addon:${ADDON_ID}/${name}`;
1045
+ }
1046
+ /** Per-runner series (scope = the fold key). */
1047
+ var RUNNER_CPU_MAIN_KEY = keyOf("runner-cpu-main-percent");
1048
+ var RUNNER_CPU_GC_KEY = keyOf("runner-cpu-gc-percent");
1049
+ var RUNNER_CPU_LIFETIME_KEY = keyOf("runner-cpu-lifetime-percent");
1050
+ var RUNNER_RSS_KEY = keyOf("runner-rss-mib");
1051
+ var RUNNER_PROCESSES_KEY = keyOf("runner-processes");
1052
+ /** Node totals (scope ''). */
1053
+ var NODE_CPU_KEY = keyOf("node-cpu-percent");
1054
+ var NODE_MEMORY_KEY = keyOf("node-memory-percent");
1055
+ var NODE_CPU_TEMPERATURE_KEY = keyOf("node-cpu-temperature");
1056
+ var NODE_GPU_KEY = keyOf("node-gpu-percent");
1057
+ var NODE_GPU_TEMPERATURE_KEY = keyOf("node-gpu-temperature");
1058
+ var NODE_CONTAINER_CURRENT_KEY = keyOf("container-current-mib");
1059
+ keyOf("process-cpu-percent");
1060
+ keyOf("process-rss-mib");
1061
+ var DECLARATIONS = [
1062
+ {
1063
+ name: "runner-cpu-main-percent",
1064
+ displayName: "Runner CPU % (main thread)",
1065
+ unit: "%"
1066
+ },
1067
+ {
1068
+ name: "runner-cpu-gc-percent",
1069
+ displayName: "Runner CPU % (GC)",
1070
+ unit: "%"
1071
+ },
1072
+ {
1073
+ name: "runner-cpu-lifetime-percent",
1074
+ displayName: "Runner CPU % (lifetime average)",
1075
+ unit: "%"
1076
+ },
1077
+ {
1078
+ name: "runner-rss-mib",
1079
+ displayName: "Runner RSS (MiB)",
1080
+ unit: "MiB"
1081
+ },
1082
+ {
1083
+ name: "runner-processes",
1084
+ displayName: "Runner processes",
1085
+ unit: "count"
1086
+ },
1087
+ {
1088
+ name: "node-cpu-percent",
1089
+ displayName: "Node CPU %",
1090
+ unit: "%"
1091
+ },
1092
+ {
1093
+ name: "node-memory-percent",
1094
+ displayName: "Node memory %",
1095
+ unit: "%"
1096
+ },
1097
+ {
1098
+ name: "node-cpu-temperature",
1099
+ displayName: "Node CPU temperature",
1100
+ unit: "°C"
1101
+ },
1102
+ {
1103
+ name: "node-gpu-percent",
1104
+ displayName: "Node GPU %",
1105
+ unit: "%"
1106
+ },
1107
+ {
1108
+ name: "node-gpu-temperature",
1109
+ displayName: "Node GPU temperature",
1110
+ unit: "°C"
1111
+ },
1112
+ {
1113
+ name: "container-current-mib",
1114
+ displayName: "Container memory current (MiB)",
1115
+ unit: "MiB"
1116
+ }
1117
+ ];
1027
1118
  function nodeMetricsHistoryClient(api) {
1028
1119
  return {
1029
1120
  declareSeries: (input) => api.declareSeries.mutate(input),
1030
1121
  record: (input) => api.record.mutate({ points: [...input.points] }),
1031
1122
  queryNode: async (input) => {
1032
- return { series: (await api.queryNode.query(input)).series.map((s) => ({
1123
+ return { series: (await api.queryNode.query({ ...input })).series.map((s) => ({
1033
1124
  scope: s.scope,
1034
1125
  points: s.points.map((p) => ({
1035
1126
  t: p.t,
@@ -1045,78 +1136,112 @@ function refusalCounts(result) {
1045
1136
  for (const r of result.refused) counts[r.reason] = (counts[r.reason] ?? 0) + r.count;
1046
1137
  return counts;
1047
1138
  }
1139
+ /** The fold's function key of a process: the same rule as `foldSnapshotByFunction`. */
1140
+ function runnerScopeOf(process) {
1141
+ if (process.addonId !== null) return process.addonId;
1142
+ return process.classification === "root" ? ROOT_BUCKET_KEY : UNATTRIBUTED_BUCKET_KEY;
1143
+ }
1144
+ function round1(value) {
1145
+ return Math.round(value * 10) / 10;
1146
+ }
1048
1147
  var NodeMetricsHistoryRecorder = class {
1049
1148
  deps;
1050
1149
  declared = false;
1150
+ lastTotalsAt = /* @__PURE__ */ new Map();
1151
+ /** A not-ready skip was logged and device-history has not been ready since. */
1152
+ waitLogged = false;
1051
1153
  constructor(deps) {
1052
1154
  this.deps = deps;
1053
1155
  }
1054
1156
  async declare() {
1055
1157
  if (this.declared) return;
1056
- const { deviceHistory } = this.deps;
1057
- await deviceHistory.declareSeries({
1058
- name: "process-cpu-percent",
1158
+ for (const d of DECLARATIONS) await this.deps.deviceHistory.declareSeries({
1159
+ name: d.name,
1059
1160
  subjectKind: "node",
1060
1161
  kind: "gauge",
1061
- displayName: "Process CPU %",
1162
+ displayName: d.displayName,
1062
1163
  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"
1164
+ unit: d.unit
1080
1165
  });
1081
1166
  this.declared = true;
1082
1167
  }
1168
+ /** One process snapshot: per-runner sums plus the container's memory. */
1083
1169
  async recordSample(nodeId, atMs, processes, containerMemory) {
1084
- if (!await this.ensureDeclared()) return;
1085
- const points = [];
1170
+ const subject = {
1171
+ kind: "node",
1172
+ nodeId
1173
+ };
1174
+ const point = (key, scope, value) => ({
1175
+ key,
1176
+ subject,
1177
+ scope,
1178
+ ts: atMs,
1179
+ value
1180
+ });
1181
+ const byRunner = /* @__PURE__ */ new Map();
1086
1182
  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
1183
+ const scope = runnerScopeOf(p);
1184
+ const t = byRunner.get(scope) ?? {
1185
+ main: 0,
1186
+ gc: 0,
1187
+ splitKnown: true,
1188
+ lifetime: 0,
1189
+ rss: 0,
1190
+ count: 0
1191
+ };
1192
+ const known = p.cpuMainPercent !== null && p.cpuGcPercent !== null;
1193
+ byRunner.set(scope, {
1194
+ main: t.main + (p.cpuMainPercent ?? 0),
1195
+ gc: t.gc + (p.cpuGcPercent ?? 0),
1196
+ splitKnown: t.splitKnown && known,
1197
+ lifetime: t.lifetime + p.cpuPercent,
1198
+ rss: t.rss + p.memoryRssBytes,
1199
+ count: t.count + 1
1097
1200
  });
1201
+ }
1202
+ const points = [];
1203
+ for (const [scope, t] of byRunner) {
1204
+ if (t.splitKnown) {
1205
+ points.push(point(RUNNER_CPU_MAIN_KEY, scope, round1(t.main)));
1206
+ points.push(point(RUNNER_CPU_GC_KEY, scope, round1(t.gc)));
1207
+ }
1208
+ points.push(point(RUNNER_CPU_LIFETIME_KEY, scope, round1(t.lifetime)));
1209
+ points.push(point(RUNNER_RSS_KEY, scope, Math.round(t.rss / MIB)));
1210
+ points.push(point(RUNNER_PROCESSES_KEY, scope, t.count));
1211
+ }
1212
+ if (containerMemory !== null) points.push(point(NODE_CONTAINER_CURRENT_KEY, "", Math.round(containerMemory.currentBytes / MIB)));
1213
+ await this.write(nodeId, atMs, points);
1214
+ }
1215
+ /** One resources snapshot: the node's totals, at most every {@link NODE_TOTALS_MIN_INTERVAL_MS}. */
1216
+ async recordTotals(nodeId, atMs, snapshot) {
1217
+ const last = this.lastTotalsAt.get(nodeId);
1218
+ if (last !== void 0 && atMs >= last && atMs - last < 15e3) return;
1219
+ this.lastTotalsAt.set(nodeId, atMs);
1220
+ const subject = {
1221
+ kind: "node",
1222
+ nodeId
1223
+ };
1224
+ const points = [];
1225
+ const add = (key, value) => {
1226
+ if (value === null || !Number.isFinite(value)) return;
1098
1227
  points.push({
1099
- key: NODE_PROCESS_RSS_KEY,
1100
- subject: {
1101
- kind: "node",
1102
- nodeId
1103
- },
1104
- scope,
1228
+ key,
1229
+ subject,
1230
+ scope: "",
1105
1231
  ts: atMs,
1106
- value: Math.round(p.memoryRssBytes / 1048576)
1232
+ value: round1(value)
1107
1233
  });
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
- });
1234
+ };
1235
+ add(NODE_CPU_KEY, snapshot.cpu.total);
1236
+ add(NODE_MEMORY_KEY, snapshot.memory.percent);
1237
+ add(NODE_CPU_TEMPERATURE_KEY, snapshot.cpuTemperature);
1238
+ add(NODE_GPU_KEY, snapshot.gpu?.utilization ?? null);
1239
+ add(NODE_GPU_TEMPERATURE_KEY, snapshot.gpu?.temperature ?? null);
1240
+ await this.write(nodeId, atMs, points);
1241
+ }
1242
+ async write(nodeId, atMs, points) {
1119
1243
  if (points.length === 0) return;
1244
+ if (!await this.ensureDeclared()) return;
1120
1245
  try {
1121
1246
  const result = await this.deps.deviceHistory.record({ points });
1122
1247
  if (result.refused.length > 0) {
@@ -1138,7 +1263,7 @@ var NodeMetricsHistoryRecorder = class {
1138
1263
  async readContainerPoints(nodeId, sinceMs, maxPoints) {
1139
1264
  if (!await this.ensureDeclared()) return [];
1140
1265
  const result = await this.deps.deviceHistory.queryNode({
1141
- nodeId,
1266
+ forNodeId: nodeId,
1142
1267
  key: NODE_CONTAINER_CURRENT_KEY,
1143
1268
  from: sinceMs + 1,
1144
1269
  to: Date.now(),
@@ -1151,7 +1276,7 @@ var NodeMetricsHistoryRecorder = class {
1151
1276
  out.push({
1152
1277
  atMs: p.t,
1153
1278
  source: "cgroup-v2",
1154
- currentBytes: p.value * 1048576,
1279
+ currentBytes: p.value * MIB,
1155
1280
  limitBytes: null,
1156
1281
  anonBytes: null,
1157
1282
  fileBytes: null,
@@ -1162,63 +1287,46 @@ var NodeMetricsHistoryRecorder = class {
1162
1287
  }
1163
1288
  return out.toSorted((a, b) => a.atMs - b.atMs);
1164
1289
  }
1290
+ /**
1291
+ * The cold tier as process samples the fold reads like the hot ring's. A
1292
+ * runner's sums come back as `processes` equal rows (sums preserved, count
1293
+ * right); its CPU split is `null` where it was not recorded.
1294
+ */
1165
1295
  async readProcessSamples(nodeId, sinceMs, maxPoints) {
1166
1296
  if (!await this.ensureDeclared()) return [];
1167
1297
  const to = Date.now();
1168
- const [cpuResult, rssResult] = await Promise.all([this.deps.deviceHistory.queryNode({
1169
- nodeId,
1170
- key: NODE_PROCESS_CPU_KEY,
1298
+ const read = (key) => this.deps.deviceHistory.queryNode({
1299
+ forNodeId: nodeId,
1300
+ key,
1171
1301
  from: sinceMs + 1,
1172
1302
  to,
1173
1303
  resolution: "raw",
1174
1304
  maxPoints
1175
- }), this.deps.deviceHistory.queryNode({
1176
- nodeId,
1177
- key: NODE_PROCESS_RSS_KEY,
1178
- from: sinceMs + 1,
1179
- to,
1180
- resolution: "raw",
1181
- maxPoints
1182
- })]);
1183
- const byAt = /* @__PURE__ */ new Map();
1184
- const ingest = (series, field) => {
1185
- for (const s of series) {
1186
- const pid = Number(s.scope);
1187
- if (!Number.isFinite(pid)) continue;
1188
- for (const p of s.points) {
1189
- if (p.value === null || !Number.isFinite(p.value)) continue;
1190
- let perPid = byAt.get(p.t);
1191
- if (perPid === void 0) {
1192
- perPid = /* @__PURE__ */ new Map();
1193
- byAt.set(p.t, perPid);
1194
- }
1195
- const base = perPid.get(pid) ?? {
1196
- pid,
1197
- addonId: null,
1198
- classification: "managed",
1199
- cpuPercent: 0,
1200
- memoryRssBytes: 0,
1201
- cpuMainPercent: null,
1202
- cpuGcPercent: null
1203
- };
1204
- perPid.set(pid, field === "cpuPercent" ? {
1205
- ...base,
1206
- cpuPercent: p.value
1207
- } : {
1208
- ...base,
1209
- memoryRssBytes: p.value * 1048576
1210
- });
1211
- }
1212
- }
1213
- };
1214
- ingest(cpuResult.series, "cpuPercent");
1215
- ingest(rssResult.series, "memoryRssBytes");
1216
- return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, perPid]) => ({
1217
- atMs,
1218
- processes: [...perPid.values()]
1219
- }));
1305
+ });
1306
+ const [main, gc, lifetime, rss, count] = await Promise.all([
1307
+ read(RUNNER_CPU_MAIN_KEY),
1308
+ read(RUNNER_CPU_GC_KEY),
1309
+ read(RUNNER_CPU_LIFETIME_KEY),
1310
+ read(RUNNER_RSS_KEY),
1311
+ read(RUNNER_PROCESSES_KEY)
1312
+ ]);
1313
+ return assembleRunnerSamples({
1314
+ main,
1315
+ gc,
1316
+ lifetime,
1317
+ rss,
1318
+ count
1319
+ });
1220
1320
  }
1221
1321
  async ensureDeclared() {
1322
+ if (this.deps.historyReady !== void 0 && !this.deps.historyReady()) {
1323
+ if (!this.waitLogged) {
1324
+ this.waitLogged = true;
1325
+ this.deps.logger.info("node metrics history waits for device-history — samples until it is ready are not stored");
1326
+ }
1327
+ return false;
1328
+ }
1329
+ this.waitLogged = false;
1222
1330
  try {
1223
1331
  await this.declare();
1224
1332
  return true;
@@ -1228,6 +1336,55 @@ var NodeMetricsHistoryRecorder = class {
1228
1336
  }
1229
1337
  }
1230
1338
  };
1339
+ /** `atMs -> scope -> value` of one key's answer, finite values only. */
1340
+ function indexAnswer(answer) {
1341
+ const out = /* @__PURE__ */ new Map();
1342
+ for (const s of answer.series) for (const p of s.points) {
1343
+ if (p.value === null || !Number.isFinite(p.value)) continue;
1344
+ const at = out.get(p.t) ?? /* @__PURE__ */ new Map();
1345
+ at.set(s.scope, p.value);
1346
+ out.set(p.t, at);
1347
+ }
1348
+ return out;
1349
+ }
1350
+ /**
1351
+ * Per-runner cold points back into process rows. A sample exists where the
1352
+ * runner's lifetime CPU and RSS were both recorded (always written together).
1353
+ */
1354
+ function assembleRunnerSamples(answers) {
1355
+ const main = indexAnswer(answers.main);
1356
+ const gc = indexAnswer(answers.gc);
1357
+ const rss = indexAnswer(answers.rss);
1358
+ const count = indexAnswer(answers.count);
1359
+ const samples = [];
1360
+ for (const [atMs, lifetimeByScope] of indexAnswer(answers.lifetime)) {
1361
+ const processes = [];
1362
+ for (const [scope, lifetime] of lifetimeByScope) {
1363
+ const rssMib = rss.get(atMs)?.get(scope);
1364
+ if (rssMib === void 0) continue;
1365
+ const n = Math.max(1, Math.round(count.get(atMs)?.get(scope) ?? 1));
1366
+ const m = main.get(atMs)?.get(scope);
1367
+ const g = gc.get(atMs)?.get(scope);
1368
+ const split = m !== void 0 && g !== void 0;
1369
+ const isRoot = scope === ROOT_BUCKET_KEY;
1370
+ const isUnattributed = scope === UNATTRIBUTED_BUCKET_KEY;
1371
+ for (let i = 0; i < n; i += 1) processes.push({
1372
+ pid: 0,
1373
+ addonId: isRoot || isUnattributed ? null : scope,
1374
+ classification: isRoot ? "root" : isUnattributed ? "system" : "managed",
1375
+ cpuPercent: lifetime / n,
1376
+ memoryRssBytes: rssMib * MIB / n,
1377
+ cpuMainPercent: split ? m / n : null,
1378
+ cpuGcPercent: split ? g / n : null
1379
+ });
1380
+ }
1381
+ if (processes.length > 0) samples.push({
1382
+ atMs,
1383
+ processes
1384
+ });
1385
+ }
1386
+ return samples.toSorted((a, b) => a.atMs - b.atMs);
1387
+ }
1231
1388
  //#endregion
1232
1389
  //#region src/builtins/native-metrics/gpu-probe.ts
1233
1390
  /**
@@ -2191,6 +2348,8 @@ var ThreadCpuTracker = class {
2191
2348
  };
2192
2349
  //#endregion
2193
2350
  //#region src/builtins/native-metrics/native-metrics.addon.ts
2351
+ /** The hub's node id — `isHub` below is the same test; device-history lives there. */
2352
+ var HUB_NODE_ID = "hub";
2194
2353
  /**
2195
2354
  * Cadence for the per-node metric snapshot bus events. ~5 s matches
2196
2355
  * the existing UI polling cadence (ProcessesTab, ProcessResources)
@@ -2312,7 +2471,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends BaseAddon {
2312
2471
  }
2313
2472
  /** Is this the hub? The same test every other addon uses (`addon-ai`). */
2314
2473
  get isHub() {
2315
- return (this.ctx.kernel.cluster?.broker?.nodeID ?? "hub") === "hub";
2474
+ return (this.ctx.kernel.cluster?.broker?.nodeID ?? HUB_NODE_ID) === HUB_NODE_ID;
2316
2475
  }
2317
2476
  /** Process rows observed across the whole fleet, for the cost projection. */
2318
2477
  observedFleetRows() {
@@ -2363,19 +2522,38 @@ var NativeMetricsAddon = class NativeMetricsAddon extends BaseAddon {
2363
2522
  };
2364
2523
  this.applyLoadSeriesConfig();
2365
2524
  if (this.isHub) {
2366
- this.metricsHistory = new NodeMetricsHistoryRecorder({
2525
+ const historyScope = {
2526
+ type: "node",
2527
+ nodeId: HUB_NODE_ID
2528
+ };
2529
+ const historyHandle = this.ctx.useCapability("device-history", historyScope);
2530
+ const recorder = new NodeMetricsHistoryRecorder({
2367
2531
  deviceHistory: nodeMetricsHistoryClient(this.ctx.api.deviceHistory),
2368
- logger: this.ctx.logger.child("NodeMetricsHistory")
2369
- });
2370
- this.metricsHistory.declare().catch((err) => {
2371
- this.ctx.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
2532
+ logger: this.ctx.logger.child("NodeMetricsHistory"),
2533
+ historyReady: () => historyHandle.isReady
2372
2534
  });
2535
+ this.metricsHistory = recorder;
2536
+ const declareNow = () => {
2537
+ recorder.declare().catch((err) => {
2538
+ this.ctx.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
2539
+ });
2540
+ };
2541
+ this.ctx.addDisposer(this.ctx.onCapabilityStateChange("device-history", historyScope, (state) => {
2542
+ if (state === "ready") declareNow();
2543
+ }));
2544
+ if (historyHandle.isReady) declareNow();
2373
2545
  }
2374
2546
  this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
2375
2547
  this.pidsProbe = new ContainerPidsProbe({ logger: this.ctx.logger.child("ContainerPids") });
2376
2548
  if (this.snapshotTimer) clearInterval(this.snapshotTimer);
2377
2549
  this.snapshotTimer = setInterval(() => this.runTick("resources snapshot", () => this.emitResourcesSnapshot()), METRICS_SNAPSHOT_INTERVAL_MS);
2378
2550
  this.startProcessSnapshotTimer();
2551
+ if (this.metricsHistory !== null) {
2552
+ const history = this.metricsHistory;
2553
+ this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: EventCategory.MetricsNodeResourcesSnapshot }, (event) => {
2554
+ history.recordTotals(event.data.nodeId, event.data.timestamp, event.data.snapshot);
2555
+ }));
2556
+ }
2379
2557
  this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes, event.data.containerMemory)));
2380
2558
  return [{
2381
2559
  capability: metricsProviderCapability,
@@ -1,15 +1,79 @@
1
1
  /**
2
- * Node process load samples via device-history (D850, LTS P4).
2
+ * Node load history via device-history (D850, LTS P4) — hub only: the hub's
3
+ * bus carries every node's snapshots, so the hub's writer is the cluster's.
3
4
  *
4
- * Replaces `metrics:node-load-samples` / `metrics:node-memory-samples` SQLite
5
- * collections. The in-memory rings still serve the last hour for charts.
5
+ * Per RUNNER, never per PID. A series was once scoped by process id, and a pid
6
+ * changes on every runner restart: the hub held ~11.8k node series, most of
7
+ * them one restart long, and no chart could follow a runner across a respawn.
8
+ * The scope is now the fold's own function key (`load-series-fold.ts`): the
9
+ * runner's addon id, or `__root__` / `__unattributed__`, so a runner keeps ONE
10
+ * series for life and the cold tier folds exactly like the hot ring. The old
11
+ * `process-*` keys are no longer written; their rows age out by retention.
12
+ *
13
+ * Node TOTALS ride the resources snapshot: CPU %, memory %, CPU temperature
14
+ * and, when the node has one, GPU utilisation and temperature. A reading the
15
+ * node did not take (`null`) is no point — never 0 (D393).
16
+ *
17
+ * The in-memory rings still serve the last hour for charts.
6
18
  */
7
- import type { ContainerMemoryPoint, ContainerMemorySnapshot, DeviceHistoryIngestResult, IScopedLogger } from "@camstack/types";
19
+ import { type AddonApi, type ContainerMemoryPoint, type ContainerMemorySnapshot, type DeviceHistoryIngestResult, type IScopedLogger, type SystemResourceSnapshot } from "@camstack/types";
8
20
  import type { RetainedProcess, RetainedSample } from "./load-series-types.js";
9
21
  declare const ADDON_ID: "metrics-native";
10
- export declare const NODE_PROCESS_CPU_KEY: `addon:${typeof ADDON_ID}/process-cpu-percent`;
11
- export declare const NODE_PROCESS_RSS_KEY: `addon:${typeof ADDON_ID}/process-rss-mib`;
12
- export declare const NODE_CONTAINER_CURRENT_KEY: `addon:${typeof ADDON_ID}/container-current-mib`;
22
+ type MetricsKey<N extends string> = `addon:${typeof ADDON_ID}/${N}`;
23
+ /** Per-runner series (scope = the fold key). */
24
+ export declare const RUNNER_CPU_MAIN_KEY: MetricsKey<"runner-cpu-main-percent">;
25
+ export declare const RUNNER_CPU_GC_KEY: MetricsKey<"runner-cpu-gc-percent">;
26
+ export declare const RUNNER_CPU_LIFETIME_KEY: MetricsKey<"runner-cpu-lifetime-percent">;
27
+ export declare const RUNNER_RSS_KEY: MetricsKey<"runner-rss-mib">;
28
+ export declare const RUNNER_PROCESSES_KEY: MetricsKey<"runner-processes">;
29
+ /** Node totals (scope ''). */
30
+ export declare const NODE_CPU_KEY: MetricsKey<"node-cpu-percent">;
31
+ export declare const NODE_MEMORY_KEY: MetricsKey<"node-memory-percent">;
32
+ export declare const NODE_CPU_TEMPERATURE_KEY: MetricsKey<"node-cpu-temperature">;
33
+ export declare const NODE_GPU_KEY: MetricsKey<"node-gpu-percent">;
34
+ export declare const NODE_GPU_TEMPERATURE_KEY: MetricsKey<"node-gpu-temperature">;
35
+ export declare const NODE_CONTAINER_CURRENT_KEY: MetricsKey<"container-current-mib">;
36
+ /**
37
+ * The PID-scoped keys of the earlier build. NOT written any more; kept only so
38
+ * the one-shot split-database migration (`scripts/ops/split-databases`) still
39
+ * names what it once wrote.
40
+ */
41
+ export declare const NODE_PROCESS_CPU_KEY: MetricsKey<"process-cpu-percent">;
42
+ export declare const NODE_PROCESS_RSS_KEY: MetricsKey<"process-rss-mib">;
43
+ /**
44
+ * Node totals are written at most this often per node. The resources snapshot
45
+ * fires on any coarse change (every 5 s at worst) and device-history refuses a
46
+ * point closer than 10 s to the previous one of its series (`rate`).
47
+ */
48
+ export declare const NODE_TOTALS_MIN_INTERVAL_MS = 15e3;
49
+ export interface NodeHistoryPoint {
50
+ readonly key: string;
51
+ readonly subject: {
52
+ readonly kind: "node";
53
+ readonly nodeId: string;
54
+ };
55
+ readonly scope: string;
56
+ readonly ts: number;
57
+ readonly value: number;
58
+ }
59
+ export interface NodeHistoryQueryInput {
60
+ readonly forNodeId: string;
61
+ readonly key: string;
62
+ readonly from: number;
63
+ readonly to: number;
64
+ readonly resolution: "raw";
65
+ readonly maxPoints?: number;
66
+ }
67
+ export interface NodeHistoryQueryAnswer {
68
+ readonly series: ReadonlyArray<{
69
+ readonly scope: string;
70
+ readonly points: ReadonlyArray<{
71
+ t: number;
72
+ value: number | null;
73
+ samples: number;
74
+ }>;
75
+ }>;
76
+ }
13
77
  export interface NodeMetricsHistoryClient {
14
78
  declareSeries(input: {
15
79
  name: string;
@@ -22,49 +86,56 @@ export interface NodeMetricsHistoryClient {
22
86
  key: string;
23
87
  }>;
24
88
  record(input: {
25
- points: ReadonlyArray<{
26
- key: string;
27
- subject: {
28
- kind: "node";
29
- nodeId: string;
30
- };
31
- scope: string;
32
- ts: number;
33
- value: number;
34
- }>;
89
+ points: readonly NodeHistoryPoint[];
35
90
  }): Promise<DeviceHistoryIngestResult>;
36
- queryNode(input: {
37
- nodeId: string;
38
- key: string;
39
- from: number;
40
- to: number;
41
- resolution: "raw";
42
- maxPoints?: number;
43
- }): Promise<{
44
- series: ReadonlyArray<{
45
- scope: string;
46
- points: ReadonlyArray<{
47
- t: number;
48
- value: number | null;
49
- samples: number;
50
- }>;
51
- }>;
52
- }>;
91
+ queryNode(input: NodeHistoryQueryInput): Promise<NodeHistoryQueryAnswer>;
53
92
  }
54
- import type { AddonApi } from "@camstack/types";
55
93
  export declare function nodeMetricsHistoryClient(api: AddonApi["deviceHistory"]): NodeMetricsHistoryClient;
56
94
  export interface NodeMetricsHistoryRecorderDeps {
57
95
  readonly deviceHistory: NodeMetricsHistoryClient;
58
96
  readonly logger: IScopedLogger;
97
+ /**
98
+ * The registry's answer to "is device-history up?" — read without blocking.
99
+ * While it is not, a sample is not stored (logged once per wait) and no
100
+ * declare is attempted: a declare against an absent provider is a failure
101
+ * line per sample, not a wait. Omitted ⇒ always ready (specs).
102
+ */
103
+ readonly historyReady?: () => boolean;
59
104
  }
105
+ /** The fold's function key of a process: the same rule as `foldSnapshotByFunction`. */
106
+ export declare function runnerScopeOf(process: RetainedProcess): string;
60
107
  export declare class NodeMetricsHistoryRecorder {
61
108
  private readonly deps;
62
109
  private declared;
110
+ private readonly lastTotalsAt;
111
+ /** A not-ready skip was logged and device-history has not been ready since. */
112
+ private waitLogged;
63
113
  constructor(deps: NodeMetricsHistoryRecorderDeps);
64
114
  declare(): Promise<void>;
115
+ /** One process snapshot: per-runner sums plus the container's memory. */
65
116
  recordSample(nodeId: string, atMs: number, processes: readonly RetainedProcess[], containerMemory: ContainerMemorySnapshot | null): Promise<void>;
117
+ /** One resources snapshot: the node's totals, at most every {@link NODE_TOTALS_MIN_INTERVAL_MS}. */
118
+ recordTotals(nodeId: string, atMs: number, snapshot: SystemResourceSnapshot): Promise<void>;
119
+ private write;
66
120
  readContainerPoints(nodeId: string, sinceMs: number, maxPoints: number): Promise<readonly ContainerMemoryPoint[]>;
121
+ /**
122
+ * The cold tier as process samples the fold reads like the hot ring's. A
123
+ * runner's sums come back as `processes` equal rows (sums preserved, count
124
+ * right); its CPU split is `null` where it was not recorded.
125
+ */
67
126
  readProcessSamples(nodeId: string, sinceMs: number, maxPoints: number): Promise<readonly RetainedSample[]>;
68
127
  private ensureDeclared;
69
128
  }
129
+ export interface RunnerAnswers {
130
+ readonly main: NodeHistoryQueryAnswer;
131
+ readonly gc: NodeHistoryQueryAnswer;
132
+ readonly lifetime: NodeHistoryQueryAnswer;
133
+ readonly rss: NodeHistoryQueryAnswer;
134
+ readonly count: NodeHistoryQueryAnswer;
135
+ }
136
+ /**
137
+ * Per-runner cold points back into process rows. A sample exists where the
138
+ * runner's lifetime CPU and RSS were both recorded (always written together).
139
+ */
140
+ export declare function assembleRunnerSamples(answers: RunnerAnswers): readonly RetainedSample[];
70
141
  export {};