@camstack/system 1.2.145 → 1.2.147

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 (65) hide show
  1. package/dist/addon-runner.js +2 -1
  2. package/dist/addon-runner.mjs +2 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/load-series-config.d.ts +74 -0
  30. package/dist/builtins/native-metrics/load-series-reader.d.ts +37 -0
  31. package/dist/builtins/native-metrics/load-series-store.d.ts +232 -0
  32. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +118 -35
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1463 -199
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1463 -199
  35. package/dist/builtins/native-metrics/node-load-ring.d.ts +214 -0
  36. package/dist/builtins/native-metrics/process-classification.d.ts +51 -0
  37. package/dist/builtins/native-metrics/thread-cpu-sampler.d.ts +116 -0
  38. package/dist/builtins/platform-probe/index.js +1 -1
  39. package/dist/builtins/platform-probe/index.mjs +1 -1
  40. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  42. package/dist/builtins/snapshot/index.js +1 -1
  43. package/dist/builtins/snapshot/index.mjs +1 -1
  44. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +25 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -1
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{dist-BVU5JADq.js → dist-Dl6MFXPr.js} +496 -35
  56. package/dist/{dist-CG7kwIZp.mjs → dist-DrBc-7Y3.mjs} +479 -36
  57. package/dist/index.d.ts +2 -2
  58. package/dist/index.js +45 -5
  59. package/dist/index.mjs +37 -6
  60. package/dist/kernel/heap-watch.d.ts +209 -1
  61. package/dist/{manifest-python-deps-BRyNCnnG.mjs → manifest-python-deps-DhT0oFOZ.mjs} +200 -8
  62. package/dist/{manifest-python-deps-GjlyPjm0.js → manifest-python-deps-FYZBHc3v.js} +253 -7
  63. package/dist/{retired-settings-keys-_PLI9w0k.js → retired-settings-keys-6w_JOqBg.js} +1 -1
  64. package/dist/{retired-settings-keys-Dm1z4mO4.mjs → retired-settings-keys-CF5r4YKq.mjs} +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,214 @@
