Skip to content

Commit 741648a

Browse files
authored
Merge pull request #2 from JetBrains/st
Windows fixes and error handling
2 parents 2b0d1e9 + ad90a6b commit 741648a

7 files changed

Lines changed: 79 additions & 22 deletions

File tree

readme-dev.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
Last checked version: `codex-cli 0.64.0-alpha.9`)
66

7+
###### Develop on Windows?
8+
- Download and install [bum](https://bun.com/docs/installation#windows)
9+
- Download and install [C++ redistributable package](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-supported-redistributable-version)
10+
711
#### Adjust acp config for IDE
812

913
Run from sources

src/CodexAcpServer.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,27 @@ export class CodexAcpServer implements acp.Agent {
1717
private readonly codexAcpClient: CodexAcpClient;
1818
private readonly connection: acp.AgentSideConnection;
1919
private readonly defaultAuthRequest: CodexAuthRequest | null;
20+
private readonly getExitCode: () => number | null;
2021

2122
private readonly sessions: Map<string, SessionState>;
2223

2324
constructor(
2425
connection: acp.AgentSideConnection,
2526
codexAcpClient: CodexAcpClient,
2627
defaultAuthRequest?: CodexAuthRequest,
28+
getExitCode?: () => number | null,
2729
) {
2830
this.sessions = new Map();
2931
this.connection = connection;
30-
this.defaultAuthRequest = defaultAuthRequest ?? null;
3132
this.codexAcpClient = codexAcpClient;
33+
this.defaultAuthRequest = defaultAuthRequest ?? null;
34+
this.getExitCode = getExitCode ?? (() => null);
3235
}
3336

3437
async initialize(
3538
_params: acp.InitializeRequest,
3639
): Promise<acp.InitializeResponse> {
37-
await this.codexAcpClient.initialize(_params);
40+
await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
3841
return {
3942
protocolVersion: acp.PROTOCOL_VERSION,
4043
agentCapabilities: {
@@ -47,15 +50,15 @@ export class CodexAcpServer implements acp.Agent {
4750
async newSession(
4851
_params: acp.NewSessionRequest,
4952
): Promise<acp.NewSessionResponse> {
50-
if (await this.codexAcpClient.authRequired()) {
53+
if (await this.runWithProcessCheck(() => this.codexAcpClient.authRequired())) {
5154
if (this.defaultAuthRequest) {
5255
await this.authenticate(this.defaultAuthRequest)
5356
} else {
5457
throw RequestError.authRequired();
5558
}
5659
}
5760

58-
const sessionMetadata = await this.codexAcpClient.newSession(_params);
61+
const sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.newSession(_params));
5962
const {sessionId, currentModelId, models} = sessionMetadata;
6063
this.sessions.set(sessionId, {
6164
sessionMetadata: sessionMetadata,
@@ -76,7 +79,7 @@ export class CodexAcpServer implements acp.Agent {
7679
async authenticate(
7780
_params: acp.AuthenticateRequest,
7881
): Promise<acp.AuthenticateResponse> {
79-
const isAuthenticated = await this.codexAcpClient.authenticate(_params);
82+
const isAuthenticated = await this.runWithProcessCheck(() => this.codexAcpClient.authenticate(_params));
8083
if (!isAuthenticated) {
8184
throw RequestError.invalidParams();
8285
}
@@ -118,10 +121,10 @@ export class CodexAcpServer implements acp.Agent {
118121
}
119122

120123

121-
await this.codexAcpClient.setModel({
124+
await this.runWithProcessCheck(() => this.codexAcpClient.setModel({
122125
model: model.model,
123126
reasoningEffort,
124-
});
127+
}));
125128
sessionState.sessionMetadata.currentModelId = ModelId.fromComponents(model, reasoningEffort).toString();
126129

127130
return {};
@@ -155,7 +158,7 @@ export class CodexAcpServer implements acp.Agent {
155158

156159
try {
157160
const messageHandler = new CodexEventHandler(this.connection, sessionState);
158-
await this.codexAcpClient.sendPrompt(params, (event) => messageHandler.handleNotification(event));
161+
await this.runWithProcessCheck(() => this.codexAcpClient.sendPrompt(params, (event) => messageHandler.handleNotification(event)));
159162
} catch (err) {
160163
if (sessionState.pendingPrompt.signal.aborted) {
161164
return {stopReason: "cancelled"};
@@ -171,6 +174,22 @@ export class CodexAcpServer implements acp.Agent {
171174
};
172175
}
173176

177+
private async runWithProcessCheck<T>(operation: () => Promise<T>): Promise<T> {
178+
try {
179+
return await operation();
180+
} catch (err) {
181+
const exitCode = this.getExitCode();
182+
const requestErrorCode = 1001 // Just some magic number
183+
if (exitCode == 3221225781) {
184+
throw new RequestError(requestErrorCode, `VC++ redistributable should be installed`);
185+
}
186+
if (exitCode !== null) {
187+
throw new RequestError(requestErrorCode, `Codex process has exited with code ${exitCode}`);
188+
}
189+
throw err;
190+
}
191+
}
192+
174193
async cancel(params: acp.CancelNotification): Promise<void> {
175194
//TODO not supported yet
176195
this.sessions.get(params.sessionId)?.pendingPrompt?.abort();

src/CodexEventHandler.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {ACPSessionConnection, type UpdateSessionEvent} from "./ACPSessionConnect
77
import type {
88
AgentMessageDeltaNotification,
99
CommandAction,
10+
ErrorNotification,
1011
FileUpdateChange,
1112
ItemCompletedNotification,
1213
ItemStartedNotification,
@@ -50,6 +51,8 @@ export class CodexEventHandler {
5051
return await this.completeItemEvent(notification.params);
5152
case "turn/plan/updated":
5253
return await this.updatePlan(notification.params);
54+
case "error":
55+
return await this.createErrorEvent(notification.params);
5356
case "item/reasoning/summaryTextDelta": //TODO streaming reasoning?
5457
case "item/reasoning/summaryPartAdded":
5558
//skipped events
@@ -59,7 +62,6 @@ export class CodexEventHandler {
5962
case "turn/diff/updated":
6063
case "item/commandExecution/outputDelta":
6164
case "item/fileChange/outputDelta":
62-
case "error":
6365
case "thread/tokenUsage/updated":
6466
case "item/mcpToolCall/progress":
6567
case "account/updated":
@@ -219,4 +221,14 @@ export class CodexEventHandler {
219221
entries: plan,
220222
}
221223
}
224+
225+
private async createErrorEvent(params: ErrorNotification): Promise<UpdateSessionEvent> {
226+
return {
227+
sessionUpdate: "agent_message_chunk",
228+
content: {
229+
type: "text",
230+
text: `❌ ${params.error.message}\n\n`
231+
}
232+
}
233+
}
222234
}

src/CodexJsonRpcConnection.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ import fs from "node:fs";
77
import path from "node:path";
88
import {createJSONRPCReader, createJSONRPCWriter} from "./StdUtils";
99

10-
export function startCodexConnection(codexPath: string, logPath?: string): MessageConnection {
11-
const codex: ChildProcessWithoutNullStreams = spawn(codexPath, ["app-server"]);
10+
export interface CodexConnection {
11+
readonly connection: MessageConnection
12+
readonly process: ChildProcessWithoutNullStreams;
13+
}
14+
15+
export function startCodexConnection(codexPath: string, logPath?: string): CodexConnection {
16+
const codex: ChildProcessWithoutNullStreams = process.platform === 'win32'
17+
? spawn(`"${codexPath}" app-server`, { shell: true })
18+
: spawn(codexPath, ['app-server']);
1219

1320
if (logPath) {
1421
attachLogs(codex, logPath);
@@ -21,7 +28,12 @@ export function startCodexConnection(codexPath: string, logPath?: string): Messa
2128

2229
connection.listen();
2330

24-
return connection;
31+
// Terminate all current activities on process termination
32+
codex.on("exit", _ => {
33+
connection.dispose();
34+
});
35+
36+
return {connection: connection, process: codex};
2537
}
2638

2739
function attachLogs(proc: ChildProcessWithoutNullStreams, logPath: string) {
@@ -39,4 +51,7 @@ function attachLogs(proc: ChildProcessWithoutNullStreams, logPath: string) {
3951
proc.stdout.on("data", (data: Buffer) => {
4052
log("[STDOUT] " + data.toString());
4153
});
54+
proc.on("exit", (code) => {
55+
log("[EXIT] " + code?.toString());
56+
});
4257
}

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {createTestFixture, type TestFixture} from "../acp-test-utils";
44
import type {ServerNotification} from "../../app-server";
55
import type {SessionState} from "../../CodexAcpServer";
66

7-
describe('ACP server test', () => {
7+
describe('ACP server test', { timeout: 40_000 }, () => {
88

99
let fixture: TestFixture;
1010
beforeEach(() => {
@@ -20,7 +20,7 @@ describe('ACP server test', () => {
2020

2121
fixture.getCodexAcpClient().authRequired = vi.fn().mockResolvedValue(false);
2222
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
23-
codexAcpAgent.prompt({ sessionId: newSessionResponse.sessionId, prompt: [{type: "text", text: "Hi!"}] });
23+
codexAcpAgent.prompt({sessionId: newSessionResponse.sessionId, prompt: [{type: "text", text: "Hi!"}]});
2424

2525
const transportDump = fixture.getCodexConnectionDump(ignoredFields);
2626
await expect(transportDump).toMatchFileSnapshot("data/start-conversation.json");
@@ -93,7 +93,7 @@ describe('ACP server test', () => {
9393

9494
expect(fixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/output-acp-events.json");
9595

96-
}, 90_000);
96+
});
9797

9898
//dev-time test
9999
it.skip('should convert session notification to acp events', async () => {
@@ -105,5 +105,5 @@ describe('ACP server test', () => {
105105
const newSessionResponse = await codexAcpAgent.newSession({cwd: "/home/alex/work/spring-petclinic/", mcpServers: []});
106106
fixture.clearCodexConnectionDump();
107107
await codexAcpAgent.prompt({ sessionId: newSessionResponse.sessionId, prompt: [{type: "text", text: "Add method `minus` to Math Utils."}] });
108-
}, 90_000);
108+
});
109109
});

src/__tests__/acp-test-utils.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {type CodexConnectionEvent, CodexAppServerClient} from '../CodexAppServer
33
import {startCodexConnection} from "../CodexJsonRpcConnection";
44
import {CodexAcpServer} from "../CodexAcpServer";
55
import type {AgentSideConnection} from "@agentclientprotocol/sdk";
6+
import path from "node:path";
7+
import fs from "node:fs";
68

79
export type MethodCallEvent = { method: string; args: any[] };
810

@@ -32,17 +34,22 @@ export interface TestFixture {
3234
}
3335

3436
export function createTestFixture(): TestFixture {
35-
const pathToCodex = "././node_modules/.bin/codex"
37+
const pathToCodex = path.resolve(process.cwd(), "node_modules", ".bin", process.platform === 'win32' ? "codex.cmd" : "codex");
38+
if (!fs.existsSync(pathToCodex)) {
39+
throw new Error(`Codex binary not found at ${pathToCodex}. Did you run 'npm install'?`);
40+
}
41+
3642
const acpConnectionEvents: MethodCallEvent[] = []
3743
const acpEventHandlers: ((event: MethodCallEvent) => void)[] = [];
3844
const acpConnection = createSmartMock<AgentSideConnection>((event) => {
3945
acpConnectionEvents.push(event);
4046
acpEventHandlers.forEach(handler => handler(event));
4147
});
42-
const codexAppServerClient = new CodexAppServerClient(startCodexConnection(pathToCodex));
48+
const codexConnection = startCodexConnection(pathToCodex);
49+
const codexAppServerClient = new CodexAppServerClient(codexConnection.connection);
4350

4451
const codexAcpClient = new CodexAcpClient(codexAppServerClient);
45-
const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient);
52+
const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient, undefined, () => codexConnection.process.exitCode);
4653

4754
const transportEvents: CodexConnectionEvent[] = []
4855
const codexEventHandlers: ((event: CodexConnectionEvent) => void)[] = [];

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ if (process.argv.includes("--version")) {
1717
const codexPath = process.env["CODEX_PATH"] ?? "codex";
1818
const logPath = process.env["APP_SERVER_LOGS"];
1919

20-
const appServerConnection = startCodexConnection(codexPath, logPath);
20+
const codexConnection = startCodexConnection(codexPath, logPath);
2121
const acpJsonStream = createJsonStream(process.stdin, process.stdout);
2222

2323
function createAgent(connection: acp.AgentSideConnection): CodexAcpServer {
@@ -27,9 +27,9 @@ function createAgent(connection: acp.AgentSideConnection): CodexAcpServer {
2727
const authRequestString = process.env["DEFAULT_AUTH_REQUEST"];
2828
const parsedRequest = authRequestString ? JSON.parse(authRequestString) : undefined;
2929
const defaultAuthRequest = parsedRequest && isCodexAuthRequest(parsedRequest) ? parsedRequest : undefined;
30-
const appServerClient = new CodexAppServerClient(appServerConnection);
30+
const appServerClient = new CodexAppServerClient(codexConnection.connection);
3131
const codexClient = new CodexAcpClient(appServerClient, config, modelProvider)
32-
return new CodexAcpServer(connection, codexClient, defaultAuthRequest);
32+
return new CodexAcpServer(connection, codexClient, defaultAuthRequest, () => codexConnection.process.exitCode);
3333
}
3434

3535
new acp.AgentSideConnection(createAgent, acpJsonStream);

0 commit comments

Comments
 (0)