Skip to content

Commit 191bd7d

Browse files
committed
fix(codex): scope app-server migration cleanup
1 parent b30face commit 191bd7d

3 files changed

Lines changed: 67 additions & 3 deletions

File tree

extensions/codex/src/app-server/shared-client.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
3939
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
4040
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
4141
let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent;
42+
let clearSharedCodexAppServerClientIfCurrentAndWait: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrentAndWait;
4243
let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
4344
let getSharedCodexAppServerClient: typeof import("./shared-client.js").getSharedCodexAppServerClient;
4445
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
@@ -111,6 +112,7 @@ describe("shared Codex app-server client", () => {
111112
({
112113
clearSharedCodexAppServerClient,
113114
clearSharedCodexAppServerClientIfCurrent,
115+
clearSharedCodexAppServerClientIfCurrentAndWait,
114116
createIsolatedCodexAppServerClient,
115117
getSharedCodexAppServerClient,
116118
resetSharedCodexAppServerClientForTests,
@@ -476,6 +478,44 @@ describe("shared Codex app-server client", () => {
476478
expect(second.process.stdin.destroyed).toBe(true);
477479
});
478480

481+
it("waits only for the shared client that is still current", async () => {
482+
const first = createClientHarness();
483+
const second = createClientHarness();
484+
vi.spyOn(CodexAppServerClient, "start")
485+
.mockReturnValueOnce(first.client)
486+
.mockReturnValueOnce(second.client);
487+
const firstCloseAndWait = vi.spyOn(first.client, "closeAndWait");
488+
const secondCloseAndWait = vi.spyOn(second.client, "closeAndWait");
489+
490+
const firstList = listCodexAppServerModels({
491+
timeoutMs: 1000,
492+
agentDir: "/tmp/openclaw-agent-one",
493+
});
494+
await sendInitializeResult(first, "openclaw/0.125.0 (macOS; test)");
495+
await sendEmptyModelList(first);
496+
await expect(firstList).resolves.toEqual({ models: [] });
497+
498+
const secondList = listCodexAppServerModels({
499+
timeoutMs: 1000,
500+
agentDir: "/tmp/openclaw-agent-two",
501+
});
502+
await sendInitializeResult(second, "openclaw/0.125.0 (macOS; test)");
503+
await sendEmptyModelList(second);
504+
await expect(secondList).resolves.toEqual({ models: [] });
505+
506+
await expect(
507+
clearSharedCodexAppServerClientIfCurrentAndWait(first.client, {
508+
exitTimeoutMs: 25,
509+
forceKillDelayMs: 5,
510+
}),
511+
).resolves.toBe(true);
512+
513+
expect(firstCloseAndWait).toHaveBeenCalledTimes(1);
514+
expect(secondCloseAndWait).not.toHaveBeenCalled();
515+
expect(first.process.stdin.destroyed).toBe(true);
516+
expect(second.process.stdin.destroyed).toBe(false);
517+
});
518+
479519
it("uses a fresh websocket Authorization header after shared-client token rotation", async () => {
480520
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
481521
const authHeaders: Array<string | undefined> = [];

extensions/codex/src/app-server/shared-client.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,27 @@ export function clearSharedCodexAppServerClientIfCurrent(
198198
return false;
199199
}
200200

201+
export async function clearSharedCodexAppServerClientIfCurrentAndWait(
202+
client: CodexAppServerClient | undefined,
203+
options?: {
204+
exitTimeoutMs?: number;
205+
forceKillDelayMs?: number;
206+
},
207+
): Promise<boolean> {
208+
if (!client) {
209+
return false;
210+
}
211+
const state = getSharedCodexAppServerClientState();
212+
for (const [key, entry] of state.clients) {
213+
if (entry.client === client) {
214+
state.clients.delete(key);
215+
await client.closeAndWait(options);
216+
return true;
217+
}
218+
}
219+
return false;
220+
}
221+
201222
export async function clearSharedCodexAppServerClientAndWait(options?: {
202223
exitTimeoutMs?: number;
203224
forceKillDelayMs?: number;

extensions/codex/src/migration/apply.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { buildCodexPluginAppCacheKey } from "../app-server/plugin-app-cache-key.
4040
import type { v2 } from "../app-server/protocol.js";
4141
import { requestCodexAppServerJson } from "../app-server/request.js";
4242
import {
43-
clearSharedCodexAppServerClientAndWait,
43+
clearSharedCodexAppServerClientIfCurrentAndWait,
4444
getSharedCodexAppServerClient,
4545
} from "../app-server/shared-client.js";
4646
import { buildCodexMigrationPlan } from "./plan.js";
@@ -84,19 +84,22 @@ export function prepareTargetCodexAppServer(
8484
): CodexMigrationTargetAppServerPreparation {
8585
const appServer = resolveTargetCodexAppServer(ctx);
8686
const targets = resolveCodexMigrationTargets(ctx);
87+
let warmedClient: Awaited<ReturnType<typeof getSharedCodexAppServerClient>> | undefined;
8788
const ready = getSharedCodexAppServerClient({
8889
startOptions: appServer.start,
8990
timeoutMs: 60_000,
9091
agentDir: targets.agentDir,
9192
config: ctx.config,
9293
}).then(
93-
() => undefined,
94+
(client) => {
95+
warmedClient = client;
96+
},
9497
() => undefined,
9598
);
9699
return {
97100
async dispose() {
98101
await ready;
99-
await clearSharedCodexAppServerClientAndWait({
102+
await clearSharedCodexAppServerClientIfCurrentAndWait(warmedClient, {
100103
exitTimeoutMs: 2_000,
101104
forceKillDelayMs: 250,
102105
});

0 commit comments

Comments
 (0)