1
+ /**
2
+ * In-memory retention for the per-node load snapshots — the shared analogue of
3
+ * the log ring (`packages/system/src/logging/partitioned-log-buffer.ts`), and
4
+ * built to the same shape for the same reason: a fixed-capacity circular
5
+ * partition per key, plus **declared hard ceilings** on the total.
6
+ *
7
+ * ## What this is, and what it deliberately is not
8
+ *
9
+ * `native-metrics` emits `metrics.node-processes-snapshot` every 20 s on every
10
+ * node whether or not anyone is watching (D250). Until now nobody kept it: the
11
+ * Load charts accumulated 180 points inside the browser tab, so closing the
12
+ * page threw the hour away and two operators looking at the same node saw two
13
+ * different windows.
14
+ *
15
+ * This ring RETAINS what already arrives. It adds no timer, no sampler, no
16
+ * probe and no new event — subscribing to a fan-out that exists is the whole
17
+ * mechanism. It is **not** durable retention: nothing here reaches disk, and a
18
+ * stored series remains its own decision with its own resolution, retention and
19
+ * owner (D250 § Consequences).
20
+ *
21
+ * ## It does not know what a load snapshot looks like
22
+ *
23
+ * `TProcess` is a type parameter and the payload is stored **verbatim**. The
24
+ * ring reads exactly two things off the envelope — which node, and when — and
25
+ * never a field of a process row. A field added to the snapshot (a `deviceId`
26
+ * next to the pid, say) flows through untouched, because there is nothing here
27
+ * for it to flow through.
28
+ *
29
+ * ## Measured cost — see `docs/architecture/load-ledger.md`
30
+ *
31
+ * Measured, not estimated, against the real process payloads of the live
32
+ * cluster (`metricsProvider.listNodeProcesses` on all three nodes: 42 + 20 + 14
33
+ * rows) replayed 180 deep and weighed as V8 retained heap after a forced GC:
34
+ *
35
+ * **464 B per process row**, so the live cluster's full hour —
36
+ * 3 nodes x 180 samples x 76 rows = 13 680 rows — is **6.06 MiB**.
37
+ *
38
+ * Linear in rows (checked at 360 samples: 463.9 B/row). Every ceiling below is
39
+ * expressed in ROWS for that reason: rows are what the memory is proportional
40
+ * to, and samples are not.
41
+ */
42
+ /** One retained snapshot: when it arrived, and the payload exactly as it came. */
43
+ export interface LoadSample<TProcess> {
44
+ readonly atMs: number;
45
+ readonly processes: readonly TProcess[];
46
+ }
47
+ /** What a reader gets back for one node. */
48
+ export interface NodeLoadSeries<TProcess> {
49
+ readonly nodeId: string;
50
+ /** Oldest-first, and filtered by the caller's `sinceMs` when given. */
51
+ readonly samples: readonly LoadSample<TProcess>[];
52
+ /** Samples retained for this node right now — independent of `sinceMs`. */
53
+ readonly retainedSamples: number;
54
+ /** Oldest retained sample's timestamp, or `null` when nothing is retained. */
55
+ readonly oldestAtMs: number | null;
56
+ /** The per-node ceiling in force, so a caller can say what it is looking at. */
57
+ readonly capacity: number;
58
+ }
59
+ /**
60
+ * Per-node ceiling, in SAMPLES.
61
+ *
62
+ * 180 at the 20 s process-snapshot cadence is **one hour** — the window the
63
+ * operator asked to keep, and the same 180 the browser buffer used before this
64
+ * existed (`LOAD_SERIES_MAX_POINTS`). The cap is on points, not on age, so a
65
+ * node whose emits are deduped away covers a longer wall-clock window at the
66
+ * same cost.
67
+ */
68
+ export declare const DEFAULT_SAMPLES_PER_NODE = 180;
69
+ /**
70
+ * Fleet ceiling, in PROCESS ROWS across every node.
71
+ *
72
+ * This is the bound the per-node ceiling cannot provide. 180 samples is a fixed
73
+ * number of samples, not a fixed amount of memory: a node whose process count
74
+ * explodes — a leaked tsx tree, an inference pool respawning, the ghost storms
75
+ * the Processes tab exists to surface — multiplies its own partition without
76
+ * ever touching the per-node cap. That is precisely the defect the RTP pre-roll
77
+ * rings have in `docs/architecture/load-ledger.md` (#5): a maximum per instance
78
+ * and no budget for the fleet, which reached 898 MB across 47 rings.
79
+ *
80
+ * 32 000 rows x the measured 464 B = **14.2 MiB**, which is deliberately the
81
+ * same envelope hub-main already accepted for the log ring
82
+ * (`DEFAULT_MAX_TOTAL_LOG_ENTRIES` 50 000 x ~306 B ~ 15 MB). It is 2.3x the
83
+ * live cluster's full hour (13 680 rows), so no real deployment evicts on it —
84
+ * it exists so a runaway node costs 14 MiB instead of everything.
85
+ */
86
+ export declare const DEFAULT_MAX_TOTAL_PROCESS_ROWS = 32000;
87
+ /**
88
+ * Ceiling on the number of PARTITIONS.
89
+ *
90
+ * The backstop for an unbounded key space, exactly as `DEFAULT_MAX_LOG_BUCKETS`
91
+ * is: a partition is created on first write, and a node id that appears once —
92
+ * a renamed agent, a node that joined for an afternoon — must not hold a ring
93
+ * for the life of the process. 16 is >5x the live fleet of 3.
94
+ */
95
+ export declare const DEFAULT_MAX_NODES = 16;
96
+ /**
97
+ * A partition whose newest sample is older than this is dropped whole.
98
+ *
99
+ * **Why age, and why this age.** The alternative — evict when the node leaves
100
+ * the cluster registry — was rejected twice over: it couples this ring to a
101
+ * registry it has no other reason to know, and a node that is merely offline is
102
+ * never "removed", so the case that actually happens (an agent that stops
103
+ * answering) would keep its partition forever. That is the failure mode being
104
+ * bounded.
105
+ *
106
+ * The number is not chosen, it is derived: `DEFAULT_SAMPLES_PER_NODE` x the 20 s
107
+ * snapshot cadence = **one hour** = the entire window this ring can represent.
108
+ * A node silent for that long holds nothing that overlaps the hour an operator
109
+ * is asking about; every point in it has aged out of the question. Below that
110
+ * threshold the partition is still answering a live question and is kept, even
111
+ * though the node is quiet — a node emits at least once a minute
112
+ * (`METRICS_SNAPSHOT_HEARTBEAT_MS`), so an hour of silence is not a slow node,
113
+ * it is a gone one.
114
+ */
115
+ export declare const DEFAULT_NODE_IDLE_EVICTION_MS = 3600000;
116
+ export interface NodeLoadRingOptions {
117
+ readonly samplesPerNode?: number;
118
+ readonly maxTotalProcessRows?: number;
119
+ readonly maxNodes?: number;
120
+ readonly idleEvictionMs?: number;
121
+ /** Injectable clock. Tests drive eviction without sleeping for an hour. */
122
+ readonly now?: () => number;
123
+ }
124
+ /**
125
+ * The partitioned ring: one {@link LoadPartition} per node id.
126
+ *
127
+ * Three bounds, doing three different jobs — the same division as the log
128
+ * buffer, for the same reason:
129
+ *
130
+ * - **`samplesPerNode`** is HARD and per partition. It is what the operator
131
+ * asked for (an hour) and it can never be exceeded.
132
+ * - **`maxTotalProcessRows`** is the FLEET budget, and it is denominated in the
133
+ * thing memory is proportional to. When it bites, the oldest sample of the
134
+ * HEAVIEST partition is dropped — the node creating the pressure is the node
135
+ * that pays, and a quiet node keeps its full hour. The node just written to is
136
+ * NOT protected here: if it is the heaviest, it is the culprit.
137
+ * - **`maxNodes`** is the backstop against an unbounded key space, and it drops
138
+ * whole partitions least-recently-written first, never the one being written.
139
+ *
140
+ * Plus {@link DEFAULT_NODE_IDLE_EVICTION_MS}, which is what stops a node that
141
+ * simply stopped emitting from living in memory forever.
142
+ */
143
+ export declare class NodeLoadRing<TProcess> {
144
+ private readonly partitions;
145
+ /**
146
+ * Node id → write ordinal of its last accepted sample. A monotonic counter,
147
+ * not a clock: partition eviction must follow the order writes actually
148
+ * happened, and node clocks disagree.
149
+ */
150
+ private readonly lastWriteSeq;
151
+ private writeSeq;
152
+ private totalRows;
153
+ private readonly samplesPerNode;
154
+ private readonly maxTotalProcessRows;
155
+ private readonly maxNodes;
156
+ private readonly idleEvictionMs;
157
+ private readonly now;
158
+ constructor(options?: NodeLoadRingOptions);
159
+ /**
160
+ * Retain one snapshot. `processes` is stored by reference — the payload is
161
+ * already an immutable arrival off the bus, and copying it would double the
162
+ * measured cost for nothing.
163
+ *
164
+ * Returns whether the sample was ACCEPTED — that is, whether it was new
165
+ * rather than a replay of a timestamp this node has already delivered.
166
+ *
167
+ * The return value is not diagnostics. It is the idempotence gate the
168
+ * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
169
+ * appended to the table, which is what lets that table carry an `INTEGER`
170
+ * rowid key instead of a composite unique index over two million rows. The
171
+ * gate is one monotonic comparison in memory; the index it replaces was
172
+ * measured at ~40 bytes per row.
173
+ */
174
+ record(nodeId: string, atMs: number, processes: readonly TProcess[]): boolean;
175
+ /**
176
+ * Read one node's retained series, oldest-first.
177
+ *
178
+ * `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
179
+ * holds and gets back only what it is missing. That is the whole contract
180
+ * that lets the admin UI seed from here and then continue live without
181
+ * doubling a point it already drew.
182
+ *
183
+ * A node nobody has recorded answers with an empty series, not an error —
184
+ * unknown is the truth about a node that has not reported.
185
+ */
186
+ read(nodeId: string, sinceMs?: number): NodeLoadSeries<TProcess>;
187
+ /** Node ids with a live partition. Observability for the fleet bound. */
188
+ nodeIds(): readonly string[];
189
+ /** Process rows retained across every partition — the number that IS memory. */
190
+ rowCount(): number;
191
+ /** Samples retained across every partition. */
192
+ sampleCount(): number;
193
+ /**
194
+ * Drop partitions whose newest sample is older than the retention window.
195
+ *
196
+ * Lazy, on write and on read — never a timer. A timer would be a new
197
+ * periodic cost in a subsystem whose entire premise is that it adds none,
198
+ * and a ring that nobody writes to and nobody reads is not growing either.
199
+ */
200
+ private sweepIdle;
201
+ /**
202
+ * Bring the fleet back under the row budget by dropping the OLDEST sample of
203
+ * the HEAVIEST partition, repeatedly.
204
+ *
205
+ * Terminates: every iteration removes one sample from a non-empty partition,
206
+ * and the population of samples is finite and strictly decreasing.
207
+ */
208
+ private enforceRowBudget;
209
+ private heaviestPartition;
210
+ /** Drop `count` whole partitions, least-recently-written first. */
211
+ private evictPartitions;
212
+ /** Remove a partition and everything that indexes it. */
213
+ private dropPartition;
214
+ }
@@ -0,0 +1,51 @@
1
+ import { NodeProcess } from '@camstack/types';
2
+ import { ThreadCpuSplit } from './thread-cpu-sampler.js';
3
+ /** The classes a node process can carry. Mirrors `NodeProcessSchema`. */
4
+ export type ProcessClassification = NodeProcess['classification'];
5
+ /** One parsed `ps -eo pid,ppid,pgid,pcpu,rss,etime,command` row. */
6
+ export interface PsProcessRow {
7
+ readonly pid: number;
8
+ readonly ppid: number;
9
+ readonly pgid: number;
10
+ readonly cpuPercent: number;
11
+ readonly memoryRssBytes: number;
12
+ readonly uptimeSec: number;
13
+ readonly command: string;
14
+ }
15
+ /** A pid the kernel's `$process.list` attributes to an addon runner. */
16
+ export interface ManagedProcessEntry {
17
+ readonly addonId: string;
18
+ readonly nodeId: string;
19
+ }
20
+ export interface BuildNodeProcessesInput {
21
+ /** Every visible `ps` row on this node, camstack-shaped or not. */
22
+ readonly psRows: readonly PsProcessRow[];
23
+ /** This node's own pid. */
24
+ readonly selfPid: number;
25
+ /** Node id reported for the `root` row. */
26
+ readonly selfNodeId: string;
27
+ /** pid → addon attribution from `$process.list`. */
28
+ readonly managed: ReadonlyMap<number, ManagedProcessEntry>;
29
+ /** pid → thread CPU split, when the sampler had an answer this tick. */
30
+ readonly cpuSplits: ReadonlyMap<number, ThreadCpuSplit>;
31
+ }
32
+ /**
33
+ * Regex matching camstack-related command lines. We surface anything with
34
+ * an obvious camstack fingerprint so operators can spot stray inference
35
+ * pools, leaked tsx watchers, and zombie node workers in the Cluster →
36
+ * Agent → Processes tab.
37
+ *
38
+ * Patterns mirror `scripts/hunt-zombies.sh` — keep them in sync when
39
+ * adding a new long-lived subprocess.
40
+ */
41
+ export declare const CAMSTACK_CMD_RE: RegExp;
42
+ /**
43
+ * Classify one pid.
44
+ *
45
+ * Identity-driven, not ancestry-driven: a process is `root` because it IS
46
+ * this node, `managed` because the kernel named it, and `system` otherwise.
47
+ * There is no third source of truth to consult and no walk to get wrong.
48
+ */
49
+ export declare function classifyProcess(pid: number, selfPid: number, managed: ReadonlyMap<number, ManagedProcessEntry>): ProcessClassification;
50
+ /** Turn a `ps` snapshot into the node's process table. */
51
+ export declare function buildNodeProcesses(input: BuildNodeProcessesInput): readonly NodeProcess[];
@@ -0,0 +1,116 @@
1
+ /** Raw tick counts for one process at one instant, already classified. */
2
+ export interface ThreadTickSample {
3
+ /** Sum of utime+stime over threads that are NOT in the V8 helper pool. */
4
+ readonly mainTicks: number;
5
+ /** Sum of utime+stime over the V8 helper-pool threads. */
6
+ readonly gcTicks: number;
7
+ /** Number of threads seen, both classes together. */
8
+ readonly threadCount: number;
9
+ /** `Date.now()` at the moment the sample was taken. */
10
+ readonly atMs: number;
11
+ /**
12
+ * The process's `starttime` (field 22 of `/proc/<pid>/stat`) — jiffies since
13
+ * boot at which THIS incarnation of the pid began. Read from the main
14
+ * thread, whose tid equals the pid.
15
+ *
16
+ * It is the process's identity, and it is the only thing that distinguishes
17
+ * "the same process, later" from "a different process that got the same pid".
18
+ * `null` when the main thread's stat could not be read, which is also
19
+ * treated as "identity unknown" — see {@link cpuSplitBetween}.
20
+ */
21
+ readonly startTicks: number | null;
22
+ }
23
+ /** The CPU split over the window between two {@link ThreadTickSample}s. */
24
+ export interface ThreadCpuSplit {
25
+ /** CPU% of the process's own threads. 100 = one core saturated. */
26
+ readonly mainPercent: number;
27
+ /** CPU% of the V8 helper pool — see the module docblock on what this holds. */
28
+ readonly gcPercent: number;
29
+ /** Threads seen in the newer sample. */
30
+ readonly threadCount: number;
31
+ }
32
+ /**
33
+ * Parse one `/proc/<pid>/task/<tid>/stat` line.
34
+ *
35
+ * The `comm` field is field 2 and is wrapped in parentheses — and it can
36
+ * itself contain spaces and parentheses (`(V8Worker)`, but also
37
+ * `(some (weird) name)`). Splitting the line on whitespace therefore corrupts
38
+ * every field after it. The only correct parse is to cut at the LAST `)`, take
39
+ * `comm` from inside the parens, and index the remaining fields from there:
40
+ * after the cut, field 3 (`state`) is index 0, so `utime` (field 14) is index
41
+ * 11 and `stime` (field 15) is index 12.
42
+ *
43
+ * `starttime` is field 22, so index 19 by the same arithmetic. It is the
44
+ * thread's own start, which for the main thread (tid == pid) is the PROCESS's
45
+ * start — the identity {@link cpuSplitBetween} checks before it believes a
46
+ * window belongs to the process it belonged to last time.
47
+ *
48
+ * Returns `null` for a line that does not parse — a thread that exited between
49
+ * the readdir and the read leaves a truncated or empty file, and that is
50
+ * normal, not an error.
51
+ */
52
+ export interface ParsedThreadStat {
53
+ readonly comm: string;
54
+ readonly ticks: number;
55
+ /** Field 22 (`starttime`), in jiffies since boot. */
56
+ readonly startTicks: number;
57
+ }
58
+ export declare function parseThreadStat(line: string): ParsedThreadStat | null;
59
+ /** Injectable procfs surface — the whole reason this module is testable. */
60
+ export interface ProcFsReader {
61
+ readonly listTaskIds: (pid: number) => Promise<readonly string[]>;
62
+ readonly readTaskStat: (pid: number, tid: string) => Promise<string>;
63
+ }
64
+ export declare const nodeProcFsReader: ProcFsReader;
65
+ /**
66
+ * Read one instant's tick counts for `pid`, or `null` when the process has no
67
+ * readable `/proc/<pid>/task` — it exited, we lack permission, or this is not
68
+ * Linux. `null` is "unknown"; it must never be turned into a zero downstream.
69
+ */
70
+ export declare function readThreadTicks(pid: number, reader?: ProcFsReader): Promise<ThreadTickSample | null>;
71
+ /**
72
+ * The CPU split between two samples of the SAME process.
73
+ *
74
+ * Returns `null` — never a zero — when the window cannot produce a rate:
75
+ * - the two samples are not separated in time (no window to divide by);
76
+ * - the pid's `starttime` changed, or is unknown on either side. A changed
77
+ * starttime means the pid was RECYCLED: this is a different process that
78
+ * happens to wear the same number. See below for why the tick check alone
79
+ * does not catch it;
80
+ * - either tick count went BACKWARDS, which is the same fault seen from the
81
+ * other side. A negative delta divided by a window would render as a
82
+ * plausible small number for the wrong process, and a plausible wrong
83
+ * number is the failure this whole surface exists to avoid.
84
+ *
85
+ * ## Why `starttime` and not just the backwards-tick check
86
+ *
87
+ * The backwards check only fires when the NEW process has burned FEWER ticks
88
+ * than the dead one had. A recycled pid landing on a busier process — an
89
+ * ffmpeg that has been transcoding for a minute inheriting the pid of an
90
+ * ffmpeg that idled — produces a large POSITIVE delta, which divides cleanly
91
+ * and renders as a spike. That is not a hypothetical rounding concern now that
92
+ * the process snapshot attributes each pid to a camera: it would charge one
93
+ * camera for another camera's whole lifetime of CPU, in a chart built
94
+ * specifically to answer "why is 617 worse than 615". `starttime` is the pid's
95
+ * incarnation number and it catches both directions.
96
+ */
97
+ export declare function cpuSplitBetween(prev: ThreadTickSample, next: ThreadTickSample): ThreadCpuSplit | null;
98
+ /**
99
+ * Keeps the previous tick sample per pid so the next process-snapshot tick can
100
+ * turn it into a rate.
101
+ *
102
+ * Bounded by construction: {@link sample} replaces the whole map with only the
103
+ * pids it was just asked about, so a pid that stops appearing in the scan is
104
+ * dropped on the following tick rather than accumulating forever.
105
+ */
106
+ export declare class ThreadCpuTracker {
107
+ private readonly reader;
108
+ private previous;
109
+ constructor(reader?: ProcFsReader);
110
+ /**
111
+ * Sample `pids` and return the split for each one that HAS a usable previous
112
+ * sample. A pid absent from the result has no answer yet — the caller must
113
+ * report `null`, not `0`.
114
+ */
115
+ sample(pids: readonly number[]): Promise<ReadonlyMap<number, ThreadCpuSplit>>;
116
+ }
@@ -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-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Et as BaseAddon, Qt as EventCategory, Tt as errMsg, V as enumerateInferenceDevices, Vt as emitReadiness, ht as scoreRuntimes, lt as platformProbeCapability } from "../../dist-CG7kwIZp.mjs";
2
+ import { Ot as errMsg, V as enumerateInferenceDevices, Wt as emitReadiness, kt as BaseAddon, tn as EventCategory, ut as platformProbeCapability, vt as scoreRuntimes } from "../../dist-DrBc-7Y3.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Et as BaseAddon, Qt as EventCategory } from "../../dist-CG7kwIZp.mjs";
1
+ import { kt as BaseAddon, tn as EventCategory } from "../../dist-DrBc-7Y3.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  let zod = require("zod");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
@@ -1,4 +1,4 @@
1
- import { At as DeviceFeature, Et as BaseAddon, Gt as nodePin, I as deriveBatteryPresence, Mt as DeviceType, O as bareAddonId, Qt as EventCategory, Tt as errMsg, _t as snapshotCapability, i as BatteryStatusSchema, k as batteryCapability, xt as streamQualityLabel } from "../../dist-CG7kwIZp.mjs";
1
+ import { Ft as DeviceType, I as deriveBatteryPresence, Jt as nodePin, Nt as DeviceFeature, O as bareAddonId, Ot as errMsg, bt as snapshotCapability, i as BatteryStatusSchema, k as batteryCapability, kt as BaseAddon, tn as EventCategory, wt as streamQualityLabel } from "../../dist-DrBc-7Y3.mjs";
2
2
  import { z } from "zod";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
