@camstack/system 1.2.151 → 1.2.153

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 (66) hide show
  1. package/dist/addon-runner.js +14 -15
  2. package/dist/addon-runner.mjs +14 -14
  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 +3 -3
  26. package/dist/builtins/local-network/local-network.addon.mjs +3 -3
  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/cgroup-memory.d.ts +193 -0
  30. package/dist/builtins/native-metrics/container-memory-series.d.ts +226 -0
  31. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +26 -0
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1536 -661
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1536 -660
  34. package/dist/builtins/platform-probe/index.js +3 -3
  35. package/dist/builtins/platform-probe/index.mjs +2 -2
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +2 -2
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +2 -2
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-CbRciAbu.mjs → dist-BLhrrxIs.mjs} +63 -4
  51. package/dist/{dist-CImxMt5h.js → dist-C7uZ_fOb.js} +63 -4
  52. package/dist/index.d.ts +6 -2
  53. package/dist/index.js +24 -5
  54. package/dist/index.mjs +5 -5
  55. package/dist/kernel/event-plane-report.d.ts +202 -0
  56. package/dist/kernel/heap-spaces.d.ts +138 -0
  57. package/dist/kernel/heap-watch.d.ts +142 -5
  58. package/dist/kernel/index.d.ts +2 -0
  59. package/dist/kernel/transport/local-child-registry.d.ts +22 -0
  60. package/dist/{manifest-python-deps-BqWrDjSN.js → manifest-python-deps-CRTx_j4k.js} +636 -15
  61. package/dist/{manifest-python-deps-DuQ3Uqoq.mjs → manifest-python-deps-QompQxhG.mjs} +522 -15
  62. package/dist/{retired-settings-keys-C7gLUS3x.js → retired-settings-keys-CBU4-TJO.js} +1 -1
  63. package/dist/{retired-settings-keys-DhFPXIRD.mjs → retired-settings-keys-q9mt_DLX.mjs} +1 -1
  64. package/package.json +1 -1
  65. package/dist/{tls-u8QCJCFE.js → tls-BxQlomxd.js} +1 -1
  66. package/dist/{tls-CQhPGSJm.mjs → tls-DOTmtLCW.mjs} +1 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * event-plane-report — the fan-out counters this cluster has always kept and
