@openclaw/acpx 2026.9.1-beta.1 → 2026.9.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.
@@ -1,7 +1,8 @@
1
- import { n as createLazyAcpRuntimeProxy } from "./register.runtime-Dj29TKFy.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { n as createLazyAcpRuntimeProxy } from "./register.runtime-Ba7FPMGu.js";
2
3
  import "./config-schema-DN_uAi4R.js";
3
- import { _ as quoteCommandPart, a as createAcpxProcessLeaseStore, f as ACPX_GATEWAY_INSTANCE_KEY, g as normalizeAcpxGatewayInstanceRecord, l as openAcpxProcessLeaseStateStore, n as OPENCLAW_ACPX_LEASE_ID_ARG, p as ACPX_GATEWAY_INSTANCE_NAMESPACE, r as OPENCLAW_GATEWAY_INSTANCE_ID_ARG, v as splitCommandParts } from "./process-lease-Cwvj7WGe.js";
4
- import { a as resolveAcpxPluginConfig, c as CODEX_ACP_BIN, d as OPENCLAW_CODEX_CONFIG_ARG, i as reapStaleOpenClawOwnedAcpxOrphans, l as CODEX_ACP_PACKAGE, n as cleanupOpenClawOwnedAcpxProcessTree, o as resolveAcpxPluginRoot, s as toAcpMcpServers, t as cleanupOpenClawOwnedAcpxPendingLease, u as LEGACY_CODEX_ACP_PACKAGE } from "./process-reaper-DduWm_7N.js";
4
+ import { _ as quoteCommandPart, a as createAcpxProcessLeaseStore, f as ACPX_GATEWAY_INSTANCE_KEY, g as normalizeAcpxGatewayInstanceRecord, l as openAcpxProcessLeaseStateStore, n as OPENCLAW_ACPX_LEASE_ID_ARG, p as ACPX_GATEWAY_INSTANCE_NAMESPACE, r as OPENCLAW_GATEWAY_INSTANCE_ID_ARG, u as readAcpxProcessLeaseIdentity, v as splitCommandParts } from "./process-lease-Cwvj7WGe.js";
5
+ import { n as resolveAcpxPluginRoot, r as toAcpMcpServers, t as resolveAcpxPluginConfig } from "./config-v8M2tNu6.js";
5
6
  import { createRequire } from "node:module";
6
7
  import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
7
8
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
@@ -11,10 +12,335 @@ import os from "node:os";
11
12
  import path from "node:path";
12
13
  import fs$1 from "node:fs/promises";
13
14
  import { randomUUID } from "node:crypto";
15
+ import { isPidAlive, runExec } from "openclaw/plugin-sdk/process-runtime";
14
16
  import { inspect } from "node:util";
15
17
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
16
18
  import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
17
19
  import { parse, stringify } from "smol-toml";
