@openclaw/codex 2026.5.1-beta.1 → 2026.5.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,7 @@ import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
25
25
  import * as elicitationBridge from "./elicitation-bridge.js";
26
26
  import type { CodexServerNotification } from "./protocol.js";
27
27
  import { runCodexAppServerAttempt, __testing } from "./run-attempt.js";
28
- import { writeCodexAppServerBinding } from "./session-binding.js";
28
+ import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
29
29
  import { createCodexTestModel } from "./test-support.js";
30
30
  import {
31
31
  buildThreadResumeParams,
@@ -50,6 +50,7 @@ function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAtt
50
50
  disableTools: true,
51
51
  timeoutMs: 5_000,
52
52
  authStorage: {} as never,
53
+ authProfileStore: { version: 1, profiles: {} },
53
54
  modelRegistry: {} as never,
54
55
  } as EmbeddedRunAttemptParams;
55
56
  }
@@ -1940,6 +1941,134 @@ describe("runCodexAppServerAttempt", () => {
1940
1941
  expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
1941
1942
  });
1942
1943
 
1944
+ it("preserves the binding when the app-server closes during thread resume", async () => {
1945
+ const sessionFile = path.join(tempDir, "session.jsonl");
1946
+ const workspaceDir = path.join(tempDir, "workspace");
1947
+ await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
1948
+ const appServer = createThreadLifecycleAppServerOptions();
1949
+ const request = vi.fn(async (method: string) => {
1950
+ if (method === "thread/resume") {
1951
+ throw new Error("codex app-server client is closed");
1952
+ }
1953
+ throw new Error(`unexpected method: ${method}`);
1954
+ });
1955
+
1956
+ await expect(
1957
+ startOrResumeThread({
1958
+ client: { request } as never,
1959
+ params: createParams(sessionFile, workspaceDir),
1960
+ cwd: workspaceDir,
1961
+ dynamicTools: [],
1962
+ appServer,
1963
+ }),
1964
+ ).rejects.toThrow("codex app-server client is closed");
1965
+
1966
+ expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/resume"]);
1967
+ await expect(readCodexAppServerBinding(sessionFile)).resolves.toMatchObject({
1968
+ threadId: "thread-existing",
1969
+ });
1970
+ });
1971
+
1972
+ it("restarts the app-server once when a shared client closes during startup", async () => {
1973
+ const sessionFile = path.join(tempDir, "session.jsonl");
1974
+ const workspaceDir = path.join(tempDir, "workspace");
1975
+ await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
1976
+ const requests: string[][] = [];
1977
+ let starts = 0;
1978
+ let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
1979
+ __testing.setCodexAppServerClientFactoryForTests(async () => {
1980
+ const startIndex = starts++;
1981
+ const methods: string[] = [];
1982
+ requests.push(methods);
1983
+ return {
1984
+ request: vi.fn(async (method: string) => {
1985
+ methods.push(method);
1986
+ if (method === "thread/resume" && startIndex === 0) {
1987
+ throw new Error("codex app-server client is closed");
1988
+ }
1989
+ if (method === "thread/resume") {
1990
+ return threadStartResult("thread-existing");
1991
+ }
1992
+ if (method === "turn/start") {
1993
+ return turnStartResult();
1994
+ }
1995
+ return {};
1996
+ }),
1997
+ addNotificationHandler: (handler: typeof notify) => {
1998
+ notify = handler;
1999
+ return () => undefined;
2000
+ },
2001
+ addRequestHandler: () => () => undefined,
2002
+ } as never;
2003
+ });
2004
+
2005
+ const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
2006
+ await vi.waitFor(() => expect(requests[1]).toContain("turn/start"), { interval: 1 });
2007
+ await notify({
2008
+ method: "turn/completed",
2009
+ params: {
2010
+ threadId: "thread-existing",
2011
+ turnId: "turn-1",
2012
+ turn: { id: "turn-1", status: "completed" },
2013
+ },
2014
+ });
2015
+
2016
+ await expect(run).resolves.toMatchObject({ aborted: false });
2017
+ expect(requests).toEqual([["thread/resume"], ["thread/resume", "turn/start"]]);
2018
+ });
2019
+
2020
+ it("tolerates a second app-server close while retrying startup", async () => {
2021
+ const sessionFile = path.join(tempDir, "session.jsonl");
2022
+ const workspaceDir = path.join(tempDir, "workspace");
2023
+ await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
2024
+ const requests: string[][] = [];
2025
+ let starts = 0;
2026
+ let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
2027
+ __testing.setCodexAppServerClientFactoryForTests(async () => {
2028
+ const startIndex = starts++;
2029
+ const methods: string[] = [];
2030
+ requests.push(methods);
2031
+ return {
2032
+ request: vi.fn(async (method: string) => {
2033
+ methods.push(method);
2034
+ if (method === "thread/resume" && startIndex < 2) {
2035
+ throw new Error("codex app-server client is closed");
2036
+ }
2037
+ if (method === "thread/resume") {
2038
+ return threadStartResult("thread-existing");
2039
+ }
2040
+ if (method === "turn/start") {
2041
+ return turnStartResult();
2042
+ }
2043
+ return {};
2044
+ }),
2045
+ addNotificationHandler: (handler: typeof notify) => {
2046
+ notify = handler;
2047
+ return () => undefined;
2048
+ },
2049
+ addRequestHandler: () => () => undefined,
2050
+ } as never;
2051
+ });
2052
+
2053
+ const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
2054
+ await vi.waitFor(() => expect(requests[2]).toContain("turn/start"), { interval: 1 });
2055
+ await notify({
2056
+ method: "turn/completed",
2057
+ params: {
2058
+ threadId: "thread-existing",
2059
+ turnId: "turn-1",
2060
+ turn: { id: "turn-1", status: "completed" },
2061
+ },
2062
+ });
2063
+
2064
+ await expect(run).resolves.toMatchObject({ aborted: false });
2065
+ expect(requests).toEqual([
2066
+ ["thread/resume"],
2067
+ ["thread/resume"],
2068
+ ["thread/resume", "turn/start"],
2069
+ ]);
2070
+ });
2071
+
1943
2072
  it("passes native hook relay config on thread start and resume", async () => {
1944
2073
  const sessionFile = path.join(tempDir, "session.jsonl");
1945
2074
  const workspaceDir = path.join(tempDir, "workspace");
@@ -1,6 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
- import { SessionManager } from "@mariozechner/pi-coding-agent";
4
3
  import {
5
4
  assembleHarnessContextEngine,
6
5
  bootstrapHarnessContextEngine,
@@ -43,7 +42,11 @@ import {
43
42
  createCodexAppServerClientFactoryTestHooks,
44
43
  defaultCodexAppServerClientFactory,
45
44
  } from "./client-factory.js";
46
- import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js";
45
+ import {
46
+ isCodexAppServerApprovalRequest,
47
+ isCodexAppServerConnectionClosedError,
48
+ type CodexAppServerClient,
49
+ } from "./client.js";
47
50
  import { ensureCodexComputerUse } from "./computer-use.js";
48
51
  import {
49
52
  readCodexPluginConfig,
@@ -51,6 +54,7 @@ import {
51
54
  type CodexPluginConfig,
52
55
  } from "./config.js";
53
56
  import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
57
+ import { applyCodexDynamicToolProfile } from "./dynamic-tool-profile.js";
54
58
  import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
55
59
  import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
56
60
  import { CodexAppServerEventProjector } from "./event-projector.js";
@@ -75,7 +79,7 @@ import {
75
79
  } from "./protocol.js";
76
80
  import { readCodexAppServerBinding, type CodexAppServerThreadBinding } from "./session-binding.js";
77
81
  import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
78
- import { clearSharedCodexAppServerClient } from "./shared-client.js";
82
+ import { clearSharedCodexAppServerClientIfCurrent } from "./shared-client.js";
79
83
  import {
80
84
  buildDeveloperInstructions,
81
85
  buildTurnStartParams,
@@ -92,18 +96,10 @@ import { createCodexUserInputBridge } from "./user-input-bridge.js";
92
96
  import { filterToolsForVisionInputs } from "./vision-tools.js";
93
97
 
94
98
  const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 30_000;
99
+ const CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS = 3;
95
100
  const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
96
101
  const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
97
102
  const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
98
- const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [
99
- "read",
100
- "write",
101
- "edit",
102
- "apply_patch",
103
- "exec",
104
- "process",
105
- "update_plan",
106
- ] as const;
107
103
  const LOG_FIELD_MAX_LENGTH = 160;
108
104
 
109
105
  type OpenClawCodingToolsOptions = NonNullable<
@@ -401,9 +397,7 @@ export async function runCodexAppServerAttempt(
401
397
  },
402
398
  });
403
399
  const hadSessionFile = await fileExists(params.sessionFile);
404
- const sessionManager = activeContextEngine ? SessionManager.open(params.sessionFile) : undefined;
405
- let historyMessages =
406
- (await readMirroredSessionHistoryMessages(params.sessionFile, sessionManager)) ?? [];
400
+ let historyMessages = (await readMirroredSessionHistoryMessages(params.sessionFile)) ?? [];
407
401
  const hookContext = {
408
402
  runId: params.runId,
409
403
  agentId: sessionAgentId,
@@ -421,7 +415,6 @@ export async function runCodexAppServerAttempt(
421
415
  sessionId: params.sessionId,
422
416
  sessionKey: sandboxSessionKey,
423
417
  sessionFile: params.sessionFile,
424
- sessionManager,
425
418
  runtimeContext: buildHarnessContextEngineRuntimeContext({
426
419
  attempt: runtimeParams,
427
420
  workspaceDir: effectiveWorkspace,
@@ -429,6 +422,7 @@ export async function runCodexAppServerAttempt(
429
422
  tokenBudget: params.contextTokenBudget,
430
423
  }),
431
424
  runMaintenance: runHarnessContextEngineMaintenance,
425
+ config: params.config,
432
426
  warn: (message) => embeddedAgentLog.warn(message),
433
427
  });
434
428
  historyMessages =
@@ -489,6 +483,7 @@ export async function runCodexAppServerAttempt(
489
483
  let thread: CodexAppServerThreadBinding;
490
484
  let trajectoryEndRecorded = false;
491
485
  let nativeHookRelay: NativeHookRelayRegistrationHandle | undefined;
486
+ let startupClientForCleanup: CodexAppServerClient | undefined;
492
487
  try {
493
488
  emitCodexAppServerEvent(params, {
494
489
  stream: "codex_app_server.lifecycle",
@@ -516,32 +511,87 @@ export async function runCodexAppServerAttempt(
516
511
  timeoutFloorMs: options.startupTimeoutFloorMs,
517
512
  signal: runAbortController.signal,
518
513
  operation: async () => {
519
- const startupClient = await clientFactory(appServer.start, startupAuthProfileId, agentDir);
520
- await ensureCodexComputerUse({
521
- client: startupClient,
522
- pluginConfig: options.pluginConfig,
523
- timeoutMs: appServer.requestTimeoutMs,
524
- signal: runAbortController.signal,
525
- });
526
- const startupThread = await startOrResumeThread({
527
- client: startupClient,
528
- params,
529
- cwd: effectiveWorkspace,
530
- dynamicTools: toolBridge.specs,
531
- appServer,
532
- developerInstructions: promptBuild.developerInstructions,
533
- config: nativeHookRelayConfig,
534
- });
535
- return { client: startupClient, thread: startupThread };
514
+ let attemptedClient: CodexAppServerClient | undefined;
515
+ const startupAttempt = async () => {
516
+ const startupClient = await clientFactory(
517
+ appServer.start,
518
+ startupAuthProfileId,
519
+ agentDir,
520
+ );
521
+ attemptedClient = startupClient;
522
+ startupClientForCleanup = startupClient;
523
+ await ensureCodexComputerUse({
524
+ client: startupClient,
525
+ pluginConfig: options.pluginConfig,
526
+ timeoutMs: appServer.requestTimeoutMs,
527
+ signal: runAbortController.signal,
528
+ });
529
+ const startupThread = await startOrResumeThread({
530
+ client: startupClient,
531
+ params,
532
+ cwd: effectiveWorkspace,
533
+ dynamicTools: toolBridge.specs,
534
+ appServer,
535
+ developerInstructions: promptBuild.developerInstructions,
536
+ config: nativeHookRelayConfig,
537
+ });
538
+ return { client: startupClient, thread: startupThread };
539
+ };
540
+ for (
541
+ let attempt = 1;
542
+ attempt <= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS;
543
+ attempt += 1
544
+ ) {
545
+ try {
546
+ return await startupAttempt();
547
+ } catch (error) {
548
+ if (
549
+ runAbortController.signal.aborted ||
550
+ !isCodexAppServerConnectionClosedError(error)
551
+ ) {
552
+ throw error;
553
+ }
554
+ const failedClient = attemptedClient;
555
+ const clearedSharedClient = clearSharedCodexAppServerClientIfCurrent(failedClient);
556
+ if (startupClientForCleanup === failedClient) {
557
+ startupClientForCleanup = undefined;
558
+ }
559
+ attemptedClient = undefined;
560
+ if (attempt >= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS) {
561
+ embeddedAgentLog.warn(
562
+ "codex app-server connection closed during startup; retries exhausted",
563
+ {
564
+ attempt,
565
+ maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
566
+ clearedSharedClient,
567
+ error: formatErrorMessage(error),
568
+ },
569
+ );
570
+ throw error;
571
+ }
572
+ embeddedAgentLog.warn(
573
+ "codex app-server connection closed during startup; restarting app-server and retrying",
574
+ {
575
+ attempt,
576
+ nextAttempt: attempt + 1,
577
+ maxAttempts: CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS,
578
+ clearedSharedClient,
579
+ error: formatErrorMessage(error),
580
+ },
581
+ );
582
+ }
583
+ }
584
+ throw new Error("codex app-server startup retry loop exited unexpectedly");
536
585
  },
537
586
  }));
587
+ startupClientForCleanup = undefined;
538
588
  emitCodexAppServerEvent(params, {
539
589
  stream: "codex_app_server.lifecycle",
540
590
  data: { phase: "thread_ready", threadId: thread.threadId },
541
591
  });
542
592
  } catch (error) {
543
593
  nativeHookRelay?.unregister();
544
- clearSharedCodexAppServerClient();
594
+ clearSharedCodexAppServerClientIfCurrent(startupClientForCleanup);
545
595
  params.abortSignal?.removeEventListener("abort", abortFromUpstream);
546
596
  throw error;
547
597
  }
@@ -1121,7 +1171,7 @@ export async function runCodexAppServerAttempt(
1121
1171
  promptCache: result.promptCache,
1122
1172
  }),
1123
1173
  runMaintenance: runHarnessContextEngineMaintenance,
1124
- sessionManager,
1174
+ config: params.config,
1125
1175
  warn: (message) => embeddedAgentLog.warn(message),
1126
1176
  });
1127
1177
  }
@@ -1441,26 +1491,6 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
1441
1491
  });
