diff --git a/packages/cli/index.ts b/packages/cli/index.ts index f13d4707b0..7cd28116e1 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -20,14 +20,21 @@ import { // Suppress known race condition error in node-pty on Windows // Tracking bug: https://github.com/microsoft/node-pty/issues/827 process.on('uncaughtException', (error) => { - if ( - process.platform === 'win32' && - error instanceof Error && - error.message === 'Cannot resize a pty that has already exited' - ) { - // This error happens on Windows with node-pty when resizing a pty that has just exited. - // It is a race condition in node-pty that we cannot prevent, so we silence it. - return; + if (error instanceof Error) { + const message = error.message || ''; + const isPtyResizeError = + message === 'Cannot resize a pty that has already exited'; + const isEbadfError = + message.includes('EBADF') || + (error as { code?: string }).code === 'EBADF'; + const isFromNodePty = + error.stack?.includes('node-pty') || error.stack?.includes('PtyResize'); + + if ((isPtyResizeError || isEbadfError) && isFromNodePty) { + // This error happens with node-pty when resizing a pty that has just exited. + // It is a race condition in node-pty that we cannot prevent, so we silence it. + return; + } } // For other errors, we rely on the default behavior, but since we attached a listener, diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 2408dc3e11..e12aa367ae 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -38,6 +38,7 @@ import { } from './sandboxManager.js'; import type { SandboxConfig } from '../config/config.js'; import { killProcessGroup } from '../utils/process-utils.js'; +import { isNodeError } from '../utils/errors.js'; import { ExecutionLifecycleService, type ExecutionHandle, @@ -1541,30 +1542,45 @@ export class ShellExecutionService { } const activePty = this.activePtys.get(pid); - if (activePty) { - try { - activePty.ptyProcess.resize(cols, rows); - activePty.headlessTerminal.resize(cols, rows); - } catch (e) { - // Ignore errors if the pty has already exited, which can happen - // due to a race condition between the exit event and this call. - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const err = e as { code?: string; message?: string }; - const isEsrch = err.code === 'ESRCH'; - const isWindowsPtyError = err.message?.includes( - 'Cannot resize a pty that has already exited', - ); + if (!activePty) { + return; + } - if (isEsrch || isWindowsPtyError) { - // On Unix, we get an ESRCH error. - // On Windows, we get a message-based error. - // In both cases, it's safe to ignore. - } else { - throw e; + // Skip Windows: process.kill(pid, 0) is heavy and native errors are catchable there. + if (process.platform !== 'win32') { + try { + process.kill(pid, 0); + } catch (e) { + // Bail only if the process is explicitly confirmed dead (ESRCH). + if (isNodeError(e) && e.code === 'ESRCH') { + return; } } } + try { + activePty.ptyProcess.resize(cols, rows); + activePty.headlessTerminal.resize(cols, rows); + } catch (e) { + // Ignore errors if the pty has already exited, which can happen + // due to a race condition between the exit event and this call. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const err = e as { code?: string; message?: string }; + const isEsrch = err.code === 'ESRCH'; + const isEbadf = err.code === 'EBADF' || err.message?.includes('EBADF'); + const isWindowsPtyError = err.message?.includes( + 'Cannot resize a pty that has already exited', + ); + + if (isEsrch || isEbadf || isWindowsPtyError) { + // On Unix, we get an ESRCH or EBADF error. + // On Windows, we get a message-based error. + // In both cases, it's safe to ignore. + } else { + throw e; + } + } + // Force emit the new state after resize if (activePty) { const endLine = activePty.headlessTerminal.buffer.active.length;