diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index a41109bde1a..63256172665 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -144,4 +144,47 @@ describe("shared Codex app-server client", () => { expect(startSpy).toHaveBeenCalledTimes(2); expect(first.process.kill).toHaveBeenCalledWith("SIGTERM"); }); + + it("does not let a superseded shared-client failure tear down the newer client", async () => { + const first = createClientHarness(); + const second = createClientHarness(); + vi.spyOn(CodexAppServerClient, "start") + .mockReturnValueOnce(first.client) + .mockReturnValueOnce(second.client); + + const firstList = listCodexAppServerModels({ + timeoutMs: 1000, + startOptions: { + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok-first", + headers: {}, + }, + }); + const firstFailure = firstList.catch((error: unknown) => error); + await vi.waitFor(() => expect(first.writes.length).toBeGreaterThanOrEqual(1)); + + const secondList = listCodexAppServerModels({ + timeoutMs: 1000, + startOptions: { + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok-second", + headers: {}, + }, + }); + await vi.waitFor(() => expect(second.writes.length).toBeGreaterThanOrEqual(1)); + + await expect(firstFailure).resolves.toBeInstanceOf(Error); + + await sendInitializeResult(second, "openclaw/0.118.0 (macOS; test)"); + await sendEmptyModelList(second); + await expect(secondList).resolves.toEqual({ models: [] }); + + expect(second.process.kill).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 5dcee5fdecb..d9f973d733d 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -40,28 +40,32 @@ export async function getSharedCodexAppServerClient(options?: { clearSharedCodexAppServerClient(); } state.key = key; - state.promise ??= (async () => { - const client = CodexAppServerClient.start(startOptions); - state.client = client; - client.addCloseHandler(clearSharedClientIfCurrent); - try { - await client.initialize(); + const sharedPromise = + state.promise ?? + (state.promise = (async () => { + const client = CodexAppServerClient.start(startOptions); + state.client = client; + client.addCloseHandler(clearSharedClientIfCurrent); + try { + await client.initialize(); return client; } catch (error) { // Startup failures happen before callers own the shared client, so close // the child here instead of leaving a rejected daemon attached to stdio. - client.close(); - throw error; - } - })(); + client.close(); + throw error; + } + })()); try { return await withTimeout( - state.promise, + sharedPromise, options?.timeoutMs ?? 0, "codex app-server initialize timed out", ); } catch (error) { - clearSharedCodexAppServerClient(); + if (state.promise === sharedPromise && state.key === key) { + clearSharedCodexAppServerClient(); + } throw error; } }