@openclaw/acpx 2026.9.7 → 2026.10.1-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.
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- import { t as createAcpxRuntimeService } from "./.setup/register.runtime-Bm4HrMCp.mjs";
2
- import { a as ACPX_NATIVE_AGENT_IDS, o as AcpxNativeAgentsSchema, t as resolveAcpxPluginConfig } from "./.setup/config-CdPsIued.mjs";
1
+ import { t as createAcpxRuntimeService } from "./.setup/register.runtime-C0nVOTzT.mjs";
2
+ import { g as AcpxNativeAgentsSchema, h as ACPX_NATIVE_AGENT_IDS, t as resolveAcpxPluginConfig } from "./.setup/config-76krYh6C.mjs";
3
3
  import { a as PI_SESSIONS_CAPABILITY, c as PI_SESSION_READ_COMMAND, l as PI_TERMINAL_RESUME_COMMAND, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN } from "./.setup/pi-session-paths-CIvyk6KB.mjs";
4
4
  import { createAgentRegistry } from "acpx/agent-registry";
5
5
  import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
6
6
  import { createLazyRuntimeModule, createLazyRuntimeSurface } from "openclaw/plugin-sdk/lazy-runtime";
7
7
  import { inspectAgentModels } from "acpx/runtime";
8
+ import { createNativeSessionCommitFinalizer } from "openclaw/plugin-sdk/agent-harness-session-runtime";
8
9
  import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
9
10
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
10
11
  import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
@@ -157,26 +158,47 @@ function createAcpAgentHarness(params) {
157
158
  },
158
159
  async loadModelCatalog(input) {
159
160
  generation.signal.throwIfAborted();
160
- if (!params.isEnabled()) return [];
161
+ if (!params.isEnabled()) return { entries: [] };
161
162
  const inspection = inspectAgent();
162
- if (inspection?.launch.kind !== "installed") return [];
163
+ if (inspection?.launch.kind !== "installed") return { entries: [] };
163
164
  const config = resolveAcpxPluginConfig({
164
165
  rawConfig: params.api.pluginConfig,
165
166
  workspaceDir: input.workspaceDir
166
167
  });
167
- const models = await inspectAgentModels({
168
- agentCommand: inspection.launch.argv,
169
- cwd: input.workspaceDir ?? config.cwd,
170
- signal: generation.signal,
171
- timeoutMs: config.timeoutSeconds === void 0 ? void 0 : finiteSecondsToTimerSafeMilliseconds(config.timeoutSeconds) ?? 1
172
- });
173
- generation.signal.throwIfAborted();
174
- return (params.isEnabled() ? models?.availableModels ?? [] : []).map((model) => ({
175
- provider: id,
176
- id: model.modelId,
177
- name: model.name,
178
- nativeRuntime: id
179
- }));
168
+ try {
169
+ const models = await inspectAgentModels({
170
+ agentCommand: inspection.launch.argv,
171
+ cwd: input.workspaceDir ?? config.cwd,
172
+ signal: generation.signal,
173
+ timeoutMs: config.timeoutSeconds === void 0 ? void 0 : finiteSecondsToTimerSafeMilliseconds(config.timeoutSeconds) ?? 1
174
+ });
175
+ generation.signal.throwIfAborted();
176
+ if (!params.isEnabled()) return { entries: [] };
177
+ return {
178
+ entries: (models?.availableModels ?? []).map((model) => ({
179
+ provider: id,
180
+ id: model.modelId,
181
+ name: model.name,
182
+ nativeRuntime: id
183
+ })),
184
+ outcomes: [{
185
+ provider: id,
186
+ status: "ready"
187
+ }]
188
+ };
189
+ } catch (error) {
190
+ generation.signal.throwIfAborted();
191
+ if (!params.isEnabled()) return { entries: [] };
192
+ const authRequired = error instanceof Error && error.name === "RequestError" && "code" in error && error.code === -32e3 && (error.message === "Authentication required" || error.message.startsWith("Authentication required: "));
193
+ return {
194
+ entries: [],
195
+ outcomes: [{
196
+ provider: id,
197
+ status: authRequired ? "auth-rejected" : "unavailable",
198
+ rejectionScope: "catalog"
199
+ }]
200
+ };
201
+ }
180
202
  },
