diff --git a/packages/plugins/agent-codex/src/app-server-client.test.ts b/packages/plugins/agent-codex/src/app-server-client.test.ts index 70768b63e..1e58489c0 100644 --- a/packages/plugins/agent-codex/src/app-server-client.test.ts +++ b/packages/plugins/agent-codex/src/app-server-client.test.ts @@ -983,6 +983,27 @@ describe("CodexAppServerClient", () => { expect(client.isConnected).toBe(true); await closeClient(client, proc); }); + + it("resets connecting flag if spawn throws synchronously", async () => { + // Make spawn throw (e.g. EMFILE, ENOMEM) + mockSpawn.mockImplementationOnce(() => { + throw new Error("spawn EMFILE"); + }); + + const client = new CodexAppServerClient({ requestTimeout: 500 }); + await expect(client.connect()).rejects.toThrow("spawn EMFILE"); + + // connecting flag should be reset — a retry should not throw "already connecting" + const proc = createFakeProcess(); + const connectPromise = client.connect(); + await new Promise((r) => setTimeout(r, 10)); + const initReq = findRequest(proc, "initialize"); + proc.sendLine(JSON.stringify({ id: initReq!["id"], result: {} })); + await connectPromise; + + expect(client.isConnected).toBe(true); + await closeClient(client, proc); + }); }); describe("readline cleanup on process exit/error", () => { diff --git a/packages/plugins/agent-codex/src/app-server-client.ts b/packages/plugins/agent-codex/src/app-server-client.ts index 01151988e..74bd5f4ce 100644 --- a/packages/plugins/agent-codex/src/app-server-client.ts +++ b/packages/plugins/agent-codex/src/app-server-client.ts @@ -174,37 +174,34 @@ export class CodexAppServerClient extends EventEmitter { this.connecting = true; - this.process = spawn(this.binaryPath, ["app-server"], { - cwd: this.cwd, - env: this.env ? { ...process.env, ...this.env } : undefined, - stdio: ["pipe", "pipe", "pipe"], - }); - - if (!this.process.stdout || !this.process.stdin) { - this.connecting = false; - throw new Error("Failed to open stdio pipes for codex app-server"); - } - - // Drain stderr to prevent the child process from blocking when - // the pipe buffer fills up. - this.process.stderr?.resume(); - - // Set up line-based reading from stdout - this.readline = createInterface({ input: this.process.stdout }); - this.readline.on("line", (line) => this.handleLine(line)); - - // Handle process exit - this.process.once("exit", (code, signal) => { - this.handleProcessExit(code, signal); - }); - - this.process.once("error", (err) => { - this.handleProcessError(err); - }); - - // Perform initialization handshake. On failure, clean up the spawned - // process so the caller doesn't need to call close() explicitly. try { + this.process = spawn(this.binaryPath, ["app-server"], { + cwd: this.cwd, + env: this.env ? { ...process.env, ...this.env } : undefined, + stdio: ["pipe", "pipe", "pipe"], + }); + + if (!this.process.stdout || !this.process.stdin) { + throw new Error("Failed to open stdio pipes for codex app-server"); + } + + // Drain stderr to prevent the child process from blocking when + // the pipe buffer fills up. + this.process.stderr?.resume(); + + // Set up line-based reading from stdout + this.readline = createInterface({ input: this.process.stdout }); + this.readline.on("line", (line) => this.handleLine(line)); + + // Handle process exit + this.process.once("exit", (code, signal) => { + this.handleProcessExit(code, signal); + }); + + this.process.once("error", (err) => { + this.handleProcessError(err); + }); + await this.initialize(); } catch (err) { this.connecting = false; diff --git a/packages/plugins/agent-codex/src/index.test.ts b/packages/plugins/agent-codex/src/index.test.ts index de5eea31f..d47fb7612 100644 --- a/packages/plugins/agent-codex/src/index.test.ts +++ b/packages/plugins/agent-codex/src/index.test.ts @@ -638,10 +638,11 @@ describe("getSessionInfo", () => { expect(result!.summary).toBe("Codex session (o3-mini)"); expect(result!.summaryIsFallback).toBe(true); expect(result!.cost).toBeDefined(); - // input: 1000 + 200 (cached) + 2000 + 0 = 3200 - // output: 500 + 100 (reasoning) + 300 + 0 = 900 - expect(result!.cost!.inputTokens).toBe(3200); - expect(result!.cost!.outputTokens).toBe(900); + // cached_tokens/reasoning_tokens are subsets, not additive + // input: 1000 + 2000 = 3000 + // output: 500 + 300 = 800 + expect(result!.cost!.inputTokens).toBe(3000); + expect(result!.cost!.outputTokens).toBe(800); expect(result!.cost!.estimatedCostUsd).toBeGreaterThan(0); }); diff --git a/packages/plugins/agent-codex/src/index.ts b/packages/plugins/agent-codex/src/index.ts index 028c663b0..a6be6697f 100644 --- a/packages/plugins/agent-codex/src/index.ts +++ b/packages/plugins/agent-codex/src/index.ts @@ -443,9 +443,9 @@ function extractCodexCost(lines: CodexJsonlLine[]): CostEstimate | undefined { for (const line of lines) { if (line.type === "event_msg" && line.msg?.type === "token_count") { inputTokens += line.msg.input_tokens ?? 0; - inputTokens += line.msg.cached_tokens ?? 0; + // cached_tokens is a subset of input_tokens (not additive) outputTokens += line.msg.output_tokens ?? 0; - outputTokens += line.msg.reasoning_tokens ?? 0; + // reasoning_tokens is a subset of output_tokens (not additive) } }