diff --git a/src/main/daemon/node-pty-error-hints.test.ts b/src/main/daemon/node-pty-error-hints.test.ts index 1b36eada05a..249cc167131 100644 --- a/src/main/daemon/node-pty-error-hints.test.ts +++ b/src/main/daemon/node-pty-error-hints.test.ts @@ -1,6 +1,22 @@ import { describe, expect, it } from 'vitest' import { addNodePtyRecoveryHint, parseNodePtyDiagnostic } from './node-pty-error-hints' +const PTY_ALLOCATION_HINT = [ + 'Your system cannot allocate any more pty devices.', + '', + 'Orca requires a pty device to launch a new terminal. This error is usually due to having too many terminal windows or terminal sessions open, either in Orca or another program.', + '', + 'Free up some pty devices and try again.' +].join('\n') + +const TERMINAL_PROCESS_LIMIT_HINT = [ + 'Your system cannot start another terminal process.', + '', + 'This is usually due to having too many terminal sessions or other processes running.', + '', + 'Close unused terminals or quit unused processes and try again.' +].join('\n') + describe('node-pty diagnostic error hints', () => { it('parses the native step and errno without dropping the original message', () => { const message = @@ -16,18 +32,42 @@ describe('node-pty diagnostic error hints', () => { const message = "node-pty: open_slave failed: EMFILE (errno 24, Too many open files) - slave='/dev/ttys003'" - expect(addNodePtyRecoveryHint(message)).toBe( - `Daemon hit the file-descriptor limit. Restart the daemon. ${message}` - ) + expect(addNodePtyRecoveryHint(message)).toBe(`${PTY_ALLOCATION_HINT} ${message}`) + }) + + it('hints when the system cannot allocate a pty master', () => { + const message = + 'node-pty: posix_openpt failed: ENFILE (errno 23, Too many open files in system)' + + expect(addNodePtyRecoveryHint(message)).toBe(`${PTY_ALLOCATION_HINT} ${message}`) + }) + + it('hints local wrapped spawn errors from pty allocation failures', () => { + const message = + 'Failed to spawn shell "/bin/zsh": node-pty: open_slave failed: EMFILE (errno 24, Too many open files) - slave=\'/dev/ttys003\' (shell: /bin/zsh, cwd: /tmp, arch: arm64, platform: darwin 25.0.0). If this persists, please file an issue.' + + expect(addNodePtyRecoveryHint(message)).toBe(`${PTY_ALLOCATION_HINT} ${message}`) + }) + + it('hints unstructured openpty allocation failures', () => { + const message = 'Failed to spawn shell "/bin/bash": openpty(3) failed.' + + expect(addNodePtyRecoveryHint(message)).toBe(`${PTY_ALLOCATION_HINT} ${message}`) }) it('hints when posix_spawn reports the per-user process limit', () => { const message = "node-pty: posix_spawn failed: EAGAIN (errno 35, Resource temporarily unavailable) - helper='/tmp/node-pty/spawn-helper'" - expect(addNodePtyRecoveryHint(message)).toBe( - `Per-user process limit reached. Quit some agents and retry. ${message}` - ) + expect(addNodePtyRecoveryHint(message)).toBe(`${TERMINAL_PROCESS_LIMIT_HINT} ${message}`) + }) + + it('does not duplicate an existing recovery hint', () => { + const message = + "node-pty: open_slave failed: EMFILE (errno 24, Too many open files) - slave='/dev/ttys003'" + const hinted = `${PTY_ALLOCATION_HINT} ${message}` + + expect(addNodePtyRecoveryHint(hinted)).toBe(hinted) }) it('leaves unrelated and unhinted node-pty diagnostics unchanged', () => { diff --git a/src/main/daemon/node-pty-error-hints.ts b/src/main/daemon/node-pty-error-hints.ts index 97c13baf5d9..a440a27639a 100644 --- a/src/main/daemon/node-pty-error-hints.ts +++ b/src/main/daemon/node-pty-error-hints.ts @@ -4,9 +4,45 @@ export type NodePtyDiagnostic = { } const NODE_PTY_DIAGNOSTIC_RE = /^node-pty: ([A-Za-z0-9_]+) failed: .*?\(errno (\d+)(?:, [^)]*)?\)/ +const NODE_PTY_DIAGNOSTIC_ANYWHERE_RE = + /node-pty: ([A-Za-z0-9_]+) failed: .*?\(errno (\d+)(?:, [^)]*)?\)/ +const GENERIC_PTY_ALLOCATION_RE = /\b(?:openpty|forkpty)\(3\) failed\b/i + +const PTY_ALLOCATION_STEPS = new Set([ + 'posix_openpt', + 'grantpt', + 'unlockpt', + 'ioctl_TIOCPTYGNAME', + 'open_slave' +]) + +const RESOURCE_EXHAUSTION_ERRNOS = new Set([ + 11, // EAGAIN on Linux + 12, // ENOMEM + 23, // ENFILE on macOS + 24, // EMFILE on macOS/Linux + 35 // EAGAIN on macOS +]) + +const PTY_ALLOCATION_HINT = [ + 'Your system cannot allocate any more pty devices.', + '', + 'Orca requires a pty device to launch a new terminal. This error is usually due to having too many terminal windows or terminal sessions open, either in Orca or another program.', + '', + 'Free up some pty devices and try again.' +].join('\n') + +const TERMINAL_PROCESS_LIMIT_HINT = [ + 'Your system cannot start another terminal process.', + '', + 'This is usually due to having too many terminal sessions or other processes running.', + '', + 'Close unused terminals or quit unused processes and try again.' +].join('\n') export function parseNodePtyDiagnostic(message: string): NodePtyDiagnostic | null { - const match = NODE_PTY_DIAGNOSTIC_RE.exec(message) + const match = + NODE_PTY_DIAGNOSTIC_RE.exec(message) ?? NODE_PTY_DIAGNOSTIC_ANYWHERE_RE.exec(message) if (!match) { return null } @@ -21,11 +57,14 @@ export function getNodePtyRecoveryHint(diagnostic: NodePtyDiagnostic): string | if (diagnostic.step === 'posix_spawn' && diagnostic.errno === 2) { return "Daemon's node-pty install is gone (worktree deleted?). Restart Orca." } - if (diagnostic.step === 'open_slave' && diagnostic.errno === 24) { - return 'Daemon hit the file-descriptor limit. Restart the daemon.' + if ( + PTY_ALLOCATION_STEPS.has(diagnostic.step) && + RESOURCE_EXHAUSTION_ERRNOS.has(diagnostic.errno) + ) { + return PTY_ALLOCATION_HINT } - if (diagnostic.step === 'posix_spawn' && diagnostic.errno === 35) { - return 'Per-user process limit reached. Quit some agents and retry.' + if (diagnostic.step === 'posix_spawn' && RESOURCE_EXHAUSTION_ERRNOS.has(diagnostic.errno)) { + return TERMINAL_PROCESS_LIMIT_HINT } return null } @@ -33,9 +72,15 @@ export function getNodePtyRecoveryHint(diagnostic: NodePtyDiagnostic): string | export function addNodePtyRecoveryHint(message: string): string { const diagnostic = parseNodePtyDiagnostic(message) if (!diagnostic) { + if (GENERIC_PTY_ALLOCATION_RE.test(message) && !message.startsWith(PTY_ALLOCATION_HINT)) { + return `${PTY_ALLOCATION_HINT} ${message}` + } return message } const hint = getNodePtyRecoveryHint(diagnostic) + if (hint && message.startsWith(hint)) { + return message + } return hint ? `${hint} ${message}` : message }