181
203
  async runAttempt(input) {
182
204
  generation.signal.throwIfAborted();
@@ -206,14 +228,14 @@ function createAcpAgentHarness(params) {
206
228
  async withSessionDeletion(input, run) {
207
229
  let committed = false;
208
230
  try {
209
- return await run({
231
+ return await run(createNativeSessionCommitFinalizer({
210
232
  commit: () => {
211
233
  committed = true;
212
234
  },
213
235
  rollback: () => {
214
236
  committed = false;
215
237
  }
216
- });
238
+ }));
217
239
  } finally {
218
240
  if (committed) await retire(input, input.assertCurrent);
219
241
  }
@@ -226,12 +248,11 @@ function createAcpAgentHarness(params) {
226
248
  }
227
249
  //#endregion
228
250
  //#region extensions/acpx/src/pi-session-catalog-plugin.ts
229
- const loadPiSessionCatalogModule = createLazyRuntimeModule(() => import("./.setup/pi-session-catalog-runtime-UhiQSaDw.mjs"));
251
+ const loadPiSessionCatalogModule = createLazyRuntimeModule(() => import("./.setup/pi-session-catalog-runtime-7jc50ZJ4.mjs"));
230
252
  function fullConfigCatalogEnabled(config) {
231
253
  if (!isRecord(config) || !isRecord(config.plugins) || !isRecord(config.plugins.entries)) return true;
232
254
  const entry = config.plugins.entries.acpx;
233
- if (!isRecord(entry) || !isRecord(entry.config) || !isRecord(entry.config.piSessionCatalog)) return true;
234
- return entry.config.piSessionCatalog.enabled !== false;
255
+ return isPiSessionCatalogEnabled(isRecord(entry) ? entry.config : void 0);
235
256
  }
236
257
  function isPiSessionCatalogEnabled(pluginConfig) {
237
258
  return !isRecord(pluginConfig) || !isRecord(pluginConfig.piSessionCatalog) || pluginConfig.piSessionCatalog.enabled !== false;
@@ -1,2 +1,2 @@
1
- import { t as createAcpxRuntimeService } from "./.setup/register.runtime-Bm4HrMCp.mjs";
1
+ import { t as createAcpxRuntimeService } from "./.setup/register.runtime-C0nVOTzT.mjs";
2
2
  export { createAcpxRuntimeService };
@@ -16,7 +16,6 @@
16
16
  "doctorContract": {
17
17
  "configRepair": true,
18
18
  "stateMigrations": [
19
- { "id": "acpx-runtime-state-to-plugin-state" },
20
19
  {
21
20
  "id": "acpx-session-owner-resources",
22
21
  "doctorOnly": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/acpx",
3
- "version": "2026.9.7",
3
+ "version": "2026.10.1-beta.1",
4
4
  "description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,8 +8,9 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "@agentclientprotocol/claude-agent-acp": "0.76.0",
12
- "@agentclientprotocol/codex-acp": "1.11.0",
11
+ "@agentclientprotocol/claude-agent-acp": "0.79.0",
12
+ "@agentclientprotocol/codex-acp": "1.12.0",
13
+ "@openclaw/fs-safe": "0.23.0",
13
14
  "acpx": "0.19.1",
14
15
  "smol-toml": "1.8.0",
15
16
  "zod": "4.6.5"
@@ -44,20 +45,10 @@
44
45
  ]
45
46
  },
46
47
  "compat": {
47
- "pluginApi": ">=2026.9.5"
48
+ "pluginApi": ">=2026.10.1-beta.1"
48
49
  },
49
50
  "build": {
50
- "openclawVersion": "2026.9.5",
51
- "staticAssets": [
52
- {
53
- "source": "./src/runtime-internals/mcp-proxy.mjs",
54
- "output": "mcp-proxy.mjs"
55
- },
56
- {
57
- "source": "./src/runtime-internals/mcp-command-line.mjs",
58
- "output": "mcp-command-line.mjs"
59
- }
60
- ]
51
+ "openclawVersion": "2026.10.1-beta.1"
61
52
  },
62
53
  "release": {
63
54
  "publishToClawHub": true,
@@ -77,7 +68,7 @@
77
68
  "skills/**"
78
69
  ],
79
70
  "peerDependencies": {
80
- "openclaw": ">=2026.9.5"
71
+ "openclaw": ">=2026.10.1-beta.1"
81
72
  },
82
73
  "peerDependenciesMeta": {
83
74
  "openclaw": {
@@ -209,8 +209,8 @@ ${ACPX_CMD} codex sessions close oc-codex-<conversationId>
209
209
  Defaults are:
210
210
 
211
211
  - `openclaw -> openclaw acp`
212
- - `claude -> bundled @agentclientprotocol/claude-agent-acp@0.76.0`
213
- - `codex -> bundled @agentclientprotocol/codex-acp@1.11.0 through OpenClaw's isolated CODEX_HOME wrapper`
212
+ - `claude -> bundled @agentclientprotocol/claude-agent-acp@0.79.0`
213
+ - `codex -> bundled @agentclientprotocol/codex-acp@1.12.0 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`
@@ -1,138 +0,0 @@
1
- //#region extensions/acpx/src/codex-adapter.ts
2
- const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
3
- const CODEX_ACP_BIN = "codex-acp";
4
- const LEGACY_CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
5
- const OPENCLAW_CODEX_CONFIG_ARG = "--openclaw-codex-config";
6
- //#endregion
7
- //#region extensions/acpx/src/command-line.ts
8
- /** Match ACPX's persisted argv identity; scalar records keep their original bytes. */
9
- function renderAgentCommand(command) {
10
- return typeof command === "string" ? command : command.map((part) => /^[A-Za-z0-9_@%+=:,./^~-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
11
- }
12
- /** Split a command string into argv-like parts using simple quote/backslash rules. */
13
- function splitCommandParts(value) {
14
- if (Array.isArray(value)) return value;
15
- const windows = process.platform === "win32";
16
- const parts = [];
17
- let current = "";
18
- let quote = null;
19
- let escaping = false;
20
- let hasPart = false;
21
- for (const ch of value) {
22
- if (escaping) {
23
- current += ch;
24
- escaping = false;
25
- hasPart = true;
26
- continue;
27
- }
28
- if (ch === "\\" && quote !== "'" && !windows) {
29
- escaping = true;
30
- hasPart = true;
31
- continue;
32
- }
33
- if (windows && ch === "\"" && quote !== "'") {
34
- const backslashes = current.match(/\\+$/)?.[0].length ?? 0;
35
- current = current.slice(0, current.length - backslashes) + "\\".repeat(Math.floor(backslashes / 2));
36
- if (backslashes % 2 === 1) {
37
- current += "\"";
38
- continue;
39
- }
40
- }
41
- if (quote) {
42
- if (ch === quote) quote = null;
43
- else current += ch;
44
- continue;
45
- }
46
- if (ch === "'" || ch === "\"") {
47
- quote = ch;
48
- hasPart = true;
49
- continue;
50
- }
51
- if (/\s/.test(ch)) {
52
- if (hasPart) {
53
- parts.push(current);
54
- current = "";
55
- hasPart = false;
56
- }
57
- continue;
58
- }
59
- current += ch;
60
- hasPart = true;
61
- }
62
- if (escaping) current += "\\";
63
- if (quote) throw new Error("Invalid agent command: unterminated quote");
64
- if (hasPart) parts.push(current);
65
- return parts;
66
- }
67
- const OPENCLAW_BRIDGE_EXECUTABLE = "openclaw";
68
- const OPENCLAW_BRIDGE_SUBCOMMAND = "acp";
69
- function normalizeAgentName(value) {
70
- const normalized = value?.trim().toLowerCase();
71
- return normalized ? normalized : void 0;
72
- }
73
- function basename(value) {
74
- return value.split(/[\\/]/).pop() ?? value;
75
- }
76
- function isEnvAssignment(value) {
77
- return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value);
78
- }
79
- function unwrapEnvCommand(parts) {
80
- const command = parts.at(0);
81
- if (!command || basename(command) !== "env") return parts;
82
- let index = 1;
83
- while (true) {
84
- const part = parts.at(index);
85
- if (!part || !isEnvAssignment(part)) break;
86
- index += 1;
87
- }
88
- return parts.slice(index);
89
- }
90
- function matchesExecutableName(value, executableName) {
91
- const normalized = basename(value).toLowerCase();
92
- return normalized === executableName || normalized === `${executableName}.exe`;
93
- }
94
- function matchesPackageSpec(value, packageName) {
95
- const normalized = value.trim().toLowerCase();
96
- return normalized === packageName || normalized.startsWith(`${packageName}@`);
97
- }
98
- function stripModuleExtension(value) {
99
- return value.replace(/\.[cm]?js$/i, "").toLowerCase();
100
- }
101
- function isAcpCommand(command, params) {
102
- if (!command) return false;
103
- const parts = unwrapEnvCommand(splitCommandParts(command));
104
- if (!parts.length) return false;
105
- if (parts.some((part) => matchesPackageSpec(part, params.packageName))) return true;
106
- const commandName = basename(parts[0] ?? "");
107
- if (matchesExecutableName(commandName, params.executableName)) return true;
108
- if (!matchesExecutableName(commandName, "node")) return false;
109
- const scriptName = stripModuleExtension(basename(parts[1] ?? ""));
110
- return scriptName === params.executableName || scriptName === `${params.executableName}-wrapper`;
111
- }
112
- function isOpenClawBridgeCommand(command) {
113
- if (!command) return false;
114
- const parts = unwrapEnvCommand(splitCommandParts(command));
115
- if (basename(parts[0] ?? "") === OPENCLAW_BRIDGE_EXECUTABLE) return parts[1] === OPENCLAW_BRIDGE_SUBCOMMAND;
116
- if (basename(parts[0] ?? "") !== "node") return false;
117
- const scriptName = basename(parts[1] ?? "");
118
- return /^openclaw(?:\.[cm]?js)?$/i.test(scriptName) && parts[2] === OPENCLAW_BRIDGE_SUBCOMMAND;
119
- }
120
- function isCodexAcpCommand(command) {
121
- return isAcpCommand(command, {
122
- packageName: CODEX_ACP_PACKAGE,
123
- executableName: "codex-acp"
124
- });
125
- }
126
- function isClaudeAcpCommand(command) {
127
- return isAcpCommand(command, {
128
- packageName: "@agentclientprotocol/claude-agent-acp",
129
- executableName: "claude-agent-acp"
130
- });
131
- }
132
- function resolveAgentCommand(params) {
133
- const normalizedAgentName = normalizeAgentName(params.agentName);
134
- if (!normalizedAgentName) return;
135
- return splitCommandParts(params.agentRegistry.resolve(normalizedAgentName));
136
- }
137
- //#endregion
138
- export { renderAgentCommand as a, CODEX_ACP_BIN as c, OPENCLAW_CODEX_CONFIG_ARG as d, normalizeAgentName as i, CODEX_ACP_PACKAGE as l, isCodexAcpCommand as n, resolveAgentCommand as o, isOpenClawBridgeCommand as r, splitCommandParts as s, isClaudeAcpCommand as t, LEGACY_CODEX_ACP_PACKAGE as u };
@@ -1,138 +0,0 @@
1
- import { a as renderAgentCommand, s as splitCommandParts } from "./command-line-CPBLOiZM.mjs";
2
- import { createHash } from "node:crypto";
3
- //#region extensions/acpx/src/state.ts
4
- const ACPX_PROCESS_LEASE_NAMESPACE = "process-leases";
5
- const ACPX_PROCESS_LEASE_MAX_ENTRIES = 4096;
6
- const ACPX_LEGACY_PROCESS_LEASE_FILE = "process-leases.json";
7
- const ACPX_GATEWAY_INSTANCE_NAMESPACE = "gateway-instance";
8
- const ACPX_GATEWAY_INSTANCE_KEY = "current";
9
- const ACPX_LEGACY_GATEWAY_INSTANCE_FILE = "gateway-instance-id";
10
- function normalizeAcpxGatewayInstanceRecord(value) {
11
- if (typeof value !== "object" || value === null) return;
12
- const record = value;
13
- if (typeof record.instanceId !== "string" || !record.instanceId.trim()) return;
14
- const createdAt = typeof record.createdAt === "number" && Number.isFinite(record.createdAt) ? Math.trunc(record.createdAt) : 0;
15
- return {
16
- instanceId: record.instanceId.trim(),
17
- createdAt
18
- };
19
- }
20
- //#endregion
21
- //#region extensions/acpx/src/process-lease.ts
22
- /**
23
- * Persistent lease store for ACPX wrapper processes. Leases let OpenClaw attach
24
- * gateway/session identity to spawned ACP processes and clean them up later.
25
- */
26
- /** CLI argument carrying the ACPX process lease id. */
27
- const OPENCLAW_ACPX_LEASE_ID_ARG = "--openclaw-acpx-lease-id";
28
- /** CLI argument carrying the owning gateway instance id. */
29
- const OPENCLAW_GATEWAY_INSTANCE_ID_ARG = "--openclaw-gateway-instance-id";
30
- /** Synthetic session identity for generated-wrapper health probes. */
31
- const ACPX_PROBE_LEASE_SESSION_KEY = "openclaw:acpx:probe";
32
- /** Read OpenClaw lease identity from a generated wrapper command. */
33
- function readAcpxProcessLeaseIdentity(command) {
34
- const parts = typeof command === "string" ? Array.from(command.matchAll(/(?:^|\s)(--openclaw-(?:acpx-lease-id|gateway-instance-id))\s+(?:"([^"]*)"|'([^']*)'|(\S+))(?=\s|$)/g)).flatMap((match) => [match[1], match[2] ?? match[3] ?? match[4]]) : command ?? [];
35
- const leaseIndex = parts.lastIndexOf(OPENCLAW_ACPX_LEASE_ID_ARG);
36
- const gatewayIndex = parts.lastIndexOf(OPENCLAW_GATEWAY_INSTANCE_ID_ARG);
37
- const leaseId = leaseIndex >= 0 ? parts[leaseIndex + 1]?.trim() : "";
38
- const gatewayInstanceId = gatewayIndex >= 0 ? parts[gatewayIndex + 1]?.trim() : "";
39
- if (!leaseId || !gatewayInstanceId) return;
40
- return {
41
- leaseId,
42
- gatewayInstanceId
43
- };
44
- }
45
- function normalizeAcpxProcessLease(value) {
46
- if (typeof value !== "object" || value === null) return;
47
- const record = value;
48
- 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" || ![
49
- "open",
50
- "closing",
51
- "closed",
52
- "lost"
53
- ].includes(String(record.state))) return;
54
- return {
55
- leaseId: record.leaseId,
56
- gatewayInstanceId: record.gatewayInstanceId,
57
- sessionKey: record.sessionKey,
58
- wrapperRoot: record.wrapperRoot,
59
- wrapperPath: record.wrapperPath,
60
- rootPid: record.rootPid,
61
- ...typeof record.processGroupId === "number" ? { processGroupId: record.processGroupId } : {},
62
- commandHash: record.commandHash,
63
- startedAt: record.startedAt,
64
- state: record.state
65
- };
66
- }
67
- function normalizeAcpxProcessLeaseFile(value) {
68
- const root = typeof value === "object" && value !== null ? value : {};
69
- return {
70
- version: 1,
71
- leases: Array.isArray(root.leases) ? root.leases.map(normalizeAcpxProcessLease).filter((lease) => Boolean(lease)) : []
72
- };
73
- }
74
- function openAcpxProcessLeaseStateStore(openKeyedStore) {
75
- return openKeyedStore({
76
- namespace: ACPX_PROCESS_LEASE_NAMESPACE,
77
- maxEntries: ACPX_PROCESS_LEASE_MAX_ENTRIES,
78
- overflowPolicy: "reject-new"
79
- });
80
- }
81
- /** Create a serialized SQLite-backed ACPX process lease store. */
82
- function createAcpxProcessLeaseStore(params) {
83
- let updateQueue = Promise.resolve();
84
- async function update(mutator) {
85
- const run = updateQueue.then(async () => {
86
- await mutator();
87
- });
88
- updateQueue = run.catch(() => {});
89
- await run;
90
- }
91
- async function readCurrent() {
92
- await updateQueue;
93
- return (await params.store.entries()).map((entry) => normalizeAcpxProcessLease(entry.value)).filter((lease) => Boolean(lease));
94
- }
95
- return {
96
- async load(leaseId) {
97
- await updateQueue;
98
- return normalizeAcpxProcessLease(await params.store.lookup(leaseId));
99
- },
100
- async listOpen(gatewayInstanceId) {
101
- return (await readCurrent()).filter((lease) => (lease.state === "open" || lease.state === "closing") && (!gatewayInstanceId || lease.gatewayInstanceId === gatewayInstanceId));
102
- },
103
- async save(lease) {
104
- await update(async () => {
105
- await params.store.register(lease.leaseId, lease);
106
- });
107
- },
108
- async markState(leaseId, state) {
109
- await update(async () => {
110
- if (state === "closed" || state === "lost") {
111
- await params.store.delete(leaseId);
112
- return;
113
- }
114
- const lease = normalizeAcpxProcessLease(await params.store.lookup(leaseId));
115
- if (lease) await params.store.register(leaseId, {
116
- ...lease,
117
- state
118
- });
119
- });
120
- }
121
- };
122
- }
123
- /** Hash a wrapper command so process leases can detect command drift. */
124
- function hashAcpxProcessCommand(command) {
125
- return createHash("sha256").update(renderAgentCommand(command)).digest("hex");
126
- }
127
- /** Append portable wrapper arguments without changing the executable or argument bytes. */
128
- function withAcpxLeaseArgs(params) {
129
- return [
130
- ...splitCommandParts(params.command),
131
- OPENCLAW_ACPX_LEASE_ID_ARG,
132
- params.leaseId,
133
- OPENCLAW_GATEWAY_INSTANCE_ID_ARG,
134
- params.gatewayInstanceId
135
- ];
136
- }
137
- //#endregion
138
- export { hashAcpxProcessCommand as a, openAcpxProcessLeaseStateStore as c, ACPX_GATEWAY_INSTANCE_KEY as d, ACPX_GATEWAY_INSTANCE_NAMESPACE as f, normalizeAcpxGatewayInstanceRecord as h, createAcpxProcessLeaseStore as i, readAcpxProcessLeaseIdentity as l, ACPX_LEGACY_PROCESS_LEASE_FILE as m, OPENCLAW_ACPX_LEASE_ID_ARG as n, normalizeAcpxProcessLease as o, ACPX_LEGACY_GATEWAY_INSTANCE_FILE as p, OPENCLAW_GATEWAY_INSTANCE_ID_ARG as r, normalizeAcpxProcessLeaseFile as s, ACPX_PROBE_LEASE_SESSION_KEY as t, withAcpxLeaseArgs as u };
@@ -1,203 +0,0 @@
1
- import { t as resolveAcpxPluginConfig } from "./config-CdPsIued.mjs";
2
- import { a as hashAcpxProcessCommand, c as openAcpxProcessLeaseStateStore, l as readAcpxProcessLeaseIdentity, o as normalizeAcpxProcessLease } from "./process-lease-C3dNPtu8.mjs";
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 { isDeepStrictEqual } from "node:util";
7
- //#region extensions/acpx/src/session-owner-migration.ts
8
- function sessionDirectory(input) {
9
- if (!input.serviceWorkspaceDir) throw new Error("ACP ownership repair requires the Gateway service workspace; upgrade OpenClaw Doctor.");
10
- return path.join(resolveAcpxPluginConfig({
11
- rawConfig: input.config.plugins?.entries?.acpx?.config,
12
- workspaceDir: input.serviceWorkspaceDir
13
- }).stateDir, "sessions");
14
- }
15
- async function legacyRecords(input) {
16
- const directory = sessionDirectory(input);
17
- const ids = (await fs.readdir(directory).catch((error) => {
18
- if (asObjectRecord(error)?.code === "ENOENT") return [];
19
- throw error;
20
- })).filter((name) => name.endsWith(".json")).map((name) => decodeURIComponent(name.slice(0, -5))).filter((id) => !id.startsWith("agent:") && !id.startsWith(".openclaw-owner-"));
21
- if (ids.length === 0) return {
22
- directory,
23
- ids
24
- };
25
- const { resolveAcpxSessionResource } = await import("./session-resource-BaJ6bs5d.mjs").then((n) => n.n);
26
- const evidence = await input.context.inspectAcpSessionClaims?.();
27
- const { decodeAcpxRuntimeHandleState } = await import("acpx/runtime");
28
- return {
29
- directory,
30
- ids: ids.filter((id) => !evidence?.claims.some((claim) => {
31
- const locator = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
32
- return evidence.incomplete.length === 0 && claim.meta.identity?.state === "resolved" && claim.meta.identity.acpxRecordId === id && locator?.acpxRecordId === id && locator.name === resolveAcpxSessionResource(claim);
33
- })).filter((id) => !id.includes(":oneshot:") || evidence?.claims.some((claim) => {
34
- const locator = decodeAcpxRuntimeHandleState(claim.meta.runtimeSessionName);
35
- return claim.meta.identity?.acpxRecordId === id && locator?.name !== resolveAcpxSessionResource(claim);
36
- })).toSorted()
37
- };
38
- }
39
- function requireStoppedPid(pid) {
40
- if (pid === void 0 || pid === null) return;
41
- if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) throw new Error("record process identity is uncertain");
42
- try {
43
- process.kill(pid, 0);
44
- } catch (error) {
45
- if (asObjectRecord(error)?.code === "ESRCH") return;
46
- throw new Error("record process liveness cannot be verified", { cause: error });
47
- }
48
- throw new Error("record still has a live process; stop the harness before Doctor repair");
49
- }
50
- function recordPath(directory, recordId) {
51
- return path.join(directory, `${encodeURIComponent(recordId)}.json`);
52
- }
53
- function matchesClaimRecord(claim, raw, oldId, resource, decode) {
54
- const state = decode(claim.meta.runtimeSessionName);
55
- const identity = claim.meta.identity;
56
- if (!state || !identity || identity.state !== "resolved" || !identity.acpxSessionId && !identity.agentSessionId || state.mode !== claim.meta.mode) return false;
57
- const recordId = claim.meta.mode === "oneshot" ? oldId : resource;
58
- const oldLocator = state.name === raw.name && identity.acpxRecordId === oldId && state.acpxRecordId === oldId;
59
- const newLocator = state.name === resource && identity.acpxRecordId === recordId && state.acpxRecordId === recordId;
60
- 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);
61
- }
62
- async function migrateRecord(input, directory, oldId, claims, changes, warnings) {
63
- const sourcePath = recordPath(directory, oldId);
64
- const sourceBytes = await fs.readFile(sourcePath, "utf8");
65
- const raw = asObjectRecord(JSON.parse(sourceBytes));
66
- 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");
67
- requireStoppedPid(raw.pid);
68
- const { resolveAcpxSessionResource } = await import("./session-resource-BaJ6bs5d.mjs").then((n) => n.n);
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 source = await createFileSessionStore({ stateDir: path.dirname(directory) }).load(oldId);
99
- if (!source) throw new Error("pinned ACPX reader rejected the source record");
100
- const temporaryDirectory = await fs.mkdtemp(path.join(directory, ".openclaw-owner-"));
101
- const temporarySessions = path.join(temporaryDirectory, "sessions");
102
- const temporaryPath = recordPath(temporarySessions, recordId);
103
- try {
104
- await fs.mkdir(temporarySessions, { mode: 448 });
105
- const file = await fs.open(temporaryPath, "wx", 384);
106
- try {
107
- await file.writeFile(candidateBytes);
108
- await file.sync();
109
- } finally {
110
- await file.close();
111
- }
112
- const interpreted = await createFileSessionStore({ stateDir: temporaryDirectory }).load(recordId);
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(temporaryDirectory, {
162
- recursive: true,
163
- force: true
164
- });
165
- }
166
- }
167
- const acpxSessionOwnerMigration = {
168
- id: "acpx-session-owner-resources",
169
- label: "ACP session owners",
170
- doctorOnly: true,
171
- phase: "after-session-repair",
172
- async detectLegacyState(input) {
173
- const { ids } = await legacyRecords(input);
174
- 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;
175
- },
176
- async migrateLegacyState(input) {
177
- const changes = [];
178
- const warnings = [];
179
- if (!input.context.inspectAcpSessionClaims || !input.context.updateAcpSessionIdentity) return {
180
- changes,
181
- warnings: ["ACP owner repair requires current offline Doctor maintenance authority."]
182
- };
183
- const { directory, ids } = await legacyRecords(input);
184
- const evidence = await input.context.inspectAcpSessionClaims();
185
- if (evidence.incomplete.length) return {
186
- changes,
187
- warnings: [`ACP owner evidence is incomplete; all records retained: ${evidence.incomplete.join("; ")}`]
188
- };
189
- for (const oldId of ids) try {
190
- const current = await input.context.inspectAcpSessionClaims();
191
- if (current.incomplete.length) throw new Error("canonical ownership evidence became incomplete");
192
- await migrateRecord(input, directory, oldId, current.claims, changes, warnings);
193
- } catch (error) {
194
- warnings.push(`ACP record ${oldId} retained: ${String(error)}`);
195
- }
196
- return {
197
- changes,
198
- warnings
199
- };
200
- }
201
- };
202
- //#endregion
203
- export { acpxSessionOwnerMigration };