@bendyline/gezel 0.1.0

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.
@@ -0,0 +1,650 @@
1
+ export { D as DEVICE_HARD_TEMPERATURE_C } from '../device-safety-DezzpNyR.js';
2
+
3
+ /**
4
+ * Runtime backend detection for the bundled llama-server.
5
+ *
6
+ * Phase 2 ships multiple llama-server variants by platform and
7
+ * architecture — CUDA, Vulkan, CPU, or Metal — and this module picks
8
+ * which one to launch on a given host. The probe is pure filesystem /
9
+ * linker inspection; it does NOT spawn a GPU workload. Binary
10
+ * availability fallback is handled by `resolveAvailableLlamaBinary`;
11
+ * failures after a selected binary starts are surfaced by the native
12
+ * engine supervisor.
13
+ *
14
+ * Usage from the Electron supervisor:
15
+ *
16
+ * import { detectLlamaBackend } from './llama-backend.js';
17
+ * import { resolveNativeBinaryPath } from './native-bin.js';
18
+ *
19
+ * const probe = detectLlamaBackend({
20
+ * engineVersion: 'b9843',
21
+ * home: gezelHome,
22
+ * });
23
+ * const bin = resolveNativeBinaryPath('llama-server', import.meta.url, probe.backend);
24
+ *
25
+ * Caching: the result is memoized at
26
+ * `<home>/engines/llama-cpp/backend.json` so subsequent launches
27
+ * skip the probe. Cache invalidates whenever `engineVersion`
28
+ * changes — bumping the llama.cpp pin may prefer a different
29
+ * backend.
30
+ */
31
+ type LlamaBackend = 'cuda' | 'vulkan' | 'metal' | 'cpu';
32
+ /**
33
+ * Which GPU vendor the host *primarily* has, independent of which
34
+ * llama-server backend was picked. Used to label the running engine
35
+ * in the UI: a Radeon user on the Vulkan backend should see "AMD GPU"
36
+ * not just "GPU." `cuda` already implies NVIDIA, so the vendor field
37
+ * is most useful for disambiguating Vulkan and CPU fallbacks.
38
+ *
39
+ * Detection is best-effort and pure filesystem inspection — no shell-out
40
+ * to wmic / lspci. `undefined` means we couldn't tell; the UI should
41
+ * fall back to a generic label.
42
+ */
43
+ type GpuVendorHint = 'amd' | 'nvidia' | 'intel';
44
+ interface DetectInput {
45
+ /** Engine pin from native/engines/llama-cpp/VERSION (e.g. 'b9843'). Bumps invalidate the cache. */
46
+ engineVersion: string;
47
+ /** GEZEL_HOME — backend cache lives at `<home>/engines/llama-cpp/backend.json`. */
48
+ home: string;
49
+ /**
50
+ * User-pinned backend (from `config.llamaCppBackendOverride`). When
51
+ * set to a concrete backend (`cuda` / `vulkan` / `metal` / `cpu`),
52
+ * the auto-probe is skipped and the chosen backend is returned
53
+ * verbatim — even if the OS doesn't have the corresponding driver.
54
+ * `resolveNativeBinaryPath` will then fail to find the binary and
55
+ * the supervisor surfaces an actionable error (which is the right
56
+ * behavior — the user picked an unavailable backend, we tell them).
57
+ *
58
+ * `'auto'` or `undefined` keeps the existing CUDA → Vulkan → CPU
59
+ * (or Metal on Mac) probe behavior.
60
+ *
61
+ * Cache bypass: when an override is in effect we don't read OR write
62
+ * the cache file. The override is the source of truth; we don't want
63
+ * a future `'auto'` to come back and re-use what was actually a
64
+ * forced choice.
65
+ */
66
+ override?: 'auto' | 'cuda' | 'vulkan' | 'metal' | 'cpu';
67
+ /** Optional injection points for tests. */
68
+ probe?: {
69
+ fileExists?: (path: string) => boolean;
70
+ commandOk?: (cmd: string) => boolean;
71
+ /**
72
+ * Read a small text file (no size limit checks — the caller only
73
+ * uses this on tiny sysfs files like
74
+ * `/sys/class/drm/card0/device/vendor`). Returns `undefined` for
75
+ * any failure mode (missing, EACCES, etc.) — the caller treats
76
+ * that the same as "vendor unknown."
77
+ */
78
+ readFileText?: (path: string) => string | undefined;
79
+ /**
80
+ * List a directory's entries. Used to enumerate `/sys/class/drm/`
81
+ * for `card*` devices. Returns `[]` for any failure mode
82
+ * (directory missing on non-Linux containers, permission denied).
83
+ */
84
+ readDir?: (path: string) => string[];
85
+ platform?: NodeJS.Platform;
86
+ arch?: NodeJS.Architecture;
87
+ };
88
+ }
89
+ interface DetectResult {
90
+ /**
91
+ * What the supervisor should use for this launch. When the user pinned
92
+ * `config.llamaCppBackendOverride`, this is the override; otherwise it's
93
+ * the hardware-probe result (same as `detectedBackend`).
94
+ */
95
+ backend: LlamaBackend;
96
+ /**
97
+ * What the hardware probe found, independent of any override. The
98
+ * Settings UI uses this to populate the dropdown so a `cpu` pin on a
99
+ * CUDA machine still shows CUDA / Vulkan as switchable options.
100
+ *
101
+ * Equal to `backend` when no override is in effect.
102
+ */
103
+ detectedBackend: LlamaBackend;
104
+ cached: boolean;
105
+ /** Human-readable explanation of why this backend was chosen. */
106
+ reason: string;
107
+ /** ISO 8601 timestamp of when the underlying probe ran (cache hits show the cached value). */
108
+ probedAt: string;
109
+ /**
110
+ * Best-effort guess of the host's primary GPU vendor. Independent of
111
+ * the backend pick — a Radeon user on the Vulkan backend reports
112
+ * `vendorHint='amd'` so the UI can label the engine "AMD GPU"
113
+ * instead of the generic "GPU." `undefined` when no GPU could be
114
+ * identified (e.g. headless container, macOS where the field is
115
+ * meaningless because Apple Silicon is unified memory).
116
+ */
117
+ vendorHint?: GpuVendorHint;
118
+ }
119
+ interface ResolvedLlamaBinary {
120
+ /** Backend the selected binary was built for. */
121
+ backend: LlamaBackend;
122
+ /** Absolute path returned by the caller's binary resolver. */
123
+ path: string;
124
+ /**
125
+ * Present when automatic backend selection had to use the CPU build
126
+ * because the detected GPU variant was not bundled for this platform.
127
+ */
128
+ fallbackFrom?: LlamaBackend;
129
+ /**
130
+ * Backends passed over because `isUsable` rejected them — a build that
131
+ * is present but known to crash on this machine. Distinct from
132
+ * `fallbackFrom`, which covers a variant that simply isn't bundled:
133
+ * this one shipped, exists on disk, and does not run here. Callers
134
+ * surface it so the demotion is visible instead of silent.
135
+ */
136
+ skippedUnusable?: LlamaBackend[];
137
+ }
138
+ /**
139
+ * Resolve the preferred llama-server build, with optional lower-tier
140
+ * fallbacks.
141
+ *
142
+ * Detection and packaging support are deliberately separate: a machine can
143
+ * expose a Vulkan loader even when the release does not ship a Vulkan build
144
+ * for its architecture (for example Linux ARM64 under Parallels). In auto
145
+ * mode, that should still produce a working on-device engine by selecting the
146
+ * next bundled build in CUDA → Vulkan → CPU order. Explicit user overrides
147
+ * pass `allowFallbacks=false`
148
+ * so a missing pinned backend remains an actionable configuration error.
149
+ *
150
+ * Returning the backend together with the path prevents callers from launching
151
+ * a CPU binary while incorrectly advertising it as Vulkan or CUDA.
152
+ */
153
+ declare function resolveAvailableLlamaBinary(preferredBackend: LlamaBackend, resolveBinary: (backend: LlamaBackend) => string | null, allowFallbacks: boolean,
154
+ /**
155
+ * Optional veto on a binary that exists. Returning false continues down
156
+ * the fallback chain as if the build were absent — the quarantine hook
157
+ * (see `llama-quarantine.ts`) uses this to route around a variant that
158
+ * crashed before it was ever ready.
159
+ *
160
+ * Only consulted while fallbacks are allowed. With an explicit user
161
+ * pin there is nowhere to demote TO, and silently refusing the pinned
162
+ * backend would replace one confusing failure with a stranger one.
163
+ */
164
+ isUsable?: (backend: LlamaBackend, path: string) => boolean): ResolvedLlamaBinary | null;
165
+ declare function detectLlamaBackend(input: DetectInput): DetectResult;
166
+
167
+ /**
168
+ * Per-machine record of llama-server builds that cannot run here.
169
+ *
170
+ * `resolveAvailableLlamaBinary` picks a backend by asking whether the
171
+ * binary EXISTS, never whether it RUNS. When the preferred variant is
172
+ * present but dies the instant it is spawned, nothing in the chain
173
+ * notices: the engine supervisor just relaunches the same binary on the
174
+ * next request, forever, while a working lower-tier build sits unused in
175
+ * the same directory.
176
+ *
177
+ * That is not hypothetical. A CUDA build shipped in native-v0.1.29 died
178
+ * with SIGILL before binding its port on a Haswell CPU, and the user's
179
+ * only route out was to hand-pin Vulkan in Settings — after the crash had
180
+ * been misdiagnosed several times, because the error surfaced as a bare
181
+ * signal name with no attribution.
182
+ *
183
+ * So: when a variant crashes before it is ever ready, write it down here,
184
+ * and let backend resolution route around it on the next launch.
185
+ *
186
+ * ## Why the fingerprint, and not a version string
187
+ *
188
+ * An entry pins the exact bytes that crashed — `<size>:<mtimeMs>` of the
189
+ * binary — rather than an engine or release version. A quarantine has to
190
+ * expire the moment the offending build is replaced, and a version key
191
+ * cannot promise that: the fix for the SIGILL above was a compiler-flag
192
+ * change at an UNCHANGED llama.cpp pin. Keying on `LLAMA_ENGINE_VERSION`
193
+ * would have left every affected machine demoted to Vulkan permanently,
194
+ * with the repaired CUDA build sitting right there. Fingerprinting the
195
+ * file means an upgrade re-enables the backend on its own, and a machine
196
+ * that is genuinely too old re-quarantines after one cheap crash.
197
+ *
198
+ * Deliberately NOT consulted when the user has pinned a backend
199
+ * explicitly: an override is a decision, and silently overriding the
200
+ * override would be worse than the failure it avoids. The pin stands, the
201
+ * crash still reports.
202
+ */
203
+
204
+ interface LlamaQuarantineEntry {
205
+ backend: LlamaBackend;
206
+ /** `<size>:<mtimeMs>` of the binary that crashed — see the module docstring. */
207
+ fingerprint: string;
208
+ /** Signal that killed it (`SIGILL`), or `exit:<code>` when it exited normally. */
209
+ signal: string;
210
+ /** Human-readable explanation, surfaced in the backend-resolution reason. */
211
+ reason: string;
212
+ /** ISO 8601 timestamp of the crash that created this entry. */
213
+ at: string;
214
+ }
215
+ /** Injection seam — tests supply fakes, production uses `node:fs`. */
216
+ interface QuarantineIo {
217
+ readFile?: (path: string) => string;
218
+ writeFile?: (path: string, data: string) => void;
219
+ statFile?: (path: string) => {
220
+ size: number;
221
+ mtimeMs: number;
222
+ };
223
+ mkdir?: (path: string) => void;
224
+ now?: () => Date;
225
+ }
226
+ declare function llamaQuarantinePath(home: string): string;
227
+ /**
228
+ * Identity of the bytes at `path`, or null when it cannot be stat'd.
229
+ *
230
+ * Size alone is nearly sufficient — a rebuilt engine is essentially never
231
+ * byte-identical in length — but mtime costs nothing extra from the same
232
+ * stat call and closes the gap.
233
+ */
234
+ declare function binaryFingerprint(path: string, io?: QuarantineIo): string | null;
235
+ declare function readLlamaQuarantine(home: string, io?: QuarantineIo): LlamaQuarantineEntry[];
236
+ /**
237
+ * True when `backend`'s binary at `binaryPath` is the same file that
238
+ * crashed before. A fingerprint mismatch means the build was replaced, so
239
+ * the entry no longer applies and the backend gets another chance.
240
+ */
241
+ declare function isBinaryQuarantined(entries: readonly LlamaQuarantineEntry[], backend: LlamaBackend, binaryPath: string, io?: QuarantineIo): boolean;
242
+ /**
243
+ * Record that `backend`'s binary crashed. Replaces any existing entry for
244
+ * the same backend and drops entries whose binary no longer matches, so
245
+ * the file stays small and self-pruning.
246
+ *
247
+ * Returns the entry written, or null when the binary could not be
248
+ * fingerprinted (it vanished mid-crash) — there is nothing meaningful to
249
+ * pin the quarantine to in that case, and a stale-forever entry is worse
250
+ * than none.
251
+ */
252
+ declare function recordLlamaQuarantine(home: string, input: {
253
+ backend: LlamaBackend;
254
+ binaryPath: string;
255
+ signal: string;
256
+ reason: string;
257
+ }, io?: QuarantineIo): LlamaQuarantineEntry | null;
258
+
259
+ /**
260
+ * Service-side native-binary discovery.
261
+ *
262
+ * The Electron supervisor runs its own discovery in
263
+ * `packages/app/src/supervisor/index.ts#connectOrStart` before spawning
264
+ * the service, so embedded / dev / packaged-spawn launches already
265
+ * have `GEZEL_LLAMA_SERVER_BIN`, `GEZEL_SD_SERVER_BIN`,
266
+ * `GEZEL_WHISPER_SERVER_BIN`, `GEZEL_UV_BIN`, and
267
+ * `GEZEL_DEVICE_HEALTH_BIN` in their env. The OS
268
+ * system services (Windows NSSM `GezelService`, macOS LaunchDaemon
269
+ * `com.bendyline.gezeld`, Linux `gezeld.service` systemd unit) do NOT
270
+ * — they're started directly by the OS with a clean environment, so
271
+ * the supervisor's env-stamping never reaches them. Without this
272
+ * module, chat against the on-device provider fails with
273
+ * "no local engine is available on this machine" on every packaged
274
+ * install.
275
+ *
276
+ * Discovery rules per binary:
277
+ *
278
+ * - `GEZEL_*_BIN` already set → no-op (supervisor or operator wins).
279
+ * - `GEZEL_NATIVE_BIN_DIR` unset → no-op (no source of binaries to
280
+ * probe). System-service installers stamp this var; bare CLI runs
281
+ * without it just get the existing actionable error on first chat.
282
+ * - llama-server: needs a backend probe (CUDA / Vulkan / Metal / CPU).
283
+ * Results are cached at `<home>/engines/llama-cpp/backend.json` —
284
+ * same file the supervisor writes, so the two share state.
285
+ * - sd-server / whisper-server / uv: variant-less, resolved directly
286
+ * under `<dir>/<platform>/<binary>`.
287
+ *
288
+ * Cross-platform note: same code on Windows, macOS, and Linux. The
289
+ * `resolvePlatformKey` helper handles the per-OS subdirectory naming
290
+ * (`win32-x64`, `darwin-arm64`, `linux-x64`, `linux-arm64`).
291
+ */
292
+
293
+ type NativeBinaryName = 'llama-server' | 'ds4-server' | 'sd-server' | 'whisper-server' | 'device-health' | 'uv';
294
+ interface DiscoverInput {
295
+ /** GEZEL_HOME — backend cache file lives at `<home>/engines/llama-cpp/`. */
296
+ home: string;
297
+ /**
298
+ * Optional user-pinned backend override (from `GezelConfig.
299
+ * llamaCppBackendOverride`). Forwarded to the probe unchanged. The
300
+ * supervisor reads this from config.json directly; the service has
301
+ * already loaded config by the time discovery runs, so the caller
302
+ * can pass the resolved value in.
303
+ */
304
+ llamaCppBackendOverride?: 'auto' | LlamaBackend;
305
+ /**
306
+ * Override the native-bin root (test-only). Production callers leave
307
+ * this unset and let the function read `GEZEL_NATIVE_BIN_DIR`.
308
+ */
309
+ nativeBinDirOverride?: string;
310
+ /** Test seam — production callers default to `process.platform` / `process.arch`. */
311
+ platform?: NodeJS.Platform;
312
+ arch?: string;
313
+ /** Test seam for existence checks; defaults to `node:fs.existsSync`. */
314
+ fileExists?: (p: string) => boolean;
315
+ /**
316
+ * Test seam — forwarded to `detectLlamaBackend` so tests can fake
317
+ * driver presence (`libcuda.so.1`, `nvcuda.dll`, vendor sysfs).
318
+ * Production callers leave this unset and the probe uses real fs.
319
+ */
320
+ llamaProbeOverride?: DetectInput['probe'];
321
+ /**
322
+ * Test seam — pre-read quarantine entries. Production callers leave
323
+ * this unset and the list is read from `<home>/engines/llama-cpp/`.
324
+ */
325
+ quarantine?: readonly LlamaQuarantineEntry[];
326
+ /** Optional logger; the service passes its own to thread service-style logs. */
327
+ logger?: {
328
+ info?: (m: string) => void;
329
+ warn?: (m: string) => void;
330
+ };
331
+ }
332
+ interface DiscoverResult {
333
+ /** One entry per binary the discovery attempted. */
334
+ binaries: Array<{
335
+ name: NativeBinaryName;
336
+ /** `'pre-set'` — env var was already populated, discovery skipped. */
337
+ source: 'pre-set' | 'discovered' | 'no-native-bin-dir' | 'no-platform-key' | 'not-found';
338
+ /** Resolved path when `source` is `'pre-set'` or `'discovered'`. */
339
+ path?: string;
340
+ /**
341
+ * For llama-server, the picked backend (so logs can record
342
+ * "discovered cuda variant"). Undefined for variant-less binaries.
343
+ */
344
+ variant?: LlamaBackend;
345
+ }>;
346
+ /** Backend probe result, present when llama-server discovery ran. */
347
+ llamaBackend?: {
348
+ backend: LlamaBackend;
349
+ detectedBackend: LlamaBackend;
350
+ reason: string;
351
+ cached: boolean;
352
+ vendorHint?: GpuVendorHint;
353
+ /**
354
+ * Backends skipped because they crashed on this machine. Present so
355
+ * the UI can say the GPU engine was demoted rather than leaving the
356
+ * user to infer it from a slow session.
357
+ */
358
+ quarantined?: LlamaBackend[];
359
+ };
360
+ }
361
+ /**
362
+ * Resolve a single bundled binary under `<root>/<subdir>/<file>`. The
363
+ * `subdir` is `<platform>[-<variant>]`; the file is `<name>[.exe]`.
364
+ *
365
+ * Returns `null` when the file isn't on disk. Caller decides whether
366
+ * to fall through to a variant-less subdir or surface "not found."
367
+ */
368
+ declare function resolveNativeBinaryUnder(root: string, name: NativeBinaryName, subdir: string, platform?: NodeJS.Platform, fileExists?: (p: string) => boolean): string | null;
369
+ /**
370
+ * Discover bundled native binaries and stamp the matching env vars on
371
+ * `process.env`. Idempotent: a second call after the env is set is a
372
+ * no-op. Tolerant of partial bundling — a missing binary just leaves
373
+ * its env var unset; the dependent provider then surfaces its own
374
+ * actionable error on first use.
375
+ */
376
+ declare function discoverNativeBinaries(input: DiscoverInput): DiscoverResult;
377
+
378
+ /**
379
+ * Windows console-allocation policy for spawned native children.
380
+ *
381
+ * Console-subsystem executables (llama-server, ds4-server, bundled Node) get
382
+ * a console allocated by the loader unless the creator says otherwise. Under
383
+ * the machine-wide service there is nothing to allocate one from: the daemon
384
+ * runs in non-interactive Session 0, where — as
385
+ * `native/helpers/service-host/src/main.cpp` records — `AllocConsole` fails
386
+ * with error 317. `DETACHED_PROCESS`, which Node exposes as `detached: true`,
387
+ * asks for no console at all.
388
+ *
389
+ * `windowsHide` is NOT that flag. It maps to `CREATE_NO_WINDOW`, which still
390
+ * allocates a console and only withholds the window.
391
+ *
392
+ * `detached` is deliberately scoped to win32. On POSIX the same option means
393
+ * `setsid()`, which changes process-group and signal semantics that callers
394
+ * may depend on; there is no console problem to solve there.
395
+ *
396
+ * ## What this does not fix
397
+ *
398
+ * This helper was introduced in v1.26215.31 believing it fixed the
399
+ * `spawn EPERM` that killed every native-engine launch under the machine
400
+ * service. It did not. That release shipped the flag and the failure
401
+ * continued, on two machines, with the daemon's own log showing the engine,
402
+ * the bundled device-health helper, `nvidia-smi`, `amd-smi` and `rocm-smi`
403
+ * all denied at once. The cause was the service token: `sc sidtype ...
404
+ * restricted` write-restricts it, and libuv creates a named pipe per piped
405
+ * stdio handle before every `CreateProcess`, which that token cannot do. The
406
+ * installer now assigns `unrestricted` (see
407
+ * `packages/app/installer/nsis-hooks.nsh`), and `probeChildProcessSpawn` in
408
+ * the service catches a recurrence at boot.
409
+ *
410
+ * The flag is still correct and still used — a console the service cannot
411
+ * allocate is one Windows should not be asked for — but it is a tidiness
412
+ * measure, not a fix for a permission error. If `spawn EPERM` appears again,
413
+ * do not reach for spawn flags: look at the token.
414
+ */
415
+ declare function windowsDetachedSpawnOptions(platform?: NodeJS.Platform): {
416
+ detached: true;
417
+ } | Record<string, never>;
418
+
419
+ /**
420
+ * Cache-bust key for the llama-cpp backend probe.
421
+ *
422
+ * Bumped alongside `native/engines/llama-cpp/VERSION`. Both the Electron
423
+ * supervisor (pre-spawn discovery) and the service (system-service-side
424
+ * discovery) write/read the same cache file at
425
+ * `<home>/engines/llama-cpp/backend.json`. A mismatch between the two
426
+ * would cause cache thrash; keep them in one constant.
427
+ */
428
+ declare const LLAMA_ENGINE_VERSION = "b10353";
429
+
430
+ /**
431
+ * Map a (platform, arch) pair to the subdirectory name used under
432
+ * the bundled `native-bin/` tree (e.g. `win32-x64`, `darwin-arm64`,
433
+ * `linux-arm64`). Returns `null` on platforms we don't ship engine
434
+ * binaries for; callers should treat that as "no bundled engine
435
+ * available" and avoid further probing.
436
+ *
437
+ * Mirrors `packages/app/src/supervisor/native-bin.ts` — kept in sync
438
+ * because both the supervisor (pre-spawn) and the service (system
439
+ * service launches) resolve the same per-platform subtree.
440
+ */
441
+ declare function resolvePlatformKey(platform?: NodeJS.Platform, arch?: string): string | null;
442
+
443
+ /**
444
+ * Device-agnostic accelerator admission and health probing.
445
+ *
446
+ * Policy is deliberately independent of CUDA/Vulkan/ROCm/Metal. Optional
447
+ * command adapters translate vendor telemetry into one small reading shape;
448
+ * the gate then applies the same temperature, thermal-margin, throttle, and
449
+ * telemetry-failure rules everywhere. Unsupported devices remain usable by
450
+ * default (`onTelemetryFailure: allow`) while unattended safety-sensitive
451
+ * runs can fail closed.
452
+ */
453
+
454
+ type DeviceSafetyMode = 'off' | 'observe' | 'guard';
455
+ type DeviceTelemetryFailurePolicy = 'allow' | 'block';
456
+ type DeviceVendor = 'nvidia' | 'amd' | 'apple' | 'generic';
457
+ interface DeviceSafetyPolicyInput {
458
+ mode?: DeviceSafetyMode;
459
+ maxStartTemperatureC?: number;
460
+ resumeTemperatureC?: number;
461
+ minThermalMarginC?: number;
462
+ pollIntervalMs?: number;
463
+ maxWaitMs?: number;
464
+ consecutiveHealthySamples?: number;
465
+ onTelemetryFailure?: DeviceTelemetryFailurePolicy;
466
+ }
467
+ interface ResolvedDeviceSafetyPolicy {
468
+ mode: DeviceSafetyMode;
469
+ maxStartTemperatureC: number;
470
+ resumeTemperatureC: number;
471
+ minThermalMarginC: number;
472
+ pollIntervalMs: number;
473
+ maxWaitMs: number;
474
+ consecutiveHealthySamples: number;
475
+ onTelemetryFailure: DeviceTelemetryFailurePolicy;
476
+ }
477
+ declare const DEFAULT_DEVICE_SAFETY_POLICY: ResolvedDeviceSafetyPolicy;
478
+ interface DeviceHealthReading {
479
+ vendor: DeviceVendor;
480
+ deviceId: string;
481
+ name?: string;
482
+ temperatureC?: number;
483
+ /** Degrees remaining before the device's thermal limit, when exposed. */
484
+ thermalMarginC?: number;
485
+ utilizationPercent?: number;
486
+ memoryUsedMb?: number;
487
+ memoryTotalMb?: number;
488
+ thermalSlowdown?: boolean;
489
+ powerBrake?: boolean;
490
+ }
491
+ type DeviceGpuProcessOwner = 'machine-engine' | 'app-engine' | 'development-engine' | 'gezel-engine' | 'external';
492
+ interface DeviceGpuProcess {
493
+ pid: number;
494
+ name?: string;
495
+ dedicatedBytes: number;
496
+ owner: DeviceGpuProcessOwner;
497
+ }
498
+ interface DeviceHealthSample {
499
+ sampledAt: string;
500
+ sources: string[];
501
+ readings: DeviceHealthReading[];
502
+ /** Dedicated GPU-memory owners when the platform exposes process counters. */
503
+ processes?: DeviceGpuProcess[];
504
+ errors: string[];
505
+ }
506
+ interface DeviceHealthDecision {
507
+ admissible: boolean;
508
+ /** True when a reading crossed the non-optional emergency temperature cutoff. */
509
+ hardBlocked: boolean;
510
+ telemetryAvailable: boolean;
511
+ reasons: string[];
512
+ summary: string;
513
+ }
514
+ type DeviceHealthState = 'off' | 'healthy' | 'warm' | 'cooling' | 'blocked' | 'unavailable';
515
+ /** Authenticated service/UI snapshot; contains no device identifiers beyond display names. */
516
+ interface DeviceHealthStatusSnapshot {
517
+ state: DeviceHealthState;
518
+ mode: DeviceSafetyMode;
519
+ sampledAt: string | null;
520
+ sources: string[];
521
+ readings: DeviceHealthReading[];
522
+ processes?: DeviceGpuProcess[];
523
+ reasons: string[];
524
+ summary: string;
525
+ }
526
+ interface DeviceHealthProbe {
527
+ sample(): Promise<DeviceHealthSample>;
528
+ }
529
+ interface CommandResult {
530
+ stdout: string;
531
+ stderr: string;
532
+ }
533
+ type DeviceHealthCommandRunner = (command: string, args: string[], timeoutMs: number) => Promise<CommandResult>;
534
+ interface SystemDeviceHealthProbeOptions {
535
+ preferredVendor?: 'nvidia' | 'amd';
536
+ commandRunner?: DeviceHealthCommandRunner;
537
+ timeoutMs?: number;
538
+ /**
539
+ * Bundled native helper. `undefined` reads GEZEL_DEVICE_HEALTH_BIN;
540
+ * `null` disables the helper (primarily useful in tests/operators).
541
+ */
542
+ helperPath?: string | null;
543
+ }
544
+ interface DeviceHealthGateOptions {
545
+ probe: DeviceHealthProbe;
546
+ policy?: DeviceSafetyPolicyInput;
547
+ log?: (message: string) => void;
548
+ sleep?: (ms: number) => Promise<void>;
549
+ now?: () => number;
550
+ }
551
+ declare function resolveDeviceSafetyPolicy(input: DeviceSafetyPolicyInput | undefined, env?: NodeJS.ProcessEnv): ResolvedDeviceSafetyPolicy;
552
+ /** Parse the stable CSV query emitted by NVIDIA SMI. Exported for tests. */
553
+ declare function parseNvidiaSmiCsv(stdout: string): DeviceHealthReading[];
554
+ /**
555
+ * Parse AMD SMI or ROCm SMI JSON without pinning to one release's field
556
+ * spelling. Both CLIs have changed nesting and labels over time, while their
557
+ * semantic keys consistently contain temperature/use/memory/throttle terms.
558
+ */
559
+ declare function parseAmdSmiJson(stdout: string, source?: string): DeviceHealthReading[];
560
+ /**
561
+ * Create a best-effort multi-vendor probe. The bundled helper gets first
562
+ * chance, then optional host SMI CLIs preserve compatibility with custom and
563
+ * older installations.
564
+ */
565
+ declare function createSystemDeviceHealthProbe(opts?: SystemDeviceHealthProbeOptions): DeviceHealthProbe;
566
+ declare function evaluateDeviceHealth(sample: DeviceHealthSample, policy: ResolvedDeviceSafetyPolicy, cooling?: boolean): DeviceHealthDecision;
567
+ /**
568
+ * Stateful admission gate with cooling hysteresis. A device that crosses a
569
+ * start threshold must satisfy the lower resume threshold for consecutive
570
+ * samples before work restarts, preventing hot start/stop oscillation.
571
+ */
572
+ declare class DeviceHealthGate {
573
+ private policy;
574
+ private readonly probe;
575
+ private readonly log;
576
+ private readonly sleep;
577
+ private readonly now;
578
+ private cooling;
579
+ private pendingAdmission?;
580
+ private pendingSample?;
581
+ private lastSample?;
582
+ private lastSampleAt;
583
+ private admissionBlocked;
584
+ private lastUnavailableDiagnostic;
585
+ constructor(opts: DeviceHealthGateOptions);
586
+ setPolicy(policy: DeviceSafetyPolicyInput): void;
587
+ getPolicy(): ResolvedDeviceSafetyPolicy;
588
+ /**
589
+ * Return a cached-or-fresh normalized snapshot for status surfaces. The
590
+ * cache keeps a UI polling every few seconds from spawning overlapping SMI
591
+ * processes, while admission can still force a fresh sample after it ages.
592
+ */
593
+ status(maxAgeMs?: number): Promise<DeviceHealthStatusSnapshot>;
594
+ admit(context: string): Promise<DeviceHealthDecision>;
595
+ private runAdmission;
596
+ private sampleDevice;
597
+ }
598
+
599
+ /**
600
+ * macOS GPU kernel-panic detection.
601
+ *
602
+ * Heavy Metal workloads on Apple Silicon can trip a latent Apple GPU *driver*
603
+ * bug: a kernel panic in `IOGPUMemory.cpp` ("completeMemory() prepare count
604
+ * underflow", driver family `AGXAcceleratorG16X`). Wild-caught on
605
+ * an M4 Max running local MLX models — an identical panic hit two days
606
+ * running. It is NOT out-of-memory and NOT gezel/MLX code: a userspace
607
+ * process can't panic the kernel; only a driver bug can. Our GPU
608
+ * allocate/free churn (loading/unloading models, KV cache) merely *triggers*
609
+ * it.
610
+ *
611
+ * No thermal/memory telemetry threshold catches a driver refcount underflow,
612
+ * so the one thing gezel can do is DETECT that a panic happened (macOS writes
613
+ * a `.panic` report on the next boot) and stop re-triggering the loop —
614
+ * refuse to auto-spawn a fresh local GPU engine right after a panic until the
615
+ * operator acknowledges. This module is the pure detector; consumers (the
616
+ * service's engine-spawn guard, the eval harness) decide policy.
617
+ *
618
+ * Node built-ins only, so it lives in core native alongside device-health.
619
+ * No-op off macOS.
620
+ */
621
+ /**
622
+ * Panic strings that implicate the GPU / Metal kernel driver — the class
623
+ * Metal churn triggers. Broad across Apple GPU driver families (AGX = Apple
624
+ * GPU) and the IOAccelerator/IOGPU memory paths so a slightly different
625
+ * underflow site or a newer SoC's accelerator name still matches.
626
+ */
627
+ declare const GPU_PANIC_RE: RegExp;
628
+ interface GpuPanicRecord {
629
+ /** Absolute path to the `.panic`/`.ips` report. */
630
+ file: string;
631
+ /** Report file mtime — when macOS wrote it (≈ the reboot after the panic). */
632
+ when: Date;
633
+ /** The matched driver signature, for the operator message. */
634
+ signature: string;
635
+ }
636
+ interface FindGpuPanicOptions {
637
+ /** Only report panics newer than this many ms ago. Default 24h. */
638
+ withinMs?: number;
639
+ /** Injected for tests: current time. Defaults to `Date.now()`. */
640
+ now?: number;
641
+ /** Injected for tests: directories to scan. Defaults to the macOS report dirs. */
642
+ dirs?: string[];
643
+ }
644
+ /**
645
+ * Return GPU-driver kernel panics recorded within the window, newest first.
646
+ * Empty on non-macOS (no report dirs) or when nothing matches.
647
+ */
648
+ declare function findRecentGpuPanics(opts?: FindGpuPanicOptions): GpuPanicRecord[];
649
+
650
+ export { type CommandResult, DEFAULT_DEVICE_SAFETY_POLICY, type DetectInput, type DetectResult, type DeviceHealthCommandRunner, type DeviceHealthDecision, DeviceHealthGate, type DeviceHealthGateOptions, type DeviceHealthProbe, type DeviceHealthReading, type DeviceHealthSample, type DeviceHealthState, type DeviceHealthStatusSnapshot, type DeviceSafetyMode, type DeviceSafetyPolicyInput, type DeviceTelemetryFailurePolicy, type DeviceVendor, type DiscoverInput, type DiscoverResult, type FindGpuPanicOptions, GPU_PANIC_RE, type GpuPanicRecord, type GpuVendorHint, LLAMA_ENGINE_VERSION, type LlamaBackend, type LlamaQuarantineEntry, type NativeBinaryName, type QuarantineIo, type ResolvedDeviceSafetyPolicy, type ResolvedLlamaBinary, type SystemDeviceHealthProbeOptions, binaryFingerprint, createSystemDeviceHealthProbe, detectLlamaBackend, discoverNativeBinaries, evaluateDeviceHealth, findRecentGpuPanics, isBinaryQuarantined, llamaQuarantinePath, parseAmdSmiJson, parseNvidiaSmiCsv, readLlamaQuarantine, recordLlamaQuarantine, resolveAvailableLlamaBinary, resolveDeviceSafetyPolicy, resolveNativeBinaryUnder, resolvePlatformKey, windowsDetachedSpawnOptions };