@openclaw/acpx 2026.9.3 → 2026.9.5

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,56 +1,125 @@
1
- import { _ as splitCommandParts, a as hashAcpxProcessCommand, g as renderAgentCommand, l as readAcpxProcessLeaseIdentity, t as ACPX_PROBE_LEASE_SESSION_KEY, u as withAcpxLeaseArgs } from "./process-lease-B83BGiLj.js";
2
- import { AcpRuntimeError } from "./runtime-api.js";
3
- import { t as resolveAcpxSessionResource } from "./session-resource-UWe7qD3m.js";
4
- import { a as CODEX_ACP_PACKAGE, i as isOpenClawLeaseAwareAcpxProcessCommand, n as cleanupOpenClawOwnedAcpxPendingLease, o as OPENCLAW_CODEX_CONFIG_ARG, r as cleanupOpenClawOwnedAcpxProcessTree } from "./service-BAf6nuci.js";
5
- import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
1
+ import { _ as splitCommandParts, a as hashAcpxProcessCommand, g as renderAgentCommand, l as readAcpxProcessLeaseIdentity, t as ACPX_PROBE_LEASE_SESSION_KEY, u as withAcpxLeaseArgs } from "./process-lease-B83BGiLj.mjs";
2
+ import { AcpRuntimeError } from "../runtime-api.js";
3
+ import { t as resolveAcpxSessionResource } from "./session-resource-Dzl0U7kK.mjs";
4
+ import { a as CODEX_ACP_PACKAGE, i as isOpenClawLeaseAwareAcpxProcessCommand, n as cleanupOpenClawOwnedAcpxPendingLease, o as OPENCLAW_CODEX_CONFIG_ARG, r as cleanupOpenClawOwnedAcpxProcessTree } from "./service-DZYyo6-7.mjs";
6
5
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
6
  import path, { resolve } from "node:path";
8
7
  import fs from "node:fs/promises";
9
8
  import { randomUUID } from "node:crypto";
10
9
  import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
10
+ import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
11
11
  import { AsyncLocalStorage } from "node:async_hooks";
12
12
  import { isDeepStrictEqual } from "node:util";
13
13
  import { ACPX_BACKEND_ID, AcpxRuntime as AcpxRuntime$1, createAcpRuntime, createAgentRegistry, createFileSessionStore, decodeAcpxRuntimeHandleState, decodeAcpxRuntimeHandleState as decodeAcpxRuntimeHandleState$1, encodeAcpxRuntimeHandleState, isRequestedModelUnsupportedError } from "acpx/runtime";
14
14
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
15
15
  import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