3
+ * never read, on the line it already emits.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * hub-main's memory is a RATE, not a set. Measured 2026-08-29 over the first 25
8
+ * minutes of telemetry this process has ever produced: every forced compaction
9
+ * drops `arrayBuffers` to **12–29 MB** — ten passes out of ten — so a reading of
10
+ * 726 MB is 707 MB of dead Buffers not yet swept, not a retained set. The real
11
+ * quantity is the slope: **181–330 MB/min steady, with one burst of ≥1.3 GB in
12
+ * ≤11 s**.
13
+ *
14
+ * And it is sockets, not media. `/proc/<pid>/io` over the same windows: ~21 800
15
+ * reads/s and ~22 500 writes/s at a **0.33 KB mean**, with regular-file reads at
16
+ * 0.73 MB/min. A 64 KB proxy chunk or a 200 KB JPEG cannot hide inside a 0.33 KB
17
+ * mean, so the cost is per MESSAGE on a fan-out plane nobody has ever counted —
18
+ * an IDLE addon still reads 82 messages/s of it, across ~38 runners.
19
+ *
20
+ * The strongest single number available is that **78–82% of hub-main's outbound
21
+ * bytes correlate with `hub/pipeline-analytics`, in both windows** — but `rchar`
22
+ * on that runner also counts page-cache reads and it owns a vector store, so the
23
+ * correlation is not an attribution. `fanoutTop` turns it into one.
24
+ *
25
+ * ## And the burn-in that has been running forever
26
+ *
27
+ * `event-bus-core.ts:310` states the procedure in its own words: *"Recommended
28
+ * first fleet-wide enable: `shadow` on both, watch the suppressed counters
29
+ * (`getMoleculerEventStats` / `getChildEventStats`), then flip to `filter` once
30
+ * burn-in shows zero would-be-starved deliveries."*
31
+ *
32
+ * The hub runs with `CAMSTACK_MOLECULER_EVENT_FANOUT=shadow` — which computes
33
+ * the drop decision, counts the would-suppresses and **still delivers every
34
+ * event** — and **neither counter has ever had a production caller**: both
35
+ * functions are reachable only from barrel re-exports and unit tests. So the
36
+ * burn-in has no end condition, and the promotion to `filter` never happened
37
+ * because the evidence that would authorise it was never surfaced. This module
38
+ * surfaces it. It changes no default and flips no env: the promotion stays a
39
+ * one-env decision for the operator, and it now has a number behind it.
40
+ *
41
+ * Read the modes with the counts, always. A suppressed count of 0 means
42
+ * "nothing would have been dropped" under `shadow` and "nothing WAS dropped"
43
+ * under `filter`, and those are different claims. A count printed without its
44
+ * regime is unreadable, and the regime is read ONCE at construction — invisible
45
+ * at runtime until now.
46
+ *
47
+ * ## The fail-open nobody can see
48
+ *
49
+ * A child that never declares its pattern set (`eventPatterns === null`,
50
+ * `local-child-registry.ts:170`) makes the per-child gate fail OPEN, and per
51
+ * `setNodeEventInterest` ANY undeclared child makes the whole node's inbound
52
+ * cross-node gate fail open too. **One legacy runner silently disables the
53
+ * filtering on both planes**, and nothing reported it. `childrenUndeclared` is
54
+ * printed even at zero for the same reason `largeObjectSpace=0MB` is: absence
55
+ * must never be ambiguous.
56
+ *
57
+ * ## Deltas, never totals
58
+ *
59
+ * Both registries count cumulatively for the life of the process. A cumulative
60
+ * number on a heartbeat answers "how much since boot", which nobody asked; the
61
+ * question is "which peer receives the 22 500 writes/s", and that is a rate. So
62
+ * the meter is read ONCE PER LINE and reports the window since the previous read
63
+ * — the same discipline the loop-delay histogram already follows, for the same
64
+ * reason.
65
+ *
66
+ * ## What this is NOT evidence of
67
+ *
68
+ * `shadow` is the CROSS-NODE plane. The local UDS plane defaults to `filter` and
69
+ * its env is unset, so it is filtering. hub-main's ~44 000 socket syscalls/s are
70
+ * mostly to LOCAL runners, so the shadow mode is a real defect and is **not**
71
+ * established as the source of that load. Do not read these two facts as one.
72
+ */
73
+ /** Fan-out regime for one plane. Same three values on both. */
74
+ export type EventFanoutMode = 'filter' | 'shadow' | 'broadcast';
75
+ /** One child's fan-out counters, as the registry keeps them (cumulative). */
76
+ export interface ChildFanoutSample {
77
+ readonly childId: string;
78
+ readonly sent: number;
79
+ readonly suppressed: number;
80
+ }
81
+ /** One child's fan-out over the window since the previous read. */
82
+ export interface ChildFanoutDelta {
83
+ readonly childId: string;
84
+ readonly sent: number;
85
+ readonly suppressed: number;
86
+ }
87
+ /** What `LocalChildRegistry` exposes, structurally — the kernel's heartbeat
88
+ * must not depend on the transport layer's class. */
89
+ export interface ChildFanoutRegistryChild {
90
+ readonly childId: string;
91
+ }
92
+ export interface ChildFanoutRegistryStats {
93
+ readonly sent: number;
94
+ readonly suppressed: number;
95
+ }
96
+ export interface ChildFanoutRegistry {
97
+ readonly listChildren: () => readonly ChildFanoutRegistryChild[];
98
+ readonly getChildEventStats: (childId: string) => ChildFanoutRegistryStats | null;
99
+ readonly undeclaredChildCount: () => number;
100
+ readonly eventFanoutMode: EventFanoutMode;
101
+ }
102
+ /** What `getMoleculerEventStats` returns, structurally. */
103
+ export interface CrossNodeEventStats {
104
+ readonly delivered: number;
105
+ readonly suppressed: number;
106
+ }
107
+ /** One cumulative reading of both planes. */
108
+ export interface EventPlaneCounters {
109
+ readonly udsMode: EventFanoutMode;
110
+ readonly crossNodeMode: EventFanoutMode;
111
+ readonly children: readonly ChildFanoutSample[];
112
+ readonly childrenConnected: number;
113
+ readonly childrenUndeclared: number;
114
+ readonly crossNodeDelivered: number;
115
+ readonly crossNodeSuppressed: number;
116
+ }
117
+ /** `undefined` = this process has no event plane to report (every forked
118
+ * runner, and hub-main before the registry exists). */
119
+ export type EventPlaneReader = () => EventPlaneCounters | undefined;
120
+ /** Where a reader gets each part. Every one is late-bound, because the
121
+ * heartbeat starts before any of them exist. */
122
+ export interface EventPlaneSources {
123
+ readonly registry: () => ChildFanoutRegistry | null;
124
+ readonly crossNodeStats: () => CrossNodeEventStats | null;
125
+ readonly crossNodeMode: () => EventFanoutMode;
126
+ }
127
+ /**
128
+ * Build a reader over the two registries.
129
+ *
130
+ * Returns `undefined` until the child registry exists — the heartbeat starts as
131
+ * the FIRST statement of `bootstrap()` (a process that dies early still leaves
132
+ * samples) and `LocalChildRegistry` is built hundreds of lines later inside
133
+ * `MoleculerService.onModuleInit`. Printing zeros in that window would report a
134
+ * silent plane where there is no plane at all.
135
+ *
136
+ * Wrapped: a registry mid-teardown may throw, and a diagnostic that can crash
137
+ * the process it watches is worse than none.
138
+ */
139
+ export declare function createEventPlaneReader(sources: EventPlaneSources): EventPlaneReader;
140
+ /**
141
+ * How many children get named on the line.
142
+ *
143
+ * Five, because the question is which peer DOMINATES and the measured
144
+ * distribution is not flat: one peer accounted for 78–82% of outbound bytes and
145
+ * the runner-up for ~6%. Five names the dominant peer, its challenger and enough
146
+ * of the tail to see the shape change; naming all ~38 would put a kilobyte-long
147
+ * line in the hub's shared log every minute, which is how a log stops being read.
148
+ */
149
+ export declare const EVENT_PLANE_TOP_N = 5;
150
+ /** Both planes over one heartbeat window. */
151
+ export interface EventPlaneWindow {
152
+ readonly udsMode: EventFanoutMode;
153
+ readonly crossNodeMode: EventFanoutMode;
154
+ readonly childrenConnected: number;
155
+ readonly childrenUndeclared: number;
156
+ readonly fanoutSent: number;
157
+ readonly fanoutSuppressed: number;
158
+ readonly crossNodeDelivered: number;
159
+ readonly crossNodeSuppressed: number;
160
+ /** Largest `sent` first, at most {@link EVENT_PLANE_TOP_N} entries. */
161
+ readonly top: readonly ChildFanoutDelta[];
162
+ }
163
+ /** Previous cumulative reading, kept between lines. */
164
+ export interface EventPlaneBaseline {
165
+ readonly children: ReadonlyMap<string, ChildFanoutSample>;
166
+ readonly crossNodeDelivered: number;
167
+ readonly crossNodeSuppressed: number;
168
+ }
169
+ /** A process that has never read a plane. */
170
+ export declare const EMPTY_EVENT_PLANE_BASELINE: EventPlaneBaseline;
171
+ /**
172
+ * Subtract the previous cumulative reading from the current one.
173
+ *
174
+ * A child that RESTARTED has counters that went backwards — the registry keys by
175
+ * childId and a respawned runner reconnects under the same id with a fresh
176
+ * entry. Clamping at 0 reports the restart as a quiet window rather than as a
177
+ * negative rate: the window's traffic is genuinely unknown, and a negative
178
+ * number on a rate reads as a broken meter.
179
+ */
180
+ export declare function diffEventPlane(baseline: EventPlaneBaseline, current: EventPlaneCounters, topN?: number): EventPlaneWindow;
181
+ /** Reads the delta since the PREVIOUS read. Same contract as the loop meter. */
182
+ export interface EventPlaneMeter {
183
+ readonly read: () => EventPlaneWindow | undefined;
184
+ }
185
+ export declare function createEventPlaneMeter(reader: EventPlaneReader, topN?: number): EventPlaneMeter;
186
+ /**
187
+ * The suffix appended to a `[mem]` line — leading space included, empty when
188
+ * this process has no event plane.
189
+ *
190
+ * Only two processes in the cluster have one (hub-main, and an agent's main), so
191
+ * these fields never appear on the ~38 runner lines that share the same log
192
+ * stream. On the two that do, every field is printed on every line INCLUDING at
193
+ * zero: the modes because a suppressed count without its regime is unreadable,
194
+ * `childrenUndeclared` because one undeclared child silently disables both
195
+ * gates, and the deltas because "the plane went quiet for a minute" is a
196
+ * reading, not a blank.
197
+ *
198
+ * `fanoutTop` is the one conditional field — a ranked list of nothing is noise —
199
+ * and it is a single comma-separated value so the field COUNT does not change
200
+ * from line to line: `childId:sent/suppressed`, largest first.
201
+ */
202
+ export declare function formatEventPlane(window: EventPlaneWindow | undefined): string;
@@ -0,0 +1,138 @@
1
+ import { HeapWatchSink } from './heap-watch.js';
2
+ /** One V8 space, as V8 names it, with the bytes it is holding. */
3
+ export interface HeapSpaceUsage {
4
+ /** V8's own `space_name`, e.g. `old_space`. Never renamed here. */
5
+ readonly name: string;
6
+ readonly usedBytes: number;
7
+ }
8
+ /** A reading of every space. Injectable so a test can pose a known heap state. */
9
+ export type HeapSpaceReader = () => readonly HeapSpaceUsage[];
10
+ /**
11
+ * The two spaces that ANSWER the question, printed always — including at zero.
12
+ *
13
+ * Node 24 reports thirteen spaces (measured on 24.17: `read_only_space`,
14
+ * `new_space`, `old_space`, `code_space`, `shared_space`, `trusted_space`,
15
+ * `shared_trusted_space`, `new_large_object_space`, `large_object_space`,
16
+ * `code_large_object_space`, `shared_large_object_space`,
17
+ * `shared_trusted_large_object_space`, `trusted_large_object_space`). Printing
18
+ * all thirteen once a minute per process would add ~13 fields to a line that
19
+ * already carries eleven, on every runner, forever — and eleven of the thirteen
20
+ * cannot be the answer to a 1.4 GB retained-set question.
21
+ *
22
+ * These two are printed unconditionally because **absence must never be
23
+ * ambiguous**. A threshold-only rule would drop `large_object_space` at 20 MB,
24
+ * and a reader would not be able to tell "small" from "this build does not
25
+ * report it" — which is the same class of mistake as a heartbeat with no
26
+ * destination (D285). `largeObjectSpace=0MB` is a load-bearing reading: it says
27
+ * the retainer is NOT a handful of huge objects.
28
+ */
29
+ export declare const DECISIVE_HEAP_SPACES: readonly string[];
30
+ /**
31
+ * A non-decisive space is printed only once it holds at least this many MB.
32
+ *
33
+ * 32 MB, and the number comes from the smallest heap this line is ever asked to
34
+ * explain: the control in the 2026-08-29 pass is an idle `recorder` runner at
35
+ * **52 MB** of `heapUsed`. A threshold below that means no space that could
36
+ * dominate even the smallest watched process is ever hidden, while a quiet
37
+ * process — measured on a bare Node 24.17: `new_space` 0.7 MB, `code_space`
38
+ * 0.25 MB, `trusted_space` 0.9 MB — prints nothing but the decisive pair.
39
+ *
40
+ * What this drops, and why it is right to drop it: `new_space` is the scavenger's
41
+ * semi-space and is bounded by `--max-semi-space-size`, so it can never be a
42
+ * retained set; `code_space` and `read_only_space` are JIT output and V8's own
43
+ * immutable roots, both bounded by the code that exists rather than by the data
44
+ * it touches; the `shared_*` spaces are empty outside a worker-threads
45
+ * deployment, which this cluster does not run. None of them has ever been the
46
+ * answer to "what is this process holding", and each would cost a field on
47
+ * every `[mem]` line in the fleet.
48
+ *
49
+ * They are not censored, though — a space over the threshold prints, whatever it
50
+ * is. If `code_space` ever reaches 32 MB that is itself the finding.
51
+ */
52
+ export declare const HEAP_SPACE_REPORT_MIN_MB = 32;
53
+ /**
54
+ * Read every space from V8.
55
+ *
56
+ * Wrapped: a diagnostic that can crash the process it watches is worse than
57
+ * none, and this is called from a heartbeat whose whole contract is that it
58
+ * survives whatever it is measuring. An empty reading prints no fields, which
59
+ * is exactly what a heartbeat that could not read the heap should say.
60
+ */
61
+ export declare function readHeapSpaces(): readonly HeapSpaceUsage[];
62
+ /**
63
+ * `old_space` → `oldSpace`. Mechanical and total, so a space V8 adds tomorrow
64
+ * gets a field name without anyone editing a table.
65
+ *
66
+ * camelCase because that is the vocabulary of every other field on this line
67
+ * (`heapUsed`, `heapTotal`, `arrayBuffers`, `rssBudget`, `loopP99`). A line that
68
+ * mixed `heapUsed=` with `old_space=` would be two conventions for one record.
69
+ */
70
+ export declare function heapSpaceField(v8Name: string): string;
71
+ /**
72
+ * The spaces worth a field on this line: the decisive pair (in that order,
73
+ * always), then every other space at or above `minMb`, largest first.
74
+ *
75
+ * Largest-first for the tail because the tail only ever appears when something
76
+ * unexpected is holding memory, and the biggest surprise should be the first
77
+ * thing read. The decisive pair keeps a FIXED position so the two numbers a
78
+ * reader is comparing are always in the same place on every line.
79
+ */
80
+ export declare function selectReportedSpaces(spaces: readonly HeapSpaceUsage[], minMb?: number): readonly HeapSpaceUsage[];
81
+ /**
82
+ * The suffix appended to a `[mem]` line — leading space included, empty when
83
+ * there is nothing to say.
84
+ *
85
+ * APPENDED, never interleaved: every existing field keeps its name AND its
86
+ * position, so a `hl loki-query` pipeline written against the old line still
87
+ * matches. Prefix stability is the only change to a log line that cannot break
88
+ * a query someone already wrote.
89
+ */
90
+ export declare function formatHeapSpaces(spaces: readonly HeapSpaceUsage[], minMb?: number): string;
91
+ /**
92
+ * Env var that authorises a heap snapshot on SIGUSR2.
93
+ *
94
+ * A snapshot is an admin mutation on a production hub: `writeHeapSnapshot`
95
+ * streams a file the size of the heap to `/tmp` and stops the world while it
96
+ * walks it — on the recorder that is a multi-GB file and a stall measured in
97
+ * seconds. The per-space breakdown is ~1.2 µs and changes nothing. Welding them
98
+ * to one signal priced the free answer at the cost of the expensive one.
99
+ */
100
+ export declare const RUNNER_HEAP_SNAPSHOT_ENV = "CAMSTACK_RUNNER_HEAP_SNAPSHOT";
101
+ /**
102
+ * Whether the operator has authorised the snapshot half of SIGUSR2.
103
+ *
104
+ * Explicit `'on'` only. Anything else — unset, empty, `'true'`, `'1'` — is NOT
105
+ * authorisation: a multi-GB stop-the-world write must never be reachable by a
106
+ * value someone set for a different purpose.
107
+ */
108
+ export declare function heapSnapshotAuthorised(env?: NodeJS.ProcessEnv): boolean;
109
+ /** What a diagnostic report is ABOUT. */
110
+ export interface HeapDiagnosticRequest {
111
+ /** The runner's nodeId, so the line groups with its `[mem]` heartbeat. */
112
+ readonly label: string;
113
+ readonly addons: readonly string[];
114
+ readonly sink: HeapWatchSink;
115
+ /** Where a snapshot would be written, if one is authorised. */
116
+ readonly snapshotPath: string;
117
+ }
118
+ /** Everything the report READS. Injected so the split itself is testable. */
119
+ export interface HeapDiagnosticDeps {
120
+ readonly readMemory: () => NodeJS.MemoryUsage;
121
+ readonly readSpaces: HeapSpaceReader;
122
+ readonly writeSnapshot: (path: string) => void;
123
+ readonly env: NodeJS.ProcessEnv;
124
+ }
125
+ /**
126
+ * The SIGUSR2 report — memory + the FULL per-space breakdown, always; the
127
+ * snapshot only when {@link heapSnapshotAuthorised}.
128
+ *
129
+ * The breakdown here keeps every non-empty space at 0.1 MB, unlike the
130
+ * heartbeat's filtered whole-MB pair: this is a hand-fired diagnostic read once
131
+ * by a human who asked for it, so completeness costs nothing. The heartbeat runs
132
+ * unattended on every runner forever, so it pays for every field it prints.
133
+ *
134
+ * A refused snapshot SAYS SO. A branch that drops work silently reads as "it was
135
+ * never requested", and this one is fired by an operator who is standing there
136
+ * waiting for a file.
137
+ */
138
+ export declare function emitHeapDiagnosticReport(request: HeapDiagnosticRequest, deps?: HeapDiagnosticDeps): void;
@@ -1,3 +1,5 @@
1
+ import { EventPlaneReader } from './event-plane-report.js';
2
+ import { HeapSpaceReader } from './heap-spaces.js';
1
3
  /**
2
4
  * What hub-main calls itself in a `[mem]` line.
3
5
  *
@@ -137,12 +139,95 @@ export declare const HEAP_RECLAIM_MIN_INTERVAL_MS = 120000;
137
139
  * Six is 12 minutes of back-to-back passes: long enough that a genuine burst
138
140
  * (which subsides) never reaches it, short enough that a process which has
139
141
  * quietly made the stop-the-world part of its steady state is named within the
140
- * quarter-hour. The warn changes no behaviour — the pass is load-bearing and
141
- * keeps running. It exists so the OPERATOR is told that the safety net has
142
- * become the mechanism, which is the moment to go after the allocation source
143
- * instead of the symptom.
142
+ * quarter-hour.
143
+ *
144
+ * ## Until 2026-08-29 this counter changed nothing, and that was the bug
145
+ *
146
+ * It used to WARN and leave the cadence alone — "the pass is load-bearing and
147
+ * keeps running". Then hub-main's first 25 minutes of telemetry arrived and
148
+ * showed what that costs. See {@link HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS}:
149
+ * the system already knew the difference between a rescue and a steady state
150
+ * and simply refused to act on it.
144
151
  */
