diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 5cee51ff..acab9fb3 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -345,6 +345,12 @@ export class CodexAcpClient { baseUrl: gatewayConfig.config.base_url, } : this.getNativeProviderConfig(); + logger.log("providers/list", { + providerId: OPENAI_PROVIDER_ID, + overrideActive: gatewayConfig !== null, + apiType: current.apiType, + baseUrl: current.baseUrl, + }); return [ { providerId: OPENAI_PROVIDER_ID, @@ -387,6 +393,12 @@ export class CodexAcpClient { baseUrl: request.baseUrl, headers: request.headers, }); + logger.log("providers/set applied", { + providerId: request.providerId, + apiType: request.apiType, + baseUrl: request.baseUrl, + headerNames: Object.keys(request.headers ?? {}), + }); } /** @@ -394,9 +406,24 @@ export class CodexAcpClient { * unknown provider id is idempotent success (RFD behavior ยง7). */ disableProvider(request: acp.DisableProviderRequest): void { + const overrideWasActive = this.gatewayConfig !== null; if (request.providerId === OPENAI_PROVIDER_ID) { this.gatewayConfig = null; } + const current = this.gatewayConfig + ? { + apiType: gatewayApiTypeFromConfig(this.gatewayConfig), + baseUrl: this.gatewayConfig.config.base_url, + } + : this.getNativeProviderConfig(); + logger.log("providers/disable applied", { + providerId: request.providerId, + knownProvider: request.providerId === OPENAI_PROVIDER_ID, + overrideWasActive, + overrideActive: this.gatewayConfig !== null, + restoredApiType: current.apiType, + restoredBaseUrl: current.baseUrl, + }); } async getAccount(): Promise { @@ -579,6 +606,19 @@ export class CodexAcpClient { mcpServers: Array ): Promise { const sessionRoots = [projectPath, ...additionalDirectories]; + const activeProvider = this.gatewayConfig + ? { + apiType: gatewayApiTypeFromConfig(this.gatewayConfig), + baseUrl: this.gatewayConfig.config.base_url, + } + : this.getNativeProviderConfig(); + logger.log("Creating session config", { + projectPath, + overrideActive: this.gatewayConfig !== null, + modelProvider: this.getModelProvider(), + apiType: activeProvider.apiType, + baseUrl: activeProvider.baseUrl, + }); const mergedConfig = { ...mergeGatewayConfig(this.config, this.gatewayConfig), projects: Object.fromEntries(sessionRoots.map(root => [root, { diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index db3c6e34..70abe91c 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -7,6 +7,7 @@ import {type CodexAuthRequest, getCodexAuthMethods, isCodexAuthRequest} from "./ import {clientSupportsUrlElicitation} from "./ElicitationCapabilities"; import { CodexAcpClient, + OPENAI_PROVIDER_ID, type SessionMetadata, type SessionMetadataWithThread, type UrlElicitationRequester @@ -122,6 +123,7 @@ export interface SessionState { authProvider: string | null; cwd: string; additionalDirectories: string[]; + mcpServers?: Array; fastModeEnabled: boolean; currentModelSupportsFast: boolean; sessionMcpServers?: Array; @@ -194,6 +196,8 @@ interface ActivePrompt { complete: () => void; } +export type RestartCodexClient = () => Promise; + export class CodexAcpServer { private static readonly MODEL_NAME_TOKEN_OVERRIDES: Record = { gpt: "GPT", @@ -201,13 +205,13 @@ export class CodexAcpServer { codex: "Codex", }; - private readonly codexAcpClient: CodexAcpClient; + private codexAcpClient: CodexAcpClient; private readonly connection: AcpClientConnection; private readonly defaultAuthRequest: CodexAuthRequest | null; private readonly getExitCode: () => number | null; private readonly getRecentStderr: () => string; private readonly sessionFailureEpoch: string; - private readonly availableCommands: CodexCommands; + private availableCommands: CodexCommands; private clientInfo: acp.Implementation | null; private clientCapabilities: acp.ClientCapabilities | null; private terminalOutputMode: TerminalOutputMode; @@ -222,6 +226,9 @@ export class CodexAcpServer { private readonly sessionGenerations: Map; private readonly sessionOpenGenerations: Map; private readonly goalControlGenerations: Map; + private readonly restartCodexClient: RestartCodexClient | null; + private initializeRequest: acp.InitializeRequest | null = null; + private providerUpdate: Promise | null = null; constructor( connection: AcpClientConnection, @@ -229,6 +236,7 @@ export class CodexAcpServer { defaultAuthRequest?: CodexAuthRequest, getExitCode?: () => number | null, getRecentStderr?: () => string, + restartCodexClient?: RestartCodexClient, ) { this.sessions = new Map(); this.pendingMcpStartupSessions = new Map(); @@ -244,14 +252,19 @@ export class CodexAcpServer { this.defaultAuthRequest = defaultAuthRequest ?? null; this.getExitCode = getExitCode ?? (() => null); this.getRecentStderr = getRecentStderr ?? (() => ""); + this.restartCodexClient = restartCodexClient ?? null; this.sessionFailureEpoch = randomUUID(); this.clientInfo = null; this.clientCapabilities = null; this.terminalOutputMode = "terminal_output_delta"; this.booleanConfigOptionsSupported = false; - this.availableCommands = new CodexCommands( - connection, - codexAcpClient, + this.availableCommands = this.createAvailableCommands(codexAcpClient); + } + + private createAvailableCommands(client: CodexAcpClient): CodexCommands { + return new CodexCommands( + this.connection, + client, (operation) => this.runWithProcessCheck(operation), () => this.refreshSessionsAuthState(null) ); @@ -263,6 +276,7 @@ export class CodexAcpServer { logger.log("Initialize request received"); this.clientInfo = _params.clientInfo ?? null; this.clientCapabilities = _params.clientCapabilities ?? null; + this.initializeRequest = _params; this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities); this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities); await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params)); @@ -571,6 +585,7 @@ export class CodexAcpServer { authProvider: authProvider, cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, + mcpServers: requestedMcpServers, fastModeEnabled: sessionMetadata.currentServiceTier === "fast", currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, @@ -633,6 +648,7 @@ export class CodexAcpServer { } async loadSession(params: acp.LoadSessionRequest): Promise { + await this.waitForProviderUpdate(); logger.log("Loading session...", {sessionId: params.sessionId}); const { sessionId, @@ -656,6 +672,7 @@ export class CodexAcpServer { } async resumeSession(params: acp.ResumeSessionRequest): Promise { + await this.waitForProviderUpdate(); logger.log("Resuming session...", {sessionId: params.sessionId}); const [sessionId, modelState, modeState] = await this.getOrCreateSession(params); @@ -764,6 +781,7 @@ export class CodexAcpServer { async newSession( params: acp.NewSessionRequest, ): Promise { + await this.waitForProviderUpdate(); logger.log("Starting new session..."); const [sessionId, modelState, modeState] = await this.getOrCreateSession(params); @@ -821,16 +839,81 @@ export class CodexAcpServer { return { providers: this.codexAcpClient.listProviders() }; } - setProvider(params: acp.SetProviderRequest): acp.SetProviderResponse { + async setProvider(params: acp.SetProviderRequest): Promise { this.codexAcpClient.setProvider(params); + await this.enqueueProviderUpdate((client) => client.setProvider(params)); return { }; } - disableProvider(params: acp.DisableProviderRequest): acp.DisableProviderResponse { + async disableProvider(params: acp.DisableProviderRequest): Promise { this.codexAcpClient.disableProvider(params); + if (params.providerId !== OPENAI_PROVIDER_ID) { + return { }; + } + await this.enqueueProviderUpdate((client) => client.disableProvider(params)); return { }; } + private async enqueueProviderUpdate(apply: (client: CodexAcpClient) => void): Promise { + const previous = this.providerUpdate?.catch(() => undefined) ?? Promise.resolve(); + const update = previous.then(async () => { + if (this.sessions.size === 0 || this.restartCodexClient === null) { + return; + } + + const activePrompts = [...this.activePrompts.values()].map(prompt => prompt.completion); + if (activePrompts.length > 0) { + logger.log("Waiting for active prompts before provider restart", {count: activePrompts.length}); + await Promise.all(activePrompts); + } + + logger.log("Restarting Codex app-server for provider update", {sessionCount: this.sessions.size}); + const replacement = await this.restartCodexClient(); + apply(replacement); + if (this.initializeRequest === null) { + throw new Error("Cannot restart Codex app-server before ACP initialization"); + } + await replacement.initialize(this.initializeRequest); + this.codexAcpClient = replacement; + this.availableCommands = this.createAvailableCommands(replacement); + + const resumeErrors: unknown[] = []; + for (const session of this.sessions.values()) { + try { + await replacement.resumeSession({ + sessionId: session.sessionId, + cwd: session.cwd, + additionalDirectories: session.additionalDirectories, + mcpServers: session.mcpServers ?? [], + }); + session.authProvider = replacement.getModelProvider(); + logger.log("Resumed session after provider restart", {sessionId: session.sessionId}); + } catch (error) { + resumeErrors.push(error); + logger.error(`Failed to resume session ${session.sessionId} after provider restart`, error); + } + } + if (resumeErrors.length > 0) { + throw new AggregateError(resumeErrors, `Failed to resume ${resumeErrors.length} session(s) after provider restart`); + } + }); + this.providerUpdate = update; + try { + await update; + } finally { + if (this.providerUpdate === update) { + this.providerUpdate = null; + } + } + } + + private async waitForProviderUpdate(): Promise { + const update = this.providerUpdate; + if (update !== null) { + await update; + } + } + private async refreshSessionsAuthState(authProvider: string | null): Promise { if (this.sessions.size === 0) return; @@ -1460,6 +1543,7 @@ export class CodexAcpServer { authProvider: authProvider, cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, + mcpServers: requestedMcpServers, fastModeEnabled: sessionMetadata.currentServiceTier === "fast", currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, @@ -2025,6 +2109,7 @@ export class CodexAcpServer { signal?: AbortSignal, onTurnStarted?: () => void, ): Promise { + await this.waitForProviderUpdate(); logger.log("Prompt received", { sessionId: params.sessionId, prompt: params.prompt, diff --git a/src/Logger.ts b/src/Logger.ts index b64fdec6..fd588777 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -18,6 +18,7 @@ class Logger { try { fs.mkdirSync(logDir, {recursive: true}); this.logFilePath = path.join(logDir, "app-server.log"); + this.log("Logger initialized", {logFilePath: this.logFilePath}); } catch (ex) { console.error("Failed to initialize logger directory", ex); this.logFilePath = null; @@ -32,7 +33,7 @@ class Logger { if (!this.logFilePath) return; try { const timestamp = this.formatTimestamp(new Date()); - const serializedContext = context ? ` ${JSON.stringify(context)}` : ""; + const serializedContext = ` ${JSON.stringify({pid: process.pid, ...context})}`; if (!message.startsWith('[')) message = `[SYS] ${message}`; const line = `${timestamp} ${message}${serializedContext}`; diff --git a/src/__tests__/CodexACPAgent/providers.test.ts b/src/__tests__/CodexACPAgent/providers.test.ts index 113c63fd..45bc744c 100644 --- a/src/__tests__/CodexACPAgent/providers.test.ts +++ b/src/__tests__/CodexACPAgent/providers.test.ts @@ -1,15 +1,10 @@ import {describe, expect, it, vi} from "vitest"; import * as acp from "@agentclientprotocol/sdk"; -import {createCodexMockTestFixture} from "../acp-test-utils"; +import {createCodexMockTestFixture, createTestSessionState} from "../acp-test-utils"; import {CodexAcpClient, CUSTOM_GATEWAY_PROVIDER_ID, OPENAI_PROVIDER_ID} from "../../CodexAcpClient"; -function expectInvalidParams(fn: () => unknown): void { - let caught: unknown; - try { - fn(); - } catch (err) { - caught = err; - } +async function expectInvalidParams(fn: () => unknown): Promise { + const caught = await Promise.resolve().then(fn).catch((err: unknown) => err); expect(caught).toBeInstanceOf(acp.RequestError); expect((caught as acp.RequestError).code).toBe(-32602); } @@ -82,30 +77,30 @@ describe("Configurable LLM providers (providers/*)", () => { expect(JSON.stringify(provider)).not.toContain("super-secret"); }); - it("rejects an unsupported apiType with invalid_params", () => { + it("rejects an unsupported apiType with invalid_params", async () => { const fixture = createCodexMockTestFixture(); const agent = fixture.getCodexAcpAgent(); - expectInvalidParams(() => agent.setProvider({ + await expectInvalidParams(() => agent.setProvider({ providerId: OPENAI_PROVIDER_ID, apiType: "anthropic", baseUrl: "https://example.com", })); }); - it("rejects an unknown providerId with invalid_params", () => { + it("rejects an unknown providerId with invalid_params", async () => { const fixture = createCodexMockTestFixture(); const agent = fixture.getCodexAcpAgent(); - expectInvalidParams(() => agent.setProvider({ + await expectInvalidParams(() => agent.setProvider({ providerId: "does-not-exist", apiType: "openai", baseUrl: "https://example.com", })); }); - it("rejects a malformed baseUrl with invalid_params", () => { + it("rejects a malformed baseUrl with invalid_params", async () => { const fixture = createCodexMockTestFixture(); const agent = fixture.getCodexAcpAgent(); - expectInvalidParams(() => agent.setProvider({ + await expectInvalidParams(() => agent.setProvider({ providerId: OPENAI_PROVIDER_ID, apiType: "openai", baseUrl: " ", @@ -173,6 +168,132 @@ describe("Configurable LLM providers (providers/*)", () => { })); }); + it("keeps native session creation unchanged when the providers API is unused", async () => { + const restart = vi.fn(); + const fixture = createCodexMockTestFixture(restart); + const agent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + const codexAppServerClient = fixture.getCodexAppServerClient(); + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + const threadStartSpy = vi.spyOn(codexAppServerClient, "threadStart") + .mockRejectedValue(new Error("stop after capturing config")); + + await expect(agent.newSession({cwd: "/workspace", mcpServers: []})).rejects.toThrow(); + + expect(restart).not.toHaveBeenCalled(); + expect(threadStartSpy).toHaveBeenCalledOnce(); + expect(threadStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + modelProvider: null, + config: expect.not.objectContaining({ + model_providers: expect.objectContaining({ + [CUSTOM_GATEWAY_PROVIDER_ID]: expect.anything(), + }), + }), + })); + }); + + it("restarts app-server and resumes all loaded sessions through the full provider lifecycle", async () => { + const firstGatewayReplacement = createCodexMockTestFixture().getCodexAcpClient(); + const secondGatewayReplacement = createCodexMockTestFixture().getCodexAcpClient(); + const nativeReplacement = createCodexMockTestFixture().getCodexAcpClient(); + vi.spyOn(firstGatewayReplacement, "initialize").mockResolvedValue(); + const firstGatewayResume = vi.spyOn(firstGatewayReplacement, "resumeSession").mockResolvedValue({} as never); + vi.spyOn(secondGatewayReplacement, "initialize").mockResolvedValue(); + const secondGatewayResume = vi.spyOn(secondGatewayReplacement, "resumeSession").mockResolvedValue({} as never); + vi.spyOn(nativeReplacement, "initialize").mockResolvedValue(); + const nativeResume = vi.spyOn(nativeReplacement, "resumeSession").mockResolvedValue({} as never); + const restart = vi.fn() + .mockResolvedValueOnce(firstGatewayReplacement) + .mockResolvedValueOnce(secondGatewayReplacement) + .mockResolvedValueOnce(nativeReplacement); + const fixture = createCodexMockTestFixture(restart); + const agent = fixture.getCodexAcpAgent(); + await agent.initialize({protocolVersion: acp.PROTOCOL_VERSION}); + const sessions = (agent as unknown as {sessions: Map>}).sessions; + sessions.set("thread-1", createTestSessionState({sessionId: "thread-1", cwd: "/one"})); + sessions.set("thread-2", createTestSessionState({sessionId: "thread-2", cwd: "/two"})); + + await agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://gateway.example/v1", + headers: {Authorization: "Bearer secret"}, + }); + + expect(restart).toHaveBeenCalledTimes(1); + expect(firstGatewayReplacement.getModelProvider()).toBe(CUSTOM_GATEWAY_PROVIDER_ID); + expect(firstGatewayResume).toHaveBeenCalledTimes(2); + expect(firstGatewayResume).toHaveBeenCalledWith(expect.objectContaining({sessionId: "thread-1", cwd: "/one"})); + expect(firstGatewayResume).toHaveBeenCalledWith(expect.objectContaining({sessionId: "thread-2", cwd: "/two"})); + + await agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://second-gateway.example/v1", + headers: {Authorization: "Bearer replacement"}, + }); + + expect(restart).toHaveBeenCalledTimes(2); + expect(secondGatewayReplacement.getModelProvider()).toBe(CUSTOM_GATEWAY_PROVIDER_ID); + expect(secondGatewayResume).toHaveBeenCalledTimes(2); + expect(agent.listProviders({}).providers[0]!.current).toEqual({ + apiType: "openai", + baseUrl: "https://second-gateway.example/v1", + }); + + await agent.disableProvider({providerId: OPENAI_PROVIDER_ID}); + + expect(restart).toHaveBeenCalledTimes(3); + expect(nativeReplacement.getModelProvider()).toBeNull(); + expect(nativeResume).toHaveBeenCalledTimes(2); + expect(agent.listProviders({}).providers[0]!.current).toEqual({ + apiType: "openai", + baseUrl: "https://api.openai.com/v1", + }); + }); + + it("attempts every session resume and allows a later provider update after one fails", async () => { + const failedReplacement = createCodexMockTestFixture().getCodexAcpClient(); + const recoveredReplacement = createCodexMockTestFixture().getCodexAcpClient(); + vi.spyOn(failedReplacement, "initialize").mockResolvedValue(); + const failedResume = vi.spyOn(failedReplacement, "resumeSession") + .mockRejectedValueOnce(new Error("thread-1 failed")) + .mockResolvedValue({} as never); + vi.spyOn(recoveredReplacement, "initialize").mockResolvedValue(); + const recoveredResume = vi.spyOn(recoveredReplacement, "resumeSession").mockResolvedValue({} as never); + const restart = vi.fn() + .mockResolvedValueOnce(failedReplacement) + .mockResolvedValueOnce(recoveredReplacement); + const fixture = createCodexMockTestFixture(restart); + const agent = fixture.getCodexAcpAgent(); + await agent.initialize({protocolVersion: acp.PROTOCOL_VERSION}); + const sessions = (agent as unknown as {sessions: Map>}).sessions; + sessions.set("thread-1", createTestSessionState({sessionId: "thread-1", cwd: "/one"})); + sessions.set("thread-2", createTestSessionState({sessionId: "thread-2", cwd: "/two"})); + + await expect(agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://broken-gateway.example/v1", + })).rejects.toThrow("Failed to resume 1 session(s)"); + + expect(failedResume).toHaveBeenCalledTimes(2); + + await expect(agent.setProvider({ + providerId: OPENAI_PROVIDER_ID, + apiType: "openai", + baseUrl: "https://recovered-gateway.example/v1", + })).resolves.toEqual({}); + + expect(restart).toHaveBeenCalledTimes(2); + expect(recoveredResume).toHaveBeenCalledTimes(2); + expect(agent.listProviders({}).providers[0]!.current).toEqual({ + apiType: "openai", + baseUrl: "https://recovered-gateway.example/v1", + }); + }); + it("shares state with the legacy gateway auth method", async () => { const fixture = createCodexMockTestFixture(); const codexAcpClient = fixture.getCodexAcpClient(); diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index de992ccd..15f84f90 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -85,6 +85,7 @@ export interface ConnectionConfig { connection: MessageConnection; getExitCode: () => number | null; acpConnection?: AcpConnectionConfig; + restartCodexClient?: (() => Promise) | undefined; } export function createBaseTestFixture(config: ConnectionConfig): TestFixture { @@ -98,7 +99,14 @@ export function createBaseTestFixture(config: ConnectionConfig): TestFixture { const codexAppServerClient = new CodexAppServerClient(config.connection); const codexAcpClient = new CodexAcpClient(codexAppServerClient); - const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient, undefined, config.getExitCode); + const codexAcpAgent = new CodexAcpServer( + acpConnection, + codexAcpClient, + undefined, + config.getExitCode, + undefined, + config.restartCodexClient, + ); const transportEvents: CodexConnectionEvent[] = []; const codexEventHandlers: ((event: CodexConnectionEvent) => void)[] = []; @@ -255,7 +263,9 @@ export interface CodexMockTestFixture extends TestFixture { * Provides `sendServerRequest()` to simulate server-initiated requests (e.g., approval requests). * Provides `setPermissionResponse()` to control ACP permission dialog responses. */ -export function createCodexMockTestFixture(): CodexMockTestFixture { +export function createCodexMockTestFixture( + restartCodexClient?: () => Promise, +): CodexMockTestFixture { let unhandledNotificationHandler: ((notification: any) => void) | null = null; const requestHandlers = new Map Promise>(); @@ -303,6 +313,7 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { const baseFixture = createBaseTestFixture({ connection: mockCodexConnection, getExitCode: () => null, + restartCodexClient, acpConnection: { connection: acpConnection, events: acpConnectionEvents, diff --git a/src/index.ts b/src/index.ts index 0bddc4e2..c627e202 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { GOAL_CONTROL_METHOD, LEGACY_SET_SESSION_MODEL_METHOD, SESSION_STEERING_METHOD, } from "./AcpExtensions"; +import {once} from "node:events"; const emptyExtensionParamsParser = z.preprocess( (params) => params ?? {}, @@ -88,13 +89,16 @@ function startAcpServer() { defaultAuthRequest: defaultAuthRequest ?? null, }); - const codexConnection = startCodexConnection(codexPath); + let codexConnection = startCodexConnection(codexPath); const maxStderrTailChars = 2 * 1024; let stderr = ""; - codexConnection.process.stderr.addListener("data", (data: Buffer) => { - stderr = (stderr + data.toString()).slice(-maxStderrTailChars); - }); + const captureStderr = () => { + codexConnection.process.stderr.addListener("data", (data: Buffer) => { + stderr = (stderr + data.toString()).slice(-maxStderrTailChars); + }); + }; + captureStderr(); process.stdin.on("close", () => { codexConnection.process.stdin.end(); @@ -109,10 +113,38 @@ function startAcpServer() { const acpJsonStream = createJsonStream(process.stdin, process.stdout); + async function restartCodexClient(): Promise { + const previous = codexConnection; + const exited = previous.process.exitCode === null + ? once(previous.process, "exit") + : Promise.resolve(); + previous.process.stdin.end(); + const forceKill = setTimeout(() => { + if (previous.process.exitCode === null) { + logger.log("Codex still running 2s after provider restart; terminating process"); + previous.process.kill(); + } + }, 2000); + await exited; + clearTimeout(forceKill); + + stderr = ""; + codexConnection = startCodexConnection(codexPath); + captureStderr(); + return new CodexAcpClient(new CodexAppServerClient(codexConnection.connection), config, modelProvider); + } + function createAgent(connection: acp.AgentContext): CodexAcpServer { const appServerClient = new CodexAppServerClient(codexConnection.connection); const codexClient = new CodexAcpClient(appServerClient, config, modelProvider); - return new CodexAcpServer(connection, codexClient, defaultAuthRequest, () => codexConnection.process.exitCode, () => stderr); + return new CodexAcpServer( + connection, + codexClient, + defaultAuthRequest, + () => codexConnection.process.exitCode, + () => stderr, + restartCodexClient, + ); } let codexAcpServer: CodexAcpServer | null = null;