Files
orca/src/shared/runtime-capability-degradation.ts
T
Neil b552bcb91f fix(relay): diagnose why node-pty will not load instead of hedging (#17891)
The relay could only say "terminals are unavailable" and then list three
remedies for four different faults, none of which the user could verify
(#17830). Two things were destroying the evidence:

- `loadPtyUncached` caught the load error into bare `catch {}` blocks
  (pty-handler.ts:539, :551) and returned null. The only cause anyone had
  was discarded on the spot.
- node-pty's own loader walks three directories and rethrows only the LAST
  failure, so even an uncaught error arrives as `Cannot find module
  '../prebuilds/...'` — the GLIBC/ABI/arch sentence is already gone.

The relay now keeps the load error, recovers the real dlopen message with an
out-of-process load of the file node-pty would have opened, reads what
node-gyp configured the binding for (`build/config.gypi`), captures the
host's Node ABI, arch and glibc, and probes the toolchain only when nothing
was compiled. Each fault gets its own message naming values the user can
check: toolchain_missing, dependency_missing, abi_mismatch, arch_mismatch,
libc_floor, shared_library_missing, load_crashed, and load_failed which
quotes the loader verbatim. A probe that did not answer stays `unverifiable`
and prescribes nothing.

The classification is now also structured data on the error, so a client can
repair the host instead of printing a paragraph: an additive, schema-validated
`data` field on an existing JSON-RPC error, with `repairable` true only for a
proved fault that recompiling on the host actually fixes.

Reuses orcad's loader-message parsers and out-of-process probe rather than
adding a second copy; `classifyLoaderMessage` moves to a shared module and
gains architecture and missing-shared-library cases, which the orcad boot
precondition picks up too.
2026-09-01 22:38:07 -07:00

74 lines
3.6 KiB
TypeScript

export const TERMINAL_UNAVAILABLE_ERROR_CODE = 'terminal_unavailable' as const
export const TERMINAL_PTY_DEGRADATION_CAPABILITY = 'terminal.pty.v1' as const
export type RuntimeBrowserUnavailableReason =
| 'unconfigured'
| 'driver_missing'
| 'executable_not_found'
| 'executable_not_executable'
| 'electron_start_failed'
| 'chromium_start_failed'
| 'provider_unhealthy'
| 'desktop_window_unavailable'
| 'unknown'
/**
* Why this host cannot spawn PTYs. Members are opaque to clients: render `message`,
* never switch exhaustively. Dynamic-loader failures are proved out of process because
* an incompatible native binary can terminate the host before JavaScript can catch it.
*/
export type RuntimeTerminalUnavailableReason =
| 'dependency_missing'
| 'toolchain_missing'
| 'libc_floor'
| 'shared_library_missing'
| 'abi_mismatch'
| 'arch_mismatch'
| 'load_failed'
| 'load_crashed'
| 'spawn_helper_missing'
| 'unknown'
export type RuntimeDegradation = {
/**
* Open vocabulary. New codes ship without a protocol bump, so clients must render
* `message` and must not switch exhaustively on this or the `reason` field.
*/
code: 'browser_unavailable' | typeof TERMINAL_UNAVAILABLE_ERROR_CODE
capability: 'browser.headless.v1' | typeof TERMINAL_PTY_DEGRADATION_CAPABILITY
message: string
reason?: RuntimeBrowserUnavailableReason | RuntimeTerminalUnavailableReason
/** Underlying error text when the host has one. Diagnostic only; never load-bearing. */
detail?: string
}
const TERMINAL_UNAVAILABLE_MESSAGES: Record<RuntimeTerminalUnavailableReason, string> = {
dependency_missing:
'Terminals are unavailable on this host: node-pty has no native binary for this platform. Install or rebuild it, or deploy a build that ships a prebuilt binary for this platform.',
toolchain_missing:
'Terminals are unavailable on this host: node-pty has no prebuilt binary for Linux and this host is missing the C/C++ build tools needed to compile one. Install them, then reconnect.',
libc_floor:
"This host's node-pty binary was built against a newer C library than the host provides, so the dynamic loader refuses it. Rebuild node-pty on this host, or deploy a build whose prebuilt binary matches this platform's libc.",
shared_library_missing:
'Terminals are unavailable on this host: a shared library that node-pty links against is not installed, so the dynamic loader cannot open the binary. Install the named library, then reconnect.',
abi_mismatch:
"This host's node-pty binary was built for a different Node ABI than the running Node, so it cannot be loaded. Rebuild node-pty against this Node version.",
arch_mismatch:
"This host's node-pty binary was built for a different CPU architecture than the running Node, so the dynamic loader refuses it. Rebuild node-pty on this host, or deploy a build for this architecture.",
load_failed: 'Terminals are unavailable on this host: node-pty failed to load.',
load_crashed:
'Terminals are unavailable on this host: loading node-pty terminated the probe process, which means the binary is incompatible with this host rather than merely missing.',
spawn_helper_missing:
'node-pty loaded, but its spawn-helper executable is missing or not executable, so every terminal spawn would fail. Reinstall node-pty on this host.',
unknown: 'Terminals are unavailable on this host, and the cause could not be determined.'
}
export function terminalUnavailableMessage(
reason: RuntimeTerminalUnavailableReason,
detail?: string
): string {
const base = TERMINAL_UNAVAILABLE_MESSAGES[reason]
return detail ? `${base} (${detail})` : base
}