145
152
  export declare const RECLAIM_STEADY_STATE_PASSES = 6;
153
+ /**
154
+ * Floor on the pass once it has PROVEN it is steady state, not a rescue.
155
+ *
156
+ * ## Why `stranded` cannot discriminate on hub-main
157
+ *
158
+ * {@link strandedMb} is `rss − heapUsed − external`: everything the process
159
+ * holds that those two numbers do not name. That lumps together two things with
160
+ * opposite fixes —
161
+ *
162
+ * - V8 pages committed at a sawtooth peak, which `gc(last-resort)` RETURNS;
163
+ * - glibc arena memory already freed and never handed back, which **no V8
164
+ * flavor can reach** (nothing in this repository calls `malloc_trim`).
165
+ *
166
+ * {@link HEAP_RECLAIM_TRIGGER_MB} was calibrated against a hub-main whose strand
167
+ * was mostly the first kind, and a pass then took it back under the trigger. Once
168
+ * the second kind grows past 1024 MB the quantity can never fall back under it,
169
+ * and the gate degenerates. Measured on the live hub 2026-08-29, first 25
170
+ * minutes of hub-main telemetry ever collected:
171
+ *
172
+ * - `stranded` sat at **1031–2313 MB for the entire boot**, i.e.
173
+ * {@link shouldReclaim} never once said no. **What is written as a threshold
174
+ * is, on this process, an unconditional 120-second timer.**
175
+ * - Nine minutes out of nine that contained a reclaim show `loopMax` ≈ the
176
+ * reclaim's own duration (555–869 ms). Every minute without one is
177
+ * 107–435 ms. **The largest recurring event-loop stall on the process that
178
+ * mediates every cross-runner call is the memory patch it applies to itself.**
179
+ * - Each pass freed less than the last (1193 → 462 MB) while the post-pass RSS
180
+ * floor ratcheted **1940 → 3510 MB in fourteen minutes**, with `heapUsed`
181
+ * never moving. It is compacting a heap that is not the problem.
182
+ *
183
+ * ## The fix is not a bigger trigger
184
+ *
185
+ * Raising {@link HEAP_RECLAIM_TRIGGER_MB} would move the number at which the same
186
+ * degeneration happens, and it would silence `hub/recorder` — whose single pass
187
+ * in six hours is the only cheap proof anyone has that the recorder's 1.4 GB is
188
+ * RETAINED (a forced compaction freed 507 MB and V8 still held 1.44 GB live).
189
+ * The discriminator is not the level of the strand; it is **whether the strand
190
+ * ever falls back under the trigger between passes**, which is precisely what
191
+ * {@link RECLAIM_STEADY_STATE_PASSES} already counts and used only to talk about.
192
+ *
193
+ * So the counter becomes load-bearing: once the pass has fired at the floor
194
+ * {@link RECLAIM_STEADY_STATE_PASSES} times running, the trigger has demonstrably
195
+ * stopped discriminating on this process, and the cadence falls back to a period
196
+ * chosen for its COST rather than to a threshold pretending to be a gate.
197
+ *
198
+ * ## Why 600_000, and why that is not a regression
199
+ *
200
+ * It is the value {@link HEAP_RECLAIM_MIN_INTERVAL_MS} held before 2026-08-17,
201
+ * and the reason it came down is recorded there: *"the ten-minute floor let a
202
+ * peak that re-formed in seconds stand for ten minutes"*. That argument is about
203
+ * a BURST — and a burst subsides, which resets the run and restores the 120 s
204
+ * floor. It never applied to a process in proven steady state. So the burst
205
+ * bound is untouched and the steady state gets the period the burst argument was
206
+ * never about.
207
+ *
208
+ * ## What each process does after this change
209
+ *
210
+ * - **hub-main**: first six passes at 120 s, unchanged. From the seventh
211
+ * consecutive floor-to-floor pass, every 600 s. Stop-the-world cost on the
212
+ * router falls from ~0.58% of wall clock to ~0.12%. The ArrayBuffer probe
213
+ * survives at six passes an hour — the finding it produced needed ten passes
214
+ * total, not ten in twenty-five minutes.
215
+ * - **`hub/recorder`**: zero change, by construction. `passesAtFloor` only
216
+ * increments when a pass fires within `minIntervalMs * 2` of the previous
217
+ * one; the recorder's strand takes six hours to re-form, so the counter is
218
+ * reset to 1 on every pass and can never reach six. The 507 MB probe is
219
+ * intact.
220
+ * - **Anything that recovers**: the run resets the moment the strand falls back
221
+ * under the trigger, and the 120 s floor returns with it.
222
+ */
223
+ export declare const HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS = 600000;
224
+ /**
225
+ * The floor in force for the NEXT pass, given how many have fired at the floor.
226
+ *
227
+ * Pure, so the cadence change is testable without stranding a gigabyte to reach
228
+ * it — and so the one place that decides it can be read on its own.
229
+ */
230
+ export declare function reclaimIntervalMs(passesAtFloor: number, minIntervalMs?: number, steadyStateIntervalMs?: number, steadyStatePasses?: number): number;
146
231
  /**
147
232
  * Event-loop delay over the window since the previous read, in milliseconds.
148
233
  *
@@ -181,6 +266,37 @@ export interface LoopDelayMeter {
181
266
  * naively would print `loopMax=9223372036854ms` and be read as a catastrophe on
182
267
  * every boot. `count === 0` short-circuits to {@link ZERO_LOOP_DELAY}.
183
268
  *
269
+ * ## `loopP50=20ms` was the SAMPLER, not the loop (fixed 2026-08-29)
270
+ *
271
+ * `monitorEventLoopDelay` records the whole INTERVAL between its own timer
272
+ * firings, not the excess over the interval it asked for. So on a loop with
273
+ * nothing wrong the histogram reads `resolution`, and every `[mem]` line this
274
+ * process has ever emitted said `loopP50=20ms` — which was read, repeatedly in
275
+ * one session, as "hub-main is chronically 20 ms behind". It is not; 20 was the
276
+ * default `resolutionMs` of this very function.
277
+ *
278
+ * The delay is `interval − resolution`, and that is not a fudge factor — it is
279
+ * the only reading the four resolutions agree on. Measured here, one 120 ms
280
+ * block observed by four histograms at once:
281
+ *
282
+ * | resolution | raw p50 | raw max | max − resolution |
283
+ * | --- | --- | --- | --- |
284
+ * | 20 ms | 20.40 ms | 137.63 ms | 117.6 ms |
285
+ * | 10 ms | 10.22 ms | 123.60 ms | 113.6 ms |
286
+ * | 5 ms | 5.11 ms | 123.60 ms | 118.6 ms |
287
+ * | 1 ms | 1.27 ms | 121.04 ms | 120.0 ms |
288
+ *
289
+ * Raw, the four disagree by 16 ms about the same event and none of them reports
290
+ * the block. Corrected, they agree within 7 ms and all four name it. The
291
+ * subtraction costs nothing and needs no faster sampler — a 1 ms resolution
292
+ * would be 1 000 extra timer wakeups per second on the process that already
293
+ * serves ~44 000 socket operations per second, to buy 2 ms of accuracy on a
294
+ * quantity nobody reads below 100 ms.
295
+ *
296
+ * **Readings from before this change are 20 ms high.** `loopP99=21–28ms` in the
297
+ * 2026-08-29 census is really 1–8 ms; `loopMax=2804ms` is really 2784 ms. Do not
298
+ * compare a pre-fix series with a post-fix one without shifting it.
299
+ *
184
300
  * Returns undefined if the platform declines, so the caller keeps a heartbeat
185
301
  * either way.
186
302
  */
