Skip to content

Commit c533218

Browse files
committed
fix(codex): narrow spawned thread-start preservation
1 parent c181bd8 commit c533218

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
2727
import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
2828
import * as authBridge from "./auth-bridge.js";
2929
import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js";
30+
import { CodexAppServerRpcError } from "./client.js";
3031
import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
3132
import {
3233
CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE,
@@ -3498,7 +3499,10 @@ describe("runCodexAppServerAttempt", () => {
34983499
const c = {
34993500
request: vi.fn(async (method: string) => {
35003501
if (method === "thread/start") {
3501-
throw new Error("401 authentication_error: Invalid bearer token");
3502+
throw new CodexAppServerRpcError(
3503+
{ message: "401 authentication_error: Invalid bearer token" },
3504+
"thread/start",
3505+
);
35023506
}
35033507
return {};
35043508
}),
@@ -3553,6 +3557,36 @@ describe("runCodexAppServerAttempt", () => {
35533557
clearSpy.mockRestore();
35543558
});
35553559

3560+
it("retires the shared Codex client when a spawned helper hits a thread/start write failure", async () => {
3561+
const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
3562+
clearSpy.mockClear();
3563+
let failedClient: unknown;
3564+
setCodexAppServerClientFactoryForTest(async () => {
3565+
const c = {
3566+
request: vi.fn(async (method: string) => {
3567+
if (method === "thread/start") {
3568+
throw new Error("write EPIPE");
3569+
}
3570+
return {};
3571+
}),
3572+
addNotificationHandler: vi.fn(() => () => undefined),
3573+
addRequestHandler: vi.fn(() => () => undefined),
3574+
};
3575+
failedClient = c;
3576+
return c as never;
3577+
});
3578+
const params = createParams(
3579+
path.join(tempDir, "session.jsonl"),
3580+
path.join(tempDir, "workspace"),
3581+
);
3582+
params.spawnedBy = "agent:main:session-parent";
3583+
3584+
await expect(runCodexAppServerAttempt(params)).rejects.toThrow("write EPIPE");
3585+
const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient);
3586+
expect(calledWithFailedClient).toBe(true);
3587+
clearSpy.mockRestore();
3588+
});
3589+
35563590
it("retires the shared Codex client when a top-level run fails with a logical thread/start error", async () => {
35573591
const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent");
35583592
clearSpy.mockClear();
@@ -3561,7 +3595,10 @@ describe("runCodexAppServerAttempt", () => {
35613595
const c = {
35623596
request: vi.fn(async (method: string) => {
35633597
if (method === "thread/start") {
3564-
throw new Error("401 authentication_error: Invalid bearer token");
3598+
throw new CodexAppServerRpcError(
3599+
{ message: "401 authentication_error: Invalid bearer token" },
3600+
"thread/start",
3601+
);
35653602
}
35663603
return {};
35673604
}),

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { buildCodexUserMcpServersThreadConfigPatch } from "openclaw/plugin-sdk/c
88
import { listRegisteredPluginAgentPromptGuidance } from "openclaw/plugin-sdk/plugin-runtime";
99
import { CODEX_GPT5_HEARTBEAT_PROMPT_OVERLAY } from "../../prompt-overlay.js";
1010
import { isModernCodexModel } from "../../provider.js";
11-
import { isCodexAppServerConnectionClosedError, type CodexAppServerClient } from "./client.js";
11+
import {
12+
CodexAppServerRpcError,
13+
isCodexAppServerConnectionClosedError,
14+
type CodexAppServerClient,
15+
} from "./client.js";
1216
import { codexSandboxPolicyForTurn, type CodexAppServerRuntimeOptions } from "./config.js";
1317
import {
1418
resolveCodexContextEngineProjectionMaxChars,
@@ -564,10 +568,10 @@ export async function startOrResumeThread(params: {
564568
try {
565569
return await params.client.request("thread/start", startParams);
566570
} catch (error) {
567-
if (isCodexAppServerConnectionClosedError(error)) {
568-
throw error;
571+
if (error instanceof CodexAppServerRpcError) {
572+
throw new CodexThreadStartRequestError(error);
569573
}
570-
throw new CodexThreadStartRequestError(error);
574+
throw error;
571575
}
572576
});
573577
const response = assertCodexThreadStartResponse(threadStartResponse);

0 commit comments

Comments
 (0)