@@ -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-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Et as BaseAddon, K as filesystemBrowseCapability, bt as storageProviderCapability } from "../../dist-CG7kwIZp.mjs";
1
+ import { Ct as storageProviderCapability, K as filesystemBrowseCapability, kt as BaseAddon } from "../../dist-DrBc-7Y3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -1,7 +1,16 @@
1
- import { CollectionColumn, CollectionIndex, DataStoreEngineInfo, HistogramBucket, IScopedLogger, ISettingsBackend, QueryFilter, SettingsCountInput, SettingsDeleteInput, SettingsGetInput, SettingsAggregateInput, SettingsAggregateResult, SettingsHistogramInput, SettingsInsertInput, SettingsIsEmptyInput, SettingsQueryInput, SettingsRecord, SettingsSetInput, SettingsUpdateInput } from '@camstack/types';
1
+ import { BulkRecord, CollectionColumn, CollectionIndex, DataStoreEngineInfo, HistogramBucket, IScopedLogger, ISettingsBackend, QueryFilter, SettingsCountInput, SettingsDeleteInput, SettingsGetInput, SettingsAggregateInput, SettingsAggregateResult, SettingsHistogramInput, SettingsInsertInput, SettingsIsEmptyInput, SettingsQueryInput, SettingsRecord, SettingsSetInput, SettingsUpdateInput } from '@camstack/types';
2
2
  import { default as Database } from 'better-sqlite3';