@@ -396,6 +512,9 @@ export interface HeapReclaimOptions {
396
512
  readonly heapLimitBytes?: number;
397
513
  readonly triggerMb?: number;
398
514
  readonly minIntervalMs?: number;
515
+ /** Floor once the pass is proven steady state. See
516
+ * {@link HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS}. */
517
+ readonly steadyStateIntervalMs?: number;
399
518
  readonly now?: () => number;
400
519
  /** Sampling cadence once escalated. See {@link HEAP_WATCH_FAST_INTERVAL_MS}. */
401
520
  readonly fastIntervalMs?: number;
@@ -462,13 +581,31 @@ export interface RssBudgetOptions {
462
581
  /** See {@link RSS_BUDGET_REANNOUNCE_MIN_MS}. */
463
582
  readonly reannounceMinMs?: number;
464
583
  }
584
+ /**
585
+ * The optional per-line probes.
586
+ *
587
+ * An OBJECT rather than two more positional parameters: `startHeapWatch` already
588
+ * takes eight, and a ninth and tenth `undefined` at a call site is how a caller
589
+ * ends up passing the loop meter into the space reader. Every field is optional,
590
+ * so a caller that wants neither passes nothing.
591
+ */
592
+ export interface HeapWatchProbes {
593
+ /** Per-V8-space breakdown. Defaults to the real reader. */
594
+ readonly readSpaces?: HeapSpaceReader;
595
+ /**
596
+ * UDS + cross-node event fan-out. No default: a forked runner has no child
597
+ * registry, and printing zeros for a plane that does not exist is worse than
598
+ * printing nothing. Only hub-main and an agent's main pass one.
599
+ */
600
+ readonly eventPlane?: EventPlaneReader;
601
+ }
465
602
  /**
466
603
  * Start the heartbeat. Returns a stop function.
467
604
  *
468
605
  * `unref`ed so it can never hold the process open, and every read is wrapped:
469
606
  * a diagnostic that can crash the process it watches is worse than none.
470
607
  */
471
- export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions, loopDelay?: LoopDelayMeter | undefined, execArgv?: readonly string[], announceCeilingOrigin?: boolean, rssBudget?: RssBudgetOptions): () => void;
608
+ export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions, loopDelay?: LoopDelayMeter | undefined, execArgv?: readonly string[], announceCeilingOrigin?: boolean, rssBudget?: RssBudgetOptions, probes?: HeapWatchProbes): () => void;
472
609
  /**
473
610
  * Steady heartbeat period for a HEAVY addon runner.
474
611
  *
@@ -40,6 +40,8 @@ export { createCoreCapService, CORE_CAP_SERVICE_NAME } from './moleculer/core-ca
40
40
  export type { CoreCapAction, CoreCapCallOrigin, CoreCapServiceOptions, } from './moleculer/core-cap-service.js';
41
41
  export { createAddonContext, createUdsAddonContext, setHubConnected, adaptBrokerToCluster, registerEventBusService, setNodeEventInterest, getMoleculerEventStats, subscribePassthrough, getBrokerEventBus, clusterEventTopic, EVENT_TOPIC_PREFIX, } from './moleculer/addon-context-factory.js';
42
42
  export type { MoleculerEventStats } from './moleculer/addon-context-factory.js';
43
+ export type { MoleculerEventFanoutMode } from './moleculer/event-bus-core.js';
44
+ export { readMoleculerFanoutMode } from './moleculer/event-bus-core.js';
43
45
  export { HubLogForwarder } from './moleculer/hub-log-forwarder.js';
44
46
  export type { AddonContextOptions } from './moleculer/addon-context-factory.js';
45
47
  export { createHubService } from './moleculer/hub-service.js';
@@ -183,6 +183,28 @@ export declare class LocalChildRegistry {
183
183
  * surface (shadow burn-in + operator debug).
184
184
  */
