mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix: make codex app-server cleanup ownership-aware
This commit is contained in:
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5.
|
||||
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.
|
||||
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
|
||||
- Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc.
|
||||
- Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427.
|
||||
- Plugins: clarify config-selected duplicate plugin override diagnostics and document manifest schema updates for bundled-plugin forks. Fixes #8582. Thanks @sachah.
|
||||
- CLI backends/Claude: make live-session JSONL turn caps bounded and configurable via `reliability.outputLimits`, raising the default guard for tool-heavy Claude CLI turns while preserving memory limits. Fixes #75838. Thanks @hcordoba840.
|
||||
|
||||
@@ -78,7 +78,7 @@ import {
|
||||
} from "./protocol.js";
|
||||
import { readCodexAppServerBinding, type CodexAppServerThreadBinding } from "./session-binding.js";
|
||||
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
|
||||
import { clearSharedCodexAppServerClient } from "./shared-client.js";
|
||||
import { clearSharedCodexAppServerClientIfCurrent } from "./shared-client.js";
|
||||
import {
|
||||
buildDeveloperInstructions,
|
||||
buildTurnStartParams,
|
||||
@@ -489,6 +489,7 @@ export async function runCodexAppServerAttempt(
|
||||
let thread: CodexAppServerThreadBinding;
|
||||
let trajectoryEndRecorded = false;
|
||||
let nativeHookRelay: NativeHookRelayRegistrationHandle | undefined;
|
||||
let startupClientForCleanup: CodexAppServerClient | undefined;
|
||||
try {
|
||||
emitCodexAppServerEvent(params, {
|
||||
stream: "codex_app_server.lifecycle",
|
||||
@@ -516,12 +517,15 @@ export async function runCodexAppServerAttempt(
|
||||
timeoutFloorMs: options.startupTimeoutFloorMs,
|
||||
signal: runAbortController.signal,
|
||||
operation: async () => {
|
||||
let attemptedClient: CodexAppServerClient | undefined;
|
||||
const startupAttempt = async () => {
|
||||
const startupClient = await clientFactory(
|
||||
appServer.start,
|
||||
startupAuthProfileId,
|
||||
agentDir,
|
||||
);
|
||||
attemptedClient = startupClient;
|
||||
startupClientForCleanup = startupClient;
|
||||
await ensureCodexComputerUse({
|
||||
client: startupClient,
|
||||
pluginConfig: options.pluginConfig,
|
||||
@@ -549,18 +553,24 @@ export async function runCodexAppServerAttempt(
|
||||
"codex app-server connection closed during startup; restarting app-server and retrying",
|
||||
{ error },
|
||||
);
|
||||
clearSharedCodexAppServerClient();
|
||||
const failedClient = attemptedClient;
|
||||
clearSharedCodexAppServerClientIfCurrent(failedClient);
|
||||
if (startupClientForCleanup === failedClient) {
|
||||
startupClientForCleanup = undefined;
|
||||
}
|
||||
attemptedClient = undefined;
|
||||
return await startupAttempt();
|
||||
}
|
||||
},
|
||||
}));
|
||||
startupClientForCleanup = undefined;
|
||||
emitCodexAppServerEvent(params, {
|
||||
stream: "codex_app_server.lifecycle",
|
||||
data: { phase: "thread_ready", threadId: thread.threadId },
|
||||
});
|
||||
} catch (error) {
|
||||
nativeHookRelay?.unregister();
|
||||
clearSharedCodexAppServerClient();
|
||||
clearSharedCodexAppServerClientIfCurrent(startupClientForCleanup);
|
||||
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
|
||||
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
|
||||
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
|
||||
let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent;
|
||||
let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
|
||||
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
||||
|
||||
@@ -54,6 +55,7 @@ describe("shared Codex app-server client", () => {
|
||||
({ listCodexAppServerModels } = await import("./models.js"));
|
||||
({
|
||||
clearSharedCodexAppServerClient,
|
||||
clearSharedCodexAppServerClientIfCurrent,
|
||||
createIsolatedCodexAppServerClient,
|
||||
resetSharedCodexAppServerClientForTests,
|
||||
} = await import("./shared-client.js"));
|
||||
@@ -293,6 +295,32 @@ describe("shared Codex app-server client", () => {
|
||||
expect(second.process.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only clears the shared client that is still current", async () => {
|
||||
const first = createClientHarness();
|
||||
const second = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start")
|
||||
.mockReturnValueOnce(first.client)
|
||||
.mockReturnValueOnce(second.client);
|
||||
|
||||
const firstList = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||
await sendInitializeResult(first, "openclaw/0.125.0 (macOS; test)");
|
||||
await sendEmptyModelList(first);
|
||||
await expect(firstList).resolves.toEqual({ models: [] });
|
||||
|
||||
expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(true);
|
||||
expect(first.process.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
|
||||
const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||
await sendInitializeResult(second, "openclaw/0.125.0 (macOS; test)");
|
||||
await sendEmptyModelList(second);
|
||||
await expect(secondList).resolves.toEqual({ models: [] });
|
||||
|
||||
expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(false);
|
||||
expect(second.process.kill).not.toHaveBeenCalled();
|
||||
expect(clearSharedCodexAppServerClientIfCurrent(second.client)).toBe(true);
|
||||
expect(second.process.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
});
|
||||
|
||||
it("uses a fresh websocket Authorization header after shared-client token rotation", async () => {
|
||||
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
||||
const authHeaders: Array<string | undefined> = [];
|
||||
|
||||
@@ -134,6 +134,23 @@ export function clearSharedCodexAppServerClient(): void {
|
||||
client?.close();
|
||||
}
|
||||
|
||||
export function clearSharedCodexAppServerClientIfCurrent(
|
||||
client: CodexAppServerClient | undefined,
|
||||
): boolean {
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
const state = getSharedCodexAppServerClientState();
|
||||
if (state.client !== client) {
|
||||
return false;
|
||||
}
|
||||
state.client = undefined;
|
||||
state.promise = undefined;
|
||||
state.key = undefined;
|
||||
client.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function clearSharedCodexAppServerClientAndWait(options?: {
|
||||
exitTimeoutMs?: number;
|
||||
forceKillDelayMs?: number;
|
||||
|
||||
Reference in New Issue
Block a user