mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix: tighten codex app-server lifecycle
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildCodexProvider, buildCodexProviderCatalog } from "./provider.js";
|
||||
import { CodexAppServerClient } from "./src/app-server/client.js";
|
||||
import { resetSharedCodexAppServerClientForTests } from "./src/app-server/shared-client.js";
|
||||
import {
|
||||
getSharedCodexAppServerClient,
|
||||
resetSharedCodexAppServerClientForTests,
|
||||
} from "./src/app-server/shared-client.js";
|
||||
|
||||
afterEach(() => {
|
||||
resetSharedCodexAppServerClientForTests();
|
||||
@@ -37,7 +40,7 @@ describe("codex provider", () => {
|
||||
});
|
||||
|
||||
expect(listModels).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ limit: 100, timeoutMs: 1234 }),
|
||||
expect.objectContaining({ limit: 100, timeoutMs: 1234, sharedClient: false }),
|
||||
);
|
||||
expect(result.provider).toMatchObject({
|
||||
auth: "token",
|
||||
@@ -103,6 +106,32 @@ describe("codex provider", () => {
|
||||
expect(client.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not close an active shared app-server client during live discovery", async () => {
|
||||
const activeClient = {
|
||||
initialize: vi.fn(async () => undefined),
|
||||
request: vi.fn(async () => ({ data: [] })),
|
||||
addCloseHandler: vi.fn(() => () => undefined),
|
||||
close: vi.fn(),
|
||||
} as unknown as CodexAppServerClient;
|
||||
const discoveryClient = {
|
||||
initialize: vi.fn(async () => undefined),
|
||||
request: vi.fn(async () => ({ data: [] })),
|
||||
addCloseHandler: vi.fn(() => () => undefined),
|
||||
close: vi.fn(),
|
||||
} as unknown as CodexAppServerClient;
|
||||
vi.spyOn(CodexAppServerClient, "start")
|
||||
.mockReturnValueOnce(activeClient)
|
||||
.mockReturnValueOnce(discoveryClient);
|
||||
|
||||
await getSharedCodexAppServerClient({ timeoutMs: 1000 });
|
||||
await buildCodexProviderCatalog({
|
||||
env: { OPENCLAW_CODEX_DISCOVERY_LIVE: "1" },
|
||||
});
|
||||
|
||||
expect(activeClient.close).not.toHaveBeenCalled();
|
||||
expect(discoveryClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves arbitrary Codex app-server model ids through the codex provider", () => {
|
||||
const provider = buildCodexProvider();
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
readCodexPluginConfig,
|
||||
resolveCodexAppServerRuntimeOptions,
|
||||
} from "./src/app-server/config.js";
|
||||
import { clearSharedCodexAppServerClient } from "./src/app-server/shared-client.js";
|
||||
|
||||
const PROVIDER_ID = "codex";
|
||||
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
||||
@@ -28,6 +27,7 @@ type CodexModelLister = (options: {
|
||||
timeoutMs: number;
|
||||
limit?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
sharedClient?: boolean;
|
||||
}) => Promise<CodexAppServerModelListResult>;
|
||||
|
||||
type BuildCodexProviderOptions = {
|
||||
@@ -102,15 +102,11 @@ export async function buildCodexProviderCatalog(
|
||||
const timeoutMs = normalizeTimeoutMs(config.discovery?.timeoutMs);
|
||||
let discovered: CodexAppServerModel[] = [];
|
||||
if (config.discovery?.enabled !== false && !shouldSkipLiveDiscovery(options.env)) {
|
||||
try {
|
||||
discovered = await listModelsBestEffort({
|
||||
listModels: options.listModels ?? listCodexAppServerModels,
|
||||
timeoutMs,
|
||||
startOptions: appServer.start,
|
||||
});
|
||||
} finally {
|
||||
clearSharedCodexAppServerClient();
|
||||
}
|
||||
discovered = await listModelsBestEffort({
|
||||
listModels: options.listModels ?? listCodexAppServerModels,
|
||||
timeoutMs,
|
||||
startOptions: appServer.start,
|
||||
});
|
||||
}
|
||||
const models = (discovered.length > 0 ? discovered : FALLBACK_CODEX_MODELS).map(
|
||||
codexModelToDefinition,
|
||||
@@ -180,6 +176,7 @@ async function listModelsBestEffort(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
limit: 100,
|
||||
startOptions: params.startOptions,
|
||||
sharedClient: false,
|
||||
});
|
||||
return result.models.filter((model) => !model.hidden);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { CodexAppServerStartOptions } from "./config.js";
|
||||
import { type JsonObject, type JsonValue } from "./protocol.js";
|
||||
import { getSharedCodexAppServerClient } from "./shared-client.js";
|
||||
import { withTimeout } from "./timeout.js";
|
||||
import {
|
||||
createIsolatedCodexAppServerClient,
|
||||
getSharedCodexAppServerClient,
|
||||
} from "./shared-client.js";
|
||||
|
||||
export type CodexAppServerModel = {
|
||||
id: string;
|
||||
@@ -26,32 +28,39 @@ export type CodexAppServerListModelsOptions = {
|
||||
includeHidden?: boolean;
|
||||
timeoutMs?: number;
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
sharedClient?: boolean;
|
||||
};
|
||||
|
||||
export async function listCodexAppServerModels(
|
||||
options: CodexAppServerListModelsOptions = {},
|
||||
): Promise<CodexAppServerModelListResult> {
|
||||
const timeoutMs = options.timeoutMs ?? 2500;
|
||||
return await withTimeout(
|
||||
(async () => {
|
||||
const client = await getSharedCodexAppServerClient({
|
||||
const useSharedClient = options.sharedClient !== false;
|
||||
const client = useSharedClient
|
||||
? await getSharedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
})
|
||||
: await createIsolatedCodexAppServerClient({
|
||||
startOptions: options.startOptions,
|
||||
timeoutMs,
|
||||
});
|
||||
const response = await client.request<JsonObject>(
|
||||
"model/list",
|
||||
{
|
||||
limit: options.limit ?? null,
|
||||
cursor: options.cursor ?? null,
|
||||
includeHidden: options.includeHidden ?? null,
|
||||
},
|
||||
{ timeoutMs },
|
||||
);
|
||||
return readModelListResult(response);
|
||||
})(),
|
||||
timeoutMs,
|
||||
"codex app-server model/list timed out",
|
||||
);
|
||||
try {
|
||||
const response = await client.request<JsonObject>(
|
||||
"model/list",
|
||||
{
|
||||
limit: options.limit ?? null,
|
||||
cursor: options.cursor ?? null,
|
||||
includeHidden: options.includeHidden ?? null,
|
||||
},
|
||||
{ timeoutMs },
|
||||
);
|
||||
return readModelListResult(response);
|
||||
} finally {
|
||||
if (!useSharedClient) {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readModelListResult(value: JsonValue | undefined): CodexAppServerModelListResult {
|
||||
|
||||
@@ -227,14 +227,10 @@ export async function runCodexAppServerAttempt(
|
||||
);
|
||||
|
||||
const abortListener = () => {
|
||||
void client
|
||||
.request("turn/interrupt", {
|
||||
threadId: thread.threadId,
|
||||
turnId: activeTurnId,
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
|
||||
});
|
||||
interruptCodexTurnBestEffort(client, {
|
||||
threadId: thread.threadId,
|
||||
turnId: activeTurnId,
|
||||
});
|
||||
resolveCompletion?.();
|
||||
};
|
||||
runAbortController.signal.addEventListener("abort", abortListener, { once: true });
|
||||
@@ -268,6 +264,20 @@ export async function runCodexAppServerAttempt(
|
||||
}
|
||||
}
|
||||
|
||||
function interruptCodexTurnBestEffort(
|
||||
client: CodexAppServerClient,
|
||||
params: {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
},
|
||||
): void {
|
||||
void Promise.resolve()
|
||||
.then(() => client.request("turn/interrupt", params))
|
||||
.catch((error: unknown) => {
|
||||
embeddedAgentLog.debug("codex app-server turn interrupt failed during abort", { error });
|
||||
});
|
||||
}
|
||||
|
||||
type DynamicToolBuildParams = {
|
||||
params: EmbeddedRunAttemptParams;
|
||||
resolvedWorkspace: string;
|
||||
|
||||
@@ -59,6 +59,23 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createIsolatedCodexAppServerClient(options?: {
|
||||
startOptions?: CodexAppServerStartOptions;
|
||||
timeoutMs?: number;
|
||||
}): Promise<CodexAppServerClient> {
|
||||
const startOptions = options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||
const client = CodexAppServerClient.start(startOptions);
|
||||
const initialize = client.initialize();
|
||||
try {
|
||||
await withTimeout(initialize, options?.timeoutMs ?? 0, "codex app-server initialize timed out");
|
||||
return client;
|
||||
} catch (error) {
|
||||
client.close();
|
||||
await initialize.catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetSharedCodexAppServerClientForTests(): void {
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
state.client = undefined;
|
||||
|
||||
Reference in New Issue
Block a user