185
185
  getChildEventStats(childId: string): ChildEventStats | null;
186
+ /**
187
+ * The regime the counters above were produced under, read once from
188
+ * `CAMSTACK_UDS_EVENT_FANOUT` at construction.
189
+ *
190
+ * A suppressed count means "nothing WAS dropped" under `filter` and "nothing
191
+ * WOULD have been dropped" under `shadow`. Those are different claims, and
192
+ * until this getter existed the mode was invisible at runtime — the counters
193
+ * were readable and the regime that produced them was not.
194
+ */
195
+ get eventFanoutMode(): UdsEventFanoutMode;
196
+ /**
197
+ * Connected children that never declared a subscription pattern set.
198
+ *
199
+ * Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
200
+ * an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
201
+ * `aggregateEventInterest` returns `null` — accept everything — if ANY
202
+ * connected child is undeclared, which disables the node's inbound cross-node
203
+ * gate as well. **One legacy runner silently turns off the filtering for the
204
+ * whole node**, and nothing reported it, so a zero here is as load-bearing as
205
+ * a non-zero one.
206
+ */
207
+ undeclaredChildCount(): number;
186
208
  /**
187
209
  * Does `entry` want to receive an event in `category`? Honours the fan-out
188
210
  * mode: `broadcast` → always; undeclared patterns (`null`) → always