@camstack/types 1.2.86 → 1.2.88
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.
- package/dist/addon.js +2 -2
- package/dist/addon.mjs +2 -2
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/notification-rules.cap.d.ts +16 -0
- package/dist/capabilities/osd-manager.cap.d.ts +6 -0
- package/dist/capabilities/videoclips.cap.d.ts +4 -0
- package/dist/enums/event-category.d.ts +16 -0
- package/dist/enums.js +1 -1
- package/dist/enums.mjs +1 -1
- package/dist/{event-category-DBHdQVIy.js → event-category-CRPORAAz.js} +16 -0
- package/dist/{event-category-C0lyLd5U.mjs → event-category-XfKNtfCc.mjs} +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +328 -3
- package/dist/index.mjs +318 -4
- package/dist/interfaces/event-bus.d.ts +17 -0
- package/dist/{sleep-DZAjGv1f.js → sleep-CMRLJj2e.js} +1 -1
- package/dist/{sleep-oYerbR6F.mjs → sleep-zMxKWD0M.mjs} +1 -1
- package/dist/utils/pool-memory-watchdog.d.ts +178 -0
- package/package.json +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool memory watchdog — bounds the RSS of the long-lived Python inference
|
|
3
|
+
* subprocesses (detection `inference_pool.py`, audio `yamnet_audio.py`,
|
|
4
|
+
* post-analysis `raw_tensor_inference.py` / `text_encoder_inference.py`)
|
|
5
|
+
* BEFORE the kernel's OOM killer does, and emits the periodic per-pool
|
|
6
|
+
* memory line that makes an hours-long leak legible after the fact.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists (2026-08-18 audit): the pools leak over hours — measured
|
|
9
|
+
* 97 / 78 / 171 / 689 MB with the 689 MB one climbing ~7 MB per 30 min —
|
|
10
|
+
* then get OOM-killed (anon-rss 3.8 GB inside the CamStack cgroup at 11:45,
|
|
11
|
+
* 8.8 GB host-wide at 17:02, same day), respawn at ~100 MB and repeat. The
|
|
12
|
+
* host is 62 GiB with NO swap and the pressure killed the co-hosted Home
|
|
13
|
+
* Assistant VM twice. The kernel was the only bound; this module makes the
|
|
14
|
+
* bound ours, and makes every step of the ramp visible in Loki.
|
|
15
|
+
*
|
|
16
|
+
* Placement: this lives in `@camstack/types` because BOTH `addon-pipeline`
|
|
17
|
+
* and `addon-post-analysis` need it, addons must not import each other, and
|
|
18
|
+
* self-contained addon bundles INLINE the bare `@camstack/types` entry at
|
|
19
|
+
* build time (see tools/build/vite-lib.preset.ts) — so the code ships with a
|
|
20
|
+
* plain `camstack deploy` of each addon, no framework train required. It is
|
|
21
|
+
* deliberately free of Node imports (`fs`, `child_process`): the pure policy
|
|
22
|
+
* below is also consumed through the types index that UI builds import.
|
|
23
|
+
* The `/proc` READ lives in each addon; the parse lives here.
|
|
24
|
+
*/
|
|
25
|
+
import type { IScopedLogger } from '../interfaces/logging.js';
|
|
26
|
+
export interface PoolMemoryPolicy {
|
|
27
|
+
/** Sweep cadence. 60 s makes a 6-hour ramp 360 points (legible) at ~4
|
|
28
|
+
* lines/min across all pools (no flood); the addon log ring only holds
|
|
29
|
+
* ~10 min, but these lines are shipped to Loki which holds days. */
|
|
30
|
+
readonly sampleIntervalMs: number;
|
|
31
|
+
/** Samples IGNORED after a pool (re)appears before baseline capture
|
|
32
|
+
* starts. Model loads land in the first minutes and inflate RSS —
|
|
33
|
+
* a baseline taken at spawn would be the empty interpreter (~40 MB)
|
|
34
|
+
* and every loaded pool would instantly look leaky. */
|
|
35
|
+
readonly baselineSettleSamples: number;
|
|
36
|
+
/** Post-settle samples whose MEDIAN becomes the baseline. Median, not
|
|
37
|
+
* mean: one inference burst mid-window must not drag the baseline up. */
|
|
38
|
+
readonly baselineSampleCount: number;
|
|
39
|
+
/** Restart when RSS exceeds `restartMultiple × baseline`. Relative, not
|
|
40
|
+
* absolute: an absolute MB number ages badly as models change, while
|
|
41
|
+
* "4× what this pool settled at with its own models loaded" survives a
|
|
42
|
+
* model swap unchanged. */
|
|
43
|
+
readonly restartMultiple: number;
|
|
44
|
+
/** Never restart below this RSS regardless of the multiple. The pools
|
|
45
|
+
* settle at 78–171 MB today; without a floor, 4× a 78 MB baseline
|
|
46
|
+
* (312 MB) would flap on legitimate working-set growth (a second model
|
|
47
|
+
* loaded, a larger frame cache). Below the floor the pool is not a
|
|
48
|
+
* host-level threat on a 62 GiB machine. */
|
|
49
|
+
readonly floorBytes: number;
|
|
50
|
+
/** Restart at this RSS even when the baseline is unknown or huge. This is
|
|
51
|
+
* the backstop for (a) a pool that ramps before its baseline exists and
|
|
52
|
+
* (b) a baseline poisoned by an already-leaking pool at guard start.
|
|
53
|
+
* 3 GiB sits well under the observed kill sizes (3.8 / 8.8 GB anon-rss)
|
|
54
|
+
* and far above any legitimate settled pool observed on this fleet. */
|
|
55
|
+
readonly ceilingBytes: number;
|
|
56
|
+
/** Minimum gap between watchdog restarts of ONE pool. A pool that
|
|
57
|
+
* re-crosses within minutes of a restart is leaking faster than a
|
|
58
|
+
* restart can pay for — restarting it in a tight loop would trade an
|
|
59
|
+
* OOM for a detection outage. */
|
|
60
|
+
readonly cooldownMs: number;
|
|
61
|
+
/** Watchdog restarts allowed per pool per `restartWindowMs`; past this
|
|
62
|
+
* the watchdog STOPS restarting and escalates (error log). Mirrors the
|
|
63
|
+
* CrashSupervisor circuit-breaker rule (D6) at pool granularity: never
|
|
64
|
+
* an unbounded restarter. */
|
|
65
|
+
readonly maxRestartsPerWindow: number;
|
|
66
|
+
/** Window for `maxRestartsPerWindow`. */
|
|
67
|
+
readonly restartWindowMs: number;
|
|
68
|
+
}
|
|
69
|
+
export declare const DEFAULT_POOL_MEMORY_POLICY: PoolMemoryPolicy;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
|
|
72
|
+
* absent values keep the default — an operator typo must never disable the
|
|
73
|
+
* bound or set it to zero.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolvePoolMemoryPolicy(env: Readonly<Record<string, string | undefined>>): PoolMemoryPolicy;
|
|
76
|
+
export interface ProcMemory {
|
|
77
|
+
readonly rssBytes: number;
|
|
78
|
+
/** Virtual size. The 2026-08-18 kills showed total-vm 47.5 GB vs 11.3 GB at
|
|
79
|
+
* similar RSS — tracking VmSize alongside Threads is the cheap lead on
|
|
80
|
+
* "allocator arenas vs thread stacks" the audit flagged. */
|
|
81
|
+
readonly vmBytes: number;
|
|
82
|
+
/** Peak RSS (VmHWM) — the `resource.getrusage` maxrss equivalent, readable
|
|
83
|
+
* from OUTSIDE the process, so it covers the pools whose wire protocol has
|
|
84
|
+
* no command channel (yamnet / raw-tensor). */
|
|
85
|
+
readonly hwmBytes: number;
|
|
86
|
+
readonly swapBytes: number;
|
|
87
|
+
readonly threads: number;
|
|
88
|
+
}
|
|
89
|
+
/** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
|
|
90
|
+
* Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
|
|
91
|
+
export declare function parseProcStatus(text: string): ProcMemory | null;
|
|
92
|
+
export interface PoolMemoryState {
|
|
93
|
+
readonly settleSeen: number;
|
|
94
|
+
readonly baselineWindow: readonly number[];
|
|
95
|
+
readonly baselineBytes: number | null;
|
|
96
|
+
/** Timestamps of WATCHDOG restarts (never kernel kills — those reset the
|
|
97
|
+
* baseline via the pid-change path but must not eat the budget). */
|
|
98
|
+
readonly restartsAt: readonly number[];
|
|
99
|
+
readonly lastRestartAt: number | null;
|
|
100
|
+
}
|
|
101
|
+
export declare function initialPoolMemoryState(): PoolMemoryState;
|
|
102
|
+
export type PoolMemoryAction = 'ok' | 'baseline-pending' | 'restart' | 'cooldown' | 'exhausted';
|
|
103
|
+
export interface PoolMemoryVerdict {
|
|
104
|
+
readonly state: PoolMemoryState;
|
|
105
|
+
readonly action: PoolMemoryAction;
|
|
106
|
+
readonly baselineBytes: number | null;
|
|
107
|
+
readonly thresholdBytes: number;
|
|
108
|
+
}
|
|
109
|
+
/** The one place a pool's restart threshold is computed. */
|
|
110
|
+
export declare function poolMemoryThreshold(baselineBytes: number | null, policy: PoolMemoryPolicy): number;
|
|
111
|
+
/**
|
|
112
|
+
* Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
|
|
113
|
+
* The caller performs (and logs) the restart, then commits it via
|
|
114
|
+
* {@link commitWatchdogRestart}.
|
|
115
|
+
*/
|
|
116
|
+
export declare function evaluatePoolMemory(state: PoolMemoryState, rssBytes: number, nowMs: number, policy: PoolMemoryPolicy): PoolMemoryVerdict;
|
|
117
|
+
/** Record a performed watchdog restart: budget consumed, baseline reset —
|
|
118
|
+
* the recreated pool is a NEW process and gets a fresh settle window. */
|
|
119
|
+
export declare function commitWatchdogRestart(state: PoolMemoryState, nowMs: number): PoolMemoryState;
|
|
120
|
+
/** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
|
|
121
|
+
* kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
|
|
122
|
+
* restart budget is kept: it bounds the WATCHDOG, not the pool. */
|
|
123
|
+
export declare function resetPoolBaseline(state: PoolMemoryState): PoolMemoryState;
|
|
124
|
+
export interface RestartCandidate {
|
|
125
|
+
readonly key: string;
|
|
126
|
+
readonly rssBytes: number;
|
|
127
|
+
readonly thresholdBytes: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
|
|
131
|
+
* several pools at once (e.g. after a model rollout grew every baseline)
|
|
132
|
+
* would take detection down on every camera simultaneously; one per sweep
|
|
133
|
+
* means the survivors keep serving while the worst offender reloads, and the
|
|
134
|
+
* next sweep (one interval later) takes the next one.
|
|
135
|
+
*/
|
|
136
|
+
export declare function pickRestartCandidate(candidates: readonly RestartCandidate[]): string | null;
|
|
137
|
+
/** One pool's sample for a sweep. `meta` carries the owner's cheap internals
|
|
138
|
+
* (backlog, shed counts, models loaded, Python-side mem stats) verbatim into
|
|
139
|
+
* the periodic `pool memory` line. */
|
|
140
|
+
export interface PoolMemoryTelemetry {
|
|
141
|
+
readonly key: string;
|
|
142
|
+
readonly pids: readonly number[];
|
|
143
|
+
/** Max worker RSS for the pool — the kernel kills a PROCESS, so the bound
|
|
144
|
+
* tracks the biggest worker, not the pool sum. */
|
|
145
|
+
readonly rssBytes: number;
|
|
146
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
147
|
+
}
|
|
148
|
+
export interface PoolMemoryWatchdogOptions {
|
|
149
|
+
readonly policy: PoolMemoryPolicy;
|
|
150
|
+
/** Enumerate the live pools with their current RSS + internals. */
|
|
151
|
+
readonly sample: () => Promise<readonly PoolMemoryTelemetry[]>;
|
|
152
|
+
/** Dispose-and-recreate the named pool via the owner's EXISTING lifecycle
|
|
153
|
+
* machinery (idle-reaper dispose path, lazy re-init) — the watchdog is a
|
|
154
|
+
* trigger, never a second process supervisor. */
|
|
155
|
+
readonly restart: (key: string) => Promise<void>;
|
|
156
|
+
readonly log: IScopedLogger;
|
|
157
|
+
readonly now?: () => number;
|
|
158
|
+
readonly setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
159
|
+
readonly clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
160
|
+
}
|
|
161
|
+
export declare class PoolMemoryWatchdog {
|
|
162
|
+
private readonly pools;
|
|
163
|
+
private timer;
|
|
164
|
+
private sweeping;
|
|
165
|
+
private stopped;
|
|
166
|
+
private readonly opts;
|
|
167
|
+
private readonly now;
|
|
168
|
+
private readonly setTimer;
|
|
169
|
+
private readonly clearTimer;
|
|
170
|
+
constructor(opts: PoolMemoryWatchdogOptions);
|
|
171
|
+
start(): void;
|
|
172
|
+
stop(): void;
|
|
173
|
+
private arm;
|
|
174
|
+
/** One sweep: sample → log every pool → restart at most one. Public so the
|
|
175
|
+
* loop is testable without fake global timers. */
|
|
176
|
+
sweep(): Promise<void>;
|
|
177
|
+
private doSweep;
|
|
178
|
+
}
|