@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/README.md +6 -0
- package/assets/icon.png +0 -0
- package/dist/.setup/{config-CdPsIued.mjs → config-76krYh6C.mjs} +145 -17
- package/dist/.setup/{pi-session-catalog-runtime-UhiQSaDw.mjs → pi-session-catalog-runtime-7jc50ZJ4.mjs} +21 -56
- package/dist/.setup/{register.runtime-Bm4HrMCp.mjs → register.runtime-C0nVOTzT.mjs} +15 -38
- package/dist/.setup/runtime-doctor-migrations-hxP-CXX7.mjs +191 -0
- package/dist/.setup/{service-DMae5pPH.mjs → service-BXgGdE56.mjs} +111 -183
- package/dist/.setup/session-owner-migration-C73lfijV.mjs +431 -0
- package/dist/.setup/{session-resource-BaJ6bs5d.mjs → session-resource-DKJsUJPu.mjs} +1 -13
- package/dist/doctor-contract-api.js +10 -104
- package/dist/index.js +43 -22
- package/dist/register.runtime.js +1 -1
- package/openclaw.plugin.json +0 -1
- package/package.json +7 -16
- package/skills/acp-router/SKILL.md +2 -2
- package/dist/.setup/command-line-CPBLOiZM.mjs +0 -138
- package/dist/.setup/process-lease-C3dNPtu8.mjs +0 -138
- package/dist/.setup/session-owner-migration-CdJSYRk1.mjs +0 -203
- package/dist/mcp-command-line.mjs +0 -128
- package/dist/mcp-proxy.mjs +0 -159
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { t as createAcpxRuntimeService } from "./.setup/register.runtime-
|
|
2
|
-
import {
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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-
|
|
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
|
-
|
|
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;
|
package/dist/register.runtime.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createAcpxRuntimeService } from "./.setup/register.runtime-
|
|
1
|
+
import { t as createAcpxRuntimeService } from "./.setup/register.runtime-C0nVOTzT.mjs";
|
|
2
2
|
export { createAcpxRuntimeService };
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/acpx",
|
|
3
|
-
"version": "2026.
|
|
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.
|
|
12
|
-
"@agentclientprotocol/codex-acp": "1.
|
|
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.
|
|
48
|
+
"pluginApi": ">=2026.10.1-beta.1"
|
|
48
49
|
},
|
|
49
50
|
"build": {
|
|
50
|
-
"openclawVersion": "2026.
|
|
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.
|
|
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.
|
|
213
|
-
- `codex -> bundled @agentclientprotocol/codex-acp@1.
|
|
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 };
|