@openclaw/acpx 2026.5.7 → 2026.5.10-beta.1

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.
@@ -0,0 +1,331 @@
1
+ import path from "node:path";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ //#region extensions/acpx/src/process-lease.ts
8
+ const OPENCLAW_ACPX_LEASE_ID_ENV = "OPENCLAW_ACPX_LEASE_ID";
9
+ const OPENCLAW_GATEWAY_INSTANCE_ID_ENV = "OPENCLAW_GATEWAY_INSTANCE_ID";
10
+ const OPENCLAW_ACPX_LEASE_ID_ARG = "--openclaw-acpx-lease-id";
11
+ const OPENCLAW_GATEWAY_INSTANCE_ID_ARG = "--openclaw-gateway-instance-id";
12
+ const LEASE_FILE = "process-leases.json";
13
+ function normalizeLease(value) {
14
+ if (typeof value !== "object" || value === null) return;
15
+ const record = value;
16
+ if (typeof record.leaseId !== "string" || typeof record.gatewayInstanceId !== "string" || typeof record.sessionKey !== "string" || typeof record.wrapperRoot !== "string" || typeof record.wrapperPath !== "string" || typeof record.rootPid !== "number" || typeof record.commandHash !== "string" || typeof record.startedAt !== "number" || ![
17
+ "open",
18
+ "closing",
19
+ "closed",
20
+ "lost"
21
+ ].includes(String(record.state))) return;
22
+ return {
23
+ leaseId: record.leaseId,
24
+ gatewayInstanceId: record.gatewayInstanceId,
25
+ sessionKey: record.sessionKey,
26
+ wrapperRoot: record.wrapperRoot,
27
+ wrapperPath: record.wrapperPath,
28
+ rootPid: record.rootPid,
29
+ ...typeof record.processGroupId === "number" ? { processGroupId: record.processGroupId } : {},
30
+ commandHash: record.commandHash,
31
+ startedAt: record.startedAt,
32
+ state: record.state
33
+ };
34
+ }
35
+ async function readLeaseFile(filePath) {
36
+ const { value } = await readJsonFileWithFallback(filePath, {
37
+ version: 1,
38
+ leases: []
39
+ });
40
+ return {
41
+ version: 1,
42
+ leases: Array.isArray(value.leases) ? value.leases.map(normalizeLease).filter((lease) => !!lease) : []
43
+ };
44
+ }
45
+ function writeLeaseFile(filePath, value) {
46
+ return writeJsonFileAtomically(filePath, value);
47
+ }
48
+ function createAcpxProcessLeaseStore(params) {
49
+ const filePath = path.join(params.stateDir, LEASE_FILE);
50
+ let updateQueue = Promise.resolve();
51
+ async function update(mutator) {
52
+ const run = updateQueue.then(async () => {
53
+ await fs.mkdir(params.stateDir, { recursive: true });
54
+ await writeLeaseFile(filePath, {
55
+ version: 1,
56
+ leases: mutator((await readLeaseFile(filePath)).leases)
57
+ });
58
+ });
59
+ updateQueue = run.catch(() => {});
60
+ await run;
61
+ }
62
+ async function readCurrent() {
63
+ await updateQueue;
64
+ return await readLeaseFile(filePath);
65
+ }
66
+ return {
67
+ async load(leaseId) {
68
+ return (await readCurrent()).leases.find((lease) => lease.leaseId === leaseId);
69
+ },
70
+ async listOpen(gatewayInstanceId) {
71
+ return (await readCurrent()).leases.filter((lease) => (lease.state === "open" || lease.state === "closing") && (!gatewayInstanceId || lease.gatewayInstanceId === gatewayInstanceId));
72
+ },
73
+ async save(lease) {
74
+ await update((leases) => [...leases.filter((entry) => entry.leaseId !== lease.leaseId), lease]);
75
+ },
76
+ async markState(leaseId, state) {
77
+ await update((leases) => leases.map((lease) => lease.leaseId === leaseId ? {
78
+ ...lease,
79
+ state
80
+ } : lease));
81
+ }
82
+ };
83
+ }
84
+ function createAcpxProcessLeaseId() {
85
+ return randomUUID();
86
+ }
87
+ function hashAcpxProcessCommand(command) {
88
+ return createHash("sha256").update(command).digest("hex");
89
+ }
90
+ function quoteEnvValue(value) {
91
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
92
+ }
93
+ function withAcpxLeaseEnvironment(params) {
94
+ if ((params.platform ?? process.platform) === "win32") return params.command;
95
+ return [
96
+ "env",
97
+ `${OPENCLAW_ACPX_LEASE_ID_ENV}=${quoteEnvValue(params.leaseId)}`,
98
+ `${OPENCLAW_GATEWAY_INSTANCE_ID_ENV}=${quoteEnvValue(params.gatewayInstanceId)}`,
99
+ params.command,
100
+ OPENCLAW_ACPX_LEASE_ID_ARG,
101
+ quoteEnvValue(params.leaseId),
102
+ OPENCLAW_GATEWAY_INSTANCE_ID_ARG,
103
+ quoteEnvValue(params.gatewayInstanceId)
104
+ ].join(" ");
105
+ }
106
+ //#endregion
107
+ //#region extensions/acpx/src/process-reaper.ts
108
+ const execFileAsync = promisify(execFile);
109
+ const GENERATED_WRAPPER_BASENAMES = new Set(["codex-acp-wrapper.mjs", "claude-agent-acp-wrapper.mjs"]);
110
+ const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
111
+ const ACP_PACKAGE_MARKERS = [
112
+ "/@zed-industries/codex-acp/",
113
+ "/@agentclientprotocol/claude-agent-acp/",
114
+ "/acpx/dist/"
115
+ ];
116
+ function normalizePathLike(value) {
117
+ return value.replaceAll("\\", "/");
118
+ }
119
+ function commandMentionsGeneratedWrapper(command) {
120
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
121
+ }
122
+ function commandWrapperBelongsToRoot(command, wrapperRoot) {
123
+ if (!wrapperRoot) return true;
124
+ const normalizedCommand = normalizePathLike(command);
125
+ const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
126
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => normalizedCommand.includes(`${normalizedRoot}/${basename}`));
127
+ }
128
+ function commandsReferToSameRootCommand(liveCommand, storedCommand) {
129
+ if (!storedCommand?.trim()) return true;
130
+ return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
131
+ }
132
+ function splitCommandParts(value) {
133
+ const parts = [];
134
+ let current = "";
135
+ let quote = null;
136
+ let escaping = false;
137
+ for (const ch of value) {
138
+ if (escaping) {
139
+ current += ch;
140
+ escaping = false;
141
+ continue;
142
+ }
143
+ if (ch === "\\" && quote !== "'") {
144
+ escaping = true;
145
+ continue;
146
+ }
147
+ if (quote) {
148
+ if (ch === quote) quote = null;
149
+ else current += ch;
150
+ continue;
151
+ }
152
+ if (ch === "'" || ch === "\"") {
153
+ quote = ch;
154
+ continue;
155
+ }
156
+ if (/\s/.test(ch)) {
157
+ if (current) {
158
+ parts.push(current);
159
+ current = "";
160
+ }
161
+ continue;
162
+ }
163
+ current += ch;
164
+ }
165
+ if (escaping) current += "\\";
166
+ if (current) parts.push(current);
167
+ return parts;
168
+ }
169
+ function commandOptionEquals(parts, option, expected) {
170
+ if (!expected) return true;
171
+ const index = parts.indexOf(option);
172
+ return index >= 0 && parts[index + 1] === expected;
173
+ }
174
+ function liveCommandMatchesLeaseIdentity(params) {
175
+ if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) return true;
176
+ const parts = splitCommandParts(params.command ?? "");
177
+ return commandOptionEquals(parts, "--openclaw-acpx-lease-id", params.expectedLeaseId) && commandOptionEquals(parts, "--openclaw-gateway-instance-id", params.expectedGatewayInstanceId);
178
+ }
179
+ function isOpenClawOwnedAcpxProcessCommand(params) {
180
+ const command = params.command?.trim();
181
+ if (!command) return false;
182
+ const normalized = normalizePathLike(command);
183
+ if (commandMentionsGeneratedWrapper(normalized)) return commandWrapperBelongsToRoot(normalized, params.wrapperRoot);
184
+ if (!normalized.includes(OPENCLAW_PLUGIN_DEPS_MARKER)) return false;
185
+ return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
186
+ }
187
+ function parseProcessList(stdout) {
188
+ const processes = [];
189
+ for (const line of stdout.split(/\r?\n/)) {
190
+ const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
191
+ if (!match?.groups) continue;
192
+ processes.push({
193
+ pid: Number.parseInt(match.groups.pid, 10),
194
+ ppid: Number.parseInt(match.groups.ppid, 10),
195
+ command: match.groups.command
196
+ });
197
+ }
198
+ return processes;
199
+ }
200
+ async function listPlatformProcesses() {
201
+ if (process.platform === "win32") return [];
202
+ const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], { maxBuffer: 8 * 1024 * 1024 });
203
+ return parseProcessList(stdout);
204
+ }
205
+ function collectProcessTree(processes, rootPid) {
206
+ const childrenByParent = /* @__PURE__ */ new Map();
207
+ for (const processInfo of processes) {
208
+ const children = childrenByParent.get(processInfo.ppid) ?? [];
209
+ children.push(processInfo);
210
+ childrenByParent.set(processInfo.ppid, children);
211
+ }
212
+ const root = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])).get(rootPid);
213
+ const collected = [];
214
+ if (root) collected.push(root);
215
+ const queue = [...childrenByParent.get(rootPid) ?? []];
216
+ while (queue.length > 0) {
217
+ const next = queue.shift();
218
+ if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) continue;
219
+ collected.push(next);
220
+ queue.push(...childrenByParent.get(next.pid) ?? []);
221
+ }
222
+ return collected;
223
+ }
224
+ function uniquePids(processes) {
225
+ return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
226
+ }
227
+ function isProcessAlive(pid) {
228
+ try {
229
+ process.kill(pid, 0);
230
+ return true;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+ async function terminatePids(pids, deps) {
236
+ const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
237
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
238
+ const terminated = [];
239
+ for (const pid of pids) try {
240
+ killProcess(pid, "SIGTERM");
241
+ terminated.push(pid);
242
+ } catch {}
243
+ if (terminated.length === 0) return terminated;
244
+ await sleep(750);
245
+ for (const pid of terminated) if (deps?.killProcess || isProcessAlive(pid)) try {
246
+ killProcess(pid, "SIGKILL");
247
+ } catch {}
248
+ return terminated;
249
+ }
250
+ async function cleanupOpenClawOwnedAcpxProcessTree(params) {
251
+ const rootPid = params.rootPid;
252
+ if (!rootPid || rootPid <= 0 || rootPid === process.pid) return {
253
+ inspectedPids: [],
254
+ terminatedPids: [],
255
+ skippedReason: "missing-root"
256
+ };
257
+ let processes = [];
258
+ try {
259
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
260
+ } catch {
261
+ processes = [];
262
+ }
263
+ const listedTree = collectProcessTree(processes, rootPid);
264
+ if (listedTree.length === 0) return {
265
+ inspectedPids: [],
266
+ terminatedPids: [],
267
+ skippedReason: "unverified-root"
268
+ };
269
+ const rootCommand = listedTree[0]?.command ?? params.rootCommand;
270
+ const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(rootCommand ?? ""));
271
+ const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(params.rootCommand ?? ""));
272
+ if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) return {
273
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
274
+ terminatedPids: [],
275
+ skippedReason: "not-openclaw-owned"
276
+ };
277
+ if (!liveCommandWasGeneratedWrapper && !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)) return {
278
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
279
+ terminatedPids: [],
280
+ skippedReason: "not-openclaw-owned"
281
+ };
282
+ if (!isOpenClawOwnedAcpxProcessCommand({
283
+ command: rootCommand,
284
+ wrapperRoot: params.wrapperRoot
285
+ })) return {
286
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
287
+ terminatedPids: [],
288
+ skippedReason: "not-openclaw-owned"
289
+ };
290
+ if (!liveCommandMatchesLeaseIdentity({
291
+ command: rootCommand,
292
+ expectedLeaseId: params.expectedLeaseId,
293
+ expectedGatewayInstanceId: params.expectedGatewayInstanceId
294
+ })) return {
295
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
296
+ terminatedPids: [],
297
+ skippedReason: "not-openclaw-owned"
298
+ };
299
+ const pids = uniquePids(listedTree.toReversed());
300
+ return {
301
+ inspectedPids: uniquePids(listedTree),
302
+ terminatedPids: await terminatePids(pids, params.deps)
303
+ };
304
+ }
305
+ async function reapStaleOpenClawOwnedAcpxOrphans(params) {
306
+ if (process.platform === "win32") return {
307
+ inspectedPids: [],
308
+ terminatedPids: [],
309
+ skippedReason: "unsupported-platform"
310
+ };
311
+ let processes;
312
+ try {
313
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
314
+ } catch {
315
+ return {
316
+ inspectedPids: [],
317
+ terminatedPids: [],
318
+ skippedReason: "process-list-unavailable"
319
+ };
320
+ }
321
+ const orphanTrees = processes.filter((processInfo) => processInfo.ppid === 1 && isOpenClawOwnedAcpxProcessCommand({
322
+ command: processInfo.command,
323
+ wrapperRoot: params.wrapperRoot
324
+ })).map((orphan) => collectProcessTree(processes, orphan.pid));
325
+ return {
326
+ inspectedPids: uniquePids(orphanTrees.flat()),
327
+ terminatedPids: await terminatePids(uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())), params.deps)
328
+ };
329
+ }
330
+ //#endregion
331
+ export { OPENCLAW_GATEWAY_INSTANCE_ID_ARG as a, hashAcpxProcessCommand as c, OPENCLAW_ACPX_LEASE_ID_ARG as i, withAcpxLeaseEnvironment as l, isOpenClawOwnedAcpxProcessCommand as n, createAcpxProcessLeaseId as o, reapStaleOpenClawOwnedAcpxOrphans as r, createAcpxProcessLeaseStore as s, cleanupOpenClawOwnedAcpxProcessTree as t };
@@ -4,7 +4,7 @@ const ACPX_BACKEND_ID = "acpx";
4
4
  const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
