@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/checks/index.d.ts +693 -0
- package/dist/checks/index.js +1848 -0
- package/dist/device-safety-DezzpNyR.d.ts +10 -0
- package/dist/index-D_dch9Qh.d.ts +59398 -0
- package/dist/index.d.ts +3217 -0
- package/dist/index.js +25602 -0
- package/dist/markdown/index.d.ts +174 -0
- package/dist/markdown/index.js +4022 -0
- package/dist/native/index.d.ts +650 -0
- package/dist/native/index.js +1121 -0
- package/dist/paths.d.ts +578 -0
- package/dist/paths.js +573 -0
- package/dist/report-action-DzdQHzGG.d.ts +4270 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +15382 -0
- package/package.json +85 -0
|
@@ -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 };
|