@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.445",
3
+ "version": "1.2.447",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,260 +0,0 @@
1
- /**
2
- * Retention for the container-memory reading — the same two tiers, the same
3
- * bounds and the same reader as the per-process load series, because it is the
4
- * same question asked at the other end.
5
- *
6
- * ## Why it is retained at all
7
- *
8
- * A single hand reading of `memory.stat` says what the container holds RIGHT
9
- * NOW and nothing about direction. That is exactly the failure this exists to
10
- * fix: on 2026-08-29 six of the hub's twenty-three gigabytes had no owner among
11
- * the processes, and there was no way to say whether page cache was steady or
12
- * climbing — so the difference was read as a leak. A number you cannot trend is
13
- * a number you will misread.
14
- *
15
- * ## Why it is not the same TABLE
16
- *
17
- * `metrics:node-load-samples` is one row PER PROCESS per sample. A
18
- * container-memory reading is one row per NODE per sample — 76x fewer rows on
19
- * the live fleet. Folding it into that table would mean either repeating the
20
- * same ten numbers on every process row of a sample, or inventing a synthetic
21
- * process that is not a process. Both are lies with a storage cost. It is its
22
- * own collection, keyed the same way, pruned by the same knobs and read in the
23
- * same call.
24
- *
25
- * ## Why it is not its own RING
26
- *
27
- * {@link NodeLoadRing} is already a fixed-capacity partitioned ring with
28
- * per-node capacity, a fleet-wide row budget, an idle-partition sweep and a
29
- * monotonic idempotence gate — all of it measured and tested. It is generic
30
- * over its payload and reads only the node id and the timestamp off an
31
- * envelope. A second ring implementation would be a second set of bounds to
32
- * get wrong. {@link ContainerMemoryRing} is a naming adapter over it, nothing
33
- * more: one reading per sample, so one "row" per sample.
34
- */
35
- import type { ContainerMemoryPoint, ContainerMemorySnapshot, IScopedLogger, SettingsStoreClient } from "@camstack/types";
36
- import type { LoadSeriesRetention } from "./load-series-store.js";
37
- /**
38
- * @durable class=audit owner=metrics-native
39
- * write="one row per NODE per sample, appended by the HUB's `native-metrics`
40
- * off its subscription to `metrics.node-processes-snapshot` — the same
41
- * subscription, the same accepted-sample gate and the same timestamps as
42
- * `metrics:node-load-samples`, so a memory row and the process rows of the
43
- * same instant share an `atMs`. A node with no cgroup writes NOTHING; it
44
- * never writes a zeroed row."
45
- * retention="The operator's `loadSeriesRetentionHours` by age and
46
- * CONTAINER_MEMORY_MAX_ROWS by count, swept in the same pass as the process
47
- * table and through the same bounded page. Losing the table loses the cold
48
- * window only: the ring keeps serving the last hour."
49
- */
50
- export declare const CONTAINER_MEMORY_COLLECTION = "metrics:node-memory-samples";
51
- /**
52
- * Per-node ceiling on the HOT ring, in samples.
53
- *
54
- * Deliberately the same 180 as the process ring — one hour at the 20 s cadence,
55
- * six at the 10 s default's own reckoning — because the two series are read
56
- * together and a memory window shorter than the process window would silently
57
- * truncate the comparison the pair exists to support.
58
- */
59
- export declare const CONTAINER_MEMORY_SAMPLES_PER_NODE = 180;
60
- /**
61
- * Fleet ceiling on the hot ring, in READINGS.
62
- *
63
- * One reading per node per sample, so this is simply the per-node ceiling times
64
- * the partition ceiling the process ring already declares (16). 2 880 readings
65
- * of ten numbers each is well under a megabyte — the bound exists so a runaway
66
- * node id space costs a fixed amount, not because the number is large.
67
- */
68
- export declare const CONTAINER_MEMORY_MAX_TOTAL_ROWS: number;
69
- /**
70
- * Hard ceiling on the durable table, in ROWS. The guarantee behind the
71
- * retention intention, exactly as `DEFAULT_MAX_ROWS` is for the process table.
72
- *
73
- * | cadence | retention | rows (3 nodes) |
74
- * | --- | --- | --- |
75
- * | 10 s | 6 h (default) | 6 480 |
76
- * | 5 s | 6 h | 12 960 |
77
- * | 5 s | 72 h, 16 nodes | 829 440 — capped here |
78
- *
79
- * 50 000 rows is 7.7x the shipped default. This row is strictly narrower than
80
- * a `metrics:node-load-samples` row — fewer columns, one index, every value an
81
- * integer varint — and that one was MEASURED at 87 B/row, so 50 000 rows is
82
- * **under 4.2 MiB** with the real figure lower still. It is not a knob: a
83
- * guarantee that moves with the hand that raised the intention is not one.
84
- */
85
- export declare const CONTAINER_MEMORY_MAX_ROWS = 5e4;
86
- /** Rows one prune pass may remove. Bounded, never a full scan. */
87
- export declare const CONTAINER_MEMORY_PRUNE_PAGE_ROWS = 2e4;
88
- export declare const CONTAINER_MEMORY_COLUMNS: readonly [{
89
- readonly name: "id";
90
- readonly type: "INTEGER";
91
- readonly primaryKey: true;
92
- readonly notNull: true;
93
- }, {
94
- readonly name: "nodeId";
95
- readonly type: "TEXT";
96
- readonly notNull: true;
97
- }, {
98
- readonly name: "atMs";
99
- readonly type: "INTEGER";
100
- readonly notNull: true;
101
- }, {
102
- readonly name: "source";
103
- readonly type: "TEXT";
104
- readonly notNull: true;
105
- }, {
106
- readonly name: "currentMib";
107
- readonly type: "INTEGER";
108
- readonly notNull: true;
109
- }, {
110
- readonly name: "limitMib";
111
- readonly type: "INTEGER";
112
- }, {
113
- readonly name: "anonMib";
114
- readonly type: "INTEGER";
115
- }, {
116
- readonly name: "fileMib";
117
- readonly type: "INTEGER";
118
- }, {
119
- readonly name: "shmemMib";
120
- readonly type: "INTEGER";
121
- }, {
122
- readonly name: "slabMib";
123
- readonly type: "INTEGER";
124
- }, {
125
- readonly name: "gpuShmemMib";
126
- readonly type: "INTEGER";
127
- }];
128
- export declare const CONTAINER_MEMORY_INDEXES: readonly [{
129
- readonly name: "idx_node_memory_node_at";
130
- readonly columns: readonly ["nodeId", "atMs"];
131
- }];
132
- /** One stored row, in the units the table stores them in. */
133
- export interface ContainerMemoryRow {
134
- readonly nodeId: string;
135
- readonly atMs: number;
136
- readonly source: string;
137
- readonly currentMib: number;
138
- readonly limitMib: number | null;
139
- readonly anonMib: number | null;
140
- readonly fileMib: number | null;
141
- readonly shmemMib: number | null;
142
- readonly slabMib: number | null;
143
- readonly gpuShmemMib: number | null;
144
- }
145
- /** The ONE encode. */
146
- export declare function snapshotToRow(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): ContainerMemoryRow;
147
- /**
148
- * The ONE decode. A row whose `source` is not a hierarchy this build knows is
149
- * skipped rather than coerced — a stored string that no longer means anything
150
- * must not be charted as though it did.
151
- */
152
- export declare function rowToPoint(row: ContainerMemoryRow): ContainerMemoryPoint | null;
153
- /** Structural validation on read. A malformed row is skipped, never charted. */
154
- export declare function recordToMemoryRow(data: Record<string, unknown>): ContainerMemoryRow | null;
155
- export interface ContainerMemoryRingOptions {
156
- readonly samplesPerNode?: number;
157
- /**
158
- * Injectable clock, forwarded to the ring's idle-partition sweep. Tests
159
- * drive eviction without sleeping for an hour; production uses `Date.now`.
160
- */
161
- readonly now?: () => number;
162
- }
163
- /**
164
- * The HOT tier — a naming adapter over {@link NodeLoadRing}, not a second ring.
165
- *
166
- * One reading per sample, so the ring's "rows" and its samples are the same
167
- * count and its fleet budget is a sample budget. Every bound, the idle sweep
168
- * and the monotonic replay gate come from there unchanged.
169
- */
170
- export declare class ContainerMemoryRing {
171
- private readonly ring;
172
- constructor(options?: ContainerMemoryRingOptions);
173
- /** Retain one reading. `false` = a timestamp this node already delivered. */
174
- record(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): boolean;
175
- /**
176
- * Read one node's retained readings, oldest-first. `sinceMs` is EXCLUSIVE,
177
- * matching every other reader in this subsystem.
178
- *
179
- * A node nobody has recorded answers EMPTY — which is what "this node has no
180
- * cgroup" looks like, and it must stay distinguishable from a zero.
181
- */
182
- read(nodeId: string, sinceMs?: number): readonly ContainerMemoryPoint[];
183
- }
184
- /**
185
- * Merge the two tiers and reduce, oldest-first.
186
- *
187
- * Dedupe is on `atMs` with the HOT copy winning, exactly as the process reader
188
- * does: the ring holds the reading as it arrived, the table holds it after the
189
- * MiB encode.
190
- *
191
- * Reduction keeps, for each bucket, the reading with the largest
192
- * `currentBytes` — WHOLE, every field from that one sample. A per-field
193
- * maximum would synthesise a row whose parts never coexisted (an `anon` peak
194
- * and a `file` peak minutes apart, summed into a total that never happened),
195
- * and a mean would smear away the spike this series exists to find. `atMs`
196
- * becomes the bucket START so the points lie on the same x-axis as the folded
197
- * process series, which is the only reason both travel in one answer.
198
- *
199
- * An empty bucket stays ABSENT — never zero, never carried forward.
200
- */
201
- export declare function reduceContainerMemory(points: readonly ContainerMemoryPoint[], bucketMs: number, origin: number): readonly ContainerMemoryPoint[];
202
- /** Merge cold + hot on `atMs`, hot wins. Exported because dedup is the
203
- * property worth testing on its own. */
204
- export declare function mergeContainerMemory(cold: readonly ContainerMemoryPoint[], hot: readonly ContainerMemoryPoint[]): readonly ContainerMemoryPoint[];
205
- export interface ContainerMemoryStoreDeps {
206
- readonly store: SettingsStoreClient;
207
- readonly logger: IScopedLogger;
208
- readonly now?: () => number;
209
- readonly prunePageRows?: number;
210
- }
211
- /** What one prune pass did. Returned so a test can assert the bound held. */
212
- export interface ContainerMemoryPruneOutcome {
213
- readonly deletedByAge: number;
214
- readonly deletedByCap: number;
215
- readonly rowsExamined: number;
216
- readonly capBit: boolean;
217
- }
218
- /**
219
- * The COLD tier. Append-only, bounded, and never the authority on anything the
220
- * hot ring can answer.
221
- *
222
- * Every method is best-effort and says so in the log rather than throwing at
223
- * the emitter: a storage hiccup must cost a gap in a chart, never a dropped
224
- * snapshot on the bus.
225
- */
226
- export declare class ContainerMemoryStore {
227
- private declared;
228
- private readonly store;
229
- private readonly logger;
230
- private readonly nowFn;
231
- private readonly prunePageRows;
232
- constructor(deps: ContainerMemoryStoreDeps);
233
- /** Idempotently declare the collection. `false` when the store refused. */
234
- declare(): Promise<boolean>;
235
- /** Append ONE reading. */
236
- append(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): Promise<number>;
237
- /** Read one node's cold readings, oldest-first. `sinceMs` is EXCLUSIVE. */
238
- read(nodeId: string, sinceMs: number, limitRows: number): Promise<readonly ContainerMemoryPoint[]>;
239
- /**
240
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
241
- *
242
- * Not rate-limited here: it is driven by the process store's own prune
243
- * cadence, so it runs at most once a minute by construction and a second
244
- * timer would be a second thing to get wrong.
245
- */
246
- prune(nodeIds: readonly string[], retention: LoadSeriesRetention): Promise<ContainerMemoryPruneOutcome | null>;
247
- /** Total rows, or `null` when the store could not answer. */
248
- count(): Promise<number | null>;
249
- /**
250
- * Delete this node's oldest rows, at most one page's worth.
251
- *
252
- * `cutoff === null` means "the oldest rows, whatever their age" — the
253
- * row-cap path, which is bounded by `limitRows` instead. Either way the page
254
- * is READ FIRST, ordered by the one index, and its last `atMs` becomes the
255
- * effective cutoff for a single bounded delete. An unbounded `deleteWhere`
256
- * would be one statement and one stalling transaction the first time a
257
- * retention is lowered.
258
- */
259
- private deleteOldestPage;
260
- }
@@ -1,293 +0,0 @@
1
- /**
2
- * Durable retention for the per-node load series — the COLD window behind the
3
- * in-memory ring's hot one.
4
- *
5
- * ## One table, and what that decides
6
- *
7
- * One row per process per sample, in `metrics:node-load-samples`, declared the
8
- * way every other typed collection in this repo is declared: explicit columns,
9
- * explicit indexes, read by `where`/`whereBetween`/`orderBy`/`limit`. There is
10
- * no dimension table, no per-addon pre-aggregate and no bucket tier. The
11
- * consequence is that the row count is DETERMINISTIC and large, and the honest
12
- * thing to do with a large number is to publish it rather than to design it
13
- * away — see `docs/architecture/load-ledger.md` § "2026-08-27 (night) — the load
14
- * series reaches disk, and what it costs".
15
- *
16
- * ## Measured cost, not estimated
17
- *
18
- * `scripts/measure-load-series-cost.mts` writes the real payload shape through
19
- * the real `SqliteSettingsBackend` and the real `declareCollection` DDL, then
20
- * reads `page_count × page_size` off the resulting file. The answer is
21
- * **{@link MEASURED_BYTES_PER_ROW} bytes per row**, flat from 164 k rows to
22
- * 3.9 M rows — table plus its one index, after a truncating WAL checkpoint.
23
- *
24
- * That number is why the columns look the way they do. Four decisions, each
25
- * paid for in bytes measured on the same harness:
26
- *
27
- * | shape | B/row |
28
- * | --- | --- |
29
- * | naive: `TEXT` id, 2 indexes, every `NodeProcess` field | 196.0 |
30
- * | one index, drop `threadCount` + `uptimeSec` | 172.8 |
31
- * | + base36 in the composite id | 159.6 |
32
- * | + CPU as integer tenths, RSS as integer MiB | 140.3 |
33
- * | + `INTEGER` rowid-alias key (this file) | **86.9** |
34
- *
35
- * - **`command` is not stored.** It is the fattest field in a `NodeProcess` and
36
- * it is the same ~100-character string on every runner — the runner id
37
- * travels in the environment, not in argv, so `ps` reports one identical
38
- * command line for all of them. `addonId` is the short identifier that
39
- * already exists, and nothing that reads this series reads `command`.
40
- * - **CPU is stored as integer tenths.** Losslessly: `ps pcpu` is rounded to
41
- * one decimal by `runPs`, and the thread sampler rounds to one decimal too
42
- * (`Math.round(x * 1000) / 10`). A SQLite `REAL` is 8 bytes always; the same
43
- * value as an integer varint is 1-2.
44
- * - **RSS is stored in whole MiB.** A byte-exact residency on a value in the
45
- * hundreds of megabytes is precision no chart can draw, and it costs 3 bytes
46
- * a row.
47
- * - **The key is an `INTEGER` rowid alias.** A composite `TEXT` key cost ~40
48
- * B/row — the string in the row plus its own B-tree — for an idempotence
49
- * this table gets for free somewhere better: {@link NodeLoadRing.record}
50
- * already refuses a sample at or before the newest it holds, and only an
51
- * ACCEPTED sample is written here. The gate is one monotonic comparison in
52
- * memory instead of a unique index on two million rows.
53
- *
54
- * ## Retention is an intention; the row cap is the guarantee
55
- *
56
- * {@link LoadSeriesRetention.retentionHours} is what the operator asked to
57
- * keep. {@link DEFAULT_MAX_ROWS} is what the table is allowed to become. They
58
- * are different promises on purpose: an operator who raises retention to three
59
- * days must not be able to take this table to two million rows with nothing
60
- * stopping it. When the cap bites, the OLDEST rows go and a line says so.
61
- */
62
- import type { IScopedLogger, SettingsStoreClient } from "@camstack/types";
63
- /**
64
- * @durable class=audit owner=metrics-native
65
- * write="one row per process per sample, appended in ONE transaction per
66
- * sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
67
- * subscription to `metrics.node-processes-snapshot`. Emission is
68
- * fixed-cadence, so the write rate is deterministic: nodes x processes /
69
- * cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
70
- * the in-memory ring ACCEPTED is written, which is what makes the append
71
- * idempotent without a unique key."
72
- * retention="TWO bounds, swept together at most once a minute. By AGE, the
73
- * operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
74
- * DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
75
- * Both prune oldest-first through a bounded page — never a full scan — and
76
- * the row cap logs when it bites. Losing the table loses the cold window
77
- * only: the ring keeps serving the last hour and the charts keep drawing."
78
- */
79
- export declare const LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
80
- /**
81
- * Bytes one stored row costs on disk, MEASURED — table plus index, after a
82
- * truncating checkpoint. Used to project the cost of a configuration for the
83
- * operator BEFORE they apply it, so the knob is honest.
84
- */
85
- export declare const MEASURED_BYTES_PER_ROW = 87;
86
- /**
87
- * Sampling cadence bounds, in seconds. Outside this range a cadence is
88
- * REFUSED — see {@link resolveLoadSeriesConfig}.
89
- */
90
- export declare const MIN_CADENCE_SEC = 5;
91
- export declare const MAX_CADENCE_SEC = 60;
92
- export declare const DEFAULT_CADENCE_SEC = 10;
93
- /** Retention the operator asked for, in hours. Six is the shipped default. */
94
- export declare const DEFAULT_RETENTION_HOURS = 6;
95
- export declare const MIN_RETENTION_HOURS = 1;
96
- export declare const MAX_RETENTION_HOURS = 72;
97
- /**
98
- * Hard ceiling on the table, in ROWS.
99
- *
100
- * The guarantee behind the retention knob. Sized against the shipped default
101
- * and against the worst configuration the knob can express:
102
- *
103
- * | cadence | retention | rows (3 nodes, 76 processes) |
104
- * | --- | --- | --- |
105
- * | 10 s | 6 h (default) | 164 160 |
106
- * | 5 s | 6 h | 328 320 |
107
- * | 5 s | 72 h | 3 939 840 — capped here |
108
- *
109
- * 500 000 rows x the measured 87 B = **41.5 MiB**, roughly 3x what the shipped
110
- * default needs and far below the 2 M rows a three-day retention would reach.
111
- * It is deliberately NOT a knob: the point of a guarantee is that it is not
112
- * subject to the same hand that raised the intention. This is the discipline
113
- * the in-memory ring already adopted (`DEFAULT_MAX_TOTAL_PROCESS_ROWS`) and
114
- * the one the RTP pre-roll rings still lack — a maximum per instance and no
115
- * budget at all for the whole, which reached 898 MB across 47 rings
116
- * (`docs/architecture/load-ledger.md` #5).
117
- */
118
- export declare const DEFAULT_MAX_ROWS = 5e5;
119
- /**
120
- * Rows a single prune pass may remove.
121
- *
122
- * The prune is BOUNDED, never a full scan. It reads at most this many keys
123
- * oldest-first through `idx_load_samples_node_at`, takes the last one's `atMs`
124
- * as the effective cutoff and issues ONE `deleteWhere` up to it. A backlog
125
- * larger than a page is worked off across passes rather than in one stall —
126
- * which is the defect corrected in the recorder the same week, where
127
- * `accountingForLocations` visited 6.7 M rows twice a minute.
128
- */
129
- export declare const PRUNE_PAGE_ROWS = 2e4;
130
- /** How often a prune pass may run. A bound, not a deadline. */
131
- export declare const PRUNE_INTERVAL_MS = 6e4;
132
- export declare const LOAD_SERIES_COLUMNS: readonly [{
133
- readonly name: "id";
134
- readonly type: "INTEGER";
135
- readonly primaryKey: true;
136
- readonly notNull: true;
137
- }, {
138
- readonly name: "nodeId";
139
- readonly type: "TEXT";
140
- readonly notNull: true;
141
- }, {
142
- readonly name: "atMs";
143
- readonly type: "INTEGER";
144
- readonly notNull: true;
145
- }, {
146
- readonly name: "pid";
147
- readonly type: "INTEGER";
148
- readonly notNull: true;
149
- }, {
150
- readonly name: "addonId";
151
- readonly type: "TEXT";
152
- }, {
153
- readonly name: "classification";
154
- readonly type: "TEXT";
155
- readonly notNull: true;
156
- }, {
157
- readonly name: "cpuDeci";
158
- readonly type: "INTEGER";
159
- readonly notNull: true;
160
- }, {
161
- readonly name: "rssMib";
162
- readonly type: "INTEGER";
163
- readonly notNull: true;
164
- }, {
165
- readonly name: "cpuMainDeci";
166
- readonly type: "INTEGER";
167
- }, {
168
- readonly name: "cpuGcDeci";
169
- readonly type: "INTEGER";
170
- }];
171
- export declare const LOAD_SERIES_INDEXES: readonly [{
172
- readonly name: "idx_load_samples_node_at";
173
- readonly columns: readonly ["nodeId", "atMs"];
174
- }];
175
- /** One stored process row, in the units the table stores them in. */
176
- export interface LoadSeriesRow {
177
- readonly nodeId: string;
178
- readonly atMs: number;
179
- readonly pid: number;
180
- readonly addonId: string | null;
181
- readonly classification: string;
182
- readonly cpuDeci: number;
183
- readonly rssMib: number;
184
- readonly cpuMainDeci: number | null;
185
- readonly cpuGcDeci: number | null;
186
- }
187
- /** The process shape both tiers hand back — the series' own row, not `ps`'s. */
188
- export interface RetainedProcess {
189
- readonly pid: number;
190
- readonly addonId: string | null;
191
- readonly classification: string;
192
- readonly cpuPercent: number;
193
- readonly memoryRssBytes: number;
194
- readonly cpuMainPercent: number | null;
195
- readonly cpuGcPercent: number | null;
196
- }
197
- /** One retained sample: when it was taken, and the processes in it. */
198
- export interface RetainedSample {
199
- readonly atMs: number;
200
- readonly processes: readonly RetainedProcess[];
201
- }
202
- /** Storage row → the shape a reader gets. The ONE decode. */
203
- export declare function rowToProcess(row: LoadSeriesRow): RetainedProcess;
204
- /** The shape a reader gets → the storage row. The ONE encode. */
205
- export declare function processToRow(nodeId: string, atMs: number, process: RetainedProcess): LoadSeriesRow;
206
- /** Group flat rows into samples, oldest-first. A sample is one `atMs`. */
207
- export declare function rowsToSamples(rows: readonly LoadSeriesRow[]): readonly RetainedSample[];
208
- /** The two bounds in force. */
209
- export interface LoadSeriesRetention {
210
- readonly retentionHours: number;
211
- readonly maxRows: number;
212
- }
213
- export interface LoadSeriesStoreDeps {
214
- readonly store: SettingsStoreClient;
215
- readonly logger: IScopedLogger;
216
- readonly now?: () => number;
217
- readonly prunePageRows?: number;
218
- readonly pruneIntervalMs?: number;
219
- }
220
- /** What one prune pass did. Returned so a test can assert the bound held. */
221
- export interface PruneOutcome {
222
- readonly deletedByAge: number;
223
- readonly deletedByCap: number;
224
- /** Rows read to find the cutoffs — the number that must stay bounded. */
225
- readonly rowsExamined: number;
226
- readonly capBit: boolean;
227
- }
228
- /**
229
- * The cold tier. Append-only, bounded, and never the authority on anything the
230
- * hot ring can answer.
231
- *
232
- * Every method is best-effort and says so in the log rather than throwing at
233
- * the emitter: a storage hiccup must cost a gap in a chart, never a dropped
234
- * snapshot on the bus.
235
- */
236
- export declare class LoadSeriesStore {
237
- private declared;
238
- private lastPruneAtMs;
239
- private readonly store;
240
- private readonly logger;
241
- private readonly nowFn;
242
- private readonly prunePageRows;
243
- private readonly pruneIntervalMs;
244
- constructor(deps: LoadSeriesStoreDeps);
245
- /** Idempotently declare the collection. `false` when the store refused. */
246
- declare(): Promise<boolean>;
247
- /**
248
- * Append ONE sample — every process row of it — in ONE transaction.
249
- *
250
- * Never a write per row. `insertMany` exists for exactly this: at the 10 s
251
- * default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
252
- * on the connection that also serves every cluster-wide configuration read
253
- * is a constant load nobody asked for.
254
- */
255
- append(nodeId: string, atMs: number, processes: readonly RetainedProcess[]): Promise<number>;
256
- /**
257
- * Read one node's cold samples, oldest-first.
258
- *
259
- * `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
260
- * timestamp it holds gets only what it is missing. `limitRows` bounds the
261
- * read in ROWS (not samples) because rows are what the query costs.
262
- */
263
- read(nodeId: string, sinceMs: number, limitRows: number): Promise<readonly RetainedSample[]>;
264
- /**
265
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
266
- *
267
- * Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
268
- * append path must not pay a sweep on every sample.
269
- */
270
- prune(nodeIds: readonly string[], retention: LoadSeriesRetention, force?: boolean): Promise<PruneOutcome | null>;
271
- /** Total rows, or `null` when the store could not answer. */
272
- count(): Promise<number | null>;
273
- /**
274
- * Delete this node's rows older than `cutoff`, at most one page's worth.
275
- *
276
- * The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
277
- * own is one statement but an UNBOUNDED one — a first pass after a retention
278
- * change would delete millions of rows inside a single stalling transaction.
279
- * So the page is read first (keys only, ordered by the index), its last
280
- * `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
281
- */
282
- private pruneNodeToCutoff;
283
- /**
284
- * Drop the oldest rows across the known nodes until `excess` is covered.
285
- *
286
- * Same bounded-page technique, walked per node so the one index serves it.
287
- * The node holding the oldest rows pays first, which is also the node
288
- * producing the pressure when a runaway process count is the cause.
289
- */
290
- private pruneOldestRows;
291
- }
292
- /** Structural validation on read. A malformed row is skipped, never charted. */
293
- export declare function recordToRow(data: Record<string, unknown>): LoadSeriesRow | null;