5
5
  let serviceModulePromise = null;
6
6
  function loadServiceModule() {
7
- serviceModulePromise ??= import("./service-DXpBsJTq.js");
7
+ serviceModulePromise ??= import("./service-DzKX5ybC.js");
8
8
  return serviceModulePromise;
9
9
  }
10
10
  function shouldRunStartupProbe(env = process.env) {
@@ -1,5 +1,7 @@
1
1
  import { AcpRuntimeError } from "./runtime-api.js";
2
+ import { c as hashAcpxProcessCommand, l as withAcpxLeaseEnvironment, n as isOpenClawOwnedAcpxProcessCommand, o as createAcpxProcessLeaseId, t as cleanupOpenClawOwnedAcpxProcessTree } from "./process-reaper-DKzgrZt9.js";
2
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { resolve } from "node:path";
3
5
  import { ACPX_BACKEND_ID, AcpxRuntime as AcpxRuntime$1, createAcpRuntime, createAgentRegistry, createFileSessionStore, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState } from "acpx/runtime";
4
6
  //#region extensions/acpx/src/runtime.ts
5
7
  function readSessionRecordName(record) {
@@ -7,17 +9,94 @@ function readSessionRecordName(record) {
7
9
  const { name } = record;
8
10
  return typeof name === "string" ? name.trim() : "";
9
11
  }
10
- function createResetAwareSessionStore(baseStore) {
12
+ function readRecordAgentCommand(record) {
13
+ if (typeof record !== "object" || record === null) return;
14
+ const { agentCommand } = record;
15
+ return typeof agentCommand === "string" ? agentCommand.trim() || void 0 : void 0;
16
+ }
17
+ function readRecordCwd(record) {
18
+ if (typeof record !== "object" || record === null) return;
19
+ const { cwd } = record;
20
+ return typeof cwd === "string" ? cwd.trim() || void 0 : void 0;
21
+ }
22
+ function readRecordResetOnNextEnsure(record) {
23
+ if (typeof record !== "object" || record === null) return false;
24
+ const { acpx } = record;
25
+ if (typeof acpx !== "object" || acpx === null) return false;
26
+ return acpx.reset_on_next_ensure === true;
27
+ }
28
+ function readRecordAgentPid(record) {
29
+ if (typeof record !== "object" || record === null) return;
30
+ const { pid, processId } = record;
31
+ const rawPid = pid ?? processId;
32
+ const numericPid = typeof rawPid === "number" ? rawPid : typeof rawPid === "string" ? Number.parseInt(rawPid, 10) : void 0;
33
+ return numericPid && Number.isInteger(numericPid) && numericPid > 0 ? numericPid : void 0;
34
+ }
35
+ function readOpenClawLeaseIdFromRecord(record) {
36
+ if (typeof record !== "object" || record === null) return;
37
+ const { openclawLeaseId } = record;
38
+ return typeof openclawLeaseId === "string" ? openclawLeaseId.trim() || void 0 : void 0;
39
+ }
40
+ function extractGeneratedWrapperPath(command) {
41
+ return splitCommandParts(command ?? "").find((part) => basename(part) === "codex-acp-wrapper.mjs" || basename(part) === "claude-agent-acp-wrapper.mjs") ?? "";
42
+ }
43
+ function selectCurrentSessionLease(params) {
44
+ const sessionKeys = new Set(params.sessionKeys.map((entry) => entry.trim()).filter(Boolean));
45
+ const candidates = params.leases.filter((lease) => sessionKeys.has(lease.sessionKey));
46
+ if (params.rootPid) return candidates.find((lease) => lease.rootPid === params.rootPid);
47
+ let selected;
48
+ for (const lease of candidates) if (!selected || lease.startedAt > selected.startedAt) selected = lease;
49
+ return selected;
50
+ }
51
+ function createResetAwareSessionStore(baseStore, params) {
11
52
  const freshSessionKeys = /* @__PURE__ */ new Set();
12
53
  return {
13
54
  async load(sessionId) {
14
55
  const normalized = sessionId.trim();
15
56
  if (normalized && freshSessionKeys.has(normalized)) return;
16
- return await baseStore.load(sessionId);
57
+ const record = await baseStore.load(sessionId);
58
+ if (!record || !params?.leaseStore || !params.gatewayInstanceId) return record;
59
+ const sessionName = readSessionRecordName(record) || normalized;
60
+ const lease = selectCurrentSessionLease({
61
+ leases: await params.leaseStore.listOpen(params.gatewayInstanceId),
62
+ sessionKeys: [sessionName, normalized],
63
+ rootPid: readRecordAgentPid(record)
64
+ });
65
+ if (!lease) return record;
66
+ return {
67
+ ...record,
68
+ openclawLeaseId: lease.leaseId,
69
+ openclawGatewayInstanceId: lease.gatewayInstanceId
70
+ };
17
71
  },
18
72
  async save(record) {
19
- await baseStore.save(record);
73
+ let recordToSave = record;
74
+ const launch = params?.launchScope?.getStore();
20
75
  const sessionName = readSessionRecordName(record);
76
+ const rootPid = readRecordAgentPid(record);
77
+ const agentCommand = readRecordAgentCommand(record);
78
+ const stableAgentCommand = launch?.stableCommand ?? agentCommand;
79
+ if (launch && params?.leaseStore && sessionName === launch.sessionKey && rootPid && stableAgentCommand) {
80
+ const lease = {
81
+ leaseId: launch.leaseId,
82
+ gatewayInstanceId: launch.gatewayInstanceId,
83
+ sessionKey: launch.sessionKey,
84
+ wrapperRoot: launch.wrapperRoot,
85
+ wrapperPath: extractGeneratedWrapperPath(stableAgentCommand),
86
+ rootPid,
87
+ commandHash: hashAcpxProcessCommand(stableAgentCommand),
88
+ startedAt: Date.now(),
89
+ state: "open"
90
+ };
91
+ await params.leaseStore.save(lease);
92
+ recordToSave = {
93
+ ...record,
94
+ agentCommand: stableAgentCommand,
95
+ openclawLeaseId: launch.leaseId,
96
+ openclawGatewayInstanceId: launch.gatewayInstanceId
97
+ };
98
+ }
99
+ await baseStore.save(recordToSave);
21
100
  if (sessionName) freshSessionKeys.delete(sessionName);
22
101
  },
23
102
  markFresh(sessionKey) {
@@ -67,9 +146,10 @@ function readAgentFromHandle(handle) {
67
146
  return readAgentFromSessionKey(handle.sessionKey);
68
147
  }
69
148
  function readAgentCommandFromRecord(record) {
70
- if (typeof record !== "object" || record === null) return;
71
- const { agentCommand } = record;
72
- return typeof agentCommand === "string" ? agentCommand.trim() || void 0 : void 0;
149
+ return readRecordAgentCommand(record);
150
+ }
151
+ function readAgentPidFromRecord(record) {
152
+ return readRecordAgentPid(record);
73
153
  }
74
154
  function splitCommandParts(value) {
75
155
  const parts = [];
@@ -197,8 +277,8 @@ function createModelScopedAgentRegistry(params) {
197
277
  resolve(agentName) {
198
278
  const command = params.agentRegistry.resolve(agentName);
199
279
  const override = params.scope.getStore();
200
- if (!override || normalizeAgentName(agentName) !== CODEX_ACP_AGENT_ID || typeof command !== "string" || !isCodexAcpCommand(command)) return command;
201
- return appendCodexAcpConfigOverrides(command, override);
280
+ if (!override || normalizeAgentName(agentName) !== CODEX_ACP_AGENT_ID || typeof command !== "string" || !isCodexAcpCommand(command)) return params.leaseCommand(command);
281
+ return params.leaseCommand(appendCodexAcpConfigOverrides(command, override));
202
282
  },
203
283
  list() {
204
284
  return params.agentRegistry.list();
@@ -228,22 +308,34 @@ function shouldUseDistinctBridgeDelegate(options) {
228
308
  var AcpxRuntime = class {
229
309
  constructor(options, testOptions) {
230
310
  this.codexAcpModelOverrideScope = new AsyncLocalStorage();
231
- this.sessionStore = createResetAwareSessionStore(options.sessionStore);
311
+ this.launchLeaseScope = new AsyncLocalStorage();
312
+ const { openclawProcessCleanup, ...delegateTestOptions } = testOptions ?? {};
313
+ this.processCleanupDeps = openclawProcessCleanup;
314
+ this.wrapperRoot = options.openclawWrapperRoot;
315
+ this.gatewayInstanceId = options.openclawGatewayInstanceId;
316
+ this.processLeaseStore = options.openclawProcessLeaseStore;
317
+ this.cwd = options.cwd;
318
+ this.sessionStore = createResetAwareSessionStore(options.sessionStore, {
319
+ gatewayInstanceId: this.gatewayInstanceId,
320
+ leaseStore: this.processLeaseStore,
321
+ launchScope: this.launchLeaseScope
322
+ });
232
323
  this.agentRegistry = options.agentRegistry;
233
324
  this.scopedAgentRegistry = createModelScopedAgentRegistry({
234
325
  agentRegistry: this.agentRegistry,
235
- scope: this.codexAcpModelOverrideScope
326
+ scope: this.codexAcpModelOverrideScope,
327
+ leaseCommand: (command) => this.commandWithLaunchLease(command)
236
328
  });
237
329
  const sharedOptions = {
238
330
  ...options,
239
331
  sessionStore: this.sessionStore,
240
332
  agentRegistry: this.scopedAgentRegistry
241
333
  };
242
- this.delegate = new AcpxRuntime$1(sharedOptions, testOptions);
334
+ this.delegate = new AcpxRuntime$1(sharedOptions, delegateTestOptions);
243
335
  this.bridgeSafeDelegate = shouldUseDistinctBridgeDelegate(options) ? new AcpxRuntime$1({
244
336
  ...sharedOptions,
245
337
  mcpServers: []
246
- }, testOptions) : this.delegate;
338
+ }, delegateTestOptions) : this.delegate;
247
339
  this.probeDelegate = this.resolveDelegateForAgent(resolveProbeAgentName(options));
248
340
  }
249
341
  resolveDelegateForAgent(agentName) {
@@ -257,7 +349,11 @@ var AcpxRuntime = class {
257
349
  return shouldUseBridgeSafeDelegateForCommand(command) ? this.bridgeSafeDelegate : this.delegate;
258
350
  }
259
351
  async resolveDelegateForHandle(handle) {
260
- const recordCommand = readAgentCommandFromRecord(await this.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey));
352
+ const record = await this.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey);
353
+ return this.resolveDelegateForLoadedRecord(handle, record);
354
+ }
355
+ resolveDelegateForLoadedRecord(handle, record) {
356
+ const recordCommand = readAgentCommandFromRecord(record);
261
357
  if (recordCommand) return this.resolveDelegateForCommand(recordCommand);
262
358
  return this.resolveDelegateForAgent(readAgentFromHandle(handle));
263
359
  }
@@ -269,6 +365,87 @@ var AcpxRuntime = class {
269
365
  agentRegistry: this.agentRegistry
270
366
  });
271
367
  }
368
+ commandWithLaunchLease(command) {
369
+ const launch = this.launchLeaseScope.getStore();
370
+ if (!command || !launch) return command;
371
+ launch.stableCommand = command;
372
+ return withAcpxLeaseEnvironment({
373
+ command,
374
+ leaseId: launch.leaseId,
375
+ gatewayInstanceId: launch.gatewayInstanceId
376
+ });
377
+ }
378
+ async canReuseStablePersistentSession(params) {
379
+ if (params.mode !== "persistent" || !params.command) return false;
380
+ const existing = await this.sessionStore.load(params.sessionKey);
381
+ if (!existing || readRecordResetOnNextEnsure(existing)) return false;
382
+ const recordCwd = readRecordCwd(existing);
383
+ if (!recordCwd || resolve(recordCwd) !== resolve(params.cwd?.trim() || this.cwd)) return false;
384
+ if (readRecordAgentCommand(existing) !== params.command) return false;
385
+ const existingSessionId = typeof existing === "object" && existing !== null ? existing.acpSessionId : void 0;
386
+ return !params.resumeSessionId || existingSessionId === params.resumeSessionId;
387
+ }
388
+ async runWithLaunchLease(params) {
389
+ if (params.enabled === false || !params.command || !this.wrapperRoot || !this.gatewayInstanceId || !this.processLeaseStore || !isOpenClawOwnedAcpxProcessCommand({
390
+ command: params.command,
391
+ wrapperRoot: this.wrapperRoot
392
+ })) return await params.run();
393
+ const launch = {
394
+ leaseId: createAcpxProcessLeaseId(),
395
+ gatewayInstanceId: this.gatewayInstanceId,
396
+ sessionKey: params.sessionKey,
397
+ wrapperRoot: this.wrapperRoot,
398
+ stableCommand: params.command
399
+ };
400
+ await this.processLeaseStore.save({
401
+ leaseId: launch.leaseId,
402
+ gatewayInstanceId: launch.gatewayInstanceId,
403
+ sessionKey: launch.sessionKey,
404
+ wrapperRoot: launch.wrapperRoot,
405
+ wrapperPath: extractGeneratedWrapperPath(params.command),
406
+ rootPid: 0,
407
+ commandHash: hashAcpxProcessCommand(params.command),
408
+ startedAt: Date.now(),
409
+ state: "open"
410
+ });
411
+ return await this.launchLeaseScope.run(launch, params.run);
412
+ }
413
+ async cleanupProcessTreeForRecord(handle, record) {
414
+ const leaseId = readOpenClawLeaseIdFromRecord(record);
415
+ const rootPid = readAgentPidFromRecord(record);
416
+ const sessionKeys = [handle.sessionKey, readSessionRecordName(record)];
417
+ const selectedLease = selectCurrentSessionLease({
418
+ leases: this.gatewayInstanceId && this.processLeaseStore ? await this.processLeaseStore.listOpen(this.gatewayInstanceId) : [],
419
+ sessionKeys,
420
+ rootPid
421
+ });
422
+ const loadedLease = leaseId ? await this.processLeaseStore?.load(leaseId) : void 0;
423
+ const lease = selectedLease ?? (loadedLease && loadedLease.gatewayInstanceId === this.gatewayInstanceId && (!rootPid || loadedLease.rootPid === rootPid) && sessionKeys.includes(loadedLease.sessionKey) ? loadedLease : void 0);
424
+ if (lease && lease.gatewayInstanceId === this.gatewayInstanceId && lease.rootPid > 0) {
425
+ await this.processLeaseStore?.markState(lease.leaseId, "closing");
426
+ const result = await cleanupOpenClawOwnedAcpxProcessTree({
427
+ rootPid: lease.rootPid,
428
+ rootCommand: readAgentCommandFromRecord(record),
429
+ expectedLeaseId: lease.leaseId,
430
+ expectedGatewayInstanceId: lease.gatewayInstanceId,
431
+ wrapperRoot: lease.wrapperRoot,
432
+ deps: this.processCleanupDeps
433
+ });
434
+ await this.processLeaseStore?.markState(lease.leaseId, result.terminatedPids.length > 0 || result.skippedReason === "missing-root" ? "closed" : "lost");
435
+ return;
436
+ }
437
+ const rootCommand = readAgentCommandFromRecord(record) ?? resolveAgentCommandForName({
438
+ agentName: readAgentFromHandle(handle),
439
+ agentRegistry: this.agentRegistry
440
+ });
441
+ if (!rootPid || !rootCommand) return;
442
+ await cleanupOpenClawOwnedAcpxProcessTree({
443
+ rootPid,
444
+ rootCommand,
445
+ wrapperRoot: this.wrapperRoot,
446
+ deps: this.processCleanupDeps
447
+ });
448
+ }
272
449
  isHealthy() {
273
450
  return this.probeDelegate.isHealthy();
274
451
  }
@@ -286,12 +463,30 @@ var AcpxRuntime = class {
286
463
  });
287
464
  const delegate = this.resolveDelegateForCommand(command);
288
465
  const codexModelOverride = normalizeAgentName(input.agent) === CODEX_ACP_AGENT_ID && isCodexAcpCommand(command) ? normalizeCodexAcpModelOverride(input.model, input.thinking) : void 0;
289
- if (!codexModelOverride) return delegate.ensureSession(input);
466
+ const stableLaunchCommand = codexModelOverride && command ? appendCodexAcpConfigOverrides(command, codexModelOverride) : command;
467
+ const shouldStartWithLease = !await this.canReuseStablePersistentSession({
468
+ sessionKey: input.sessionKey,
469
+ mode: input.mode,
470
+ cwd: input.cwd,
471
+ command: stableLaunchCommand,
472
+ resumeSessionId: input.resumeSessionId
473
+ });
474
+ if (!codexModelOverride) return await this.runWithLaunchLease({
475
+ sessionKey: input.sessionKey,
476
+ command: stableLaunchCommand,
477
+ enabled: shouldStartWithLease,
478
+ run: () => delegate.ensureSession(input)
479
+ });
290
480
  const normalizedInput = {
291
481
  ...input,
292
482
  ...codexAcpSessionModelId(codexModelOverride) ? { model: codexAcpSessionModelId(codexModelOverride) } : {}
293
483
  };
294
- return this.codexAcpModelOverrideScope.run(codexModelOverride, () => delegate.ensureSession(normalizedInput));
484
+ return await this.runWithLaunchLease({
485
+ sessionKey: input.sessionKey,
486
+ command: stableLaunchCommand,
487
+ enabled: shouldStartWithLease,
488
+ run: () => this.codexAcpModelOverrideScope.run(codexModelOverride, () => delegate.ensureSession(normalizedInput))
489
+ });
295
490
  }
296
491
  async *runTurn(input) {
297
492
  yield* (await this.resolveDelegateForHandle(input.handle)).runTurn(input);
@@ -332,18 +527,26 @@ var AcpxRuntime = class {
332
527
  await delegate.setConfigOption(input);
333
528
  }
334
529
  async cancel(input) {
335
- await (await this.resolveDelegateForHandle(input.handle)).cancel(input);
530
+ const record = await this.sessionStore.load(input.handle.acpxRecordId ?? input.handle.sessionKey);
531
+ await this.resolveDelegateForLoadedRecord(input.handle, record).cancel(input);
336
532
  }
337
533
  async prepareFreshSession(input) {
338
534
  this.sessionStore.markFresh(input.sessionKey);
339
535
  }
340
536
  async close(input) {
341
- await (await this.resolveDelegateForHandle(input.handle)).close({
342
- handle: input.handle,
343
- reason: input.reason,
344
- discardPersistentState: input.discardPersistentState
345
- });
346
- if (input.discardPersistentState) this.sessionStore.markFresh(input.handle.sessionKey);
537
+ const record = await this.sessionStore.load(input.handle.acpxRecordId ?? input.handle.sessionKey);
538
+ let closeSucceeded = false;
539
+ try {
540
+ await this.resolveDelegateForLoadedRecord(input.handle, record).close({
541
+ handle: input.handle,
542
+ reason: input.reason,
543
+ discardPersistentState: input.discardPersistentState
544
+ });
545
+ closeSucceeded = true;
546
+ } finally {
547
+ await this.cleanupProcessTreeForRecord(input.handle, record);
548
+ }
549
+ if (closeSucceeded && input.discardPersistentState) this.sessionStore.markFresh(input.handle.sessionKey);
347
550
  }
348
551
  };
349
552
  const __testing = {
@@ -1,14 +1,146 @@
1
1
  import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "./runtime-api.js";
2
+ import { a as OPENCLAW_GATEWAY_INSTANCE_ID_ARG, i as OPENCLAW_ACPX_LEASE_ID_ARG, r as reapStaleOpenClawOwnedAcpxOrphans, s as createAcpxProcessLeaseStore, t as cleanupOpenClawOwnedAcpxProcessTree } from "./process-reaper-DKzgrZt9.js";
2
3
  import { createRequire } from "node:module";
3
4
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
5
+ import path from "node:path";
6
+ import { randomUUID } from "node:crypto";
4
7
  import fs from "node:fs/promises";
8
+ import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
5
9
  import { inspect } from "node:util";
6
10
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
11
  import fsSync from "node:fs";
8
- import path from "node:path";
12
+ import os from "node:os";
9
13
  import { fileURLToPath } from "node:url";
10
14
  import { formatPluginConfigIssue } from "openclaw/plugin-sdk/extension-shared";
11
15
  import { z } from "openclaw/plugin-sdk/zod";
16
+ //#region extensions/acpx/src/codex-trust-config.ts
17
+ function stripTomlComment(line) {
18
+ let quote = null;
19
+ let escaping = false;
20
+ for (let index = 0; index < line.length; index += 1) {
21
+ const ch = line[index];
22
+ if (escaping) {
23
+ escaping = false;
24
+ continue;
25
+ }
26
+ if (quote === "\"" && ch === "\\") {
27
+ escaping = true;
28
+ continue;
29
+ }
30
+ if (quote) {
31
+ if (ch === quote) quote = null;
32
+ continue;
33
+ }
34
+ if (ch === "'" || ch === "\"") {
35
+ quote = ch;
36
+ continue;
37
+ }
38
+ if (ch === "#") return line.slice(0, index);
39
+ }
40
+ return line;
41
+ }
42
+ function parseTomlString(value) {
43
+ const trimmed = value.trim();
44
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) try {
45
+ return JSON.parse(trimmed);
46
+ } catch {
47
+ return;
48
+ }
49
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
50
+ }
51
+ function parseTomlDottedKey(value) {
52
+ const parts = [];
53
+ let current = "";
54
+ let quote = null;
55
+ let escaping = false;
56
+ for (const ch of value.trim()) {
57
+ if (escaping) {
58
+ current += ch;
59
+ escaping = false;
60
+ continue;
61
+ }
62
+ if (quote === "\"" && ch === "\\") {
63
+ current += ch;
64
+ escaping = true;
65
+ continue;
66
+ }
67
+ if (quote) {
68
+ current += ch;
69
+ if (ch === quote) quote = null;
70
+ continue;
71
+ }
72
+ if (ch === "'" || ch === "\"") {
73
+ quote = ch;
74
+ current += ch;
75
+ continue;
76
+ }
77
+ if (ch === ".") {
78
+ parts.push(current.trim());
79
+ current = "";
80
+ continue;
81
+ }
82
+ current += ch;
83
+ }
84
+ if (current.trim()) parts.push(current.trim());
85
+ return parts.map((part) => parseTomlString(part) ?? part);
86
+ }
87
+ function parseProjectHeader(line) {
88
+ const trimmed = line.trim();
89
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) return;
90
+ const parts = parseTomlDottedKey(trimmed.slice(1, -1));
91
+ return parts.length === 2 && parts[0] === "projects" ? parts[1] : void 0;
92
+ }
93
+ function parseTrustedInlineProjectEntries(value) {
94
+ const trusted = [];
95
+ for (const match of value.matchAll(/(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*\{(?<body>[^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g)) {
96
+ const key = match.groups?.key;
97
+ const body = match.groups?.body;
98
+ if (!key || !body || !/\btrust_level\s*=\s*["']trusted["']/.test(body)) continue;
99
+ const projectPath = parseTomlString(key) ?? key.trim();
100
+ if (projectPath) trusted.push(projectPath);
101
+ }
102
+ return trusted;
103
+ }
104
+ function extractTrustedCodexProjectPaths(configToml) {
105
+ const trusted = /* @__PURE__ */ new Set();
106
+ let currentProjectPath;
107
+ let inProjectsTable = false;
108
+ for (const rawLine of configToml.split(/\r?\n/)) {
109
+ const line = stripTomlComment(rawLine).trim();
110
+ if (!line) continue;
111
+ if (line.startsWith("[")) {
112
+ currentProjectPath = parseProjectHeader(line);
113
+ inProjectsTable = line === "[projects]";
114
+ continue;
115
+ }
116
+ if (currentProjectPath && /^trust_level\s*=\s*["']trusted["']\s*$/.test(line)) {
117
+ trusted.add(currentProjectPath);
118
+ continue;
119
+ }
120
+ const assignment = /^(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?<value>.+)$/.exec(line);
121
+ if (!assignment?.groups) continue;
122
+ const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key;
123
+ const value = assignment.groups.value.trim();
124
+ if (inProjectsTable && /^\{.*\}$/.test(value)) {
125
+ if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) trusted.add(key);
126
+ continue;
127
+ }
128
+ if (key === "projects" || inProjectsTable) for (const projectPath of parseTrustedInlineProjectEntries(value)) trusted.add(projectPath);
129
+ }
130
+ return Array.from(trusted);
131
+ }
132
+ function renderIsolatedCodexProjectTrustConfig(projectPaths) {
133
+ return [
134
+ "# Generated by OpenClaw for Codex ACP sessions.",
135
+ ...Array.from(new Set(projectPaths.map((projectPath) => projectPath.trim()).filter(Boolean).map((projectPath) => path.resolve(projectPath)))).toSorted((left, right) => left.localeCompare(right)).flatMap((projectPath) => [
136
+ "",
137
+ `[projects.${JSON.stringify(projectPath)}]`,
138
+ "trust_level = \"trusted\""
139
+ ]),
140
+ ""
141
+ ].join("\n");
142
+ }
143
+ //#endregion
12
144
  //#region extensions/acpx/src/config-schema.ts
13
145
  const ACPX_PERMISSION_MODES = [
14
146
  "approve-all",
@@ -34,7 +166,10 @@ const AcpxPluginConfigSchema = z.strictObject({
34
166
  timeoutSeconds: z.number({ error: "timeoutSeconds must be a number >= 0.001" }).min(.001, { error: "timeoutSeconds must be a number >= 0.001" }).default(120),
35
167
  queueOwnerTtlSeconds: z.number({ error: "queueOwnerTtlSeconds must be a number >= 0" }).min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" }).optional(),
36
168
  mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
37
- agents: z.record(z.string(), z.strictObject({ command: nonEmptyTrimmedString("agents.<id>.command must be a non-empty string") })).optional()
169
+ agents: z.record(z.string(), z.strictObject({
170
+ command: nonEmptyTrimmedString("agents.<id>.command must be a non-empty string"),
171
+ args: z.array(z.string({ error: "args must be an array of strings" })).optional()
172
+ })).optional()
38
173
  });
39
174
  //#endregion
40
175
  //#region extensions/acpx/src/config.ts
@@ -116,6 +251,10 @@ function resolveTsxImportSpecifier() {
116
251
  return "tsx";
117
252
  }
118
253
  }
254
+ function shellQuoteCommandArg(arg) {
255
+ if (!/[\s'"\\$|&;<>{}()*?[\]~`]/.test(arg)) return arg;
256
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
257
+ }
119
258
  function resolvePluginToolsMcpServerConfig(moduleUrl = import.meta.url) {
120
259
  const openClawRoot = resolveOpenClawRoot(resolveAcpxPluginRoot(moduleUrl));
121
260
  const distEntry = path.join(openClawRoot, "dist", "mcp", "plugin-tools-serve.js");
@@ -185,7 +324,12 @@ function resolveAcpxPluginConfig(params) {
185
324
  openClawToolsMcpBridge,
186
325
  moduleUrl: params.moduleUrl
187
326
  });
188
- const agents = Object.fromEntries(Object.entries(normalized.agents ?? {}).map(([name, entry]) => [normalizeLowercaseStringOrEmpty(name), entry.command.trim()]));
327
+ const agents = Object.fromEntries(Object.entries(normalized.agents ?? {}).map(([name, entry]) => {
328
+ const cmd = entry.command.trim();
329
+ const cmdArgs = entry.args ?? [];
330
+ const fullCommand = cmdArgs.length > 0 ? `${cmd} ${cmdArgs.map(shellQuoteCommandArg).join(" ")}` : cmd;
331
+ return [normalizeLowercaseStringOrEmpty(name), fullCommand];
332
+ }));
189
333
  return {
190
334
  cwd,
191
335
  stateDir,
@@ -276,7 +420,7 @@ function resolvePackageBinPath(packageJsonPath, manifest, binName) {
276
420
  async function resolveInstalledAcpPackageBinPath(packageName, binName) {
277
421
  try {
278
422
  const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
279
- const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
423
+ const { value: manifest } = await readJsonFileWithFallback(packageJsonPath, {});
280
424
  if (manifest.name !== packageName) return;
281
425
  const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName);
282
426
  if (!binPath) return;
@@ -300,7 +444,25 @@ import { spawn } from "node:child_process";
300
444
  import { fileURLToPath } from "node:url";
301
445
 
302
446
  ${params.envSetup}
303
- const configuredArgs = process.argv.slice(2);
447
+ const openClawWrapperArgs = new Set([
448
+ ${quoteCommandPart(OPENCLAW_ACPX_LEASE_ID_ARG)},
449
+ ${quoteCommandPart(OPENCLAW_GATEWAY_INSTANCE_ID_ARG)},
450
+ ]);
451
+
452
+ function stripOpenClawWrapperArgs(args) {
453
+ const stripped = [];
454
+ for (let index = 0; index < args.length; index += 1) {
455
+ const value = args[index];
456
+ if (openClawWrapperArgs.has(value)) {
457
+ index += 1;
458
+ continue;
459
+ }
460
+ stripped.push(value);
461
+ }
462
+ return stripped;
463
+ }
464
+
465
+ const configuredArgs = stripOpenClawWrapperArgs(process.argv.slice(2));
304
466
 
305
467
  function resolveNpmCliPath() {
306
468
  const candidate = path.resolve(
@@ -342,23 +504,78 @@ if (!command) {
342
504
  }
343
505
 
344
506
  const child = spawn(command, args, {
507
+ detached: process.platform !== "win32",
345
508
  env,
346
509
  stdio: "inherit",
347
510
  windowsHide: true,
348
511
  });
349
512
 
513
+ let forceKillTimer;
514
+ let orphanCleanupStarted = false;
515
+
516
+ function killChildTree(signal, options = {}) {
517
+ if (!child.pid || (!options.force && child.killed)) {
518
+ return;
519
+ }
520
+ if (process.platform !== "win32") {
521
+ try {
522
+ // The adapter can spawn grandchildren; signaling the process group keeps
523
+ // the generated wrapper from leaving an ACP tree behind.
524
+ process.kill(-child.pid, signal);
525
+ return;
526
+ } catch {
527
+ // Fall back to direct child signaling below.
528
+ }
529
+ }
530
+ child.kill(signal);
531
+ }
532
+
350
533
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
351
534
  process.once(signal, () => {
352
- child.kill(signal);
535
+ killChildTree(signal);
353
536
  });
354
537
  }
355
538
 
539
+ const originalParentPid = process.ppid;
540
+ const parentWatcher =
541
+ process.platform === "win32"
542
+ ? undefined
543
+ : setInterval(() => {
544
+ if (process.ppid === originalParentPid || process.ppid !== 1) {
545
+ return;
546
+ }
547
+ if (orphanCleanupStarted) {
548
+ return;
549
+ }
550
+ orphanCleanupStarted = true;
551
+ if (parentWatcher) {
552
+ clearInterval(parentWatcher);
553
+ }
554
+ killChildTree("SIGTERM");
555
+ // Keep the wrapper alive long enough for stubborn adapters to receive
556
+ // a forced fallback signal after SIGTERM.
557
+ forceKillTimer = setTimeout(() => {
558
+ killChildTree("SIGKILL", { force: true });
559
+ process.exit(1);
560
+ }, 1_500);
561
+ }, 1_000);
562
+ parentWatcher?.unref?.();
563
+
356
564
  child.on("error", (error) => {
357
565
  console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`);
358
566
  process.exit(1);
359
567
  });
360
568
 
361
569
  child.on("exit", (code, signal) => {
570
+ if (parentWatcher) {
571
+ clearInterval(parentWatcher);
572
+ }
573
+ if (orphanCleanupStarted) {
574
+ return;
575
+ }
576
+ if (forceKillTimer) {
577
+ clearTimeout(forceKillTimer);
578
+ }
362
579
  if (code !== null) {
363
580
  process.exit(code);
364
581
  }
@@ -390,10 +607,20 @@ function buildClaudeAcpWrapperScript(installedBinPath) {
390
607
  };`
391
608
  });
392
609
  }
393
- async function prepareIsolatedCodexHome(baseDir) {
394
- const codexHome = path.join(baseDir, "codex-home");
610
+ async function readSourceCodexConfig(codexHome) {
611
+ try {
612
+ return await fs.readFile(path.join(codexHome, "config.toml"), "utf8");
613
+ } catch (error) {
614
+ if (error.code === "ENOENT") return;
615
+ throw error;
616
+ }
617
+ }
618
+ async function prepareIsolatedCodexHome(params) {
619
+ const sourceConfig = await readSourceCodexConfig(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
620
+ const trustedProjectPaths = [...sourceConfig ? extractTrustedCodexProjectPaths(sourceConfig) : [], params.workspaceDir];
621
+ const codexHome = path.join(params.baseDir, "codex-home");
395
622
  await fs.mkdir(codexHome, { recursive: true });
396
- await fs.writeFile(path.join(codexHome, "config.toml"), "# Generated by OpenClaw for Codex ACP sessions.\n", "utf8");
623
+ await fs.writeFile(path.join(codexHome, "config.toml"), renderIsolatedCodexProjectTrustConfig(trustedProjectPaths), "utf8");
397
624
  return codexHome;
398
625
  }
399
626
  async function makeGeneratedWrapperExecutableIfPossible(wrapperPath) {
@@ -471,7 +698,10 @@ function buildClaudeAcpWrapperCommand(wrapperPath, configuredCommand) {
471
698
  async function prepareAcpxCodexAuthConfig(params) {
472
699
  params.logger;
473
700
  const codexBaseDir = path.join(params.stateDir, "acpx");
474
- await prepareIsolatedCodexHome(codexBaseDir);
701
+ await prepareIsolatedCodexHome({
702
+ baseDir: codexBaseDir,
703
+ workspaceDir: params.pluginConfig.cwd
704
+ });
475
705
  const installedCodexBinPath = await (params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath)();
476
706
  const installedClaudeBinPath = await (params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath)();
477
707
  const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedCodexBinPath);
@@ -493,7 +723,7 @@ const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
493
723
  const ACPX_BACKEND_ID = "acpx";
494
724
  let runtimeModulePromise = null;
495
725
  function loadRuntimeModule() {
496
- runtimeModulePromise ??= import("./runtime-B3LWev_t.js");
726
+ runtimeModulePromise ??= import("./runtime-QqpvXys6.js");
497
727
  return runtimeModulePromise;
498
728
  }
499
729
  function createLazyDefaultRuntime(params) {
@@ -504,6 +734,9 @@ function createLazyDefaultRuntime(params) {
504
734
  runtimePromise ??= loadRuntimeModule().then((module) => {
505
735
  runtime = new module.AcpxRuntime({
506
736
  cwd: params.pluginConfig.cwd,
737
+ openclawGatewayInstanceId: params.gatewayInstanceId,
738
+ openclawProcessLeaseStore: params.processLeaseStore,
739
+ openclawWrapperRoot: params.wrapperRoot,
507
740
  sessionStore: module.createFileSessionStore({ stateDir: params.pluginConfig.stateDir }),
508
741
  agentRegistry: module.createAgentRegistry({ overrides: params.pluginConfig.agents }),
509
742
  probeAgent: params.pluginConfig.probeAgent,
@@ -603,6 +836,57 @@ function resolveAllowedAgentsProbeAgent(ctx) {
603
836
  function shouldRunStartupProbe(env = process.env) {
604
837
  return env[ENABLE_STARTUP_PROBE_ENV] === "1";
605
838
  }
839
+ async function resolveGatewayInstanceId(stateDir) {
840
+ const filePath = path.join(stateDir, "gateway-instance-id");
841
+ try {
842
+ const existing = (await fs.readFile(filePath, "utf8")).trim();
843
+ if (existing) return existing;
844
+ } catch (error) {
845
+ if (error.code !== "ENOENT") throw error;
846
+ }
847
+ const next = randomUUID();
848
+ await fs.mkdir(stateDir, { recursive: true });
849
+ await fs.writeFile(filePath, `${next}\n`, { mode: 384 });
850
+ return next;
851
+ }
852
+ async function reapOpenAcpxProcessLeases(params) {
853
+ const leases = await params.leaseStore.listOpen(params.gatewayInstanceId);
854
+ const inspectedPids = [];
855
+ const terminatedPids = [];
856
+ const pendingLeaseRootResults = /* @__PURE__ */ new Map();
857
+ for (const lease of leases) {
858
+ if (lease.rootPid <= 0) {
859
+ await params.leaseStore.markState(lease.leaseId, "closing");
860
+ let result = pendingLeaseRootResults.get(lease.wrapperRoot);
861
+ if (!result) {
862
+ result = await reapStaleOpenClawOwnedAcpxOrphans({
863
+ wrapperRoot: lease.wrapperRoot,
864
+ deps: params.deps
865
+ });
866
+ pendingLeaseRootResults.set(lease.wrapperRoot, result);
867
+ inspectedPids.push(...result.inspectedPids);
868
+ terminatedPids.push(...result.terminatedPids);
869
+ }
870
+ await params.leaseStore.markState(lease.leaseId, result.terminatedPids.length > 0 ? "closed" : "lost");
871
+ continue;
872
+ }
873
+ await params.leaseStore.markState(lease.leaseId, "closing");
874
+ const result = await cleanupOpenClawOwnedAcpxProcessTree({
875
+ rootPid: lease.rootPid,
876
+ expectedLeaseId: lease.leaseId,
877
+ expectedGatewayInstanceId: lease.gatewayInstanceId,
878
+ wrapperRoot: lease.wrapperRoot,
879
+ deps: params.deps
880
+ });
881
+ inspectedPids.push(...result.inspectedPids);
882
+ terminatedPids.push(...result.terminatedPids);
883
+ await params.leaseStore.markState(lease.leaseId, result.terminatedPids.length > 0 ? "closed" : "lost");
884
+ }
885
+ return {
886
+ inspectedPids,
887
+ terminatedPids
888
+ };
889
+ }
606
890
  function createAcpxRuntimeService(params = {}) {
607
891
  let runtime = null;
608
892
  let lifecycleRevision = 0;
@@ -625,16 +909,32 @@ function createAcpxRuntimeService(params = {}) {
625
909
  stateDir: ctx.stateDir,
626
910
  logger: ctx.logger
627
911
  });
912
+ const wrapperRoot = path.join(ctx.stateDir, "acpx");
628
913
  await fs.mkdir(pluginConfig.stateDir, { recursive: true });
914
+ await fs.mkdir(wrapperRoot, { recursive: true });
915
+ const gatewayInstanceId = await resolveGatewayInstanceId(ctx.stateDir);
916
+ const processLeaseStore = createAcpxProcessLeaseStore({ stateDir: wrapperRoot });
917
+ const startupReap = await reapOpenAcpxProcessLeases({
918
+ gatewayInstanceId,
919
+ leaseStore: processLeaseStore,
920
+ deps: params.processCleanupDeps
921
+ });
922
+ if (startupReap.terminatedPids.length > 0) ctx.logger.info(`reaped ${startupReap.terminatedPids.length} stale OpenClaw-owned ACPX process${startupReap.terminatedPids.length === 1 ? "" : "es"}`);
629
923
  warnOnIgnoredLegacyCompatibilityConfig({
630
924
  pluginConfig,
631
925
  logger: ctx.logger
632
926
  });
633
927
  runtime = params.runtimeFactory ? await params.runtimeFactory({
634
928
  pluginConfig,
929
+ gatewayInstanceId,
930
+ processLeaseStore,
931
+ wrapperRoot,
635
932
  logger: ctx.logger
636
933
  }) : createLazyDefaultRuntime({
637
934
  pluginConfig,
935
+ gatewayInstanceId,
936
+ processLeaseStore,
937
+ wrapperRoot,
638
938
  logger: ctx.logger
639
939
  });
640
940
  registerAcpRuntimeBackend({
@@ -85,6 +85,10 @@
85
85
  "command": {
86
86
  "type": "string",
87
87
  "minLength": 1
88
+ },
89
+ "args": {
90
+ "type": "array",
91
+ "items": { "type": "string" }
88
92
  }
89
93
  },
90
94
  "required": ["command"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/acpx",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "OpenClaw ACP runtime backend",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,9 +8,9 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "@agentclientprotocol/claude-agent-acp": "0.32.0",
12
- "@zed-industries/codex-acp": "0.13.0",
13
- "acpx": "0.6.1"
11
+ "@agentclientprotocol/claude-agent-acp": "0.33.1",
12
+ "@zed-industries/codex-acp": "0.14.0",
13
+ "acpx": "0.7.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@openclaw/plugin-sdk": "workspace:*"
@@ -25,10 +25,10 @@
25
25
  "minHostVersion": ">=2026.4.25"
26
26
  },
27
27
  "compat": {
28
- "pluginApi": ">=2026.5.7"
28
+ "pluginApi": ">=2026.5.10-beta.1"
29
29
  },
30
30
  "build": {
31
- "openclawVersion": "2026.5.7",
31
+ "openclawVersion": "2026.5.10-beta.1",
32
32
  "staticAssets": [
33
33
  {
34
34
  "source": "./src/runtime-internals/mcp-proxy.mjs",
@@ -58,7 +58,7 @@
58
58
  "skills/**"
59
59
  ],
60
60
  "peerDependencies": {
61
- "openclaw": ">=2026.5.7"
61
+ "openclaw": ">=2026.5.10-beta.1"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "openclaw": {