3
3
  import { MutationFilterInput } from './filter-compiler.js';
4
4
  import { SqliteOpProfiler } from './query-profiler.js';
5
+ /** Input for {@link SqliteSettingsBackend.insertMany}. */
6
+ interface SettingsInsertManyInput {
7
+ readonly namespace?: string;
8
+ readonly collection: string;
9
+ readonly records: readonly BulkRecord[];
10
+ }
11
+ interface SettingsInsertManyResult {
12
+ readonly inserted: number;
13
+ }
5
14
  /** Input for {@link SqliteSettingsBackend.deleteWhere}. */
6
15
  interface SettingsDeleteWhereInput {
7
16
  readonly namespace?: string;
@@ -148,6 +157,21 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
148
157
  set({ namespace, collection, key, value }: SettingsSetInput): Promise<void>;
149
158
  query<T extends object = Record<string, unknown>>({ namespace, collection, filter, columns, }: SettingsQueryInput): Promise<readonly SettingsRecord<T>[]>;
150
159
  insert<T extends object = Record<string, unknown>>({ namespace, collection, record, }: SettingsInsertInput<T>): Promise<void>;
160
+ /**
161
+ * Insert a batch in ONE transaction, on ONE prepared statement.
162
+ *
163
+ * The write-side twin of {@link deleteWhere}. Every row in the batch shares
164
+ * a single column list, which is what makes one `prepare` legal: rows are
165
+ * normalised to the collection's DECLARED column set, so a row that omits a
166
+ * column binds `null` for it rather than producing a second statement shape.
167
+ *
168
+ * All or nothing — `better-sqlite3`'s `transaction()` rolls the whole batch
169
+ * back on any throw. A caller writing one process sample gets one COMMIT,
170
+ * which is the entire point: 76 rows every 10 s is 7.6 rows/s, and 7.6
171
+ * separate commits per second on the connection that also serves every
172
+ * cluster-wide configuration read is a constant load nobody asked for.
173
+ */
174
+ insertMany({ namespace, collection, records, }: SettingsInsertManyInput): Promise<SettingsInsertManyResult>;
151
175
  update({ namespace, collection, id, data }: SettingsUpdateInput): Promise<void>;
152
176
  delete({ namespace, collection, key }: SettingsDeleteInput): Promise<void>;
153
177
  /**
@@ -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-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -663,6 +663,7 @@ function createDataStoreDispatch(getEngines) {
663
663
  set: async (input) => (await engine()).set(input),
664
664
  query: async (input) => (await engine()).query(input),
665
665
  insert: async (input) => (await engine()).insert(input),
666
+ insertMany: async (input) => (await engine()).insertMany(input),
666
667
  update: async (input) => (await engine()).update(input),
667
668
  delete: async (input) => (await engine()).delete(input),
668
669
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
@@ -1,4 +1,4 @@
1
- import { Et as BaseAddon, Kt as parseJsonObject, b as StorageMigrationJobSchema, gt as settingsStoreCapability, vt as storageCapability, y as StorageLocationTypeSchema, yt as storageMigrationCapability } from "../../dist-CG7kwIZp.mjs";
1
+ import { St as storageMigrationCapability, Yt as parseJsonObject, b as StorageMigrationJobSchema, kt as BaseAddon, xt as storageCapability, y as StorageLocationTypeSchema, yt as settingsStoreCapability } from "../../dist-DrBc-7Y3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -656,6 +656,7 @@ function createDataStoreDispatch(getEngines) {
656
656
  set: async (input) => (await engine()).set(input),
657
657
  query: async (input) => (await engine()).query(input),
658
658
  insert: async (input) => (await engine()).insert(input),
659
+ insertMany: async (input) => (await engine()).insertMany(input),
659
660
  update: async (input) => (await engine()).update(input),
660
661
  delete: async (input) => (await engine()).delete(input),
661
662
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { Et as BaseAddon, Tt as errMsg, Ut as hydrateSchema } from "../../dist-CG7kwIZp.mjs";
1
+ import { Kt as hydrateSchema, Ot as errMsg, kt as BaseAddon } from "../../dist-DrBc-7Y3.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BVU5JADq.js");
6
+ const require_dist = require("../../dist-Dl6MFXPr.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { Et as BaseAddon, nt as logDestinationCapability } from "../../dist-CG7kwIZp.mjs";
1
+ import { kt as BaseAddon, rt as logDestinationCapability } from "../../dist-DrBc-7Y3.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";