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