1442
1492
  }
1443
1493
 
1444
- function applyCodexDynamicToolProfile<T extends { name: string }>(
1445
- tools: T[],
1446
- config: CodexPluginConfig,
1447
- ): T[] {
1448
- const excludes = new Set<string>();
1449
- const profile = config.codexDynamicToolsProfile ?? "native-first";
1450
- if (profile === "native-first") {
1451
- for (const name of CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES) {
1452
- excludes.add(name);
1453
- }
1454
- }
1455
- for (const name of config.codexDynamicToolsExclude ?? []) {
1456
- const trimmed = name.trim();
1457
- if (trimmed) {
1458
- excludes.add(trimmed);
1459
- }
1460
- }
1461
- return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(tool.name));
1462
- }
1463
-
1464
1494
  async function withCodexStartupTimeout<T>(params: {
1465
1495
  timeoutMs: number;
1466
1496
  timeoutFloorMs?: number;
@@ -1557,9 +1587,8 @@ function readString(record: JsonObject, key: string): string | undefined {
1557
1587
 
1558
1588
  async function readMirroredSessionHistoryMessages(
1559
1589
  sessionFile: string,
1560
- sessionManager?: Pick<SessionManager, "buildSessionContext">,
1561
1590
  ): Promise<AgentMessage[] | undefined> {
1562
- const messages = await readCodexMirroredSessionHistoryMessages(sessionFile, sessionManager);
1591
+ const messages = await readCodexMirroredSessionHistoryMessages(sessionFile);
1563
1592
  if (!messages) {
1564
1593
  embeddedAgentLog.warn("failed to read mirrored session history for codex harness hooks", {
1565
1594
  sessionFile,
@@ -1583,6 +1612,7 @@ async function mirrorTranscriptBestEffort(params: {
1583
1612
  sessionKey: params.sessionKey,
1584
1613
  messages: params.result.messagesSnapshot,
1585
1614
  idempotencyScope: `codex-app-server:${params.threadId}:${params.turnId}`,
1615
+ config: params.params.config,
1586
1616
  });
1587
1617
  } catch (error) {
1588
1618
  embeddedAgentLog.warn("failed to mirror codex app-server transcript", { error });
@@ -28,6 +28,7 @@ function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAtt
28
28
  disableTools: true,
29
29
  timeoutMs: 5_000,
30
30
  authStorage: {} as never,
31
+ authProfileStore: { version: 1, profiles: {} },
31
32
  modelRegistry: {} as never,
32
33
  } as EmbeddedRunAttemptParams;
33
34
  }
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
- import type { SessionEntry, SessionManager } from "@mariozechner/pi-coding-agent";
2
+ import type { SessionEntry } from "@mariozechner/pi-coding-agent";
3
3
  import {
4
4
  buildSessionContext,
5
5
  migrateSessionEntries,
@@ -18,12 +18,8 @@ function isMissingFileError(error: unknown): boolean {
18
18
 
19
19
  export async function readCodexMirroredSessionHistoryMessages(
20
20
  sessionFile: string,
21
- sessionManager?: Pick<SessionManager, "buildSessionContext">,
22
21
  ): Promise<AgentMessage[] | undefined> {
23
22
  try {
24
- if (sessionManager) {
25
- return sessionManager.buildSessionContext().messages;
26
- }
27
23
  const raw = await fs.readFile(sessionFile, "utf-8");
28
24
  const entries = parseSessionEntries(raw);
29
25
  const firstEntry = entries[0] as { type?: unknown; id?: unknown } | undefined;
@@ -31,6 +31,7 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
31
31
 
32
32
  let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
33
33
  let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
34
+ let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent;
34
35
  let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
35
36
  let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
36
37
 
@@ -54,6 +55,7 @@ describe("shared Codex app-server client", () => {
54
55
  ({ listCodexAppServerModels } = await import("./models.js"));
55
56
  ({
56
57
  clearSharedCodexAppServerClient,
58
+ clearSharedCodexAppServerClientIfCurrent,
57
59
  createIsolatedCodexAppServerClient,
58
60
  resetSharedCodexAppServerClientForTests,
59
61
  } = await import("./shared-client.js"));
@@ -293,6 +295,32 @@ describe("shared Codex app-server client", () => {
293
295
  expect(second.process.kill).not.toHaveBeenCalled();
294
296
  });
295
297
 
298
+ it("only clears the shared client that is still current", async () => {
299
+ const first = createClientHarness();
300
+ const second = createClientHarness();
301
+ vi.spyOn(CodexAppServerClient, "start")
302
+ .mockReturnValueOnce(first.client)
303
+ .mockReturnValueOnce(second.client);
304
+
305
+ const firstList = listCodexAppServerModels({ timeoutMs: 1000 });
306
+ await sendInitializeResult(first, "openclaw/0.125.0 (macOS; test)");
307
+ await sendEmptyModelList(first);
308
+ await expect(firstList).resolves.toEqual({ models: [] });
309
+
310
+ expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(true);
311
+ expect(first.process.kill).toHaveBeenCalledWith("SIGTERM");
312
+
313
+ const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
314
+ await sendInitializeResult(second, "openclaw/0.125.0 (macOS; test)");
315
+ await sendEmptyModelList(second);
316
+ await expect(secondList).resolves.toEqual({ models: [] });
317
+
318
+ expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(false);
319
+ expect(second.process.kill).not.toHaveBeenCalled();
320
+ expect(clearSharedCodexAppServerClientIfCurrent(second.client)).toBe(true);
321
+ expect(second.process.kill).toHaveBeenCalledWith("SIGTERM");
322
+ });
323
+
296
324
  it("uses a fresh websocket Authorization header after shared-client token rotation", async () => {
297
325
  const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
298
326
  const authHeaders: Array<string | undefined> = [];
@@ -134,6 +134,23 @@ export function clearSharedCodexAppServerClient(): void {
134
134
  client?.close();
135
135
  }
136
136
 
137
+ export function clearSharedCodexAppServerClientIfCurrent(
138
+ client: CodexAppServerClient | undefined,
139
+ ): boolean {
140
+ if (!client) {
141
+ return false;
142
+ }
143
+ const state = getSharedCodexAppServerClientState();
144
+ if (state.client !== client) {
145
+ return false;
146
+ }
147
+ state.client = undefined;
148
+ state.promise = undefined;
149
+ state.key = undefined;
150
+ client.close();
151
+ return true;
152
+ }
153
+
137
154
  export async function clearSharedCodexAppServerClientAndWait(options?: {
138
155
  exitTimeoutMs?: number;
139
156
  forceKillDelayMs?: number;
@@ -4,7 +4,7 @@ import {
4
4
  } from "openclaw/plugin-sdk/agent-harness-runtime";
5
5
  import { renderCodexPromptOverlay } from "../../prompt-overlay.js";
6
6
  import { isModernCodexModel } from "../../provider.js";
7
- import type { CodexAppServerClient } from "./client.js";
7
+ import { isCodexAppServerConnectionClosedError, type CodexAppServerClient } from "./client.js";
8
8
  import { codexSandboxPolicyForTurn, type CodexAppServerRuntimeOptions } from "./config.js";
9
9
  import {
10
10
  assertCodexThreadResumeResponse,
@@ -86,6 +86,9 @@ export async function startOrResumeThread(params: {
86
86
  dynamicToolsFingerprint,
87
87
  };
88
88
  } catch (error) {
89
+ if (isCodexAppServerConnectionClosedError(error)) {
90
+ throw error;
91
+ }
89
92
  embeddedAgentLog.warn("codex app-server thread resume failed; starting a new thread", {
90
93
  error,
91
94
  });
@@ -94,25 +97,19 @@ export async function startOrResumeThread(params: {
94
97
  }
95
98
  }
96
99
 
97
- const modelProvider = resolveCodexAppServerModelProvider(params.params.provider);
98
100
  const response = assertCodexThreadStartResponse(
99
- await params.client.request("thread/start", {
100
- model: params.params.modelId,
101
- ...(modelProvider ? { modelProvider } : {}),
102
- cwd: params.cwd,
103
- approvalPolicy: params.appServer.approvalPolicy,
104
- approvalsReviewer: params.appServer.approvalsReviewer,
105
- sandbox: params.appServer.sandbox,
106
- ...(params.appServer.serviceTier ? { serviceTier: params.appServer.serviceTier } : {}),
107
- serviceName: "OpenClaw",
108
- ...(params.config ? { config: params.config } : {}),
109
- developerInstructions:
110
- params.developerInstructions ?? buildDeveloperInstructions(params.params),
111
- dynamicTools: params.dynamicTools,
112
- experimentalRawEvents: true,
113
- persistExtendedHistory: true,
114
- } satisfies CodexThreadStartParams),
101
+ await params.client.request(
102
+ "thread/start",
103
+ buildThreadStartParams(params.params, {
104
+ cwd: params.cwd,
105
+ dynamicTools: params.dynamicTools,
106
+ appServer: params.appServer,
107
+ developerInstructions: params.developerInstructions,
108
+ config: params.config,
109
+ }),
110
+ ),
115
111
  );
112
+ const modelProvider = resolveCodexAppServerModelProvider(params.params.provider);
116
113
  const createdAt = new Date().toISOString();
117
114
  await writeCodexAppServerBinding(params.params.sessionFile, {
118
115
  threadId: response.thread.id,
@@ -137,6 +134,34 @@ export async function startOrResumeThread(params: {
137
134
  };
138
135
  }
139
136
 
137
+ export function buildThreadStartParams(
138
+ params: EmbeddedRunAttemptParams,
139
+ options: {
140
+ cwd: string;
141
+ dynamicTools: CodexDynamicToolSpec[];
142
+ appServer: CodexAppServerRuntimeOptions;
143
+ developerInstructions?: string;
144
+ config?: JsonObject;
145
+ },
146
+ ): CodexThreadStartParams {
147
+ const modelProvider = resolveCodexAppServerModelProvider(params.provider);
148
+ return {
149
+ model: params.modelId,
150
+ ...(modelProvider ? { modelProvider } : {}),
151
+ cwd: options.cwd,
152
+ approvalPolicy: options.appServer.approvalPolicy,
153
+ approvalsReviewer: options.appServer.approvalsReviewer,
154
+ sandbox: options.appServer.sandbox,
155
+ ...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
156
+ serviceName: "OpenClaw",
157
+ ...(options.config ? { config: options.config } : {}),
158
+ developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
159
+ dynamicTools: options.dynamicTools,
160
+ experimentalRawEvents: true,
161
+ persistExtendedHistory: true,
162
+ };
163
+ }
164
+
140
165
  export function buildThreadResumeParams(
141
166
  params: EmbeddedRunAttemptParams,
142
167
  options: {
@@ -6,8 +6,10 @@ import { CURRENT_SESSION_VERSION, type SessionManager } from "@mariozechner/pi-c
6
6
  import {
7
7
  acquireSessionWriteLock,
8
8
  emitSessionTranscriptUpdate,
9
+ resolveSessionWriteLockAcquireTimeoutMs,
9
10
  runAgentHarnessBeforeMessageWriteHook,
10
11
  type AgentMessage,
12
+ type SessionWriteLockAcquireTimeoutConfig,
11
13
  } from "openclaw/plugin-sdk/agent-harness-runtime";
12
14
 
13
15
  const TRANSCRIPT_APPEND_SCAN_CHUNK_BYTES = 64 * 1024;
@@ -25,6 +27,7 @@ export async function mirrorCodexAppServerTranscript(params: {
25
27
  agentId?: string;
26
28
  messages: AgentMessage[];
27
29
  idempotencyScope?: string;
30
+ config?: SessionWriteLockAcquireTimeoutConfig;
28
31
  }): Promise<void> {
29
32
  const messages = params.messages.filter(
30
33
  (message) => message.role === "user" || message.role === "assistant",
@@ -36,7 +39,7 @@ export async function mirrorCodexAppServerTranscript(params: {
36
39
  await fs.mkdir(path.dirname(params.sessionFile), { recursive: true });
37
40
  const lock = await acquireSessionWriteLock({
38
41
  sessionFile: params.sessionFile,
39
- timeoutMs: 10_000,
42
+ timeoutMs: resolveSessionWriteLockAcquireTimeoutMs(params.config),
40
43
  });
41
44
  try {
42
45
  const existingIdempotencyKeys = await readTranscriptIdempotencyKeys(params.sessionFile);