fix(codex): scope stale shared-client cleanup

This commit is contained in:
Lucenx9
2026-04-22 22:12:00 +02:00
committed by Peter Steinberger
parent 0bc5ccc706
commit 15f285c0cb
2 changed files with 59 additions and 12 deletions

View File

@@ -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();
});
});

View File

@@ -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;
}
}