diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index a55b65c..954d515 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -113,6 +113,7 @@ export class CodexAcpServer implements acp.Agent { private readonly connection: acp.AgentSideConnection; private readonly defaultAuthRequest: CodexAuthRequest | null; private readonly getExitCode: () => number | null; + private readonly getRecentStderr: () => string; private readonly availableCommands: CodexCommands; private clientInfo: acp.Implementation | null; private terminalOutputMode: TerminalOutputMode; @@ -130,6 +131,7 @@ export class CodexAcpServer implements acp.Agent { codexAcpClient: CodexAcpClient, defaultAuthRequest?: CodexAuthRequest, getExitCode?: () => number | null, + getRecentStderr?: () => string, ) { this.sessions = new Map(); this.pendingMcpStartupSessions = new Map(); @@ -142,6 +144,7 @@ export class CodexAcpServer implements acp.Agent { this.codexAcpClient = codexAcpClient; this.defaultAuthRequest = defaultAuthRequest ?? null; this.getExitCode = getExitCode ?? (() => null); + this.getRecentStderr = getRecentStderr ?? (() => ""); this.clientInfo = null; this.terminalOutputMode = "terminal_output_delta"; this.availableCommands = new CodexCommands( @@ -1458,7 +1461,9 @@ export class CodexAcpServer implements acp.Agent { throw new RequestError(requestErrorCode, `VC++ redistributable should be installed`); } if (exitCode !== null) { - throw new RequestError(requestErrorCode, `Codex process has exited with code ${exitCode}`); + const stderr = this.getRecentStderr().trim(); + const detail = stderr ? `:\n${stderr}` : ""; + throw new RequestError(requestErrorCode, `Codex process has exited with code ${exitCode}${detail}`); } throw err; } diff --git a/src/__tests__/CodexACPAgent/process-exit-error.test.ts b/src/__tests__/CodexACPAgent/process-exit-error.test.ts new file mode 100644 index 0000000..99f27d5 --- /dev/null +++ b/src/__tests__/CodexACPAgent/process-exit-error.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { once } from 'node:events'; +import * as acp from '@agentclientprotocol/sdk'; +import { startCodexConnection } from '../../CodexJsonRpcConnection'; +import { CodexAppServerClient } from '../../CodexAppServerClient'; +import { CodexAcpClient } from '../../CodexAcpClient'; +import { CodexAcpServer } from '../../CodexAcpServer'; +import { createMockConnections } from './test-utils'; + +describe('CodexACPAgent - process exit error', () => { + it.skipIf(process.platform === 'win32')('includes the crashed process stderr', async () => { + const fakeCodex = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'codex-bad-')), 'codex'); + fs.writeFileSync(fakeCodex, "#!/bin/sh\necho 'codex: failed to launch' >&2\nexit 1\n"); + fs.chmodSync(fakeCodex, 0o755); + + const connection = startCodexConnection(fakeCodex); + let stderr = ''; + connection.process.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); + + const codexClient = new CodexAcpClient(new CodexAppServerClient(connection.connection)); + const agent = new CodexAcpServer( + createMockConnections().mockAcpConnection, + codexClient, + undefined, + () => connection.process.exitCode, + () => stderr, + ); + + await once(connection.process, 'close'); // process exited and stderr flushed + + await expect(agent.initialize({ protocolVersion: acp.PROTOCOL_VERSION })) + .rejects.toThrow("Codex process has exited with code 1:\ncodex: failed to launch"); + }); +}); diff --git a/src/index.ts b/src/index.ts index c892310..01436ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,13 @@ function startAcpServer() { }); const codexConnection = startCodexConnection(codexPath); + + const maxStderrTailChars = 2 * 1024; + let stderr = ""; + codexConnection.process.stderr.addListener("data", (data: Buffer) => { + stderr = (stderr + data.toString()).slice(-maxStderrTailChars); + }); + process.stdin.on("close", (chunk: Buffer) => { codexConnection.process.stdin.end(); // Kill the codex process if it doesn't exit naturally @@ -73,7 +80,7 @@ function startAcpServer() { function createAgent(connection: acp.AgentSideConnection): CodexAcpServer { const appServerClient = new CodexAppServerClient(codexConnection.connection); const codexClient = new CodexAcpClient(appServerClient, config, modelProvider); - return new CodexAcpServer(connection, codexClient, defaultAuthRequest, () => codexConnection.process.exitCode); + return new CodexAcpServer(connection, codexClient, defaultAuthRequest, () => codexConnection.process.exitCode, () => stderr); } new acp.AgentSideConnection(createAgent, acpJsonStream);