20
+ //#region extensions/acpx/src/codex-adapter.ts
21
+ const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
22
+ const CODEX_ACP_BIN = "codex-acp";
23
+ const LEGACY_CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
24
+ const OPENCLAW_CODEX_CONFIG_ARG = "--openclaw-codex-config";
25
+ //#endregion
26
+ //#region extensions/acpx/src/process-reaper.ts
27
+ /**
28
+ * ACPX process ownership checks and cleanup. The reaper only terminates
29
+ * OpenClaw-owned wrapper trees after validating paths, packages, and lease ids.
30
+ */
31
+ const requireFromHere$1 = createRequire(import.meta.url);
32
+ const GENERATED_WRAPPER_BASENAMES = /* @__PURE__ */ new Set(["codex-acp-wrapper.mjs", "claude-agent-acp-wrapper.mjs"]);
33
+ const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
34
+ const ACPX_PROCESS_LIST_TIMEOUT_MS = 2e3;
35
+ const OWNED_ACP_PACKAGE_NAMES = [
36
+ CODEX_ACP_PACKAGE,
37
+ LEGACY_CODEX_ACP_PACKAGE,
38
+ "@zed-industries/codex-acp-darwin-arm64",
39
+ "@zed-industries/codex-acp-darwin-x64",
40
+ "@zed-industries/codex-acp-linux-arm64",
41
+ "@zed-industries/codex-acp-linux-x64",
42
+ "@zed-industries/codex-acp-win32-arm64",
43
+ "@zed-industries/codex-acp-win32-x64",
44
+ "@agentclientprotocol/claude-agent-acp",
45
+ "acpx"
46
+ ];
47
+ const PLUGIN_DEPS_CODEX_PACKAGE_NAMES = [
48
+ "@openai/codex",
49
+ "@openai/codex-darwin-arm64",
50
+ "@openai/codex-darwin-x64",
51
+ "@openai/codex-linux-arm64",
52
+ "@openai/codex-linux-x64",
53
+ "@openai/codex-win32-arm64",
54
+ "@openai/codex-win32-x64"
55
+ ];
56
+ const ACP_PACKAGE_MARKERS = [
57
+ ...OWNED_ACP_PACKAGE_NAMES.map((packageName) => `/node_modules/${packageName}/`),
58
+ ...PLUGIN_DEPS_CODEX_PACKAGE_NAMES.map((packageName) => `/node_modules/${packageName}/`),
59
+ "/acpx/dist/"
60
+ ];
61
+ function normalizePathLike(value) {
62
+ return value.replaceAll("\\", "/");
63
+ }
64
+ function resolvePackageRoot(packageName) {
65
+ try {
66
+ return normalizePathLike(path.dirname(requireFromHere$1.resolve(`${packageName}/package.json`)));
67
+ } catch {
68
+ return;
69
+ }
70
+ }
71
+ function resolveOpenClawInstallRoot(pluginRoot) {
72
+ if (path.basename(pluginRoot) === "acpx" && path.basename(path.dirname(pluginRoot)) === "extensions") {
73
+ const parent = path.dirname(path.dirname(pluginRoot));
74
+ return path.basename(parent) === "dist" ? path.dirname(parent) : parent;
75
+ }
76
+ return path.resolve(pluginRoot, "..");
77
+ }
78
+ function resolveOwnedAcpPackageRootCandidates(packageName) {
79
+ const pluginRoot = resolveAcpxPluginRoot(import.meta.url);
80
+ const openClawRoot = resolveOpenClawInstallRoot(pluginRoot);
81
+ return [
82
+ resolvePackageRoot(packageName),
83
+ path.join(pluginRoot, "node_modules", packageName),
84
+ path.join(openClawRoot, "node_modules", packageName)
85
+ ].flatMap((root) => root ? [normalizePathLike(root)] : []);
86
+ }
87
+ const OWNED_ACP_PACKAGE_ROOTS = Array.from(new Set(OWNED_ACP_PACKAGE_NAMES.flatMap(resolveOwnedAcpPackageRootCandidates)));
88
+ function commandBelongsToResolvedAcpPackage(command) {
89
+ return OWNED_ACP_PACKAGE_ROOTS.some((root) => command.includes(`${root}/`));
90
+ }
91
+ function commandMentionsGeneratedWrapper(command) {
92
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
93
+ }
94
+ function commandWrapperBelongsToRoot(command, wrapperRoot) {
95
+ if (!wrapperRoot) return true;
96
+ const normalizedCommand = normalizePathLike(command);
97
+ const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
98
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => normalizedCommand.includes(`${normalizedRoot}/${basename}`));
99
+ }
100
+ function commandContainsExactWrapperPath(command, wrapperPath) {
101
+ const expectedPath = normalizePathLike(wrapperPath);
102
+ return splitCommandParts(command).some((part) => normalizePathLike(part) === expectedPath);
103
+ }
104
+ function wrapperPathBelongsToRoot(wrapperPath, wrapperRoot) {
105
+ const normalizedPath = normalizePathLike(wrapperPath);
106
+ const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
107
+ return GENERATED_WRAPPER_BASENAMES.has(path.posix.basename(normalizedPath)) && normalizedPath.startsWith(`${normalizedRoot}/`);
108
+ }
109
+ /** Check whether a command references an OpenClaw-generated ACPX wrapper path. */
110
+ function isOpenClawLeaseAwareAcpxProcessCommand(params) {
111
+ const command = params.command?.trim();
112
+ if (!command) return false;
113
+ const normalized = normalizePathLike(command);
114
+ return commandMentionsGeneratedWrapper(normalized) && commandWrapperBelongsToRoot(normalized, params.wrapperRoot);
115
+ }
116
+ function commandsReferToSameRootCommand(liveCommand, storedCommand) {
117
+ if (!storedCommand?.trim()) return true;
118
+ return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
119
+ }
120
+ function commandOptionEquals(parts, option, expected) {
121
+ if (!expected) return true;
122
+ const index = parts.indexOf(option);
123
+ return index >= 0 && parts[index + 1] === expected;
124
+ }
125
+ function liveCommandMatchesLeaseIdentity(params) {
126
+ if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) return true;
127
+ const parts = splitCommandParts(params.command ?? "");
128
+ return commandOptionEquals(parts, "--openclaw-acpx-lease-id", params.expectedLeaseId) && commandOptionEquals(parts, "--openclaw-gateway-instance-id", params.expectedGatewayInstanceId);
129
+ }
130
+ /** Check whether a command is owned by OpenClaw ACPX runtime packages or wrappers. */
131
+ function isOpenClawOwnedAcpxProcessCommand(params) {
132
+ const command = params.command?.trim();
133
+ if (!command) return false;
134
+ const normalized = normalizePathLike(command);
135
+ if (isOpenClawLeaseAwareAcpxProcessCommand({
136
+ command: normalized,
137
+ wrapperRoot: params.wrapperRoot
138
+ })) return true;
139
+ if (commandBelongsToResolvedAcpPackage(normalized)) return true;
140
+ if (!normalized.includes(OPENCLAW_PLUGIN_DEPS_MARKER)) return false;
141
+ return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
142
+ }
143
+ function parseProcessList(stdout) {
144
+ const processes = [];
145
+ for (const line of stdout.split(/\r?\n/)) {
146
+ const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
147
+ const pid = match?.groups?.pid;
148
+ const ppid = match?.groups?.ppid;
149
+ const command = match?.groups?.command;
150
+ if (!pid || !ppid || !command) continue;
151
+ processes.push({
152
+ pid: Number.parseInt(pid, 10),
153
+ ppid: Number.parseInt(ppid, 10),
154
+ command
155
+ });
156
+ }
157
+ return processes;
158
+ }
159
+ /** List host processes in the compact shape needed by ACPX cleanup. */
160
+ async function listPlatformProcesses() {
161
+ if (process.platform === "win32") return [];
162
+ const { stdout } = await runExec("ps", ["-axo", "pid=,ppid=,command="], {
163
+ logOutput: false,
164
+ maxBuffer: 8388608,
165
+ timeoutMs: ACPX_PROCESS_LIST_TIMEOUT_MS
166
+ });
167
+ return parseProcessList(stdout);
168
+ }
169
+ function collectProcessTree(processes, rootPid) {
170
+ const childrenByParent = /* @__PURE__ */ new Map();
171
+ for (const processInfo of processes) {
172
+ const children = childrenByParent.get(processInfo.ppid) ?? [];
173
+ children.push(processInfo);
174
+ childrenByParent.set(processInfo.ppid, children);
175
+ }
176
+ const root = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])).get(rootPid);
177
+ const collected = [];
178
+ if (root) collected.push(root);
179
+ const queue = [...childrenByParent.get(rootPid) ?? []];
180
+ while (queue.length > 0) {
181
+ const next = queue.shift();
182
+ if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) continue;
183
+ collected.push(next);
184
+ queue.push(...childrenByParent.get(next.pid) ?? []);
185
+ }
186
+ return collected;
187
+ }
188
+ function uniquePids(processes) {
189
+ return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
190
+ }
191
+ async function terminatePids(pids, deps) {
192
+ const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
193
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => {
194
+ setTimeout(resolve, ms);
195
+ }));
196
+ const terminated = [];
197
+ for (const pid of pids) try {
198
+ killProcess(pid, "SIGTERM");
199
+ terminated.push(pid);
200
+ } catch {}
201
+ if (terminated.length === 0) return terminated;
202
+ await sleep(750);
203
+ for (const pid of terminated) if (deps?.killProcess || isPidAlive(pid)) try {
204
+ killProcess(pid, "SIGKILL");
205
+ } catch {}
206
+ return terminated;
207
+ }
208
+ /** Terminate one validated OpenClaw-owned ACPX wrapper process tree. */
209
+ async function cleanupOpenClawOwnedAcpxProcessTree(params) {
210
+ const rootPid = params.rootPid;
211
+ if (!rootPid || rootPid <= 0 || rootPid === process.pid) return {
212
+ inspectedPids: [],
213
+ terminatedPids: [],
214
+ skippedReason: "missing-root"
215
+ };
216
+ if ((params.deps?.platform ?? process.platform) === "win32") return {
217
+ inspectedPids: [],
218
+ terminatedPids: [],
219
+ skippedReason: "unsupported-platform"
220
+ };
221
+ let processes;
222
+ try {
223
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
224
+ } catch {
225
+ return {
226
+ inspectedPids: [],
227
+ terminatedPids: [],
228
+ skippedReason: "process-list-unavailable"
229
+ };
230
+ }
231
+ const listedTree = collectProcessTree(processes, rootPid);
232
+ if (listedTree.length === 0) return {
233
+ inspectedPids: [],
234
+ terminatedPids: [],
235
+ skippedReason: "unverified-root"
236
+ };
237
+ const rootCommand = listedTree[0]?.command ?? params.rootCommand;
238
+ const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(rootCommand ?? ""));
239
+ const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(params.rootCommand ?? ""));
240
+ if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) return {
241
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
242
+ terminatedPids: [],
243
+ skippedReason: "not-openclaw-owned"
244
+ };
245
+ if (!liveCommandWasGeneratedWrapper && !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)) return {
246
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
247
+ terminatedPids: [],
248
+ skippedReason: "not-openclaw-owned"
249
+ };
250
+ if (!isOpenClawOwnedAcpxProcessCommand({
251
+ command: rootCommand,
252
+ wrapperRoot: params.wrapperRoot
253
+ })) return {
254
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
255
+ terminatedPids: [],
256
+ skippedReason: "not-openclaw-owned"
257
+ };
258
+ if (!liveCommandMatchesLeaseIdentity({
259
+ command: rootCommand,
260
+ expectedLeaseId: params.expectedLeaseId,
261
+ expectedGatewayInstanceId: params.expectedGatewayInstanceId
262
+ })) return {
263
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
264
+ terminatedPids: [],
265
+ skippedReason: "not-openclaw-owned"
266
+ };
267
+ const pids = uniquePids(listedTree.toReversed());
268
+ return {
269
+ inspectedPids: uniquePids(listedTree),
270
+ terminatedPids: await terminatePids(pids, params.deps)
271
+ };
272
+ }
273
+ /** Recover a pending lease by matching its exact live wrapper identity. */
274
+ async function cleanupOpenClawOwnedAcpxPendingLease(params) {
275
+ if ((params.deps?.platform ?? process.platform) === "win32") return {
276
+ inspectedPids: [],
277
+ terminatedPids: [],
278
+ skippedReason: "unsupported-platform"
279
+ };
280
+ if (!params.wrapperPath || !wrapperPathBelongsToRoot(params.wrapperPath, params.wrapperRoot)) return {
281
+ inspectedPids: [],
282
+ terminatedPids: [],
283
+ skippedReason: "unverified-root"
284
+ };
285
+ let processes;
286
+ try {
287
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
288
+ } catch {
289
+ return {
290
+ inspectedPids: [],
291
+ terminatedPids: [],
292
+ skippedReason: "process-list-unavailable"
293
+ };
294
+ }
295
+ const matchingRoots = processes.filter((processInfo) => commandContainsExactWrapperPath(processInfo.command, params.wrapperPath) && liveCommandMatchesLeaseIdentity({
296
+ command: processInfo.command,
297
+ expectedLeaseId: params.leaseId,
298
+ expectedGatewayInstanceId: params.gatewayInstanceId
299
+ }));
300
+ if (matchingRoots.length === 0) return {
301
+ inspectedPids: [],
302
+ terminatedPids: [],
303
+ skippedReason: "missing-root"
304
+ };
305
+ if (matchingRoots.length > 1) return {
306
+ inspectedPids: uniquePids(matchingRoots),
307
+ terminatedPids: [],
308
+ skippedReason: "ambiguous-root"
309
+ };
310
+ const listedTree = collectProcessTree(processes, matchingRoots[0].pid);
311
+ const pids = uniquePids(listedTree.toReversed());
312
+ return {
313
+ inspectedPids: uniquePids(listedTree),
314
+ terminatedPids: await terminatePids(pids, params.deps)
315
+ };
316
+ }
317
+ /** Reap orphaned OpenClaw-owned ACPX wrapper trees during runtime startup. */
318
+ async function reapStaleOpenClawOwnedAcpxOrphans(params) {
319
+ if ((params.deps?.platform ?? process.platform) === "win32") return {
320
+ inspectedPids: [],
321
+ terminatedPids: [],
322
+ skippedReason: "unsupported-platform"
323
+ };
324
+ let processes;
325
+ try {
326
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
327
+ } catch {
328
+ return {
329
+ inspectedPids: [],
330
+ terminatedPids: [],
331
+ skippedReason: "process-list-unavailable"
332
+ };
333
+ }
334
+ const orphanTrees = processes.filter((processInfo) => processInfo.ppid === 1 && !readAcpxProcessLeaseIdentity(processInfo.command) && isOpenClawOwnedAcpxProcessCommand({
335
+ command: processInfo.command,
336
+ wrapperRoot: params.wrapperRoot
337
+ })).map((orphan) => collectProcessTree(processes, orphan.pid));
338
+ return {
339
+ inspectedPids: uniquePids(orphanTrees.flat()),
340
+ terminatedPids: await terminatePids(uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())), params.deps)
341
+ };
342
+ }
343
+ //#endregion
18
344
  //#region extensions/acpx/src/codex-trust-config.ts