16
- //#region extensions/acpx/src/session-owner.ts
17
- function requireAcpxOwnerMigration(sessionKey) {
18
- throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP session "${sessionKey}" has an unqualified or unverifiable backend locator. Stop the Gateway and run "openclaw doctor --fix" to migrate ownership without losing history, then restart.`, { detailCode: "SESSION_OWNER_MIGRATION_REQUIRED" });
19
- }
20
- function assertAcpxSessionOwnerLocator(target, legacyBareSessionKeys) {
21
- const resource = resolveAcpxSessionResource(target);
22
- const qualified = resource === target.sessionKey.trim().toLowerCase();
23
- const persisted = target.persistedHandle;
24
- if (!qualified && (legacyBareSessionKeys?.has(target.sessionKey.trim().toLowerCase()) || legacyBareSessionKeys?.has(resource) && !persisted)) requireAcpxOwnerMigration(target.sessionKey);
25
- if (persisted) {
26
- const decoded = decodeAcpxRuntimeHandleState$1(persisted.runtimeSessionName);
27
- if (!qualified && !decoded || decoded && (decoded.name !== resource || persisted.acpxRecordId && decoded.acpxRecordId !== persisted.acpxRecordId)) requireAcpxOwnerMigration(target.sessionKey);
16
+ //#region extensions/acpx/src/runtime-generations.ts
17
+ var AcpxGenerationRegistry = class {
18
+ constructor(sessionStore, delegate, createDelegate) {
19
+ this.sessionStore = sessionStore;
20
+ this.delegate = delegate;
21
+ this.createDelegate = createDelegate;
22
+ this.generations = /* @__PURE__ */ new Map();
23
+ this.isolatedSessionResources = /* @__PURE__ */ new Set();
24
+ this.resetDelegates = /* @__PURE__ */ new Set();
25
+ this.retiringDelegates = /* @__PURE__ */ new WeakSet();
26
+ this.nextGenerationId = 0;
27
+ this.generationOwner = Symbol("acpx-runtime-owner");
28
+ this.stopping = false;
28
29
  }
29
- return resource;
30
- }
31
- /** Preserve physical oneshot record IDs and the upstream-encoded runtime handle. */
32
- function toAcpxResourceInput(input) {
33
- const sessionKey = assertAcpxSessionOwnerLocator({
34
- ...input.handle,
35
- persistedHandle: input.handle
36
- });
37
- return {
38
- ...input,
39
- handle: {
40
- ...input.handle,
41
- sessionKey
30
+ get isStopping() {
31
+ return this.stopping;
32
+ }
33
+ assertRunning() {
34
+ if (this.stopping) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
35
+ }
36
+ fromCaptured(resource, captured) {
37
+ return captured?.owner === this.generationOwner ? captured : this.currentGeneration(resource);
38
+ }
39
+ prepareFresh(resource) {
40
+ const generation = this.generations.get(resource);
41
+ if (generation) this.retireGeneration(generation);
42
+ else this.sessionStore.markFresh(resource);
43
+ }
44
+ resolveDelegate(generation) {
45
+ this.assertRunning();
46
+ if (!generation.delegate) {
47
+ generation.delegate = generation.afterReset ? this.createDelegate() : this.delegate;
48
+ if (generation.delegate !== this.delegate) this.resetDelegates.add(generation.delegate);
42
49
  }
43
- };
44
- }
50
+ return generation.delegate;
51
+ }
52
+ currentGeneration(resource) {
53
+ if (this.stopping) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
54
+ let generation = this.generations.get(resource);
55
+ if (!generation) {
56
+ const fresh = this.sessionStore.isFresh(resource);
57
+ const afterReset = fresh || this.isolatedSessionResources.has(resource);
58
+ if (afterReset) this.isolatedSessionResources.add(resource);
59
+ generation = {
60
+ id: ++this.nextGenerationId,
61
+ owner: this.generationOwner,
62
+ resource,
63
+ ensureQueue: new KeyedAsyncQueue(),
64
+ retired: false,
65
+ activeOperations: 0,
66
+ activeRecordOperations: /* @__PURE__ */ new Map(),
67
+ closedRecordIds: /* @__PURE__ */ new Set(),
68
+ records: /* @__PURE__ */ new Map(),
69
+ closeCompleted: false,
70
+ afterReset,
71
+ awaitPriorWrites: fresh
72
+ };
73
+ this.generations.set(resource, generation);
74
+ }
75
+ return generation;
76
+ }
77
+ retireGeneration(generation) {
78
+ generation.retired = true;
79
+ if (this.generations.get(generation.resource) === generation) {
80
+ this.generations.delete(generation.resource);
81
+ this.sessionStore.markFresh(generation.resource);
82
+ }
83
+ this.releaseRetiredDelegate(generation);
84
+ }
85
+ releaseRetiredDelegate(generation) {
86
+ const delegate = generation.delegate;
87
+ if (!generation.retired || generation.activeOperations !== 0 || !delegate || delegate === this.delegate || this.retiringDelegates.has(delegate)) return;
88
+ this.retiringDelegates.add(delegate);
89
+ delegate.shutdown().then(() => this.resetDelegates.delete(delegate), () => {});
90
+ }
91
+ retainGenerationOperation(generation, recordId) {
92
+ generation.activeOperations += 1;
93
+ generation.activeRecordOperations.set(recordId, (generation.activeRecordOperations.get(recordId) ?? 0) + 1);
94
+ return () => {
95
+ const remaining = (generation.activeRecordOperations.get(recordId) ?? 1) - 1;
96
+ if (remaining === 0) {
97
+ generation.activeRecordOperations.delete(recordId);
98
+ generation.closedRecordIds.delete(recordId);
99
+ } else generation.activeRecordOperations.set(recordId, remaining);
100
+ generation.activeOperations -= 1;
101
+ if (!generation.retired && generation.closeCompleted && generation.activeOperations === 0 && generation.records.size === 0 && this.generations.get(generation.resource) === generation) {
102
+ generation.retired = true;
103
+ this.generations.delete(generation.resource);
104
+ }
105
+ this.releaseRetiredDelegate(generation);
106
+ };
107
+ }
108
+ assertCurrentGeneration(generation) {
109
+ if (this.stopping || generation.retired) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP runtime operation was superseded by reset.");
110
+ }
111
+ async shutdown() {
112
+ this.stopping = true;
113
+ const errors = (await Promise.allSettled([this.delegate, ...this.resetDelegates].map((delegate) => delegate.shutdown()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
114
+ if (errors.length) throw new AggregateError(errors, "ACP runtime shutdown failed.");
115
+ this.resetDelegates.clear();
116
+ this.generations.clear();
117
+ this.isolatedSessionResources.clear();
118
+ }
119
+ };
45
120
  //#endregion
46
- //#region extensions/acpx/src/runtime.ts
47
- /**
48
- * OpenClaw ACPX runtime adapter. It wraps the upstream acpx runtime with
49
- * OpenClaw session metadata, lease tracking, model scoping, and cleanup policy.
50
- */
51
- const ACPX_PLUGIN_TOOLS_MCP_SERVER_NAME = "openclaw-plugin-tools";
52
- const ACPX_OPENCLAW_TOOLS_MCP_SERVER_NAME = "openclaw-tools";
53
- const OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV = "OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY";
121
+ //#region extensions/acpx/src/runtime-session-store.ts
122
+ /** Generation-bound persistence and process-lease metadata for ACPX resets. */
54
123
  function withOpenClawLeaseSessionMetadata(record, lease) {
55
124
  return {
56
125
  ...record,
@@ -58,31 +127,12 @@ function withOpenClawLeaseSessionMetadata(record, lease) {
58
127
  openclawGatewayInstanceId: lease.gatewayInstanceId
59
128
  };
60
129
  }
61
- const CODEX_WRAPPER_STDERR_LOG_PREFIX = "codex-acp-wrapper.stderr";
62
- function safeDiagnosticFilePart(value) {
63
- return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown";
64
- }
65
- function codexWrapperStderrLogFileName(leaseId) {
66
- return `${CODEX_WRAPPER_STDERR_LOG_PREFIX}.${safeDiagnosticFilePart(leaseId)}.log`;
67
- }
68
- function compactDiagnosticText(value) {
69
- return value.replace(/\s+/g, " ").trim();
70
- }
71
- function isGenericInternalAcpErrorMessage(message) {
72
- return message.trim() === "Internal error";
73
- }
74
- function isGenericInternalAcpError(error) {
75
- return error instanceof Error && isGenericInternalAcpErrorMessage(error.message);
76
- }
77
- async function readCodexWrapperStderrTail(params) {
78
- if (!params.wrapperRoot || !params.leaseId) return "";
79
- try {
80
- const text = await fs.readFile(path.join(params.wrapperRoot, codexWrapperStderrLogFileName(params.leaseId)), "utf8");
81
- return compactDiagnosticText(redactSensitiveText(sliceUtf16Safe(text, -6e3)));
82
- } catch {
83
- return "";
84
- }
130
+ function captureGenerationRecord(generation, record) {
131
+ if (record.closed || generation.closedRecordIds.has(record.acpxRecordId)) generation.records.delete(record.acpxRecordId);
132
+ else generation.records.set(record.acpxRecordId, record);
85
133
  }
134
+ const acpxGenerationKey = Symbol("openclaw.acpxGeneration");
135
+ const acpxOperationScope = new AsyncLocalStorage();
86
136
  function readSessionRecordName(record) {
87
137
  if (typeof record !== "object" || record === null) return "";
88
138
  const { name } = record;
@@ -120,7 +170,7 @@ function readOpenClawGatewayInstanceIdFromRecord(record) {
120
170
  return typeof openclawGatewayInstanceId === "string" ? openclawGatewayInstanceId.trim() || void 0 : void 0;
121
171
  }
122
172
  function extractGeneratedWrapperPath(command) {
123
- return splitCommandParts(command ?? "").find((part) => basename(part) === "codex-acp-wrapper.mjs" || basename(part) === "claude-agent-acp-wrapper.mjs") ?? "";
173
+ return splitCommandParts(command ?? "").find((part) => (part.split(/[\\/]/).pop() ?? "") === "codex-acp-wrapper.mjs" || (part.split(/[\\/]/).pop() ?? "") === "claude-agent-acp-wrapper.mjs") ?? "";
124
174
  }
125
175
  function selectCurrentSessionLease(params) {
126
176
  const sessionKeys = new Set(normalizeStringEntries(params.sessionKeys));
@@ -132,71 +182,217 @@ function selectCurrentSessionLease(params) {
132
182
  }
133
183
  function createResetAwareSessionStore(baseStore, params) {
134
184
  const freshSessionKeys = /* @__PURE__ */ new Set();
185
+ const stateQueue = new KeyedAsyncQueue();
186
+ const pendingWrites = /* @__PURE__ */ new Map();
135
187
  return {
136
188
  async load(sessionId) {
137
- const normalized = sessionId.trim();
138
- if (normalized && freshSessionKeys.has(normalized)) return;
139
- const record = await baseStore.load(sessionId);
140
- if (!record || !params?.leaseStore || !params.gatewayInstanceId) return record;
141
- const sessionName = readSessionRecordName(record) || normalized;
142
- const lease = selectCurrentSessionLease({
143
- leases: await params.leaseStore.listOpen(params.gatewayInstanceId),
144
- sessionKeys: [sessionName, normalized],
145
- rootPid: readRecordAgentPid(record)
146
- });
147
- if (!lease) return record;
148
- return withOpenClawLeaseSessionMetadata(record, lease);
189
+ const scope = acpxOperationScope.getStore();
190
+ if (scope?.closeRecord && (sessionId === scope.generation.resource || sessionId === scope.closeRecord.acpxRecordId)) return scope.closeRecord;
191
+ const resource = scope?.generation.resource ?? sessionId.trim();
192
+ const pending = pendingWrites.get(sessionId.trim());
193
+ if (pending && (scope?.generation.awaitPriorWrites || freshSessionKeys.has(resource))) await Promise.allSettled(pending);
194
+ const load = async () => {
195
+ if (scope?.generation.retired) return;
196
+ const normalized = sessionId.trim();
197
+ if (normalized && freshSessionKeys.has(normalized)) return;
198
+ const record = await baseStore.load(sessionId);
199
+ if (scope?.generation.retired || freshSessionKeys.has(scope?.generation.resource ?? normalized)) return;
200
+ if (scope && record) captureGenerationRecord(scope.generation, record);
201
+ if (!record || !params?.leaseStore || !params.gatewayInstanceId) return record;
202
+ const sessionName = readSessionRecordName(record) || normalized;
203
+ const lease = selectCurrentSessionLease({
204
+ leases: await params.leaseStore.listOpen(params.gatewayInstanceId),
205
+ sessionKeys: [sessionName, normalized],
206
+ rootPid: readRecordAgentPid(record)
207
+ });
208
+ if (!lease) return record;
209
+ if (scope?.generation.retired) return;
210
+ const leasedRecord = withOpenClawLeaseSessionMetadata(record, lease);
211
+ if (scope) captureGenerationRecord(scope.generation, leasedRecord);
212
+ return leasedRecord;
213
+ };
214
+ return await load();
149
215
  },
150
216
  async save(record) {
151
- let recordToSave = record;
152
- const launch = params?.launchScope?.getStore();
153
- const sessionName = readSessionRecordName(record);
154
- const agentCommand = readRecordAgentCommand(record);
155
- const leasedCommand = launch?.leasedCommand ?? agentCommand;
156
- const leaseIdentity = launch ?? readAcpxProcessLeaseIdentity(leasedCommand);
157
- if (params?.leaseStore && params.gatewayInstanceId && params.wrapperRoot && (!launch || sessionName === launch.sessionKey) && leasedCommand && leaseIdentity?.gatewayInstanceId === params.gatewayInstanceId && isOpenClawLeaseAwareAcpxProcessCommand({
158
- command: leasedCommand,
159
- wrapperRoot: params.wrapperRoot
160
- })) {
161
- const existing = await params.leaseStore.load(leaseIdentity.leaseId);
162
- if (!existing || existing.gatewayInstanceId === leaseIdentity.gatewayInstanceId && existing.sessionKey === sessionName && existing.wrapperRoot === params.wrapperRoot) {
163
- const adoptingLease = Boolean(launch && !isDeepStrictEqual(splitCommandParts(launch.resolvedCommand), splitCommandParts(launch.leasedCommand)));
164
- const persistedCommand = launch && !adoptingLease ? launch.resolvedCommand : leasedCommand;
165
- const lifecycleRecord = adoptingLease ? {
166
- ...record,
167
- pid: void 0,
168
- processId: void 0,
169
- agentStartedAt: void 0
170
- } : record;
171
- const rootPid = readRecordAgentPid(lifecycleRecord);
172
- if (rootPid) await params.leaseStore.save({
173
- leaseId: leaseIdentity.leaseId,
174
- gatewayInstanceId: leaseIdentity.gatewayInstanceId,
175
- sessionKey: sessionName,
176
- wrapperRoot: params.wrapperRoot,
177
- wrapperPath: extractGeneratedWrapperPath(leasedCommand),
178
- rootPid,
179
- ...existing?.rootPid === rootPid && existing.processGroupId ? { processGroupId: existing.processGroupId } : {},
180
- commandHash: hashAcpxProcessCommand(persistedCommand),
181
- startedAt: existing?.rootPid === rootPid ? existing.startedAt : Date.now(),
182
- state: "open"
183
- });
184
- recordToSave = withOpenClawLeaseSessionMetadata({
185
- ...lifecycleRecord,
186
- agentCommand: renderAgentCommand(persistedCommand),
187
- agentArgv: Array.isArray(persistedCommand) ? persistedCommand : void 0
188
- }, leaseIdentity);
217
+ const scope = acpxOperationScope.getStore();
218
+ if (scope) captureGenerationRecord(scope.generation, record);
219
+ const resource = record.acpxRecordId;
220
+ const retiredCloseRecord = scope?.generation.retired && record.closed && record.acpx?.reset_on_next_ensure === true ? scope.closeRecord : void 0;
221
+ const writeRecord = async () => {
222
+ if (scope?.generation.retired) {
223
+ if (!retiredCloseRecord) return;
224
+ const persisted = await baseStore.load(record.acpxRecordId);
225
+ if (!persisted || persisted.acpxRecordId !== retiredCloseRecord.acpxRecordId || persisted.acpSessionId !== retiredCloseRecord.acpSessionId || persisted.createdAt !== retiredCloseRecord.createdAt) return;
226
+ await baseStore.save(record);
227
+ return;
228
+ }
229
+ let recordToSave = record;
230
+ const launch = params?.launchScope?.getStore();
231
+ const sessionName = readSessionRecordName(record);
232
+ const agentCommand = readRecordAgentCommand(record);
233
+ const leasedCommand = launch?.leasedCommand ?? agentCommand;
234
+ const leaseIdentity = launch ?? readAcpxProcessLeaseIdentity(leasedCommand);
235
+ if (params?.leaseStore && params.gatewayInstanceId && params.wrapperRoot && (!launch || sessionName === launch.sessionKey) && leasedCommand && leaseIdentity?.gatewayInstanceId === params.gatewayInstanceId && isOpenClawLeaseAwareAcpxProcessCommand({
236
+ command: leasedCommand,
237
+ wrapperRoot: params.wrapperRoot
238
+ })) {
239
+ const existing = await params.leaseStore.load(leaseIdentity.leaseId);
240
+ if (scope?.generation.retired) return;
241
+ if (!existing || existing.gatewayInstanceId === leaseIdentity.gatewayInstanceId && existing.sessionKey === sessionName && existing.wrapperRoot === params.wrapperRoot) {
242
+ const adoptingLease = Boolean(launch && !isDeepStrictEqual(splitCommandParts(launch.resolvedCommand), splitCommandParts(launch.leasedCommand)));
243
+ const persistedCommand = launch && !adoptingLease ? launch.resolvedCommand : leasedCommand;
244
+ recordToSave = withOpenClawLeaseSessionMetadata({
245
+ ...adoptingLease ? {
246
+ ...record,
247
+ pid: void 0,
248
+ processId: void 0,
249
+ agentStartedAt: void 0
250
+ } : record,
251
+ agentCommand: renderAgentCommand(persistedCommand),
252
+ agentArgv: Array.isArray(persistedCommand) ? persistedCommand : void 0
253
+ }, leaseIdentity);
254
+ }
189
255
  }
256
+ if (scope?.generation.retired) return;
257
+ await baseStore.save(recordToSave);
258
+ if (scope && !scope.generation.retired) scope.generation.awaitPriorWrites = false;
259
+ if (sessionName && !scope?.generation.retired) freshSessionKeys.delete(sessionName);
260
+ };
261
+ const writes = pendingWrites.get(resource) ?? /* @__PURE__ */ new Set();
262
+ const previous = [...writes];
263
+ const write = scope?.generation.awaitPriorWrites || retiredCloseRecord ? Promise.allSettled(previous).then(() => stateQueue.enqueue(resource, writeRecord)) : writeRecord();
264
+ writes.add(write);
265
+ pendingWrites.set(resource, writes);
266
+ try {
267
+ await write;
268
+ } finally {
269
+ writes.delete(write);
270
+ if (writes.size === 0 && pendingWrites.get(resource) === writes) pendingWrites.delete(resource);
190
271
  }
191
- await baseStore.save(recordToSave);
192
- if (sessionName) freshSessionKeys.delete(sessionName);
193
272
  },
273
+ loadForClose: (sessionKey) => baseStore.load(sessionKey),
274
+ isFresh: (sessionKey) => freshSessionKeys.has(sessionKey),
194
275
  markFresh(sessionKey) {
195
276
  const normalized = sessionKey.trim();
196
277
  if (normalized) freshSessionKeys.add(normalized);
197
278
  }
198
279
  };
199
280
  }
281
+ //#endregion
282
+ //#region extensions/acpx/src/runtime-process-cleanup.ts
283
+ /** Capture OpenClaw wrapper cleanup ownership before a backend close can yield. */
284
+ async function prepareAcpxProcessCleanup(params) {
285
+ const { leaseStore, gatewayInstanceId, wrapperRoot, deps } = params;
286
+ const rootPid = readRecordAgentPid(params.record);
287
+ const rootCommand = params.command ? renderAgentCommand(params.command) : void 0;
288
+ const identity = readAcpxProcessLeaseIdentity(params.command);
289
+ const leaseId = readOpenClawLeaseIdFromRecord(params.record) ?? identity?.leaseId;
290
+ const expectedGatewayInstanceId = readOpenClawGatewayInstanceIdFromRecord(params.record) ?? identity?.gatewayInstanceId;
291
+ const sessionKeys = [params.sessionKey, readSessionRecordName(params.record)];
292
+ const openLeases = rootPid && gatewayInstanceId && leaseStore ? await leaseStore.listOpen(gatewayInstanceId) : [];
293
+ const selectedLease = rootPid ? selectCurrentSessionLease({
294
+ leases: openLeases,
295
+ sessionKeys,
296
+ rootPid
297
+ }) : void 0;
298
+ const loadedLease = leaseId ? await leaseStore?.load(leaseId) : void 0;
299
+ const ownedLease = selectedLease ?? (loadedLease && loadedLease.gatewayInstanceId === gatewayInstanceId && (!rootPid || loadedLease.rootPid === rootPid) && sessionKeys.includes(loadedLease.sessionKey) ? loadedLease : void 0);
300
+ const lease = ownedLease ? { ...ownedLease } : void 0;
301
+ return async () => {
302
+ if (lease && lease.gatewayInstanceId === gatewayInstanceId) {
303
+ await leaseStore?.markState(lease.leaseId, "closing");
304
+ const result = lease.rootPid > 0 ? await cleanupOpenClawOwnedAcpxProcessTree({
305
+ rootPid: lease.rootPid,
306
+ rootCommand,
307
+ expectedLeaseId: lease.leaseId,
308
+ expectedGatewayInstanceId: lease.gatewayInstanceId,
309
+ wrapperRoot: lease.wrapperRoot,
310
+ deps
311
+ }) : await cleanupOpenClawOwnedAcpxPendingLease({
312
+ leaseId: lease.leaseId,
313
+ gatewayInstanceId: lease.gatewayInstanceId,
314
+ wrapperRoot: lease.wrapperRoot,
315
+ wrapperPath: lease.wrapperPath,
316
+ deps
317
+ });
318
+ await leaseStore?.markState(lease.leaseId, result.skippedReason === "process-list-unavailable" || result.skippedReason === "unsupported-platform" || lease.rootPid <= 0 && (result.skippedReason === "ambiguous-root" || result.skippedReason === "unverified-root") ? "open" : result.terminatedPids.length > 0 || result.skippedReason === "missing-root" ? "closed" : "lost");
319
+ return;
320
+ }
321
+ if (!rootPid || !rootCommand) return;
322
+ await cleanupOpenClawOwnedAcpxProcessTree({
323
+ rootPid,
324
+ rootCommand,
325
+ ...leaseId ? { expectedLeaseId: leaseId } : {},
326
+ ...expectedGatewayInstanceId ? { expectedGatewayInstanceId } : {},
327
+ wrapperRoot,
328
+ deps
329
+ });
330
+ };
331
+ }
332
+ //#endregion
333
+ //#region extensions/acpx/src/session-owner.ts
334
+ function requireAcpxOwnerMigration(sessionKey) {
335
+ throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP session "${sessionKey}" has an unqualified or unverifiable backend locator. Stop the Gateway and run "openclaw doctor --fix" to migrate ownership without losing history, then restart.`, { detailCode: "SESSION_OWNER_MIGRATION_REQUIRED" });
336
+ }
337
+ function assertAcpxSessionOwnerLocator(target, legacyBareSessionKeys) {
338
+ const resource = resolveAcpxSessionResource(target);
339
+ const qualified = resource === target.sessionKey.trim().toLowerCase();
340
+ const persisted = target.persistedHandle;
341
+ if (!qualified && (legacyBareSessionKeys?.has(target.sessionKey.trim().toLowerCase()) || legacyBareSessionKeys?.has(resource) && !persisted)) requireAcpxOwnerMigration(target.sessionKey);
342
+ if (persisted) {
343
+ const decoded = decodeAcpxRuntimeHandleState$1(persisted.runtimeSessionName);
344
+ if (!qualified && !decoded || decoded && (decoded.name !== resource || persisted.acpxRecordId && decoded.acpxRecordId !== persisted.acpxRecordId)) requireAcpxOwnerMigration(target.sessionKey);
345
+ }
346
+ return resource;
347
+ }
348
+ /** Preserve physical oneshot record IDs and the upstream-encoded runtime handle. */
349
+ function toAcpxResourceInput(input) {
350
+ const sessionKey = assertAcpxSessionOwnerLocator({
351
+ ...input.handle,
352
+ persistedHandle: input.handle
353
+ });
354
+ return {
355
+ ...input,
356
+ handle: {
357
+ ...input.handle,
358
+ sessionKey
359
+ }
360
+ };
361
+ }
362
+ //#endregion
363
+ //#region extensions/acpx/src/runtime.ts
364
+ /**
365
+ * OpenClaw ACPX runtime adapter. It wraps the upstream acpx runtime with
366
+ * OpenClaw session metadata, lease tracking, model scoping, and cleanup policy.
367
+ */
368
+ const ACPX_PLUGIN_TOOLS_MCP_SERVER_NAME = "openclaw-plugin-tools";
369
+ const ACPX_OPENCLAW_TOOLS_MCP_SERVER_NAME = "openclaw-tools";
370
+ const OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV = "OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY";
371
+ const CODEX_WRAPPER_STDERR_LOG_PREFIX = "codex-acp-wrapper.stderr";
372
+ function safeDiagnosticFilePart(value) {
373
+ return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown";
374
+ }
375
+ function codexWrapperStderrLogFileName(leaseId) {
376
+ return `${CODEX_WRAPPER_STDERR_LOG_PREFIX}.${safeDiagnosticFilePart(leaseId)}.log`;
377
+ }
378
+ function compactDiagnosticText(value) {
379
+ return value.replace(/\s+/g, " ").trim();
380
+ }
381
+ function isGenericInternalAcpErrorMessage(message) {
382
+ return message.trim() === "Internal error";
383
+ }
384
+ function isGenericInternalAcpError(error) {
385
+ return error instanceof Error && isGenericInternalAcpErrorMessage(error.message);
386
+ }
387
+ async function readCodexWrapperStderrTail(params) {
388
+ if (!params.wrapperRoot || !params.leaseId) return "";
389
+ try {
390
+ const text = await fs.readFile(path.join(params.wrapperRoot, codexWrapperStderrLogFileName(params.leaseId)), "utf8");
391
+ return compactDiagnosticText(redactSensitiveText(sliceUtf16Safe(text, -6e3)));
392
+ } catch {
393
+ return "";
394
+ }
395
+ }
200
396
  const OPENCLAW_BRIDGE_EXECUTABLE = "openclaw";
