@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.
- package/package.json +3 -6
- package/src/app-server/auth-profile-runtime-contract.test.ts +1 -0
- package/src/app-server/client.test.ts +51 -2
- package/src/app-server/client.ts +114 -14
- package/src/app-server/compact.ts +1 -0
- package/src/app-server/dynamic-tool-profile.ts +31 -0
- package/src/app-server/event-projector.ts +1 -0
- package/src/app-server/managed-binary.test.ts +46 -0
- package/src/app-server/managed-binary.ts +75 -4
- package/src/app-server/run-attempt.context-engine.test.ts +3 -0
- package/src/app-server/run-attempt.test.ts +130 -1
- package/src/app-server/run-attempt.ts +87 -57
- package/src/app-server/schema-normalization-runtime-contract.test.ts +1 -0
- package/src/app-server/session-history.ts +1 -5
- package/src/app-server/shared-client.test.ts +28 -0
- package/src/app-server/shared-client.ts +17 -0
- package/src/app-server/thread-lifecycle.ts +43 -18
- package/src/app-server/transcript-mirror.ts +4 -1
- package/test-api.ts +79 -0
- package/dist/.boundary-tsc.stamp +0 -1
- package/dist/.boundary-tsc.tsbuildinfo +0 -1
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
42
|
+
timeoutMs: resolveSessionWriteLockAcquireTimeoutMs(params.config),
|
|
40
43
|
});
|
|
41
44
|
try {
|
|
42
45
|
const existingIdempotencyKeys = await readTranscriptIdempotencyKeys(params.sessionFile);
|