19
345
  /**
20
346
  * Builds isolated Codex config for ACPX sessions. It preserves safe inherited
@@ -1019,9 +1345,13 @@ async function prepareAcpxCodexAuthConfig(params) {
1019
1345
  * ACPX plugin service lifecycle. It resolves config, prepares isolated adapter
1020
1346
  * wrappers, registers the ACP backend, and manages startup/cleanup probes.
1021
1347
  */
1348
+ var service_exports = /* @__PURE__ */ __exportAll({
1349
+ createAcpxRuntimeService: () => createAcpxRuntimeService,
1350
+ resolveAcpxTimerTimeoutMs: () => resolveAcpxTimerTimeoutMs
1351
+ });
1022
1352
  const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
1023
1353
  const SKIP_RUNTIME_PROBE_ENV = "OPENCLAW_SKIP_ACPX_RUNTIME_PROBE";
1024
- const loadRuntimeModule = createLazyRuntimeModule(() => import("./runtime-BmOxa15I.js"));
1354
+ const loadRuntimeModule = createLazyRuntimeModule(() => import("./runtime-memQfUBo.js"));
1025
1355
  /** Convert ACPX timeout seconds into timer-safe milliseconds. */
1026
1356
  function resolveAcpxTimerTimeoutMs(timeoutSeconds) {
1027
1357
  if (timeoutSeconds === void 0) return;
@@ -1032,9 +1362,20 @@ function createLazyDefaultRuntime(params) {
1032
1362
  let runtimePromise = null;
1033
1363
  async function resolveRuntime() {
1034
1364
  if (runtime) return runtime;
1035
- runtimePromise ??= loadRuntimeModule().then((module) => {
1365
+ runtimePromise ??= loadRuntimeModule().then(async (module) => {
1366
+ const names = await fs$1.readdir(path.join(params.pluginConfig.stateDir, "sessions")).catch((error) => {
1367
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
1368
+ throw error;
1369
+ });
1370
+ const legacyBareSessionKeys = /* @__PURE__ */ new Set();
1371
+ for (const name of names) {
1372
+ if (!name.endsWith(".json")) continue;
1373
+ const recordId = decodeURIComponent(name.slice(0, -5));
1374
+ if (!recordId.startsWith("agent:") && !recordId.startsWith(".openclaw-owner-") && !recordId.includes(":oneshot:")) legacyBareSessionKeys.add(recordId.toLowerCase());
1375
+ }
1036
1376
  runtime = new module.AcpxRuntime({
1037
1377
  cwd: params.pluginConfig.cwd,
1378
+ openclawLegacyBareSessionKeys: legacyBareSessionKeys,
1038
1379
  openclawGatewayInstanceId: params.gatewayInstanceId,
1039
1380
  openclawProcessLeaseStore: params.processLeaseStore,
1040
1381
  openclawWrapperRoot: params.wrapperRoot,
@@ -1279,4 +1620,4 @@ function createAcpxRuntimeService(params) {
1279
1620
  };
1280
1621
  }
1281
1622
  //#endregion
1282
- export { createAcpxRuntimeService, resolveAcpxTimerTimeoutMs };
1623
+ export { CODEX_ACP_PACKAGE as a, createAcpxRuntimeService, isOpenClawLeaseAwareAcpxProcessCommand as i, cleanupOpenClawOwnedAcpxPendingLease as n, OPENCLAW_CODEX_CONFIG_ARG as o, cleanupOpenClawOwnedAcpxProcessTree as r, resolveAcpxTimerTimeoutMs, service_exports as t };
@@ -0,0 +1,200 @@
1
+ import { l as openAcpxProcessLeaseStateStore, o as hashAcpxProcessCommand, s as normalizeAcpxProcessLease, u as readAcpxProcessLeaseIdentity } from "./process-lease-Cwvj7WGe.js";
2
+ import { t as resolveAcpxPluginConfig } from "./config-v8M2tNu6.js";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+ import { archiveLegacyStateSource, asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor-migrations";
6
+ import { randomUUID } from "node:crypto";
7
+ import { isDeepStrictEqual } from "node:util";
8
+ //#region extensions/acpx/src/session-owner-migration.ts
9
+ function sessionDirectory(input) {
10
+ if (!input.serviceWorkspaceDir) throw new Error("ACP ownership repair requires the Gateway service workspace; upgrade OpenClaw Doctor.");
11
+ return path.join(resolveAcpxPluginConfig({
12
+ rawConfig: input.config.plugins?.entries?.acpx?.config,
13
+ workspaceDir: input.serviceWorkspaceDir
14
+ }).stateDir, "sessions");
15
+ }
16
+ async function legacyRecords(input) {
17
+ const directory = sessionDirectory(input);
18
+ const ids = (await fs.readdir(directory).catch((error) => {
19
+ if (asObjectRecord(error)?.code === "ENOENT") return [];
20
+ throw error;
21
+ })).filter((name) => name.endsWith(".json")).map((name) => decodeURIComponent(name.slice(0, -5))).filter((id) => !id.startsWith("agent:") && !id.startsWith(".openclaw-owner-"));
22
+ if (ids.length === 0) return {
23
+ directory,
24
+ ids
25
+ };
26
+ const { resolveAcpxSessionResource } = await import("./session-resource-UWe7qD3m.js").then((n) => n.n);
27
+ const evidence = await input.context.inspectAcpSessionClaims?.();
28
+ const { decodeAcpxRuntimeHandleState } = await import("acpx/runtime");
29
+ return {
30
+ directory,
31
+ ids: ids.filter((id) => !evidence?.claims.some((claim) => {
32
+ const locator = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
33
+ return evidence.incomplete.length === 0 && claim.meta.identity?.state === "resolved" && claim.meta.identity.acpxRecordId === id && locator?.acpxRecordId === id && locator.name === resolveAcpxSessionResource(claim);
34
+ })).filter((id) => !id.includes(":oneshot:") || evidence?.claims.some((claim) => {
35
+ const locator = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
36
+ return claim.meta.identity?.acpxRecordId === id && locator?.name !== resolveAcpxSessionResource(claim);
37
+ })).toSorted()
38
+ };
39
+ }
40
+ function requireStoppedPid(pid) {
41
+ if (pid === void 0 || pid === null) return;
42
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) throw new Error("record process identity is uncertain");
43
+ try {
44
+ process.kill(pid, 0);
45
+ } catch (error) {
46
+ if (asObjectRecord(error)?.code === "ESRCH") return;
47
+ throw new Error("record process liveness cannot be verified", { cause: error });
48
+ }
49
+ throw new Error("record still has a live process; stop the harness before Doctor repair");
50
+ }
51
+ function recordPath(directory, recordId) {
52
+ return path.join(directory, `${encodeURIComponent(recordId)}.json`);
53
+ }
54
+ function matchesClaimRecord(claim, raw, oldId, resource, decode) {
55
+ const state = decode(claim.meta.runtimeSessionName);
56
+ const identity = claim.meta.identity;
57
+ if (!state || !identity || identity.state !== "resolved" || !identity.acpxSessionId && !identity.agentSessionId || state.mode !== claim.meta.mode) return false;
58
+ const recordId = claim.meta.mode === "oneshot" ? oldId : resource;
59
+ const oldLocator = state.name === raw.name && identity.acpxRecordId === oldId && state.acpxRecordId === oldId;
60
+ const newLocator = state.name === resource && identity.acpxRecordId === recordId && state.acpxRecordId === recordId;
61
+ return (oldLocator || newLocator) && state.agent === claim.meta.agent && (!identity.acpxSessionId || identity.acpxSessionId === raw.acp_session_id) && (!identity.agentSessionId || identity.agentSessionId === raw.agent_session_id) && (!state.backendSessionId || state.backendSessionId === raw.acp_session_id) && (!state.agentSessionId || state.agentSessionId === raw.agent_session_id);
62
+ }
63
+ async function migrateRecord(input, directory, oldId, claims, changes, warnings) {
64
+ const sourcePath = recordPath(directory, oldId);
65
+ const sourceBytes = await fs.readFile(sourcePath, "utf8");
66
+ const raw = asObjectRecord(JSON.parse(sourceBytes));
67
+ if (!raw || raw.acpx_record_id !== oldId || typeof raw.name !== "string" || !raw.name.trim() || raw.name !== oldId && !oldId.startsWith(`${raw.name}:oneshot:`)) throw new Error("record ID/name is not a recognized ACPX locator");
68
+ requireStoppedPid(raw.pid);
69
+ const { resolveAcpxSessionResource } = await import("./session-resource-UWe7qD3m.js").then((n) => n.n);
70
+ const { createFileSessionStore, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState } = await import("acpx/runtime");
71
+ const candidates = claims.filter((claim) => matchesClaimRecord(claim, raw, oldId, resolveAcpxSessionResource(claim), decodeAcpxRuntimeHandleState));
72
+ if (candidates.length !== 1) throw new Error("exactly one current canonical owner claim is required");
73
+ const claim = candidates[0];
74
+ const resource = resolveAcpxSessionResource(claim);
75
+ const oneshot = claim.meta.mode === "oneshot";
76
+ if (oneshot !== (raw.name !== oldId)) throw new Error("canonical claim and backend record mode disagree");
77
+ if (!oneshot && oldId === resource) return;
78
+ const recordId = oneshot ? oldId : resource;
79
+ const leaseStore = openAcpxProcessLeaseStateStore(input.context.openPluginStateKeyedStore);
80
+ const leases = (await leaseStore.entries()).map((row) => ({
81
+ row,
82
+ lease: normalizeAcpxProcessLease(row.value)
83
+ }));
84
+ if (leases.some(({ lease }) => !lease)) throw new Error("process lease evidence is incomplete");
85
+ const commandIdentity = typeof raw.agent_command === "string" ? readAcpxProcessLeaseIdentity(raw.agent_command) : void 0;
86
+ const matchingLeases = leases.filter(({ lease }) => lease.sessionKey === raw.name || lease.sessionKey === resource);
87
+ for (const { lease } of matchingLeases) {
88
+ if (lease.state === "open" || lease.state === "closing") throw new Error("record has a live or uncertain process lease");
89
+ requireStoppedPid(lease.rootPid || void 0);
90
+ if (!commandIdentity || lease.leaseId !== commandIdentity.leaseId || lease.gatewayInstanceId !== commandIdentity.gatewayInstanceId || lease.commandHash !== hashAcpxProcessCommand(String(raw.agent_command))) throw new Error("record lease association does not match its persisted command");
91
+ }
92
+ const destinationPath = recordPath(directory, recordId);
93
+ const candidate = oneshot ? raw : {
94
+ ...raw,
95
+ acpx_record_id: resource,
96
+ name: resource
97
+ };
98
+ const candidateBytes = `${JSON.stringify(candidate, null, 2)}\n`;
99
+ const store = createFileSessionStore({ stateDir: path.dirname(directory) });
100
+ const source = await store.load(oldId);
101
+ if (!source) throw new Error("pinned ACPX reader rejected the source record");
102
+ const temporaryId = `.openclaw-owner-${randomUUID()}`;
103
+ const temporaryPath = recordPath(directory, temporaryId);
104
+ const file = await fs.open(temporaryPath, "wx", 384);
105
+ try {
106
+ try {
107
+ await file.writeFile(candidateBytes);
108
+ await file.sync();
109
+ } finally {
110
+ await file.close();
111
+ }
112
+ const interpreted = await store.load(temporaryId);
113
+ if (!interpreted || !isDeepStrictEqual(interpreted, oneshot ? source : {
114
+ ...source,
115
+ acpxRecordId: resource,
116
+ name: resource
117
+ })) throw new Error("rekey would alter interpreted history/event references; source retained");
118
+ const existing = await fs.readFile(destinationPath, "utf8").catch((error) => {
119
+ if (asObjectRecord(error)?.code === "ENOENT") return;
120
+ throw error;
121
+ });
122
+ const originalLocator = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
123
+ if (!oneshot && originalLocator.name === resource && existing === void 0) throw new Error("migrated metadata has no published destination; source retained");
124
+ if (existing !== void 0 && !isDeepStrictEqual(JSON.parse(existing), candidate)) throw new Error("destination conflicts; no files were overwritten");
125
+ if (existing === void 0) await fs.link(temporaryPath, destinationPath);
126
+ const publicationDirectory = await fs.open(directory, "r");
127
+ try {
128
+ await publicationDirectory.sync();
129
+ } finally {
130
+ await publicationDirectory.close();
131
+ }
132
+ if (await fs.readFile(sourcePath, "utf8") !== sourceBytes) throw new Error("source changed during repair");
133
+ if (!isDeepStrictEqual(JSON.parse(await fs.readFile(destinationPath, "utf8")), candidate)) throw new Error("destination changed during repair");
134
+ for (const { row } of matchingLeases) {
135
+ if (!isDeepStrictEqual(await leaseStore.lookup(row.key), row.value)) throw new Error("lease changed during repair");
136
+ await leaseStore.register(row.key, {
137
+ ...row.value,
138
+ sessionKey: resource
139
+ });
140
+ }
141
+ const state = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
142
+ input.context.updateAcpSessionIdentity({
143
+ claim,
144
+ runtimeSessionName: encodeAcpxRuntimeHandleState({
145
+ ...state,
146
+ name: resource,
147
+ acpxRecordId: recordId
148
+ }),
149
+ acpxRecordId: recordId
150
+ });
151
+ const verified = await input.context.inspectAcpSessionClaims();
152
+ if (verified.incomplete.length || !verified.claims.some((item) => item.agentId === claim.agentId && item.sessionKey === claim.sessionKey && item.meta.identity?.acpxRecordId === recordId && isDeepStrictEqual(item.binding, claim.binding))) throw new Error("canonical metadata verification failed; source retained for rerun");
153
+ changes.push(`Migrated ACP backend history for ${claim.agentId}/${claim.sessionKey} to its owner-qualified resource.`);
154
+ if (!oneshot) await archiveLegacyStateSource({
155
+ filePath: sourcePath,
156
+ label: "ACP owner record",
157
+ changes,
158
+ warnings
159
+ });
160
+ } finally {
161
+ await fs.rm(temporaryPath, { force: true });
162
+ }
163
+ }
164
+ const acpxSessionOwnerMigration = {
165
+ id: "acpx-session-owner-resources",
166
+ label: "ACP session owners",
167
+ doctorOnly: true,
168
+ phase: "after-session-repair",
169
+ async detectLegacyState(input) {
170
+ const { ids } = await legacyRecords(input);
171
+ return ids.length ? { preview: [`ACP backend has ${ids.length} unqualified record(s). Stop the Gateway and run openclaw doctor --fix; ambiguous histories remain intact.`] } : null;
172
+ },
173
+ async migrateLegacyState(input) {
174
+ const changes = [];
175
+ const warnings = [];
176
+ if (!input.context.inspectAcpSessionClaims || !input.context.updateAcpSessionIdentity) return {
177
+ changes,
178
+ warnings: ["ACP owner repair requires current offline Doctor maintenance authority."]
179
+ };
180
+ const { directory, ids } = await legacyRecords(input);
181
+ const evidence = await input.context.inspectAcpSessionClaims();
182
+ if (evidence.incomplete.length) return {
183
+ changes,
184
+ warnings: [`ACP owner evidence is incomplete; all records retained: ${evidence.incomplete.join("; ")}`]
185
+ };
186
+ for (const oldId of ids) try {
187
+ const current = await input.context.inspectAcpSessionClaims();
188
+ if (current.incomplete.length) throw new Error("canonical ownership evidence became incomplete");
189
+ await migrateRecord(input, directory, oldId, current.claims, changes, warnings);
190
+ } catch (error) {
191
+ warnings.push(`ACP record ${oldId} retained: ${String(error)}`);
192
+ }
193
+ return {
194
+ changes,
195
+ warnings
196
+ };
197
+ }
198
+ };
199
+ //#endregion
200
+ export { acpxSessionOwnerMigration };
@@ -0,0 +1,17 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { AcpRuntimeError } from "./runtime-api.js";
3
+ import { createHash } from "node:crypto";
4
+ import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
5
+ //#region extensions/acpx/src/session-resource.ts
6
+ var session_resource_exports = /* @__PURE__ */ __exportAll({ resolveAcpxSessionResource: () => resolveAcpxSessionResource });
7
+ /** Logical OpenClaw keys stay intact; only bare backend resource names need a namespace. */
8
+ function resolveAcpxSessionResource(target) {
9
+ const sessionKey = target.sessionKey.trim().toLowerCase();
10
+ const encodedOwner = parseAgentSessionKey(sessionKey)?.agentId;
11
+ const agentId = target.agentId?.trim() ? normalizeAgentId(target.agentId) : encodedOwner;
12
+ if (!sessionKey || encodedOwner && agentId !== encodedOwner || !encodedOwner && !agentId) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session owner is missing or disagrees with its logical key. Pass the OpenClaw agentId that owns this session.", { detailCode: "SESSION_OWNER_UNSUPPORTED" });
13
+ if (encodedOwner) return sessionKey;
14
+ return `openclaw-owner-v1-${createHash("sha256").update(JSON.stringify([agentId, sessionKey])).digest("hex")}`;
15
+ }
16
+ //#endregion
17
+ export { session_resource_exports as n, resolveAcpxSessionResource as t };
@@ -1,5 +1,17 @@
1
1
  {
2
2
  "id": "acpx",
3
+ "backupResources": [
4
+ {
5
+ "disposition": "regenerable",
6
+ "scope": "state",
7
+ "relativePath": "acpx/codex-home/tmp/arg0"
8
+ },
9
+ {
10
+ "disposition": "regenerable",
11
+ "scope": "state",
12
+ "relativePath": "acpx/codex-home/.tmp/plugins"
13
+ }
14
+ ],
3
15
  "doctorContract": {
4
16
  "configRepair": true,
5
17
  "stateMigrations": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/acpx",
3
- "version": "2026.9.1-beta.1",
3
+ "version": "2026.9.2",
4
4
  "description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  "type": "module",
10
10
  "dependencies": {
11
11
  "@agentclientprotocol/claude-agent-acp": "0.70.0",
12
- "@agentclientprotocol/codex-acp": "1.6.0",
12
+ "@agentclientprotocol/codex-acp": "1.6.2",
13
13
  "acpx": "0.13.1",
14
14
  "smol-toml": "1.8.0",
15
15
  "zod": "4.4.3"
@@ -43,10 +43,10 @@
43
43
  ]
44
44
  },
45
45
  "compat": {
46
- "pluginApi": ">=2026.9.1-beta.1"
46
+ "pluginApi": ">=2026.9.2"
47
47
  },
48
48
  "build": {
49
- "openclawVersion": "2026.9.1-beta.1",
49
+ "openclawVersion": "2026.9.2",
50
50
  "staticAssets": [
51
51
  {
52
52
  "source": "./src/runtime-internals/mcp-proxy.mjs",
@@ -74,7 +74,7 @@
74
74
  "skills/**"
75
75
  ],
76
76
  "peerDependencies": {
77
- "openclaw": ">=2026.9.1-beta.1"
77
+ "openclaw": ">=2026.9.2"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "openclaw": {
@@ -210,7 +210,7 @@ Defaults are:
210
210
 
211
211
  - `openclaw -> openclaw acp`
212
212
  - `claude -> bundled @agentclientprotocol/claude-agent-acp@0.70.0`
213
- - `codex -> bundled @agentclientprotocol/codex-acp@1.6.0 through OpenClaw's isolated CODEX_HOME wrapper`
213
+ - `codex -> bundled @agentclientprotocol/codex-acp@1.6.2 through OpenClaw's isolated CODEX_HOME wrapper`
214
214
  - `copilot -> copilot --acp --stdio`
215
215
  - `cursor -> cursor-agent acp`
216
216
  - `droid -> droid exec --output-format acp`