201
397
  const OPENCLAW_BRIDGE_SUBCOMMAND = "acp";
202
398
  const CODEX_ACP_AGENT_ID = "codex";
@@ -362,7 +558,7 @@ function withAcpxSessionOptions(input) {
362
558
  ...existingOptions,
363
559
  model
364
560
  } : existingOptions;
365
- const { modelExplicit: _modelExplicit, ...rest } = input;
561
+ const { modelExplicit: _modelExplicit, thinkingExplicit: _thinkingExplicit, ...rest } = input;
366
562
  return {
367
563
  ...rest,
368
564
  ...sessionOptions ? { sessionOptions } : {}
@@ -402,10 +598,6 @@ function resolveAgentCommand(params) {
402
598
  if (!normalizedAgentName) return;
403
599
  return splitCommandParts(params.agentRegistry.resolve(normalizedAgentName));
404
600
  }
405
- function shouldUseDistinctBridgeDelegate(options) {
406
- const { mcpServers } = options;
407
- return Array.isArray(mcpServers) && mcpServers.length > 0;
408
- }
409
601
  function withManagedToolsMcpSessionEnv(params) {
410
602
  const sessionKey = params.sessionKey.trim();
411
603
  if (!params.pluginToolsEnabled && !params.openclawToolsEnabled || !sessionKey || !params.mcpServers?.length) return params.mcpServers;
@@ -436,12 +628,9 @@ var AcpxRuntime = class {
436
628
  constructor(options, testOptions) {
437
629
  this.ownerAwareSessions = 1;
438
630
  this.launchCommandScope = new AsyncLocalStorage();
439
- this.managedToolsSessionDelegates = /* @__PURE__ */ new Map();
631
+ this.sessionScope = new AsyncLocalStorage();
632
+ this.probeQueue = new KeyedAsyncQueue();
440
633
  this.launchLeaseScope = new AsyncLocalStorage();
441
- this.sessionEnsureQueue = new KeyedAsyncQueue();
442
- this.processLeaseTransitionQueue = new KeyedAsyncQueue();
443
- this.processLeaseOperationCounts = /* @__PURE__ */ new Map();
444
- this.uncertainProcessLeaseIds = /* @__PURE__ */ new Set();
445
634
  this.legacyBareSessionKeys = new Set(options.openclawLegacyBareSessionKeys);
446
635
  const { openclawProcessCleanup, ...delegateTestOptions } = testOptions ?? {};
447
636
  this.processCleanupDeps = openclawProcessCleanup;
@@ -466,69 +655,101 @@ var AcpxRuntime = class {
466
655
  },
467
656
  list: () => this.agentRegistry.list()
468
657
  };
469
- const sharedOptions = {
658
+ this.createDelegate = () => new AcpxRuntime$1({
470
659
  ...options,
471
660
  sessionStore: this.sessionStore,
472
- agentRegistry: this.scopedAgentRegistry
473
- };
474
- this.delegateOptions = sharedOptions;
475
- this.delegateTestOptions = delegateTestOptions;
476
- this.delegate = new AcpxRuntime$1(sharedOptions, this.delegateTestOptions);
477
- this.bridgeSafeDelegate = shouldUseDistinctBridgeDelegate(options) ? new AcpxRuntime$1({
478
- ...sharedOptions,
479
- mcpServers: []
480
- }, this.delegateTestOptions) : this.delegate;
661
+ agentRegistry: this.scopedAgentRegistry,
662
+ mcpServers: (context) => {
663
+ const servers = typeof options.mcpServers === "function" ? options.mcpServers(context) : options.mcpServers ?? [];
664
+ if (isOpenClawBridgeCommand(context.agentArgv ?? context.agentCommand)) return [];
665
+ const target = this.sessionScope.getStore();
666
+ if (!this.managedToolsMcpBridgeEnabled) return servers;
667
+ if (!target) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP tool bridge has no session owner");
668
+ return withManagedToolsMcpSessionEnv({
669
+ pluginToolsEnabled: this.pluginToolsMcpBridgeEnabled,
670
+ openclawToolsEnabled: this.openclawToolsMcpBridgeEnabled,
671
+ mcpServers: servers,
672
+ ...target
673
+ });
674
+ },
675
+ processLifecycle: {
676
+ onBeforeSpawn: async (launch) => {
677
+ await options.processLifecycle?.onBeforeSpawn?.(launch);
678
+ await this.recordProcessLaunch(launch);
679
+ },
680
+ onSpawned: async (process) => {
681
+ await this.recordProcessLaunch(process);
682
+ await options.processLifecycle?.onSpawned?.(process);
683
+ },
684
+ onSpawnFailed: options.processLifecycle?.onSpawnFailed,
685
+ onExit: options.processLifecycle?.onExit
686
+ }
687
+ }, delegateTestOptions);
688
+ this.delegate = this.createDelegate();
689
+ this.generationRegistry = new AcpxGenerationRegistry(this.sessionStore, this.delegate, this.createDelegate);
481
690
  this.probeAgent = normalizeAgentName(options.probeAgent) ?? "codex";
482
691
  const probeCommand = resolveAgentCommand({
483
692
  agentName: this.probeAgent,
484
693
  agentRegistry: this.agentRegistry
485
694
  });
486
695
  this.probeCommand = probeCommand;
487
- const useBridgeSafeProbe = this.managedToolsMcpBridgeEnabled || isOpenClawBridgeCommand(probeCommand);
488
- this.probeDelegate = useBridgeSafeProbe ? this.bridgeSafeDelegate : this.delegate;
489
696
  }
490
- resolveDelegateForSession(params) {
491
- if (isOpenClawBridgeCommand(params.command)) return this.bridgeSafeDelegate;
492
- return this.resolveManagedToolsDelegateForSession(params);
697
+ async runInGeneration(target, scope, run) {
698
+ const release = this.generationRegistry.retainGenerationOperation(scope.generation, scope.recordId ?? target.acpxRecordId ?? scope.generation.resource);
699
+ try {
700
+ return await this.sessionScope.run(target, () => acpxOperationScope.run(scope, run));
701
+ } finally {
702
+ release();
703
+ }
493
704
  }
494
- resolveManagedToolsDelegateForSession(target) {
495
- if (!this.managedToolsMcpBridgeEnabled) return this.delegate;
496
- const normalizedSessionKey = resolveAcpxSessionResource(target);
497
- const cached = this.managedToolsSessionDelegates.get(normalizedSessionKey);
498
- if (cached) return cached;
499
- const delegate = new AcpxRuntime$1({
500
- ...this.delegateOptions,
501
- mcpServers: withManagedToolsMcpSessionEnv({
502
- pluginToolsEnabled: this.pluginToolsMcpBridgeEnabled,
503
- openclawToolsEnabled: this.openclawToolsMcpBridgeEnabled,
504
- mcpServers: this.delegateOptions.mcpServers,
505
- sessionKey: target.sessionKey,
506
- agentId: target.agentId
507
- })
508
- }, this.delegateTestOptions);
509
- this.managedToolsSessionDelegates.set(normalizedSessionKey, delegate);
510
- return delegate;
705
+ resolveDelegateForSession(params) {
706
+ const generation = acpxOperationScope.getStore()?.generation ?? this.generationRegistry.currentGeneration(resolveAcpxSessionResource(params));
707
+ return this.generationRegistry.resolveDelegate(generation);
511
708
  }
512
- async loadOperationSnapshotForHandle(handle) {
513
- assertAcpxSessionOwnerLocator({
709
+ generationForHandle(handle) {
710
+ const resource = assertAcpxSessionOwnerLocator({
514
711
  ...handle,
515
712
  persistedHandle: handle
516
713
  }, this.legacyBareSessionKeys);
517
- const record = await this.sessionStore.load(handle.acpxRecordId ?? resolveAcpxSessionResource(handle));
714
+ const capturedGeneration = handle[acpxGenerationKey];
715
+ return this.generationRegistry.fromCaptured(resource, capturedGeneration);
716
+ }
717
+ async loadOperationSnapshotForHandle(handle, allowRetired = false, generation = this.generationForHandle(handle)) {
718
+ const resource = generation.resource;
719
+ if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
720
+ const ownedRecord = generation.records.get(handle.acpxRecordId ?? resource);
721
+ if (ownedRecord && (handle.acpxRecordId && ownedRecord.acpxRecordId !== handle.acpxRecordId || handle.backendSessionId && ownedRecord.acpSessionId && ownedRecord.acpSessionId !== handle.backendSessionId)) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP handle no longer owns this runtime generation.");
722
+ let record = allowRetired ? generation.retired ? ownedRecord : await this.sessionStore.loadForClose(handle.acpxRecordId ?? resource) : await acpxOperationScope.run({ generation }, () => this.sessionStore.load(handle.acpxRecordId ?? resource));
723
+ if (allowRetired && generation.retired && ownedRecord) record = ownedRecord;
724
+ if (allowRetired && record) captureGenerationRecord(generation, record);
725
+ if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
726
+ if (record && (handle.acpxRecordId && handle.acpxRecordId !== record.acpxRecordId || handle.backendSessionId && record.acpSessionId && handle.backendSessionId !== record.acpSessionId)) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP handle no longer owns this runtime record.");
727
+ const command = readRecordAgentCommand(record) ?? resolveAgentCommand({
728
+ agentName: readAgentFromHandle(handle),
729
+ agentRegistry: this.agentRegistry
730
+ });
731
+ const identity = readAcpxProcessLeaseIdentity(command);
732
+ if (identity && this.processLeaseStore && this.gatewayInstanceId && this.wrapperRoot) {
733
+ const lease = await this.processLeaseStore.load(identity.leaseId);
734
+ if (identity.gatewayInstanceId !== this.gatewayInstanceId) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another gateway`);
735
+ if (lease && (lease.gatewayInstanceId !== identity.gatewayInstanceId || lease.sessionKey !== resolveAcpxSessionResource(handle) || lease.wrapperRoot !== this.wrapperRoot)) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another session`);
736
+ }
737
+ if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
518
738
  return {
519
739
  record,
520
- command: readRecordAgentCommand(record) ?? resolveAgentCommand({
521
- agentName: readAgentFromHandle(handle),
522
- agentRegistry: this.agentRegistry
523
- })
740
+ command,
741
+ generation
524
742
  };
525
743
  }
526
744
  resolveDelegateForOperationSnapshot(handle, snapshot) {
527
- return this.resolveDelegateForSession({
745
+ return acpxOperationScope.run({
746
+ generation: snapshot.generation,
747
+ recordId: snapshot.record?.acpxRecordId
748
+ }, () => this.resolveDelegateForSession({
528
749
  command: snapshot.command,
529
750
  sessionKey: handle.sessionKey,
530
751
  agentId: handle.agentId
531
- });
752
+ }));
532
753
  }
533
754
  async readReusablePersistentSessionCommand(params) {
534
755
  if (params.mode !== "persistent" || !params.command) return;
@@ -556,10 +777,8 @@ var AcpxRuntime = class {
556
777
  agent: normalizeAgentName(params.agent) ?? params.agent,
557
778
  command: params.reusableCommand ?? params.command
558
779
  }, params.run);
559
- const processLeaseStore = this.processLeaseStore;
560
780
  const reusableIdentity = readAcpxProcessLeaseIdentity(params.reusableCommand);
561
- const canReuseLeaseIdentity = reusableIdentity?.gatewayInstanceId === this.gatewayInstanceId;
562
- const leaseId = canReuseLeaseIdentity ? reusableIdentity.leaseId : params.finalizeCompletedProbe ? `probe-${hashAcpxProcessCommand(`${this.gatewayInstanceId}\0${extractGeneratedWrapperPath(params.command)}`)}` : randomUUID();
781
+ const leaseId = reusableIdentity?.gatewayInstanceId === this.gatewayInstanceId ? reusableIdentity.leaseId : params.finalizeCompletedProbe ? `probe-${hashAcpxProcessCommand(`${this.gatewayInstanceId}\0${extractGeneratedWrapperPath(params.command)}`)}` : randomUUID();
563
782
  const leasedCommand = withAcpxLeaseArgs({
564
783
  command: params.command,
565
784
  leaseId,
@@ -573,149 +792,41 @@ var AcpxRuntime = class {
573
792
  resolvedCommand: params.reusableCommand ?? leasedCommand,
574
793
  leasedCommand
575
794
  };
576
- await this.retainProcessLeaseOperation(launch, async () => {
577
- const reusableLease = canReuseLeaseIdentity ? await processLeaseStore.load(launch.leaseId) : void 0;
578
- if (reusableLease && (reusableLease.gatewayInstanceId !== launch.gatewayInstanceId || reusableLease.sessionKey !== launch.sessionKey || reusableLease.wrapperRoot !== launch.wrapperRoot)) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACPX process lease ${launch.leaseId} belongs to another session`);
579
- if (!(!reusableLease && (!params.reusableCommand || isDeepStrictEqual(splitCommandParts(params.reusableCommand), splitCommandParts(leasedCommand))))) return;
580
- await processLeaseStore.save({
581
- leaseId: launch.leaseId,
582
- gatewayInstanceId: launch.gatewayInstanceId,
583
- sessionKey: launch.sessionKey,
584
- wrapperRoot: launch.wrapperRoot,
585
- wrapperPath: extractGeneratedWrapperPath(leasedCommand),
586
- rootPid: 0,
587
- commandHash: hashAcpxProcessCommand(leasedCommand),
588
- startedAt: Date.now(),
589
- state: "open"
590
- });
795
+ const result = await this.launchLeaseScope.run(launch, () => this.launchCommandScope.run({
796
+ agent: normalizeAgentName(params.agent) ?? params.agent,
797
+ command: launch.resolvedCommand
798
+ }, params.run));
799
+ if (params.finalizeCompletedProbe) await cleanupOpenClawOwnedAcpxPendingLease({
800
+ leaseId,
801
+ gatewayInstanceId: launch.gatewayInstanceId,
802
+ wrapperRoot: launch.wrapperRoot,
803
+ wrapperPath: extractGeneratedWrapperPath(leasedCommand),
804
+ deps: this.processCleanupDeps
591
805
  });
592
- try {
593
- const result = await this.launchLeaseScope.run(launch, () => this.launchCommandScope.run({
594
- agent: normalizeAgentName(params.agent) ?? params.agent,
595
- command: launch.resolvedCommand
596
- }, params.run));
597
- if (params.finalizeCompletedProbe) await this.finalizeCompletedProbeLease(launch);
598
- else await this.finalizeProcessLeaseForSession(params.sessionKey, launch);
599
- return result;
600
- } catch (error) {
601
- await this.releaseProcessLeaseAfterUncertainFailure(launch);
602
- throw error;
603
- }
806
+ return result;
604
807
  }
605
- async prepareProcessLeaseForOperation(handle, record) {
606
- if (!this.processLeaseStore || !this.gatewayInstanceId || !this.wrapperRoot) return;
607
- const processLeaseStore = this.processLeaseStore;
608
- const wrapperRoot = this.wrapperRoot;
609
- const recordPid = readRecordAgentPid(record);
610
- const command = readRecordAgentCommand(record);
808
+ async recordProcessLaunch(process) {
809
+ const command = [process.command, ...process.args];
611
810
  const identity = readAcpxProcessLeaseIdentity(command);
612
- if (!command || !isOpenClawLeaseAwareAcpxProcessCommand({
811
+ if (!identity || !this.processLeaseStore || !this.wrapperRoot) return;
812
+ const sessionKey = process.scope.kind === "runtime-session" ? process.scope.sessionKey : ACPX_PROBE_LEASE_SESSION_KEY;
813
+ const existing = await this.processLeaseStore.load(identity.leaseId);
814
+ if (identity.gatewayInstanceId !== this.gatewayInstanceId || existing && (existing.gatewayInstanceId !== identity.gatewayInstanceId || existing.sessionKey !== sessionKey || existing.wrapperRoot !== this.wrapperRoot)) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP process lease belongs to another owner");
815
+ if (!isOpenClawLeaseAwareAcpxProcessCommand({
613
816
  command,
614
- wrapperRoot
615
- })) return;
616
- if (!identity) return;
617
- if (identity.gatewayInstanceId !== this.gatewayInstanceId) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another gateway`);
618
- await this.retainProcessLeaseOperation(identity, async () => {
619
- const existing = await processLeaseStore.load(identity.leaseId);
620
- if (!existing) {
621
- await processLeaseStore.save({
622
- leaseId: identity.leaseId,
623
- gatewayInstanceId: identity.gatewayInstanceId,
624
- sessionKey: resolveAcpxSessionResource(handle),
625
- wrapperRoot,
626
- wrapperPath: extractGeneratedWrapperPath(command),
627
- rootPid: recordPid ?? 0,
628
- commandHash: hashAcpxProcessCommand(command),
629
- startedAt: Date.now(),
630
- state: "open"
631
- });
632
- return;
633
- }
634
- if (existing.gatewayInstanceId !== identity.gatewayInstanceId || existing.sessionKey !== resolveAcpxSessionResource(handle) || existing.wrapperRoot !== wrapperRoot) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another session`);
635
- });
636
- return identity;
637
- }
638
- async retainProcessLeaseOperation(identity, prepare) {
639
- await this.processLeaseTransitionQueue.enqueue(identity.leaseId, async () => {
640
- await prepare();
641
- this.processLeaseOperationCounts.set(identity.leaseId, (this.processLeaseOperationCounts.get(identity.leaseId) ?? 0) + 1);
642
- });
643
- }
644
- async releaseProcessLeaseOperation(identity, finalize) {
645
- if (!identity) return;
646
- await this.processLeaseTransitionQueue.enqueue(identity.leaseId, async () => {
647
- const count = this.processLeaseOperationCounts.get(identity.leaseId) ?? 0;
648
- if (count > 1) {
649
- this.processLeaseOperationCounts.set(identity.leaseId, count - 1);
650
- return;
651
- }
652
- if (count === 0) return;
653
- this.processLeaseOperationCounts.delete(identity.leaseId);
654
- await finalize();
655
- });
656
- }
657
- async finalizeProcessLeaseForOperation(handle, identity) {
658
- await this.finalizeProcessLeaseForSession(handle.acpxRecordId ?? resolveAcpxSessionResource(handle), identity);
659
- }
660
- async finalizeProcessLeaseForSession(sessionId, identity) {
661
- if (!identity || !this.processLeaseStore) return;
662
- const processLeaseStore = this.processLeaseStore;
663
- await this.releaseProcessLeaseOperation(identity, async () => {
664
- const lease = await processLeaseStore.load(identity.leaseId);
665
- if (!lease || lease.gatewayInstanceId !== identity.gatewayInstanceId) return;
666
- if (lease.rootPid <= 0) {
667
- if (this.uncertainProcessLeaseIds.has(identity.leaseId)) return;
668
- await processLeaseStore.markState(identity.leaseId, "lost");
669
- return;
670
- }
671
- this.uncertainProcessLeaseIds.delete(identity.leaseId);
672
- try {
673
- const record = await this.sessionStore.load(sessionId);
674
- const recordIdentity = readAcpxProcessLeaseIdentity(readRecordAgentCommand(record));
675
- if (recordIdentity?.leaseId !== identity.leaseId || recordIdentity.gatewayInstanceId !== identity.gatewayInstanceId) await processLeaseStore.markState(identity.leaseId, "lost");
676
- } catch {}
677
- });
678
- }
679
- async finalizeCompletedProbeLease(identity) {
680
- if (!this.processLeaseStore) return;
681
- const processLeaseStore = this.processLeaseStore;
682
- await this.releaseProcessLeaseOperation(identity, async () => {
683
- const lease = await processLeaseStore.load(identity.leaseId);
684
- if (!lease || lease.gatewayInstanceId !== identity.gatewayInstanceId) return;
685
- if (lease.rootPid > 0) return;
686
- await cleanupOpenClawOwnedAcpxPendingLease({
687
- leaseId: lease.leaseId,
688
- gatewayInstanceId: lease.gatewayInstanceId,
689
- wrapperRoot: lease.wrapperRoot,
690
- wrapperPath: lease.wrapperPath,
691
- deps: this.processCleanupDeps
692
- });
693
- });
694
- }
695
- async releaseProcessLeaseAfterUncertainFailure(identity) {
696
- if (!identity || !this.processLeaseStore) return;
697
- this.uncertainProcessLeaseIds.add(identity.leaseId);
698
- const processLeaseStore = this.processLeaseStore;
699
- await this.releaseProcessLeaseOperation(identity, async () => {
700
- const lease = await processLeaseStore.load(identity.leaseId);
701
- if (!lease || lease.gatewayInstanceId !== identity.gatewayInstanceId || lease.rootPid > 0) this.uncertainProcessLeaseIds.delete(identity.leaseId);
817
+ wrapperRoot: this.wrapperRoot
818
+ })) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP process lease has no owned wrapper");
819
+ await this.processLeaseStore.save({
820
+ ...identity,
821
+ sessionKey,
822
+ wrapperRoot: this.wrapperRoot,
823
+ wrapperPath: extractGeneratedWrapperPath(command),
824
+ rootPid: "pid" in process ? process.pid : 0,
825
+ commandHash: hashAcpxProcessCommand(command),
826
+ startedAt: "startedAt" in process ? Date.parse(process.startedAt) : Date.now(),
827
+ state: "open"
702
828
  });
703
829
  }
704
- async runWithProcessLeaseForHandle(handle, record, run) {
705
- const identity = await this.prepareProcessLeaseForOperation(handle, record);
706
- try {
707
- return await run();
708
- } finally {
709
- await this.finalizeProcessLeaseForOperation(handle, identity);
710
- }
711
- }
712
- async finalizeProcessLeaseAfter(handle, identityPromise, resultPromise) {
713
- try {
714
- return await resultPromise;
715
- } finally {
716
- await this.finalizeProcessLeaseForOperation(handle, await identityPromise);
717
- }
718
- }
719
830
  async withCodexWrapperDiagnostics(params) {
720
831
  try {
721
832
  return await params.run();
@@ -736,69 +847,47 @@ var AcpxRuntime = class {
736
847
  leaseId: readOpenClawLeaseIdFromRecord(record)
737
848
  });
738
849
  }
739
- async cleanupProcessTreeForRecord(handle, record) {
740
- const leaseId = readOpenClawLeaseIdFromRecord(record);
741
- const rootPid = readRecordAgentPid(record);
742
- const sessionKeys = [resolveAcpxSessionResource(handle), readSessionRecordName(record)];
743
- const selectedLease = selectCurrentSessionLease({
744
- leases: this.gatewayInstanceId && this.processLeaseStore ? await this.processLeaseStore.listOpen(this.gatewayInstanceId) : [],
745
- sessionKeys,
746
- rootPid
747
- });
748
- const loadedLease = leaseId ? await this.processLeaseStore?.load(leaseId) : void 0;
749
- const lease = selectedLease ?? (loadedLease && loadedLease.gatewayInstanceId === this.gatewayInstanceId && (!rootPid || loadedLease.rootPid === rootPid) && sessionKeys.includes(loadedLease.sessionKey) ? loadedLease : void 0);
750
- if (lease && lease.gatewayInstanceId === this.gatewayInstanceId && lease.rootPid > 0) {
751
- await this.processLeaseStore?.markState(lease.leaseId, "closing");
752
- const result = await cleanupOpenClawOwnedAcpxProcessTree({
753
- rootPid: lease.rootPid,
754
- rootCommand: record?.agentCommand,
755
- expectedLeaseId: lease.leaseId,
756
- expectedGatewayInstanceId: lease.gatewayInstanceId,
757
- wrapperRoot: lease.wrapperRoot,
758
- deps: this.processCleanupDeps
759
- });
760
- await this.processLeaseStore?.markState(lease.leaseId, result.skippedReason === "process-list-unavailable" || result.skippedReason === "unsupported-platform" ? "open" : result.terminatedPids.length > 0 || result.skippedReason === "missing-root" ? "closed" : "lost");
761
- return;
762
- }
763
- const rootCommand = readRecordAgentCommand(record) ?? resolveAgentCommand({
764
- agentName: readAgentFromHandle(handle),
765
- agentRegistry: this.agentRegistry
766
- });
767
- if (!rootPid || !rootCommand) return;
768
- const expectedGatewayInstanceId = readOpenClawGatewayInstanceIdFromRecord(record);
769
- await cleanupOpenClawOwnedAcpxProcessTree({
770
- rootPid,
771
- rootCommand: renderAgentCommand(rootCommand),
772
- ...leaseId ? { expectedLeaseId: leaseId } : {},
773
- ...expectedGatewayInstanceId ? { expectedGatewayInstanceId } : {},
774
- wrapperRoot: this.wrapperRoot,
775
- deps: this.processCleanupDeps
776
- });
850
+ async shutdown() {
851
+ await this.generationRegistry.shutdown();
777
852
  }
778
853
  isHealthy() {
779
- return this.probeDelegate.isHealthy();
854
+ return this.delegate.isHealthy();
780
855
  }
781
856
  async probeAvailability() {
782
- await this.runWithLaunchLease({
857
+ await this.probeQueue.enqueue(this.probeAgent, () => this.runWithLaunchLease({
783
858
  agent: this.probeAgent,
784
859
  sessionKey: ACPX_PROBE_LEASE_SESSION_KEY,
785
860
  command: this.probeCommand,
786
861
  finalizeCompletedProbe: true,
787
- run: () => this.probeDelegate.probeAvailability()
788
- });
862
+ run: () => this.delegate.probeAvailability()
863
+ }));
789
864
  }
790
865
  async doctor() {
791
- return await this.runWithLaunchLease({
866
+ return await this.probeQueue.enqueue(this.probeAgent, () => this.runWithLaunchLease({
792
867
  agent: this.probeAgent,
793
868
  sessionKey: ACPX_PROBE_LEASE_SESSION_KEY,
794
869
  command: this.probeCommand,
795
870
  finalizeCompletedProbe: true,
796
- run: () => this.probeDelegate.doctor()
797
- });
871
+ run: () => this.delegate.doctor()
872
+ }));
798
873
  }
799
874
  async ensureSession(input) {
800
875
  const resource = assertAcpxSessionOwnerLocator(input, this.legacyBareSessionKeys);
801
- return await this.sessionEnsureQueue.enqueue(resource.trim() || resource, () => this.ensureSessionUnlocked(input));
876
+ const generation = this.generationRegistry.currentGeneration(resource);
877
+ return await generation.ensureQueue.enqueue(resource + "\0" + generation.id, () => this.runInGeneration(input, { generation }, async () => {
878
+ this.generationRegistry.assertCurrentGeneration(generation);
879
+ const handle = {
880
+ ...await this.ensureSessionUnlocked(input),
881
+ [acpxGenerationKey]: generation
882
+ };
883
+ if (generation.retired && !this.generationRegistry.isStopping) await this.close({
884
+ handle,
885
+ reason: "superseded-initialization",
886
+ discardPersistentState: true
887
+ });
888
+ this.generationRegistry.assertCurrentGeneration(generation);
889
+ return handle;
890
+ }));
802
891
  }
803
892
  async ensureSessionUnlocked(logicalInput) {
804
893
  assertSupportedRuntimeSessionMode(logicalInput.mode);
@@ -820,20 +909,23 @@ var AcpxRuntime = class {
820
909
  sessionKey: resolveAcpxSessionResource(logicalInput)
821
910
  };
822
911
  const isCodexAcp = normalizeAgentName(input.agent) === CODEX_ACP_AGENT_ID && isCodexAcpCommand(command);
912
+ const dropInheritedCodexMax = isCodexAcp && input.thinking === "max" && input.thinkingExplicit === false;
913
+ const effectiveInput = dropInheritedCodexMax ? { ...input } : input;
914
+ if (dropInheritedCodexMax) delete effectiveInput.thinking;
823
915
  const claudeModelOverride = isClaudeAcpCommand(command) ? normalizeClaudeAcpModelOverride(input.model) : void 0;
824
- const codexClassification = isCodexAcp ? classifyCodexAcpModelRequest(input.model, input.thinking) : void 0;
916
+ const codexClassification = isCodexAcp ? classifyCodexAcpModelRequest(effectiveInput.model, effectiveInput.thinking) : void 0;
825
917
  if (codexClassification?.kind === "unsupported" && input.modelExplicit) failUnsupportedCodexAcpModel(input.model ?? "");
826
918
  const classifiedCodexOverride = codexClassification?.kind === "override" ? codexClassification.override : codexClassification?.thinkingOverride;
827
919
  const codexModelOverride = classifiedCodexOverride && Object.keys(classifiedCodexOverride).length > 0 ? classifiedCodexOverride : void 0;
828
- const requestedModel = input.model?.trim();
920
+ const requestedModel = effectiveInput.model?.trim();
829
921
  const appliedModel = isCodexAcp && requestedModel ? codexModelOverride?.model ? {
830
922
  kind: "applied",
831
923
  model: requestedModel
832
924
  } : { kind: "dropped" } : void 0;
833
- const ensureInput = isCodexAcp ? withCodexSessionModel(input, codexModelOverride) : claudeModelOverride ? {
834
- ...input,
925
+ const ensureInput = isCodexAcp ? withCodexSessionModel(effectiveInput, codexModelOverride) : claudeModelOverride ? {
926
+ ...effectiveInput,
835
927
  model: claudeModelOverride
836
- } : input;
928
+ } : effectiveInput;
837
929
  const stableLaunchCommand = codexModelOverride && command ? appendCodexAcpConfigOverrides(command, codexModelOverride) : command;
838
930
  const reusableCommand = await this.readReusablePersistentSessionCommand({
839
931
  sessionKey: input.sessionKey,
@@ -855,7 +947,8 @@ var AcpxRuntime = class {
855
947
  })
856
948
  }),
857
949
  ...logicalTarget,
858
- ...appliedModel ? { appliedModel } : {}
950
+ ...appliedModel ? { appliedModel } : {},
951
+ ...dropInheritedCodexMax ? { appliedThinking: { kind: "dropped" } } : {}
859
952
  };
860
953
  }
861
954
  async *runTurn(input) {
@@ -888,18 +981,27 @@ var AcpxRuntime = class {
888
981
  fallbackCode: "ACP_TURN_FAILED",
889
982
  run
890
983
  });
891
- const snapshotPromise = this.loadOperationSnapshotForHandle(input.handle);
892
- const turnLeasePromise = snapshotPromise.then(({ record }) => this.prepareProcessLeaseForOperation(input.handle, record));
893
- const turnPromise = Promise.all([snapshotPromise, turnLeasePromise]).then(([snapshot]) => {
894
- const { command } = snapshot;
984
+ const turnPromise = this.loadOperationSnapshotForHandle(input.handle).then((snapshot) => {
985
+ const { command, generation } = snapshot;
986
+ this.generationRegistry.assertCurrentGeneration(generation);
895
987
  const delegate = this.resolveDelegateForOperationSnapshot(input.handle, snapshot);
896
- return withTurnDiagnostics(command, async () => ({
897
- command,
898
- turn: delegate.startTurn({
899
- ...toAcpxResourceInput(input),
900
- timeoutMs: 0
901
- })
902
- }));
988
+ return this.sessionScope.run(input.handle, () => acpxOperationScope.run({ generation }, () => withTurnDiagnostics(command, async () => {
989
+ const release = this.generationRegistry.retainGenerationOperation(generation, snapshot.record?.acpxRecordId ?? input.handle.acpxRecordId ?? generation.resource);
990
+ try {
991
+ const turn = delegate.startTurn({
992
+ ...toAcpxResourceInput(input),
993
+ timeoutMs: 0
994
+ });
995
+ turn.result.then(release, release);
996
+ return {
997
+ command,
998
+ turn
999
+ };
1000
+ } catch (error) {
1001
+ release();
1002
+ throw error;
1003
+ }
1004
+ })));
903
1005
  });
904
1006
  return {
905
1007
  requestId: input.requestId,
@@ -915,7 +1017,7 @@ var AcpxRuntime = class {
915
1017
  await withTurnDiagnostics(command, () => Promise.reject(error));
916
1018
  }
917
1019
  } },
918
- result: this.finalizeProcessLeaseAfter(input.handle, turnLeasePromise, turnPromise.then(({ command, turn }) => withTurnDiagnostics(command, async () => {
1020
+ result: turnPromise.then(({ command, turn }) => withTurnDiagnostics(command, async () => {
919
1021
  const result = await turn.result;
920
1022
  if (result.status !== "failed" || !isCodexAcpCommand(command) || !isGenericInternalAcpErrorMessage(result.error.message)) return result;
921
1023
  const stderrTail = await this.readCodexTurnFailureStderr({ handle: input.handle });
@@ -928,7 +1030,7 @@ var AcpxRuntime = class {
928
1030
  message: `Internal error: ${stderrTail}`
929
1031
  }
930
1032
  };
931
- }))),
1033
+ })),
932
1034
  cancel(inputArgs) {
933
1035
  return turnPromise.then(({ turn }) => turn.cancel(inputArgs));
934
1036
  },
@@ -937,20 +1039,33 @@ var AcpxRuntime = class {
937
1039
  }
938
1040
  };
939
1041
  }
940
- getCapabilities(input) {
941
- return this.delegate.getCapabilities(input?.handle ? toAcpxResourceInput({ handle: input.handle }) : input);
1042
+ async getCapabilities(input) {
1043
+ const capabilities = await this.delegate.getCapabilities(input?.handle ? toAcpxResourceInput({ handle: input.handle }) : input);
1044
+ return {
1045
+ ...capabilities,
1046
+ controls: capabilities.controls.filter((control) => control !== "session/set_model")
1047
+ };
942
1048
  }
943
1049
  async getStatus(input) {
944
1050
  const snapshot = await this.loadOperationSnapshotForHandle(input.handle);
945
- return this.resolveDelegateForOperationSnapshot(input.handle, snapshot).getStatus(toAcpxResourceInput(input));
1051
+ return this.runInGeneration(input.handle, {
1052
+ generation: snapshot.generation,
1053
+ recordId: snapshot.record?.acpxRecordId
1054
+ }, () => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).getStatus(toAcpxResourceInput(input)));
946
1055
  }
947
1056
  async setMode(input) {
948
1057
  const snapshot = await this.loadOperationSnapshotForHandle(input.handle);
949
- await this.runWithProcessLeaseForHandle(input.handle, snapshot.record, () => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).setMode(toAcpxResourceInput(input)));
1058
+ await this.runInGeneration(input.handle, {
1059
+ generation: snapshot.generation,
1060
+ recordId: snapshot.record?.acpxRecordId
1061
+ }, () => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).setMode(toAcpxResourceInput(input)));
950
1062
  }
951
1063
  async setConfigOption(input) {
952
1064
  const snapshot = await this.loadOperationSnapshotForHandle(input.handle);
953
- return await this.runWithProcessLeaseForHandle(input.handle, snapshot.record, () => this.setConfigOptionUnlocked(input, snapshot));
1065
+ return await this.runInGeneration(input.handle, {
1066
+ generation: snapshot.generation,
1067
+ recordId: snapshot.record?.acpxRecordId
1068
+ }, () => this.setConfigOptionUnlocked(input, snapshot));
954
1069
  }
955
1070
  async setConfigOptionUnlocked(logicalInput, snapshot) {
956
1071
  const { command } = snapshot;
@@ -969,6 +1084,7 @@ var AcpxRuntime = class {
969
1084
  key: "model",
970
1085
  value: override.model
971
1086
  }) : void 0;
1087
+ this.generationRegistry.assertCurrentGeneration(snapshot.generation);
972
1088
  if (override.reasoningEffort) return await delegate.setConfigOption({
973
1089
  ...input,
974
1090
  key: "reasoning_effort",
@@ -995,39 +1111,54 @@ var AcpxRuntime = class {
995
1111
  }
996
1112
  async cancel(input) {
997
1113
  const snapshot = await this.loadOperationSnapshotForHandle(input.handle);
998
- await this.resolveDelegateForOperationSnapshot(input.handle, snapshot).cancel(toAcpxResourceInput(input));
1114
+ await this.runInGeneration(input.handle, {
1115
+ generation: snapshot.generation,
1116
+ recordId: snapshot.record?.acpxRecordId
1117
+ }, () => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).cancel(toAcpxResourceInput(input)));
999
1118
  }
1000
1119
  async prepareFreshSession(input) {
1001
1120
  const resource = assertAcpxSessionOwnerLocator(input, this.legacyBareSessionKeys);
1002
- this.sessionStore.markFresh(resource);
1121
+ this.generationRegistry.prepareFresh(resource);
1003
1122
  this.legacyBareSessionKeys.delete(resource);
1004
1123
  }
1005
1124
  async close(input) {
1006
- const snapshot = await this.loadOperationSnapshotForHandle(input.handle);
1007
- const closeLease = await this.prepareProcessLeaseForOperation(input.handle, snapshot.record);
1008
- let cleanupSucceeded = false;
1009
- try {
1125
+ const generation = this.generationForHandle(input.handle);
1126
+ await this.runInGeneration(input.handle, { generation }, async () => {
1127
+ const snapshot = await this.loadOperationSnapshotForHandle(input.handle, true, generation);
1010
1128
  const delegate = this.resolveDelegateForOperationSnapshot(input.handle, snapshot);
1011
- const handle = toAcpxResourceInput(input).handle;
1012
- try {
1013
- await delegate.close({
1014
- handle,
1015
- reason: input.reason,
1016
- discardPersistentState: input.discardPersistentState
1129
+ await acpxOperationScope.run({
1130
+ generation,
1131
+ closeRecord: snapshot.record
1132
+ }, async () => {
1133
+ if (input.discardPersistentState && decodeAcpxRuntimeHandleState(input.handle.runtimeSessionName)?.mode !== "oneshot") {
1134
+ this.generationRegistry.retireGeneration(generation);
1135
+ this.legacyBareSessionKeys.delete(generation.resource);
1136
+ }
1137
+ const cleanup = await prepareAcpxProcessCleanup({
1138
+ record: snapshot.record,
1139
+ command: snapshot.command,
1140
+ sessionKey: resolveAcpxSessionResource(input.handle),
1141
+ gatewayInstanceId: this.gatewayInstanceId,
1142
+ wrapperRoot: this.wrapperRoot,
1143
+ leaseStore: this.processLeaseStore,
1144
+ deps: this.processCleanupDeps
1145
+ }).catch((error) => async () => {
1146
+ throw error;
1017
1147
  });
1018
- if (this.managedToolsSessionDelegates.get(handle.sessionKey) === delegate) this.managedToolsSessionDelegates.delete(handle.sessionKey);
1019
- } finally {
1020
- await this.cleanupProcessTreeForRecord(input.handle, snapshot.record);
1021
- cleanupSucceeded = true;
1022
- }
1023
- if (input.discardPersistentState) await this.prepareFreshSession({
1024
- ...input.handle,
1025
- persistedHandle: input.handle
1148
+ try {
1149
+ await delegate.close(toAcpxResourceInput(input));
1150
+ } finally {
1151
+ await cleanup();
1152
+ }
1153
+ const recordId = snapshot.record?.acpxRecordId ?? input.handle.acpxRecordId ?? generation.resource;
1154
+ const currentRecord = generation.records.get(recordId);
1155
+ if (!currentRecord || currentRecord.acpSessionId === snapshot.record?.acpSessionId && currentRecord.createdAt === snapshot.record?.createdAt) {
1156
+ generation.records.delete(recordId);
1157
+ if (generation.activeRecordOperations.has(recordId)) generation.closedRecordIds.add(recordId);
1158
+ }
1159
+ generation.closeCompleted = true;
1026
1160
  });
1027
- } finally {
1028
- if (cleanupSucceeded) await this.finalizeProcessLeaseForOperation(input.handle, closeLease);
1029
- else await this.releaseProcessLeaseAfterUncertainFailure(closeLease);
1030
- }
1161
+ });
1031
1162
  }
1032
1163
  };
1033
1164
  /** Test-only hooks for ACPX runtime behavior that is otherwise private. */