@penclipai/adapter-utils 2026.704.0 → 2026.714.0

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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/dist/acpx-engine/cli.d.ts +2 -0
  3. package/dist/acpx-engine/cli.d.ts.map +1 -0
  4. package/dist/acpx-engine/cli.js +128 -0
  5. package/dist/acpx-engine/cli.js.map +1 -0
  6. package/dist/acpx-engine/constants.d.ts +16 -0
  7. package/dist/acpx-engine/constants.d.ts.map +1 -0
  8. package/dist/acpx-engine/constants.js +18 -0
  9. package/dist/acpx-engine/constants.js.map +1 -0
  10. package/dist/acpx-engine/execute.d.ts +57 -0
  11. package/dist/acpx-engine/execute.d.ts.map +1 -0
  12. package/dist/acpx-engine/execute.js +1849 -0
  13. package/dist/acpx-engine/execute.js.map +1 -0
  14. package/dist/acpx-engine/execute.test.d.ts +2 -0
  15. package/dist/acpx-engine/execute.test.d.ts.map +1 -0
  16. package/dist/acpx-engine/execute.test.js +1332 -0
  17. package/dist/acpx-engine/execute.test.js.map +1 -0
  18. package/dist/acpx-engine/index.d.ts +6 -0
  19. package/dist/acpx-engine/index.d.ts.map +1 -0
  20. package/dist/acpx-engine/index.js +6 -0
  21. package/dist/acpx-engine/index.js.map +1 -0
  22. package/dist/acpx-engine/session-codec.d.ts +3 -0
  23. package/dist/acpx-engine/session-codec.d.ts.map +1 -0
  24. package/dist/acpx-engine/session-codec.js +48 -0
  25. package/dist/acpx-engine/session-codec.js.map +1 -0
  26. package/dist/acpx-engine/ui.d.ts +3 -0
  27. package/dist/acpx-engine/ui.d.ts.map +1 -0
  28. package/dist/acpx-engine/ui.js +162 -0
  29. package/dist/acpx-engine/ui.js.map +1 -0
  30. package/dist/execution-target-sandbox.test.js +416 -5
  31. package/dist/execution-target-sandbox.test.js.map +1 -1
  32. package/dist/execution-target.d.ts +37 -1
  33. package/dist/execution-target.d.ts.map +1 -1
  34. package/dist/execution-target.js +488 -9
  35. package/dist/execution-target.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  40. package/dist/sandbox-callback-bridge.js +9 -4
  41. package/dist/sandbox-callback-bridge.js.map +1 -1
  42. package/dist/sandbox-callback-bridge.test.js +4 -0
  43. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  44. package/dist/server-utils.d.ts +18 -0
  45. package/dist/server-utils.d.ts.map +1 -1
  46. package/dist/server-utils.js +85 -19
  47. package/dist/server-utils.js.map +1 -1
  48. package/dist/server-utils.test.js +256 -8
  49. package/dist/server-utils.test.js.map +1 -1
  50. package/dist/session-compaction.d.ts.map +1 -1
  51. package/dist/session-compaction.js +0 -6
  52. package/dist/session-compaction.js.map +1 -1
  53. package/dist/types.d.ts +35 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +5 -1
@@ -0,0 +1,1849 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ import { adapterExecutionTargetSessionIdentity, formatAdapterExecutionTimeoutErrorMessage, formatAdapterExecutionTimeoutStartLogLine, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeout, startAdapterExecutionTargetPaperclipBridge, startAdapterExecutionTargetProcessSessionBridge, } from "@penclipai/adapter-utils/execution-target";
7
+ import { DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, applyPaperclipWorkspaceEnv, asNumber, asString, buildInvocationEnvForLogs, buildPaperclipEnv, ensureAbsoluteDirectory, ensurePathInEnv, ensurePaperclipSkillSymlink, joinPromptSections, materializePaperclipSkillCopy, parseObject, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, renderPaperclipWakePrompt, renderTemplate, runChildProcess, resolvePaperclipInstanceRootForAdapter, resolvePaperclipDesiredSkillNames, removeMaintainerOnlySkillSymlinks, rewriteWorkspaceCwdEnvVarsForExecution, shapePaperclipWorkspaceEnvForExecution, stringifyPaperclipWakePayload, } from "@penclipai/adapter-utils/server-utils";
8
+ import { shellQuote } from "@penclipai/adapter-utils/ssh";
9
+ import { createAcpRuntime, createAgentRegistry, createRuntimeStore, isAcpRuntimeError, } from "acpx/runtime";
10
+ import { DEFAULT_ACP_ENGINE_AGENT, DEFAULT_ACP_ENGINE_MODE, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACP_ENGINE_PERMISSION_MODE, DEFAULT_ACP_ENGINE_TIMEOUT_SEC, DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS, } from "./constants.js";
11
+ const defaultModuleDir = path.dirname(fileURLToPath(import.meta.url));
12
+ const WRAPPER_CLEANUP_RETENTION_MS = 15 * 60 * 1000;
13
+ const PAPERCLIP_MANAGED_CODEX_SKILLS_MANIFEST = ".paperclip-managed-skills.json";
14
+ const defaultWarmHandles = new Map();
15
+ function resolveEngineSettings(options) {
16
+ const moduleDir = path.resolve(options.moduleDir ?? defaultModuleDir);
17
+ return {
18
+ adapterType: options.adapterType?.trim() || "acp_engine",
19
+ moduleDir,
20
+ packageRootDir: path.resolve(options.packageRootDir ?? path.resolve(moduleDir, "../..")),
21
+ };
22
+ }
23
+ function stableJson(value) {
24
+ if (Array.isArray(value))
25
+ return `[${value.map(stableJson).join(",")}]`;
26
+ if (value && typeof value === "object") {
27
+ return `{${Object.entries(value)
28
+ .sort(([a], [b]) => a.localeCompare(b))
29
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
30
+ .join(",")}}`;
31
+ }
32
+ return JSON.stringify(value);
33
+ }
34
+ function shortHash(value) {
35
+ return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 16);
36
+ }
37
+ function defaultPaperclipInstanceDir() {
38
+ const home = process.env.PAPERCLIP_HOME?.trim() || path.join(os.homedir(), ".paperclip");
39
+ const instanceId = process.env.PAPERCLIP_INSTANCE_ID?.trim() || "default";
40
+ return resolvePaperclipInstanceRootForAdapter({
41
+ homeDir: home,
42
+ instanceId,
43
+ });
44
+ }
45
+ function defaultStateDir(companyId, agentId) {
46
+ return path.join(defaultPaperclipInstanceDir(), "companies", companyId, "acp-engine", "agents", agentId);
47
+ }
48
+ function resolveManagedCodexHomeDir(companyId) {
49
+ return path.join(defaultPaperclipInstanceDir(), "companies", companyId, "codex-home");
50
+ }
51
+ // Walk up from startDir looking for `node_modules/.bin/<binName>`. This matches
52
+ // npm/pnpm binary hoisting in packaged installs while preserving monorepo dev.
53
+ export async function findAncestorBin(startDir, binName) {
54
+ let current = path.resolve(startDir);
55
+ while (true) {
56
+ const candidate = path.join(current, "node_modules", ".bin", binName);
57
+ if (await pathExists(candidate))
58
+ return candidate;
59
+ const parent = path.dirname(current);
60
+ if (parent === current)
61
+ return null;
62
+ current = parent;
63
+ }
64
+ }
65
+ async function resolveBuiltInAgentCommand(input) {
66
+ const { agent, packageRootDir, executionTargetIsRemote } = input;
67
+ if (agent === "gemini") {
68
+ return { command: "gemini --acp", shellCommand: "gemini --acp" };
69
+ }
70
+ const binName = agent === "claude" ? "claude-agent-acp" : agent === "codex" ? "codex-acp" : null;
71
+ if (!binName)
72
+ return null;
73
+ if (executionTargetIsRemote) {
74
+ return { command: binName, shellCommand: binName };
75
+ }
76
+ const resolved = (await findAncestorBin(packageRootDir, binName)) ?? binName;
77
+ return { command: resolved, shellCommand: shellQuote(resolved) };
78
+ }
79
+ // Gemini CLI renamed --experimental-acp to --acp in 0.33.0. acpx normally
80
+ // rewrites the flag itself, but the agent wrapper script hides the gemini
81
+ // command from acpx's detection, so the engine must downgrade it here.
82
+ const GEMINI_NATIVE_ACP_FLAG_MIN_VERSION = [0, 33, 0];
83
+ const GEMINI_VERSION_PROBE_TIMEOUT_MS = 2000;
84
+ export function parseGeminiVersionParts(output) {
85
+ const match = output?.match(/(\d+)\.(\d+)\.(\d+)/);
86
+ if (!match)
87
+ return null;
88
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
89
+ }
90
+ export function geminiVersionSupportsNativeAcpFlag(parts) {
91
+ if (!parts)
92
+ return true;
93
+ for (let index = 0; index < GEMINI_NATIVE_ACP_FLAG_MIN_VERSION.length; index += 1) {
94
+ const diff = (parts[index] ?? 0) - GEMINI_NATIVE_ACP_FLAG_MIN_VERSION[index];
95
+ if (diff !== 0)
96
+ return diff > 0;
97
+ }
98
+ return true;
99
+ }
100
+ export function rewriteGeminiAcpFlagForVersion(commandShell, versionParts) {
101
+ if (geminiVersionSupportsNativeAcpFlag(versionParts))
102
+ return commandShell;
103
+ return commandShell
104
+ .trim()
105
+ .split(/\s+/)
106
+ .map((token) => (token === "--acp" ? "--experimental-acp" : token))
107
+ .join(" ");
108
+ }
109
+ function geminiAcpCommandTokens(commandShell) {
110
+ const tokens = commandShell.trim().split(/\s+/);
111
+ const bin = tokens[0];
112
+ if (!bin || bin.startsWith("'") || bin.startsWith('"'))
113
+ return null;
114
+ if (path.basename(bin) !== "gemini")
115
+ return null;
116
+ if (!tokens.includes("--acp"))
117
+ return null;
118
+ return tokens;
119
+ }
120
+ async function normalizeGeminiAcpCommandShell(commandShell, env) {
121
+ const tokens = geminiAcpCommandTokens(commandShell);
122
+ if (!tokens)
123
+ return commandShell;
124
+ let versionParts = null;
125
+ try {
126
+ const probeEnv = Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
127
+ const result = await runChildProcess(`gemini-version-probe-${randomUUID()}`, tokens[0], ["--version"], {
128
+ cwd: process.cwd(),
129
+ env: probeEnv,
130
+ timeoutSec: Math.max(1, Math.ceil(GEMINI_VERSION_PROBE_TIMEOUT_MS / 1000)),
131
+ graceSec: 1,
132
+ onLog: async () => { },
133
+ });
134
+ if (result.timedOut || result.exitCode !== 0)
135
+ return commandShell;
136
+ versionParts = parseGeminiVersionParts(result.stdout);
137
+ }
138
+ catch {
139
+ return commandShell;
140
+ }
141
+ return rewriteGeminiAcpFlagForVersion(commandShell, versionParts);
142
+ }
143
+ function normalizeAgent(config) {
144
+ const agent = asString(config.agent, DEFAULT_ACP_ENGINE_AGENT).trim();
145
+ return agent || DEFAULT_ACP_ENGINE_AGENT;
146
+ }
147
+ async function pathExists(candidate) {
148
+ return fs.access(candidate).then(() => true).catch(() => false);
149
+ }
150
+ async function ensureParentDir(target) {
151
+ await fs.mkdir(path.dirname(target), { recursive: true });
152
+ }
153
+ async function writeFileAtomically(input) {
154
+ await ensureParentDir(input.target);
155
+ const tempPath = `${input.target}.tmp-${process.pid}-${randomUUID()}`;
156
+ const handle = await fs.open(tempPath, "wx", input.mode);
157
+ try {
158
+ await handle.writeFile(input.contents, "utf8");
159
+ await handle.close();
160
+ await fs.rename(tempPath, input.target);
161
+ await fs.chmod(input.target, input.mode).catch(() => { });
162
+ }
163
+ catch (err) {
164
+ await handle.close().catch(() => { });
165
+ await fs.rm(tempPath, { force: true }).catch(() => { });
166
+ throw err;
167
+ }
168
+ }
169
+ async function ensureSymlink(target, source) {
170
+ const resolvedSource = path.resolve(source);
171
+ const existing = await fs.lstat(target).catch(() => null);
172
+ if (!existing) {
173
+ await ensureParentDir(target);
174
+ await linkOrCopySharedFile(resolvedSource, target);
175
+ return;
176
+ }
177
+ if (process.platform === "win32" && !existing.isSymbolicLink()) {
178
+ await fs.copyFile(resolvedSource, target);
179
+ return;
180
+ }
181
+ if (!existing.isSymbolicLink()) {
182
+ await fs.rm(target, { recursive: true, force: true });
183
+ await linkOrCopySharedFile(resolvedSource, target);
184
+ return;
185
+ }
186
+ const linkedPath = await fs.readlink(target).catch(() => null);
187
+ if (!linkedPath)
188
+ return;
189
+ const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
190
+ if (resolvedLinkedPath === resolvedSource)
191
+ return;
192
+ await fs.unlink(target);
193
+ await linkOrCopySharedFile(resolvedSource, target);
194
+ }
195
+ async function linkOrCopySharedFile(source, target) {
196
+ if (process.platform === "win32") {
197
+ try {
198
+ await fs.link(source, target);
199
+ }
200
+ catch {
201
+ await fs.copyFile(source, target);
202
+ }
203
+ return;
204
+ }
205
+ await fs.symlink(source, target);
206
+ }
207
+ async function ensureCopiedFile(target, source) {
208
+ if (await pathExists(target))
209
+ return;
210
+ await ensureParentDir(target);
211
+ await fs.copyFile(source, target);
212
+ }
213
+ async function prepareManagedCodexHome(input) {
214
+ const { sourceHome, targetHome, onLog } = input;
215
+ if (path.resolve(sourceHome) === path.resolve(targetHome))
216
+ return targetHome;
217
+ await fs.mkdir(targetHome, { recursive: true });
218
+ const authJson = path.join(sourceHome, "auth.json");
219
+ if (await pathExists(authJson))
220
+ await ensureSymlink(path.join(targetHome, "auth.json"), authJson);
221
+ for (const name of ["config.json", "config.toml", "instructions.md"]) {
222
+ const source = path.join(sourceHome, name);
223
+ if (await pathExists(source))
224
+ await ensureCopiedFile(path.join(targetHome, name), source);
225
+ }
226
+ await onLog("stdout", `[paperclip] Using Paperclip-managed ACPX Codex home "${targetHome}" (seeded from "${sourceHome}").\n`);
227
+ return targetHome;
228
+ }
229
+ async function hashPathContents(candidate, hash, relativePath, seenDirectories) {
230
+ const stat = await fs.lstat(candidate);
231
+ if (stat.isSymbolicLink()) {
232
+ hash.update(`symlink-skipped:${relativePath}\n`);
233
+ return;
234
+ }
235
+ if (stat.isDirectory()) {
236
+ const realDir = await fs.realpath(candidate).catch(() => candidate);
237
+ hash.update(`dir:${relativePath}\n`);
238
+ if (seenDirectories.has(realDir)) {
239
+ hash.update("loop\n");
240
+ return;
241
+ }
242
+ seenDirectories.add(realDir);
243
+ const entries = await fs.readdir(candidate, { withFileTypes: true });
244
+ entries.sort((left, right) => left.name.localeCompare(right.name));
245
+ for (const entry of entries) {
246
+ const childRelativePath = relativePath.length > 0 ? `${relativePath}/${entry.name}` : entry.name;
247
+ await hashPathContents(path.join(candidate, entry.name), hash, childRelativePath, seenDirectories);
248
+ }
249
+ return;
250
+ }
251
+ if (stat.isFile()) {
252
+ hash.update(`file:${relativePath}\n`);
253
+ hash.update(await fs.readFile(candidate));
254
+ hash.update("\n");
255
+ return;
256
+ }
257
+ hash.update(`other:${relativePath}:${stat.mode}\n`);
258
+ }
259
+ async function buildSkillSetKey(input) {
260
+ const hash = createHash("sha256");
261
+ hash.update(`paperclip-acpx-${input.label}-skills:v1\n`);
262
+ const sorted = [...input.skills].sort((left, right) => left.runtimeName.localeCompare(right.runtimeName));
263
+ for (const entry of sorted) {
264
+ hash.update(`skill:${entry.key}:${entry.runtimeName}\n`);
265
+ await hashPathContents(entry.source, hash, entry.runtimeName, new Set());
266
+ }
267
+ return hash.digest("hex");
268
+ }
269
+ async function resolveSelectedRuntimeSkills(config, moduleDir) {
270
+ const allSkills = await readPaperclipRuntimeSkillEntries(config, moduleDir);
271
+ const desiredSkillNames = resolvePaperclipDesiredSkillNames(config, allSkills);
272
+ const desiredSet = new Set(desiredSkillNames);
273
+ return {
274
+ allSkills,
275
+ selectedSkills: allSkills.filter((entry) => desiredSet.has(entry.key)),
276
+ desiredSkillNames,
277
+ };
278
+ }
279
+ async function prepareClaudeSkillRuntime(input) {
280
+ const { allSkills, selectedSkills, desiredSkillNames } = await resolveSelectedRuntimeSkills(input.config, input.moduleDir);
281
+ const skillSetKey = await buildSkillSetKey({ skills: selectedSkills, label: "claude" });
282
+ const bundleRoot = path.join(input.stateDir, "runtime-skills", "claude", skillSetKey);
283
+ const skillsHome = path.join(bundleRoot, ".claude", "skills");
284
+ await fs.mkdir(skillsHome, { recursive: true });
285
+ for (const entry of selectedSkills) {
286
+ const target = path.join(skillsHome, entry.runtimeName);
287
+ try {
288
+ const result = await materializePaperclipSkillCopy(entry.source, target);
289
+ if (result.skippedSymlinks.length > 0) {
290
+ await input.onLog("stdout", `[paperclip] Materialized ACPX Claude skill "${entry.runtimeName}" into ${skillsHome} and skipped ${result.skippedSymlinks.length} symlink(s).\n`);
291
+ }
292
+ }
293
+ catch (err) {
294
+ await input.onLog("stderr", `[paperclip] Failed to materialize ACPX Claude skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
295
+ }
296
+ }
297
+ const selectedNames = selectedSkills.map((entry) => entry.runtimeName).sort();
298
+ const promptInstructions = selectedSkills.length > 0
299
+ ? [
300
+ "Paperclip has materialized selected runtime skills for this ACPX Claude session.",
301
+ `Skill root: ${skillsHome}`,
302
+ selectedNames.length > 0 ? `Selected skills: ${selectedNames.join(", ")}` : "",
303
+ "When a task calls for one of these skills, read its SKILL.md from that root and follow it.",
304
+ ].filter(Boolean).join("\n")
305
+ : "";
306
+ return {
307
+ identity: {
308
+ mode: "claude",
309
+ skillSetKey,
310
+ desiredSkillNames,
311
+ selectedSkills: selectedNames,
312
+ skillRoot: selectedSkills.length > 0 ? skillsHome : null,
313
+ },
314
+ promptInstructions,
315
+ commandNotes: selectedSkills.length > 0
316
+ ? [`Materialized ${selectedSkills.length} Paperclip skill(s) for ACPX Claude at ${skillsHome}.`]
317
+ : [],
318
+ };
319
+ }
320
+ async function readManagedCodexSkillsManifest(skillsHome) {
321
+ const manifestPath = path.join(skillsHome, PAPERCLIP_MANAGED_CODEX_SKILLS_MANIFEST);
322
+ try {
323
+ const raw = JSON.parse(await fs.readFile(manifestPath, "utf8"));
324
+ const parsed = parseObject(raw);
325
+ const skills = Array.isArray(parsed.managedSkillNames)
326
+ ? parsed.managedSkillNames.filter((value) => typeof value === "string" && value.trim().length > 0)
327
+ : [];
328
+ return new Set(skills);
329
+ }
330
+ catch {
331
+ return new Set();
332
+ }
333
+ }
334
+ async function writeManagedCodexSkillsManifest(skillsHome, skillNames) {
335
+ const managedSkillNames = Array.from(new Set(skillNames)).sort();
336
+ await fs.writeFile(path.join(skillsHome, PAPERCLIP_MANAGED_CODEX_SKILLS_MANIFEST), `${JSON.stringify({ version: 1, managedSkillNames }, null, 2)}\n`, "utf8");
337
+ }
338
+ async function removeSkillTarget(target) {
339
+ const existing = await fs.lstat(target).catch(() => null);
340
+ if (!existing)
341
+ return false;
342
+ await fs.rm(target, { recursive: true, force: true });
343
+ return true;
344
+ }
345
+ async function reconcileManagedCodexSkills(input) {
346
+ const desired = new Set(input.selectedSkills.map((entry) => entry.runtimeName));
347
+ const managed = await readManagedCodexSkillsManifest(input.skillsHome);
348
+ const availableByRuntimeName = new Map(input.allSkills.map((entry) => [entry.runtimeName, entry]));
349
+ for (const name of managed) {
350
+ if (desired.has(name))
351
+ continue;
352
+ if (await removeSkillTarget(path.join(input.skillsHome, name))) {
353
+ await input.onLog("stdout", `[paperclip] Revoked ACPX Codex skill "${name}" from ${input.skillsHome}\n`);
354
+ }
355
+ }
356
+ for (const entry of input.allSkills) {
357
+ if (desired.has(entry.runtimeName) || managed.has(entry.runtimeName))
358
+ continue;
359
+ const target = path.join(input.skillsHome, entry.runtimeName);
360
+ const existing = await fs.lstat(target).catch(() => null);
361
+ if (!existing?.isSymbolicLink())
362
+ continue;
363
+ const linkedPath = await fs.readlink(target).catch(() => null);
364
+ if (!linkedPath)
365
+ continue;
366
+ const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
367
+ if (resolvedLinkedPath !== path.resolve(entry.source))
368
+ continue;
369
+ if (await removeSkillTarget(target)) {
370
+ await input.onLog("stdout", `[paperclip] Revoked legacy ACPX Codex skill "${entry.runtimeName}" from ${input.skillsHome}\n`);
371
+ }
372
+ }
373
+ for (const name of managed) {
374
+ if (desired.has(name) || availableByRuntimeName.has(name))
375
+ continue;
376
+ if (await removeSkillTarget(path.join(input.skillsHome, name))) {
377
+ await input.onLog("stdout", `[paperclip] Revoked unavailable ACPX Codex skill "${name}" from ${input.skillsHome}\n`);
378
+ }
379
+ }
380
+ }
381
+ async function prepareCodexSkillRuntime(input) {
382
+ const envConfig = parseObject(input.config.env);
383
+ const configuredCodexHome = typeof envConfig.CODEX_HOME === "string" && envConfig.CODEX_HOME.trim().length > 0
384
+ ? path.resolve(envConfig.CODEX_HOME.trim())
385
+ : null;
386
+ const sourceCodexHome = typeof process.env.CODEX_HOME === "string" && process.env.CODEX_HOME.trim().length > 0
387
+ ? path.resolve(process.env.CODEX_HOME.trim())
388
+ : path.join(os.homedir(), ".codex");
389
+ const managedCodexHome = resolveManagedCodexHomeDir(input.companyId);
390
+ const effectiveCodexHome = configuredCodexHome ??
391
+ await prepareManagedCodexHome({
392
+ companyId: input.companyId,
393
+ sourceHome: sourceCodexHome,
394
+ targetHome: managedCodexHome,
395
+ onLog: input.onLog,
396
+ });
397
+ const { allSkills, selectedSkills, desiredSkillNames } = await resolveSelectedRuntimeSkills(input.config, input.moduleDir);
398
+ const skillSetKey = await buildSkillSetKey({ skills: selectedSkills, label: "codex" });
399
+ const skillsHome = path.join(effectiveCodexHome, "skills");
400
+ await fs.mkdir(skillsHome, { recursive: true });
401
+ await reconcileManagedCodexSkills({
402
+ skillsHome,
403
+ allSkills,
404
+ selectedSkills,
405
+ onLog: input.onLog,
406
+ });
407
+ for (const entry of selectedSkills) {
408
+ const target = path.join(skillsHome, entry.runtimeName);
409
+ try {
410
+ const result = await materializePaperclipSkillCopy(entry.source, target);
411
+ if (result.skippedSymlinks.length > 0) {
412
+ await input.onLog("stdout", `[paperclip] Materialized ACPX Codex skill "${entry.runtimeName}" into ${skillsHome} and skipped ${result.skippedSymlinks.length} symlink(s).\n`);
413
+ }
414
+ }
415
+ catch (err) {
416
+ await input.onLog("stderr", `[paperclip] Failed to inject ACPX Codex skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
417
+ }
418
+ }
419
+ await writeManagedCodexSkillsManifest(skillsHome, selectedSkills.map((entry) => entry.runtimeName));
420
+ input.env.CODEX_HOME = effectiveCodexHome;
421
+ return {
422
+ identity: {
423
+ mode: "codex",
424
+ skillSetKey,
425
+ desiredSkillNames,
426
+ selectedSkills: selectedSkills.map((entry) => entry.runtimeName).sort(),
427
+ codexHome: effectiveCodexHome,
428
+ skillsHome,
429
+ },
430
+ commandNotes: [`Prepared ACPX Codex skill home at ${skillsHome}.`],
431
+ };
432
+ }
433
+ function resolveGeminiSkillsHome(config) {
434
+ const envConfig = parseObject(config.env);
435
+ const configuredHome = typeof envConfig.HOME === "string" && envConfig.HOME.trim().length > 0
436
+ ? path.resolve(envConfig.HOME.trim())
437
+ : os.homedir();
438
+ return path.join(configuredHome, ".gemini", "skills");
439
+ }
440
+ async function prepareGeminiSkillRuntime(input) {
441
+ const { selectedSkills, desiredSkillNames } = await resolveSelectedRuntimeSkills(input.config, input.moduleDir);
442
+ const skillSetKey = await buildSkillSetKey({ skills: selectedSkills, label: "gemini" });
443
+ const skillsHome = resolveGeminiSkillsHome(input.config);
444
+ await fs.mkdir(skillsHome, { recursive: true });
445
+ const allowedSkillNames = selectedSkills.map((entry) => entry.runtimeName);
446
+ const removedSkills = await removeMaintainerOnlySkillSymlinks(skillsHome, allowedSkillNames);
447
+ for (const skillName of removedSkills) {
448
+ await input.onLog("stdout", `[paperclip] Removed maintainer-only ACPX Gemini skill "${skillName}" from ${skillsHome}\n`);
449
+ }
450
+ for (const entry of selectedSkills) {
451
+ const target = path.join(skillsHome, entry.runtimeName);
452
+ try {
453
+ const result = await ensurePaperclipSkillSymlink(entry.source, target);
454
+ if (result === "created" || result === "repaired") {
455
+ await input.onLog("stdout", `[paperclip] ${result === "repaired" ? "Repaired" : "Linked"} ACPX Gemini skill "${entry.runtimeName}" into ${skillsHome}\n`);
456
+ }
457
+ }
458
+ catch (err) {
459
+ await input.onLog("stderr", `[paperclip] Failed to link ACPX Gemini skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
460
+ }
461
+ }
462
+ return {
463
+ identity: {
464
+ mode: "gemini",
465
+ skillSetKey,
466
+ desiredSkillNames,
467
+ selectedSkills: selectedSkills.map((entry) => entry.runtimeName).sort(),
468
+ skillsHome,
469
+ },
470
+ commandNotes: selectedSkills.length > 0
471
+ ? [`Prepared ${selectedSkills.length} ACPX Gemini skill(s) at ${skillsHome}.`]
472
+ : [],
473
+ };
474
+ }
475
+ function normalizeMode(config) {
476
+ return asString(config.mode, DEFAULT_ACP_ENGINE_MODE) === "oneshot" ? "oneshot" : "persistent";
477
+ }
478
+ function normalizePermissionMode(config) {
479
+ const value = asString(config.permissionMode, DEFAULT_ACP_ENGINE_PERMISSION_MODE).trim();
480
+ if (value === "approve-reads" || value === "deny-all")
481
+ return value;
482
+ if (value === "default")
483
+ return "approve-reads";
484
+ return "approve-all";
485
+ }
486
+ function normalizeNonInteractivePermissions(config) {
487
+ return asString(config.nonInteractivePermissions, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS) === "fail"
488
+ ? "fail"
489
+ : "deny";
490
+ }
491
+ function normalizeRequestedThinkingEffort(config) {
492
+ return (asString(config.modelReasoningEffort, "") ||
493
+ asString(config.reasoningEffort, "") ||
494
+ asString(config.thinkingEffort, "") ||
495
+ asString(config.effort, "")).trim();
496
+ }
497
+ function isCompatibleSession(params, runtime) {
498
+ if (asString(params.configFingerprint, "") !== runtime.fingerprint)
499
+ return false;
500
+ if (asString(params.sessionKey, "") !== runtime.sessionKey)
501
+ return false;
502
+ if (asString(params.agent, "") !== runtime.acpxAgent)
503
+ return false;
504
+ if (asString(params.mode, "") !== runtime.mode)
505
+ return false;
506
+ const savedCwd = asString(params.cwd, "");
507
+ if (!savedCwd || path.resolve(savedCwd) !== path.resolve(runtime.cwd))
508
+ return false;
509
+ const savedRemote = parseObject(params.remoteExecution);
510
+ return stableJson(savedRemote) === stableJson(runtime.remoteExecutionIdentity ?? {});
511
+ }
512
+ function buildSessionParams(input) {
513
+ const { prepared, handle } = input;
514
+ return {
515
+ sessionKey: prepared.sessionKey,
516
+ runtimeSessionName: handle.runtimeSessionName,
517
+ acpxRecordId: handle.acpxRecordId,
518
+ acpSessionId: handle.backendSessionId,
519
+ agentSessionId: handle.agentSessionId,
520
+ agent: prepared.acpxAgent,
521
+ cwd: prepared.cwd,
522
+ mode: prepared.mode,
523
+ stateDir: prepared.stateDir,
524
+ configFingerprint: prepared.fingerprint,
525
+ ...(prepared.requestedModel ? { model: prepared.requestedModel } : {}),
526
+ ...(prepared.requestedThinkingEffort ? { thinkingEffort: prepared.requestedThinkingEffort } : {}),
527
+ ...(prepared.fastMode ? { fastMode: true } : {}),
528
+ skills: prepared.skillsIdentity,
529
+ ...(prepared.workspaceId ? { workspaceId: prepared.workspaceId } : {}),
530
+ ...(prepared.workspaceRepoUrl ? { repoUrl: prepared.workspaceRepoUrl } : {}),
531
+ ...(prepared.workspaceRepoRef ? { repoRef: prepared.workspaceRepoRef } : {}),
532
+ ...(prepared.remoteExecutionIdentity ? { remoteExecution: prepared.remoteExecutionIdentity } : {}),
533
+ };
534
+ }
535
+ function uniqueSorted(values) {
536
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))].sort();
537
+ }
538
+ // The Claude Code SDK that `claude-agent-acp` runs uses
539
+ // `settingSources: ["user", "project", "local"]`. By writing a per-worktree
540
+ // `.claude/settings.local.json` we override the user's potentially-restrictive
541
+ // `~/.claude/settings.json` (e.g. `defaultMode: "dontAsk"`, which silently
542
+ // denies every non-allowlisted tool and never reaches `canUseTool`), and we
543
+ // widen the SDK's Read sandbox to include the Paperclip state dirs the agent
544
+ // needs to talk to its own control plane.
545
+ async function writePaperclipClaudeSettings(input) {
546
+ const filePath = path.join(input.cwd, ".claude", "settings.local.json");
547
+ const instanceRoot = defaultPaperclipInstanceDir();
548
+ const companyRoot = path.join(instanceRoot, "companies", input.companyId);
549
+ const paperclipAdditionalDirectories = uniqueSorted([
550
+ input.stateDir,
551
+ input.agentHome,
552
+ companyRoot,
553
+ ]);
554
+ const paperclipAllow = uniqueSorted([
555
+ "Bash(curl:*)",
556
+ "Bash(env:*)",
557
+ "Bash(env)",
558
+ `Bash(${input.cwd}/scripts/paperclip-issue-update.sh:*)`,
559
+ `Bash(${input.cwd}/scripts/paperclip:*)`,
560
+ ]);
561
+ let existing = {};
562
+ const existingRaw = await fs.readFile(filePath, "utf8").catch(() => null);
563
+ if (existingRaw) {
564
+ try {
565
+ const parsed = JSON.parse(existingRaw);
566
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
567
+ existing = parsed;
568
+ }
569
+ catch {
570
+ // Malformed settings file — leave it alone in `existing` and our merge will replace it with a valid one.
571
+ }
572
+ }
573
+ const existingPerms = existing.permissions && typeof existing.permissions === "object" && !Array.isArray(existing.permissions)
574
+ ? existing.permissions
575
+ : {};
576
+ const existingAllow = Array.isArray(existingPerms.allow)
577
+ ? existingPerms.allow.filter((value) => typeof value === "string")
578
+ : [];
579
+ const existingAdditionalDirectories = Array.isArray(existingPerms.additionalDirectories)
580
+ ? existingPerms.additionalDirectories.filter((value) => typeof value === "string")
581
+ : [];
582
+ const mergedAllow = uniqueSorted([...existingAllow, ...paperclipAllow]);
583
+ const mergedAdditionalDirectories = uniqueSorted([
584
+ ...existingAdditionalDirectories,
585
+ ...paperclipAdditionalDirectories,
586
+ ]);
587
+ const existingDefaultMode = typeof existingPerms.defaultMode === "string" ? existingPerms.defaultMode : "";
588
+ const defaultMode = existingDefaultMode && existingDefaultMode !== "dontAsk" ? existingDefaultMode : "default";
589
+ const overrodeDontAsk = existingDefaultMode === "dontAsk";
590
+ const nextPermissions = {
591
+ ...existingPerms,
592
+ allow: mergedAllow,
593
+ additionalDirectories: mergedAdditionalDirectories,
594
+ defaultMode,
595
+ };
596
+ const next = { ...existing, permissions: nextPermissions };
597
+ await writeFileAtomically({
598
+ target: filePath,
599
+ contents: `${JSON.stringify(next, null, 2)}\n`,
600
+ mode: 0o600,
601
+ });
602
+ return {
603
+ filePath,
604
+ allow: mergedAllow,
605
+ additionalDirectories: mergedAdditionalDirectories,
606
+ defaultMode,
607
+ overrodeDontAsk,
608
+ };
609
+ }
610
+ async function writeAgentWrapper(input) {
611
+ const wrappersDir = path.join(input.stateDir, "wrappers");
612
+ await fs.mkdir(wrappersDir, { recursive: true });
613
+ const envLines = Object.entries(input.env)
614
+ .filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
615
+ .sort(([left], [right]) => left.localeCompare(right))
616
+ .map(([key, value]) => `${key}=${shellQuote(value)}`);
617
+ const wrapperHash = shortHash({
618
+ agent: input.acpxAgent,
619
+ command: input.agentCommandShell,
620
+ env: envLines,
621
+ childStderrDir: input.childStderrDir,
622
+ });
623
+ const wrapperPath = path.join(wrappersDir, `${input.acpxAgent}-${wrapperHash}.sh`);
624
+ const envFilePath = path.join(wrappersDir, `${input.acpxAgent}-${wrapperHash}.env`);
625
+ const script = [
626
+ "#!/usr/bin/env bash",
627
+ "set -euo pipefail",
628
+ `env_file=${shellQuote(envFilePath)}`,
629
+ "if [[ -f \"$env_file\" ]]; then",
630
+ " set -a",
631
+ " source \"$env_file\"",
632
+ " set +a",
633
+ "fi",
634
+ `stderr_dir=${shellQuote(input.childStderrDir)}`,
635
+ "if [[ -n \"${PAPERCLIP_RUN_ID:-}\" ]]; then",
636
+ " mkdir -p \"$stderr_dir\"",
637
+ // Keep the run-stderr file unfiltered, but do not forward the known-benign
638
+ // ACP nes/close cleanup RPC error to Paperclip's live stderr stream.
639
+ " exec 2> >(tee -a \"$stderr_dir/$PAPERCLIP_RUN_ID.log\" | grep -Ev \"method: ['\\\"]nes/close['\\\"].*-32601\" >&2 || true)",
640
+ "fi",
641
+ `exec ${input.agentCommandShell} "$@"`,
642
+ "",
643
+ ].join("\n");
644
+ await writeFileAtomically({
645
+ target: envFilePath,
646
+ contents: `${envLines.join("\n")}\n`,
647
+ mode: 0o600,
648
+ });
649
+ await writeFileAtomically({
650
+ target: wrapperPath,
651
+ contents: script,
652
+ mode: 0o700,
653
+ });
654
+ await cleanupStaleAgentWrappers({
655
+ wrappersDir,
656
+ currentFileNames: new Set([path.basename(wrapperPath), path.basename(envFilePath)]),
657
+ });
658
+ return { wrapperPath, envFilePath };
659
+ }
660
+ async function cleanupStaleAgentWrappers(input) {
661
+ const wrappers = await fs.readdir(input.wrappersDir).catch(() => []);
662
+ const now = Date.now();
663
+ await Promise.all(wrappers.map(async (name) => {
664
+ const isManagedWrapperFile = name.endsWith(".sh") || name.endsWith(".env");
665
+ if (!isManagedWrapperFile || input.currentFileNames.has(name))
666
+ return;
667
+ const wrapperPath = path.join(input.wrappersDir, name);
668
+ const stats = await fs.stat(wrapperPath).catch(() => null);
669
+ if (!stats || now - stats.mtimeMs < WRAPPER_CLEANUP_RETENTION_MS)
670
+ return;
671
+ await fs.rm(wrapperPath, { force: true });
672
+ }));
673
+ }
674
+ async function buildRuntime(input) {
675
+ const { runId, agent, config, context, authToken } = input.ctx;
676
+ const workspaceContext = parseObject(context.paperclipWorkspace);
677
+ const secretsContext = parseObject(context.paperclipSecrets);
678
+ const secretManifest = Array.isArray(secretsContext.manifest) ? secretsContext.manifest : [];
679
+ const workspaceCwd = asString(workspaceContext.cwd, "");
680
+ const workspaceSource = asString(workspaceContext.source, "");
681
+ const workspaceStrategy = asString(workspaceContext.strategy, "");
682
+ const workspaceId = asString(workspaceContext.workspaceId, "");
683
+ const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
684
+ const workspaceRepoRef = asString(workspaceContext.repoRef, "");
685
+ const workspaceBranch = asString(workspaceContext.branchName, "");
686
+ const workspaceWorktreePath = asString(workspaceContext.worktreePath, "");
687
+ const agentHome = asString(workspaceContext.agentHome, "");
688
+ const configuredCwd = asString(config.cwd, "");
689
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
690
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
691
+ const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
692
+ const executionTarget = readAdapterExecutionTarget({
693
+ executionTarget: input.ctx.executionTarget,
694
+ legacyRemoteExecution: input.ctx.executionTransport?.remoteExecution,
695
+ });
696
+ const remoteExecutionIdentity = adapterExecutionTargetSessionIdentity(executionTarget);
697
+ const effectiveExecutionCwd = remoteExecutionIdentity && typeof remoteExecutionIdentity.remoteCwd === "string"
698
+ ? remoteExecutionIdentity.remoteCwd
699
+ : cwd;
700
+ const executionTargetIsRemote = remoteExecutionIdentity !== null;
701
+ const shapedWorkspaceEnv = shapePaperclipWorkspaceEnvForExecution({
702
+ workspaceCwd: effectiveWorkspaceCwd,
703
+ workspaceWorktreePath,
704
+ executionTargetIsRemote,
705
+ executionCwd: effectiveExecutionCwd,
706
+ });
707
+ await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
708
+ const acpxAgent = normalizeAgent(config);
709
+ const mode = normalizeMode(config);
710
+ const permissionMode = normalizePermissionMode(config);
711
+ const nonInteractivePermissions = normalizeNonInteractivePermissions(config);
712
+ const requestedModel = asString(config.model, "").trim();
713
+ const requestedThinkingEffort = normalizeRequestedThinkingEffort(config);
714
+ const fastMode = acpxAgent === "codex" && config.fastMode === true;
715
+ // Resolve the wall-clock timeout through the shared execution-target
716
+ // resolver so sandbox-backed runs pick up the 4h backstop default while
717
+ // local/SSH runs keep the historical "0 = no adapter timeout" behavior.
718
+ const timeoutResolution = resolveAdapterExecutionTargetTimeout(executionTarget, asNumber(config.timeoutSec, DEFAULT_ACP_ENGINE_TIMEOUT_SEC));
719
+ const timeoutSec = timeoutResolution.timeoutSec;
720
+ const stateDir = path.resolve(asString(config.stateDir, "") || defaultStateDir(agent.companyId, agent.id));
721
+ await fs.mkdir(stateDir, { recursive: true });
722
+ const envConfig = parseObject(config.env);
723
+ const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0;
724
+ const env = { ...buildPaperclipEnv(agent), PAPERCLIP_RUN_ID: runId };
725
+ const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim()) ||
726
+ (typeof context.issueId === "string" && context.issueId.trim()) ||
727
+ "";
728
+ const wakeReason = typeof context.wakeReason === "string" ? context.wakeReason.trim() : "";
729
+ const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim()) ||
730
+ (typeof context.commentId === "string" && context.commentId.trim()) ||
731
+ "";
732
+ const approvalId = typeof context.approvalId === "string" ? context.approvalId.trim() : "";
733
+ const approvalStatus = typeof context.approvalStatus === "string" ? context.approvalStatus.trim() : "";
734
+ const linkedIssueIds = Array.isArray(context.issueIds)
735
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
736
+ : [];
737
+ const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
738
+ const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
739
+ if (wakeTaskId)
740
+ env.PAPERCLIP_TASK_ID = wakeTaskId;
741
+ if (issueWorkMode)
742
+ env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
743
+ if (wakeReason)
744
+ env.PAPERCLIP_WAKE_REASON = wakeReason;
745
+ if (wakeCommentId)
746
+ env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
747
+ if (approvalId)
748
+ env.PAPERCLIP_APPROVAL_ID = approvalId;
749
+ if (approvalStatus)
750
+ env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
751
+ if (linkedIssueIds.length > 0)
752
+ env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
753
+ if (wakePayloadJson)
754
+ env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
755
+ applyPaperclipWorkspaceEnv(env, {
756
+ workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
757
+ workspaceSource,
758
+ workspaceStrategy,
759
+ workspaceId,
760
+ workspaceRepoUrl,
761
+ workspaceRepoRef,
762
+ workspaceBranch,
763
+ workspaceWorktreePath: shapedWorkspaceEnv.workspaceWorktreePath,
764
+ agentHome,
765
+ });
766
+ const shapedEnvConfig = rewriteWorkspaceCwdEnvVarsForExecution({
767
+ env: envConfig,
768
+ workspaceCwd: effectiveWorkspaceCwd,
769
+ executionCwd: shapedWorkspaceEnv.workspaceCwd,
770
+ executionTargetIsRemote,
771
+ });
772
+ for (const [key, value] of Object.entries(shapedEnvConfig)) {
773
+ if (typeof value === "string")
774
+ env[key] = value;
775
+ }
776
+ if (!hasExplicitApiKey && authToken)
777
+ env.PAPERCLIP_API_KEY = authToken;
778
+ // For the claude agent, set model via ANTHROPIC_MODEL at startup rather than
779
+ // via session/set_config_option — the ACP server's set_config_option handler
780
+ // validates the value against its internal available-models list and rejects
781
+ // bare model IDs (e.g. "claude-opus-4-7") that don't exactly match a model
782
+ // entry in some versions. ANTHROPIC_MODEL is read during initialization, so
783
+ // it reliably sets the model before any turns are run.
784
+ if (requestedModel && acpxAgent === "claude" && !env.ANTHROPIC_MODEL) {
785
+ env.ANTHROPIC_MODEL = requestedModel;
786
+ }
787
+ let skillPromptInstructions = "";
788
+ let skillsIdentity = { mode: "unsupported" };
789
+ const skillCommandNotes = [];
790
+ let paperclipClaudeSettings = null;
791
+ if (acpxAgent === "claude") {
792
+ const preparedSkills = await prepareClaudeSkillRuntime({
793
+ stateDir,
794
+ config,
795
+ moduleDir: input.engine.moduleDir,
796
+ onLog: input.ctx.onLog,
797
+ });
798
+ skillPromptInstructions = preparedSkills.promptInstructions;
799
+ skillsIdentity = preparedSkills.identity;
800
+ skillCommandNotes.push(...preparedSkills.commandNotes);
801
+ paperclipClaudeSettings = await writePaperclipClaudeSettings({
802
+ cwd,
803
+ stateDir,
804
+ agentHome,
805
+ companyId: agent.companyId,
806
+ });
807
+ skillCommandNotes.push(`Wrote Paperclip-managed Claude settings to ${paperclipClaudeSettings.filePath} (defaultMode=${paperclipClaudeSettings.defaultMode}${paperclipClaudeSettings.overrodeDontAsk ? "; overrode user dontAsk" : ""}, +${paperclipClaudeSettings.additionalDirectories.length} read root(s), +${paperclipClaudeSettings.allow.length} allow rule(s)).`);
808
+ }
809
+ else if (acpxAgent === "codex") {
810
+ const preparedSkills = await prepareCodexSkillRuntime({
811
+ companyId: agent.companyId,
812
+ config,
813
+ env,
814
+ moduleDir: input.engine.moduleDir,
815
+ onLog: input.ctx.onLog,
816
+ });
817
+ skillsIdentity = preparedSkills.identity;
818
+ skillCommandNotes.push(...preparedSkills.commandNotes);
819
+ }
820
+ else if (acpxAgent === "gemini") {
821
+ const preparedSkills = await prepareGeminiSkillRuntime({
822
+ config,
823
+ moduleDir: input.engine.moduleDir,
824
+ onLog: input.ctx.onLog,
825
+ });
826
+ skillsIdentity = preparedSkills.identity;
827
+ skillCommandNotes.push(...preparedSkills.commandNotes);
828
+ }
829
+ else {
830
+ const desired = resolvePaperclipDesiredSkillNames(config, await readPaperclipRuntimeSkillEntries(config, input.engine.moduleDir));
831
+ skillsIdentity = { mode: "custom_unsupported", desiredSkillNames: desired };
832
+ if (desired.length > 0) {
833
+ skillCommandNotes.push("Selected Paperclip skills are tracked only; ACPX custom commands do not expose a runtime skill contract yet.");
834
+ }
835
+ }
836
+ const configuredCommand = asString(config.agentCommand, "").trim();
837
+ const builtInCommand = await resolveBuiltInAgentCommand({
838
+ agent: acpxAgent,
839
+ packageRootDir: input.engine.packageRootDir,
840
+ executionTargetIsRemote,
841
+ });
842
+ let agentCommand = configuredCommand || builtInCommand?.command || null;
843
+ let agentCommandShell = configuredCommand || builtInCommand?.shellCommand || "";
844
+ if (acpxAgent === "gemini" && agentCommandShell) {
845
+ const normalized = await normalizeGeminiAcpCommandShell(agentCommandShell, ensurePathInEnv({ ...process.env, ...env }));
846
+ if (normalized !== agentCommandShell) {
847
+ agentCommandShell = normalized;
848
+ agentCommand = normalized;
849
+ }
850
+ }
851
+ const childStderrDir = path.join(stateDir, "run-stderr");
852
+ const childStderrLogPath = agentCommand ? path.join(childStderrDir, `${runId}.log`) : null;
853
+ const wrapper = agentCommand
854
+ ? await writeAgentWrapper({
855
+ stateDir,
856
+ acpxAgent,
857
+ agentCommandShell,
858
+ env,
859
+ childStderrDir,
860
+ })
861
+ : null;
862
+ const wrapperPath = wrapper?.wrapperPath ?? null;
863
+ let paperclipBridge = null;
864
+ if (executionTarget?.kind === "remote" &&
865
+ executionTarget.transport === "sandbox" &&
866
+ Boolean(executionTarget.runner) &&
867
+ agentCommandShell) {
868
+ paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
869
+ runId,
870
+ target: { ...executionTarget, streamRunLogs: false },
871
+ runtimeRootDir: null,
872
+ adapterKey: input.engine.adapterType,
873
+ timeoutSec,
874
+ hostApiToken: env.PAPERCLIP_API_KEY,
875
+ onLog: input.ctx.onLog,
876
+ });
877
+ if (paperclipBridge) {
878
+ Object.assign(env, paperclipBridge.env);
879
+ await input.ctx.onLog("stdout", "[paperclip] Sandbox ACP API callback bridge enabled for this run.\n");
880
+ }
881
+ }
882
+ const runtimeEnv = Object.fromEntries(Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter((entry) => typeof entry[1] === "string"));
883
+ let processSessionBridge = null;
884
+ try {
885
+ processSessionBridge =
886
+ executionTarget?.kind === "remote" &&
887
+ executionTarget.transport === "sandbox" &&
888
+ Boolean(executionTarget.runner) &&
889
+ agentCommandShell
890
+ ? await startAdapterExecutionTargetProcessSessionBridge({
891
+ runId,
892
+ target: executionTarget,
893
+ runtimeRootDir: null,
894
+ adapterKey: input.engine.adapterType,
895
+ command: "sh",
896
+ args: ["-lc", `exec ${agentCommandShell}`],
897
+ cwd: effectiveExecutionCwd,
898
+ env: runtimeEnv,
899
+ timeoutSec,
900
+ onLog: input.ctx.onLog,
901
+ })
902
+ : null;
903
+ }
904
+ catch (err) {
905
+ await paperclipBridge?.stop().catch(() => { });
906
+ throw err;
907
+ }
908
+ const overrideCommand = processSessionBridge?.agentCommand ?? wrapperPath;
909
+ const overrides = overrideCommand ? { [acpxAgent]: overrideCommand } : undefined;
910
+ const agentRegistry = createAgentRegistry({ overrides });
911
+ const fingerprint = shortHash({
912
+ acpxAgent,
913
+ agentCommand: agentCommand ?? acpxAgent,
914
+ cwd: path.resolve(cwd),
915
+ mode,
916
+ permissionMode,
917
+ nonInteractivePermissions,
918
+ requestedModel,
919
+ requestedThinkingEffort,
920
+ fastMode,
921
+ remoteExecutionIdentity,
922
+ skillsIdentity,
923
+ skillPromptInstructions,
924
+ paperclipClaudeSettings: paperclipClaudeSettings
925
+ ? {
926
+ allow: paperclipClaudeSettings.allow,
927
+ additionalDirectories: paperclipClaudeSettings.additionalDirectories,
928
+ defaultMode: paperclipClaudeSettings.defaultMode,
929
+ }
930
+ : null,
931
+ secretManifestHash: shortHash(secretManifest),
932
+ });
933
+ const taskKey = asString(input.ctx.runtime.taskKey, "") || wakeTaskId || workspaceId || "default";
934
+ const sessionKey = `paperclip:${agent.companyId}:${agent.id}:${taskKey}:${fingerprint}`;
935
+ const loggedEnv = buildInvocationEnvForLogs(env, {
936
+ runtimeEnv,
937
+ includeRuntimeKeys: ["HOME"],
938
+ resolvedCommand: wrapperPath ?? agentCommand ?? acpxAgent,
939
+ });
940
+ return {
941
+ acpxAgent,
942
+ mode,
943
+ cwd,
944
+ workspaceId,
945
+ workspaceRepoUrl,
946
+ workspaceRepoRef,
947
+ env,
948
+ loggedEnv,
949
+ stateDir,
950
+ permissionMode,
951
+ nonInteractivePermissions,
952
+ requestedModel,
953
+ requestedThinkingEffort,
954
+ fastMode,
955
+ timeoutSec,
956
+ timeoutResolution,
957
+ sessionKey,
958
+ fingerprint,
959
+ agentCommand,
960
+ agentRegistry,
961
+ processSessionBridge,
962
+ paperclipBridge,
963
+ remoteExecutionIdentity,
964
+ skillPromptInstructions,
965
+ skillsIdentity: {
966
+ ...skillsIdentity,
967
+ commandNotes: skillCommandNotes,
968
+ },
969
+ childStderrLogPath,
970
+ paperclipClaudeSettings,
971
+ };
972
+ }
973
+ function sessionConfigOptions(prepared) {
974
+ const options = [];
975
+ // Model for the claude agent is pre-set via ANTHROPIC_MODEL env var at
976
+ // startup; skip set_config_option to avoid ACP-server model-name validation
977
+ // that rejects bare IDs like "claude-opus-4-7" in some runtime versions.
978
+ if (prepared.requestedModel && prepared.acpxAgent !== "claude") {
979
+ options.push({ key: "model", value: prepared.requestedModel });
980
+ }
981
+ if (prepared.requestedThinkingEffort) {
982
+ options.push({
983
+ key: prepared.acpxAgent === "codex" ? "reasoning_effort" : "effort",
984
+ value: prepared.requestedThinkingEffort,
985
+ });
986
+ }
987
+ if (prepared.fastMode) {
988
+ options.push({ key: "service_tier", value: "fast" }, { key: "features.fast_mode", value: "true" });
989
+ }
990
+ return options;
991
+ }
992
+ async function applySessionConfigOptions(input) {
993
+ const options = sessionConfigOptions(input.prepared);
994
+ if (options.length === 0)
995
+ return;
996
+ if (!input.runtime.setConfigOption) {
997
+ const message = "ACPX runtime does not expose session config controls; upgrade ACPX or remove configured model, effort, and fast mode overrides.";
998
+ await input.onLog("stderr", `[paperclip] ${message}\n`);
999
+ throw new Error(message);
1000
+ }
1001
+ for (const option of options) {
1002
+ await input.runtime.setConfigOption({
1003
+ handle: input.handle,
1004
+ key: option.key,
1005
+ value: option.value,
1006
+ });
1007
+ await input.onLog("stdout", `[paperclip] Applied ACPX ${input.prepared.acpxAgent} config ${option.key}=${option.value}\n`);
1008
+ }
1009
+ }
1010
+ async function cleanupRemoteBridges(prepared) {
1011
+ await Promise.allSettled([
1012
+ prepared.processSessionBridge?.stop(),
1013
+ prepared.paperclipBridge?.stop(),
1014
+ ]);
1015
+ }
1016
+ function renderPaperclipEnvNote(env) {
1017
+ const paperclipKeys = Object.keys(env)
1018
+ .filter((key) => key.startsWith("PAPERCLIP_"))
1019
+ .sort();
1020
+ if (paperclipKeys.length === 0)
1021
+ return "";
1022
+ return [
1023
+ "Paperclip runtime note:",
1024
+ `The following PAPERCLIP_* environment variables are available in this run: ${paperclipKeys.join(", ")}`,
1025
+ "Do not assume these variables are missing without checking your shell environment.",
1026
+ ].join("\n");
1027
+ }
1028
+ function renderApiAccessNote(env) {
1029
+ if (!env.PAPERCLIP_API_URL || !env.PAPERCLIP_API_KEY)
1030
+ return "";
1031
+ const lines = [
1032
+ "Paperclip API access note:",
1033
+ "Use terminal commands with curl to make Paperclip API requests.",
1034
+ "Normalize the base URL before adding API paths:",
1035
+ ` PAPERCLIP_API_BASE="\${PAPERCLIP_API_URL%/}"; PAPERCLIP_API_BASE="\${PAPERCLIP_API_BASE%/api}"`,
1036
+ "GET example:",
1037
+ ` curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" "$PAPERCLIP_API_BASE/api/agents/me"`,
1038
+ ];
1039
+ if (env.PAPERCLIP_TASK_ID) {
1040
+ lines.push("Scoped issue comment example:", ` curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" -d '{"body":"Status update from agent."}' "$PAPERCLIP_API_BASE/api/issues/$PAPERCLIP_TASK_ID/comments"`);
1041
+ }
1042
+ else {
1043
+ lines.push("Use a real issue id from the current context before making issue write requests.");
1044
+ }
1045
+ return lines.join("\n");
1046
+ }
1047
+ async function buildPrompt(ctx, resumedSession, env) {
1048
+ const { agent, runId, config, context, onLog } = ctx;
1049
+ const promptTemplate = asString(config.promptTemplate, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
1050
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
1051
+ const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
1052
+ let instructionsPrefix = "";
1053
+ const commandNotes = [];
1054
+ if (instructionsFilePath) {
1055
+ try {
1056
+ const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
1057
+ instructionsPrefix =
1058
+ `${instructionsContents}\n\n` +
1059
+ `The above agent instructions were loaded from ${instructionsFilePath}. ` +
1060
+ `Resolve any relative file references from ${instructionsDir}.\n\n`;
1061
+ commandNotes.push(`Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to the ACPX prompt (relative references from ${instructionsDir}).`);
1062
+ }
1063
+ catch (err) {
1064
+ const reason = err instanceof Error ? err.message : String(err);
1065
+ await onLog("stderr", `[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
1066
+ commandNotes.push(`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read.`);
1067
+ }
1068
+ }
1069
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
1070
+ const templateData = {
1071
+ agentId: agent.id,
1072
+ companyId: agent.companyId,
1073
+ runId,
1074
+ company: { id: agent.companyId },
1075
+ agent,
1076
+ run: { id: runId, source: "on_demand" },
1077
+ context,
1078
+ };
1079
+ const renderedBootstrapPrompt = !resumedSession && bootstrapPromptTemplate.trim().length > 0
1080
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
1081
+ : "";
1082
+ const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession });
1083
+ const shouldUseResumeDeltaPrompt = resumedSession && wakePrompt.length > 0;
1084
+ const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
1085
+ const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
1086
+ const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
1087
+ const taskContextNote = asString(context.paperclipTaskMarkdown, "").trim();
1088
+ const localizationPromptNote = asString(context.paperclipLocalizationPromptMarkdown, "").trim();
1089
+ const paperclipEnvNote = renderPaperclipEnvNote(env);
1090
+ const apiAccessNote = renderApiAccessNote(env);
1091
+ const prompt = joinPromptSections([
1092
+ promptInstructionsPrefix,
1093
+ localizationPromptNote,
1094
+ renderedBootstrapPrompt,
1095
+ wakePrompt,
1096
+ sessionHandoffNote,
1097
+ taskContextNote,
1098
+ paperclipEnvNote,
1099
+ apiAccessNote,
1100
+ renderedPrompt,
1101
+ ]);
1102
+ return {
1103
+ prompt,
1104
+ commandNotes,
1105
+ promptMetrics: {
1106
+ promptChars: prompt.length,
1107
+ instructionsChars: promptInstructionsPrefix.length,
1108
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
1109
+ wakePromptChars: wakePrompt.length,
1110
+ sessionHandoffChars: sessionHandoffNote.length,
1111
+ localizationPromptChars: localizationPromptNote.length,
1112
+ taskContextChars: taskContextNote.length,
1113
+ runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
1114
+ heartbeatPromptChars: renderedPrompt.length,
1115
+ },
1116
+ };
1117
+ }
1118
+ async function emitAcpxLog(ctx, payload) {
1119
+ await ctx.onLog("stdout", `${JSON.stringify(payload)}\n`);
1120
+ }
1121
+ async function emitRuntimeEvent(ctx, event) {
1122
+ if (event.type === "text_delta") {
1123
+ await emitAcpxLog(ctx, {
1124
+ type: "acpx.text_delta",
1125
+ text: event.text,
1126
+ channel: event.stream === "thought" ? "thought" : "output",
1127
+ tag: event.tag,
1128
+ });
1129
+ return;
1130
+ }
1131
+ if (event.type === "tool_call") {
1132
+ const eventRecord = event;
1133
+ const toolInput = eventRecord.input;
1134
+ await emitAcpxLog(ctx, {
1135
+ type: "acpx.tool_call",
1136
+ name: event.title ?? "acp_tool",
1137
+ toolCallId: event.toolCallId,
1138
+ status: event.status,
1139
+ text: event.text,
1140
+ tag: event.tag,
1141
+ ...(toolInput !== undefined ? { input: toolInput } : {}),
1142
+ });
1143
+ return;
1144
+ }
1145
+ if (event.type === "status") {
1146
+ await emitAcpxLog(ctx, {
1147
+ type: "acpx.status",
1148
+ text: event.text,
1149
+ tag: event.tag,
1150
+ used: event.used,
1151
+ size: event.size,
1152
+ ...(event.cost ? { cost: event.cost } : {}),
1153
+ ...(event.breakdown ? { breakdown: event.breakdown } : {}),
1154
+ });
1155
+ return;
1156
+ }
1157
+ if (event.type === "done") {
1158
+ await emitAcpxLog(ctx, {
1159
+ type: "acpx.result",
1160
+ summary: event.stopReason ?? "completed",
1161
+ stopReason: event.stopReason,
1162
+ });
1163
+ return;
1164
+ }
1165
+ if (event.type === "error") {
1166
+ await emitAcpxLog(ctx, {
1167
+ type: "acpx.error",
1168
+ message: event.message,
1169
+ code: event.code,
1170
+ retryable: event.retryable,
1171
+ });
1172
+ }
1173
+ }
1174
+ function resultErrorMessage(result) {
1175
+ if (result.status !== "failed")
1176
+ return null;
1177
+ return result.error.message;
1178
+ }
1179
+ function usageBreakdownsEqual(left, right) {
1180
+ return (asNumber(left.inputTokens, 0) === asNumber(right.inputTokens, 0) &&
1181
+ asNumber(left.outputTokens, 0) === asNumber(right.outputTokens, 0) &&
1182
+ asNumber(left.cachedReadTokens, 0) === asNumber(right.cachedReadTokens, 0) &&
1183
+ asNumber(left.cachedWriteTokens, 0) === asNumber(right.cachedWriteTokens, 0) &&
1184
+ asNumber(left.thoughtTokens, 0) === asNumber(right.thoughtTokens, 0) &&
1185
+ asNumber(left.totalTokens, 0) === asNumber(right.totalTokens, 0));
1186
+ }
1187
+ function usdCostAmount(cost) {
1188
+ if (!cost || typeof cost.amount !== "number" || !Number.isFinite(cost.amount))
1189
+ return null;
1190
+ if (cost.currency && cost.currency.trim().toUpperCase() !== "USD")
1191
+ return null;
1192
+ return cost.amount;
1193
+ }
1194
+ async function readRuntimeStatus(runtime, handle) {
1195
+ if (!runtime.getStatus)
1196
+ return null;
1197
+ try {
1198
+ return (await runtime.getStatus({ handle })) ?? null;
1199
+ }
1200
+ catch {
1201
+ return null;
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Fold the ACP runtime's post-turn usage into the adapter execution result
1206
+ * shape. The runtime persists the latest turn's token breakdown (adapters like
1207
+ * claude-agent-acp report per-turn accumulated usage in the prompt response),
1208
+ * so tokens are per-run. Cost is reported by agents as a cumulative session
1209
+ * amount, so the per-run cost is the delta against the pre-turn snapshot; a
1210
+ * decrease means the agent process restarted and its counter reset, in which
1211
+ * case the post-turn amount alone covers this run.
1212
+ */
1213
+ export function summarizeAcpxTurnUsage(input) {
1214
+ // The persisted breakdown is overwritten per turn, so an unchanged value
1215
+ // is stale for this turn. Prefer an in-turn event breakdown when available;
1216
+ // otherwise suppress the stale value so it cannot be double-counted.
1217
+ const preBreakdown = input.preStatus?.usage?.cumulative ?? null;
1218
+ const postBreakdown = input.postStatus?.usage?.cumulative ?? null;
1219
+ const postBreakdownIsStale = preBreakdown != null &&
1220
+ postBreakdown != null &&
1221
+ usageBreakdownsEqual(preBreakdown, postBreakdown);
1222
+ const breakdown = postBreakdownIsStale
1223
+ ? input.eventBreakdown
1224
+ : postBreakdown ?? input.eventBreakdown ?? null;
1225
+ const inputTokens = Math.max(0, Math.floor(asNumber(breakdown?.inputTokens, 0)));
1226
+ const outputTokens = Math.max(0, Math.floor(asNumber(breakdown?.outputTokens, 0)));
1227
+ const cachedReadTokens = Math.max(0, Math.floor(asNumber(breakdown?.cachedReadTokens, 0)));
1228
+ const cachedWriteTokens = Math.max(0, Math.floor(asNumber(breakdown?.cachedWriteTokens, 0)));
1229
+ const hasTokens = inputTokens > 0 || outputTokens > 0 || cachedReadTokens > 0 || cachedWriteTokens > 0;
1230
+ // Cache-write tokens are prompt tokens the provider billed to create cache
1231
+ // entries; UsageSummary has no dedicated field, so count them as input.
1232
+ const usage = hasTokens
1233
+ ? {
1234
+ inputTokens: inputTokens + cachedWriteTokens,
1235
+ outputTokens,
1236
+ cachedInputTokens: cachedReadTokens,
1237
+ }
1238
+ : null;
1239
+ const usageDetail = breakdown
1240
+ ? Object.fromEntries(Object.entries({
1241
+ inputTokens: breakdown.inputTokens,
1242
+ outputTokens: breakdown.outputTokens,
1243
+ cachedReadTokens: breakdown.cachedReadTokens,
1244
+ cachedWriteTokens: breakdown.cachedWriteTokens,
1245
+ thoughtTokens: breakdown.thoughtTokens,
1246
+ totalTokens: breakdown.totalTokens,
1247
+ }).filter((entry) => typeof entry[1] === "number"))
1248
+ : null;
1249
+ const previousCostUsd = usdCostAmount(input.preStatus?.usage?.cost);
1250
+ const postCostUsd = usdCostAmount(input.postStatus?.usage?.cost);
1251
+ const postCostIsStale = input.eventCostUsd != null &&
1252
+ previousCostUsd != null &&
1253
+ postCostUsd != null &&
1254
+ postCostUsd === previousCostUsd;
1255
+ const cumulativeCostUsd = postCostIsStale ? input.eventCostUsd : postCostUsd ?? input.eventCostUsd;
1256
+ let costUsd = null;
1257
+ if (cumulativeCostUsd != null) {
1258
+ costUsd =
1259
+ previousCostUsd != null && cumulativeCostUsd >= previousCostUsd
1260
+ ? cumulativeCostUsd - previousCostUsd
1261
+ : cumulativeCostUsd;
1262
+ }
1263
+ return { usage, usageDetail, costUsd, cumulativeCostUsd };
1264
+ }
1265
+ function describeErrorDiagnostics(err) {
1266
+ const errorName = err instanceof Error ? err.name || err.constructor.name : typeof err;
1267
+ const maybeCode = err && typeof err === "object" && typeof err.code === "string"
1268
+ ? err.code
1269
+ : null;
1270
+ const acpCode = isAcpRuntimeError(err) || (maybeCode?.startsWith("ACP_") ?? false) ? maybeCode : null;
1271
+ const cause = err && typeof err === "object" && err.cause !== undefined
1272
+ ? err.cause
1273
+ : undefined;
1274
+ const causeMessage = cause instanceof Error
1275
+ ? cause.message
1276
+ : typeof cause === "string"
1277
+ ? cause
1278
+ : null;
1279
+ const retryable = err && typeof err === "object" && typeof err.retryable === "boolean"
1280
+ ? err.retryable
1281
+ : null;
1282
+ const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : "";
1283
+ const stackPreview = stack ? stack.split("\n").slice(0, 6).join("\n") : null;
1284
+ return { errorName, acpCode, causeMessage, retryable, stackPreview };
1285
+ }
1286
+ function classifyError(err, phase) {
1287
+ const message = err instanceof Error ? err.message : String(err);
1288
+ const diagnostics = describeErrorDiagnostics(err);
1289
+ const { acpCode, errorName, causeMessage, retryable, stackPreview } = diagnostics;
1290
+ const baseMeta = {
1291
+ errorName,
1292
+ ...(acpCode ? { acpCode } : {}),
1293
+ ...(causeMessage ? { causeMessage } : {}),
1294
+ ...(retryable !== null ? { retryable } : {}),
1295
+ ...(stackPreview ? { stackPreview } : {}),
1296
+ ...(phase ? { phase } : {}),
1297
+ };
1298
+ const lower = message.toLowerCase();
1299
+ const authLike = lower.includes("auth") || lower.includes("login") || lower.includes("credential");
1300
+ if (authLike) {
1301
+ return {
1302
+ errorCode: "acpx_auth_required",
1303
+ errorMeta: { category: "auth", ...baseMeta },
1304
+ };
1305
+ }
1306
+ const phaseCode = (() => {
1307
+ if (acpCode === "ACP_SESSION_INIT_FAILED")
1308
+ return "acpx_session_init_failed";
1309
+ if (acpCode === "ACP_TURN_FAILED")
1310
+ return "acpx_turn_failed";
1311
+ if (acpCode === "ACP_BACKEND_MISSING")
1312
+ return "acpx_backend_missing";
1313
+ if (acpCode === "ACP_BACKEND_UNAVAILABLE")
1314
+ return "acpx_backend_unavailable";
1315
+ if (phase === "ensure_session")
1316
+ return "acpx_session_init_failed";
1317
+ if (phase === "configure_session")
1318
+ return "acpx_session_config_failed";
1319
+ if (phase === "turn")
1320
+ return "acpx_turn_failed";
1321
+ return null;
1322
+ })();
1323
+ if (phaseCode) {
1324
+ return {
1325
+ errorCode: phaseCode,
1326
+ errorMeta: { category: acpCode ? "protocol" : "runtime", ...baseMeta },
1327
+ };
1328
+ }
1329
+ if (acpCode) {
1330
+ return {
1331
+ errorCode: "acpx_protocol_error",
1332
+ errorMeta: { category: "protocol", ...baseMeta },
1333
+ };
1334
+ }
1335
+ return {
1336
+ errorCode: "acpx_runtime_error",
1337
+ errorMeta: { category: "runtime", ...baseMeta },
1338
+ };
1339
+ }
1340
+ async function readChildStderrTail(input) {
1341
+ if (!input.logPath)
1342
+ return null;
1343
+ const maxBytes = input.maxBytes ?? 4096;
1344
+ let handle = null;
1345
+ try {
1346
+ const stat = await fs.stat(input.logPath);
1347
+ if (stat.size === 0)
1348
+ return null;
1349
+ handle = await fs.open(input.logPath, "r");
1350
+ const readBytes = Math.min(stat.size, maxBytes);
1351
+ const buffer = Buffer.alloc(readBytes);
1352
+ await handle.read(buffer, 0, readBytes, Math.max(0, stat.size - readBytes));
1353
+ const tail = buffer.toString("utf8").trim();
1354
+ return tail.length > 0 ? tail : null;
1355
+ }
1356
+ catch {
1357
+ return null;
1358
+ }
1359
+ finally {
1360
+ if (handle)
1361
+ await handle.close().catch(() => { });
1362
+ }
1363
+ }
1364
+ async function emitAcpxFailure(input) {
1365
+ const { ctx, prepared, err, phase, messageOverride } = input;
1366
+ const rawMessage = err instanceof Error ? err.message : String(err);
1367
+ const message = messageOverride ?? rawMessage;
1368
+ const classified = classifyError(err, phase);
1369
+ const childStderrTail = await readChildStderrTail({ logPath: prepared.childStderrLogPath });
1370
+ if (childStderrTail) {
1371
+ await ctx.onLog("stderr", `[paperclip] ACPX child stderr tail (${phase}):\n${childStderrTail}\n`);
1372
+ }
1373
+ await emitAcpxLog(ctx, {
1374
+ type: "acpx.error",
1375
+ message,
1376
+ phase,
1377
+ ...classified.errorMeta,
1378
+ ...(childStderrTail ? { childStderrTail } : {}),
1379
+ });
1380
+ return { classified, message, childStderrTail };
1381
+ }
1382
+ function isResumeFailure(err) {
1383
+ const message = err instanceof Error ? err.message : String(err);
1384
+ return /resume|load|not found|no session|unknown session|conversation/i.test(message);
1385
+ }
1386
+ async function cleanupIdleHandles(input) {
1387
+ if (input.idleMs <= 0)
1388
+ return;
1389
+ const stale = [];
1390
+ for (const entry of input.handles.entries()) {
1391
+ if (input.now - entry[1].lastUsedAt >= input.idleMs)
1392
+ stale.push(entry);
1393
+ }
1394
+ for (const [key, entry] of stale) {
1395
+ await closeWarmHandle({
1396
+ handles: input.handles,
1397
+ key,
1398
+ entry,
1399
+ reason: "paperclip idle cleanup",
1400
+ });
1401
+ }
1402
+ }
1403
+ function clearWarmHandleTimer(entry) {
1404
+ if (!entry.cleanupTimer)
1405
+ return;
1406
+ clearTimeout(entry.cleanupTimer);
1407
+ entry.cleanupTimer = undefined;
1408
+ }
1409
+ async function closeWarmHandle(input) {
1410
+ if (input.handles.get(input.key) === input.entry) {
1411
+ input.handles.delete(input.key);
1412
+ }
1413
+ clearWarmHandleTimer(input.entry);
1414
+ await input.entry.runtime.close({
1415
+ handle: input.entry.handle,
1416
+ reason: input.reason,
1417
+ discardPersistentState: input.discardPersistentState ?? false,
1418
+ }).catch(() => { });
1419
+ }
1420
+ function scheduleIdleHandleCleanup(input) {
1421
+ clearWarmHandleTimer(input.entry);
1422
+ if (input.idleMs <= 0)
1423
+ return;
1424
+ const delayMs = Math.max(1, input.entry.lastUsedAt + input.idleMs - input.now());
1425
+ input.entry.cleanupTimer = setTimeout(() => {
1426
+ void (async () => {
1427
+ const current = input.handles.get(input.key);
1428
+ if (current !== input.entry)
1429
+ return;
1430
+ const idleForMs = input.now() - input.entry.lastUsedAt;
1431
+ if (idleForMs < input.idleMs) {
1432
+ scheduleIdleHandleCleanup(input);
1433
+ return;
1434
+ }
1435
+ await closeWarmHandle({
1436
+ handles: input.handles,
1437
+ key: input.key,
1438
+ entry: input.entry,
1439
+ reason: "paperclip idle cleanup",
1440
+ });
1441
+ })();
1442
+ }, delayMs);
1443
+ input.entry.cleanupTimer.unref?.();
1444
+ }
1445
+ function warmHandleMatches(entry, runtime, handle) {
1446
+ return entry !== undefined && entry.runtime === runtime && entry.handle === handle;
1447
+ }
1448
+ export function createAcpxEngineExecutor(deps = {}) {
1449
+ const createRuntime = deps.createRuntime ?? createAcpRuntime;
1450
+ const now = deps.now ?? (() => Date.now());
1451
+ const warmHandles = deps.warmHandles ?? defaultWarmHandles;
1452
+ const engine = resolveEngineSettings(deps);
1453
+ return async function executeAcpxEngine(ctx) {
1454
+ let billingIdentity = null;
1455
+ try {
1456
+ billingIdentity = (await deps.resolveBillingIdentity?.(ctx)) ?? null;
1457
+ }
1458
+ catch {
1459
+ billingIdentity = null;
1460
+ }
1461
+ const billingFields = {
1462
+ provider: billingIdentity?.provider ?? "acpx",
1463
+ ...(billingIdentity?.biller ? { biller: billingIdentity.biller } : {}),
1464
+ billingType: billingIdentity?.billingType ?? "unknown",
1465
+ };
1466
+ const prepared = await buildRuntime({ ctx, engine });
1467
+ // State the effective wall-clock timeout and its source up front so a
1468
+ // later timeout is diagnosable from the run log alone. Goes to stderr:
1469
+ // the acpx stdout log stream carries JSON acpx.* event payloads and must
1470
+ // stay machine-parseable line by line.
1471
+ await ctx.onLog("stderr", `[paperclip] ${formatAdapterExecutionTimeoutStartLogLine(prepared.timeoutResolution)}\n`);
1472
+ const warmIdleMs = asNumber(ctx.config.warmHandleIdleMs, DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS);
1473
+ await cleanupIdleHandles({ handles: warmHandles, now: now(), idleMs: warmIdleMs });
1474
+ const previousParams = parseObject(ctx.runtime.sessionParams);
1475
+ const canResume = isCompatibleSession(previousParams, prepared);
1476
+ const resumeSessionId = canResume ? asString(previousParams.acpSessionId, "") || undefined : undefined;
1477
+ const cached = canResume ? warmHandles.get(prepared.sessionKey) : undefined;
1478
+ const runtimeOptions = {
1479
+ cwd: prepared.cwd,
1480
+ sessionStore: createRuntimeStore({ stateDir: prepared.stateDir }),
1481
+ agentRegistry: prepared.agentRegistry,
1482
+ permissionMode: prepared.permissionMode,
1483
+ nonInteractivePermissions: prepared.nonInteractivePermissions,
1484
+ timeoutMs: prepared.timeoutSec > 0 ? prepared.timeoutSec * 1000 : undefined,
1485
+ // Scope ACPX runtime verbose logs to the claude agent only. Codex
1486
+ // and custom agents already emit their own per-tool output and don't
1487
+ // benefit from doubling the log volume.
1488
+ verbose: prepared.acpxAgent === "claude",
1489
+ };
1490
+ const runtime = cached?.runtime ?? createRuntime(runtimeOptions);
1491
+ if (cached)
1492
+ clearWarmHandleTimer(cached);
1493
+ if (!canResume && asString(previousParams.runtimeSessionName, "")) {
1494
+ await ctx.onLog("stdout", `[paperclip] ACPX session "${asString(previousParams.runtimeSessionName, "")}" does not match the current agent/cwd/mode/runtime identity; starting fresh in "${prepared.cwd}".\n`);
1495
+ }
1496
+ let handle = cached?.handle ?? null;
1497
+ let resumedSession = Boolean(handle ?? resumeSessionId);
1498
+ let clearSession = false;
1499
+ try {
1500
+ if (!handle) {
1501
+ try {
1502
+ handle = await runtime.ensureSession({
1503
+ sessionKey: prepared.sessionKey,
1504
+ agent: prepared.acpxAgent,
1505
+ mode: prepared.mode,
1506
+ cwd: prepared.cwd,
1507
+ resumeSessionId,
1508
+ });
1509
+ }
1510
+ catch (err) {
1511
+ if (!resumeSessionId || !isResumeFailure(err))
1512
+ throw err;
1513
+ clearSession = true;
1514
+ resumedSession = false;
1515
+ await ctx.onLog("stdout", `[paperclip] ACPX resume session "${resumeSessionId}" is unavailable; retrying with a fresh session.\n`);
1516
+ handle = await runtime.ensureSession({
1517
+ sessionKey: prepared.sessionKey,
1518
+ agent: prepared.acpxAgent,
1519
+ mode: prepared.mode,
1520
+ cwd: prepared.cwd,
1521
+ });
1522
+ }
1523
+ }
1524
+ }
1525
+ catch (err) {
1526
+ const { classified, message } = await emitAcpxFailure({
1527
+ ctx,
1528
+ prepared,
1529
+ err,
1530
+ phase: "ensure_session",
1531
+ });
1532
+ await cleanupRemoteBridges(prepared);
1533
+ return {
1534
+ exitCode: 1,
1535
+ signal: null,
1536
+ timedOut: false,
1537
+ errorMessage: message,
1538
+ ...classified,
1539
+ ...billingFields,
1540
+ model: prepared.requestedModel || null,
1541
+ clearSession,
1542
+ resultJson: { phase: "ensure_session" },
1543
+ summary: message,
1544
+ };
1545
+ }
1546
+ if (!handle) {
1547
+ await cleanupRemoteBridges(prepared);
1548
+ return {
1549
+ exitCode: 1,
1550
+ signal: null,
1551
+ timedOut: false,
1552
+ errorMessage: "ACPX did not return a runtime session handle.",
1553
+ errorCode: "acpx_runtime_error",
1554
+ ...billingFields,
1555
+ model: prepared.requestedModel || null,
1556
+ resultJson: { phase: "ensure_session" },
1557
+ summary: "ACPX did not return a runtime session handle.",
1558
+ };
1559
+ }
1560
+ const sessionHandle = handle;
1561
+ try {
1562
+ await applySessionConfigOptions({
1563
+ runtime,
1564
+ handle: sessionHandle,
1565
+ prepared,
1566
+ onLog: ctx.onLog,
1567
+ });
1568
+ }
1569
+ catch (err) {
1570
+ const { classified, message } = await emitAcpxFailure({
1571
+ ctx,
1572
+ prepared,
1573
+ err,
1574
+ phase: "configure_session",
1575
+ });
1576
+ await runtime.close({
1577
+ handle: sessionHandle,
1578
+ reason: "paperclip config cleanup",
1579
+ discardPersistentState: false,
1580
+ }).catch(() => { });
1581
+ const existing = warmHandles.get(prepared.sessionKey);
1582
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1583
+ clearWarmHandleTimer(existing);
1584
+ warmHandles.delete(prepared.sessionKey);
1585
+ }
1586
+ await cleanupRemoteBridges(prepared);
1587
+ return {
1588
+ exitCode: 1,
1589
+ signal: null,
1590
+ timedOut: false,
1591
+ errorMessage: message,
1592
+ ...classified,
1593
+ ...billingFields,
1594
+ model: prepared.requestedModel || null,
1595
+ clearSession,
1596
+ resultJson: {
1597
+ phase: "configure_session",
1598
+ agent: prepared.acpxAgent,
1599
+ requestedModel: prepared.requestedModel || null,
1600
+ requestedThinkingEffort: prepared.requestedThinkingEffort || null,
1601
+ fastMode: prepared.fastMode,
1602
+ },
1603
+ summary: message,
1604
+ };
1605
+ }
1606
+ const { prompt, promptMetrics, commandNotes } = await buildPrompt(ctx, resumedSession, prepared.env);
1607
+ const runPrompt = joinPromptSections([prepared.skillPromptInstructions, prompt]);
1608
+ await emitAcpxLog(ctx, {
1609
+ type: "acpx.session",
1610
+ agent: prepared.acpxAgent,
1611
+ sessionId: sessionHandle.backendSessionId,
1612
+ acpSessionId: sessionHandle.backendSessionId,
1613
+ agentSessionId: sessionHandle.agentSessionId,
1614
+ runtimeSessionName: sessionHandle.runtimeSessionName,
1615
+ mode: prepared.mode,
1616
+ permissionMode: prepared.permissionMode,
1617
+ model: prepared.requestedModel || null,
1618
+ thinkingEffort: prepared.requestedThinkingEffort || null,
1619
+ fastMode: prepared.fastMode,
1620
+ });
1621
+ if (ctx.onMeta) {
1622
+ await ctx.onMeta({
1623
+ adapterType: engine.adapterType,
1624
+ command: prepared.agentCommand ?? prepared.acpxAgent,
1625
+ cwd: prepared.cwd,
1626
+ commandNotes: [
1627
+ `ACPX runtime embedded in Paperclip with ${prepared.mode} session mode.`,
1628
+ `Effective ACPX permission mode: ${prepared.permissionMode}.`,
1629
+ ...(prepared.requestedModel
1630
+ ? [
1631
+ prepared.acpxAgent === "claude"
1632
+ ? `Requested ACPX model: ${prepared.requestedModel} (set via ANTHROPIC_MODEL env at startup).`
1633
+ : `Requested ACPX model: ${prepared.requestedModel}.`,
1634
+ ]
1635
+ : []),
1636
+ ...(prepared.requestedThinkingEffort ? [`Requested ACPX thinking effort: ${prepared.requestedThinkingEffort}.`] : []),
1637
+ ...(prepared.fastMode ? ["Requested ACPX Codex fast mode."] : []),
1638
+ ...(Array.isArray(prepared.skillsIdentity.commandNotes)
1639
+ ? prepared.skillsIdentity.commandNotes.filter((note) => typeof note === "string")
1640
+ : []),
1641
+ ...commandNotes,
1642
+ ],
1643
+ env: prepared.loggedEnv,
1644
+ prompt: runPrompt,
1645
+ promptMetrics,
1646
+ context: ctx.context,
1647
+ });
1648
+ }
1649
+ let cancelActiveTurn = null;
1650
+ let controller = null;
1651
+ let timeout = null;
1652
+ let timedOut = false;
1653
+ const textParts = [];
1654
+ let eventBreakdown = null;
1655
+ let eventCostUsd = null;
1656
+ try {
1657
+ // Snapshot pre-turn usage so cumulative agent-reported cost can be
1658
+ // attributed to this run alone.
1659
+ const preTurnStatus = await readRuntimeStatus(runtime, sessionHandle);
1660
+ const timeoutMs = prepared.timeoutSec > 0 ? prepared.timeoutSec * 1000 : undefined;
1661
+ controller = new AbortController();
1662
+ if (timeoutMs) {
1663
+ timeout = setTimeout(() => {
1664
+ timedOut = true;
1665
+ controller?.abort();
1666
+ void cancelActiveTurn?.(formatAdapterExecutionTimeoutErrorMessage(prepared.timeoutResolution)).catch(() => { });
1667
+ }, timeoutMs);
1668
+ }
1669
+ const turn = runtime.startTurn({
1670
+ handle: sessionHandle,
1671
+ text: runPrompt,
1672
+ mode: "prompt",
1673
+ requestId: ctx.runId,
1674
+ timeoutMs,
1675
+ signal: controller?.signal,
1676
+ });
1677
+ cancelActiveTurn = async (reason) => {
1678
+ await turn.cancel({ reason });
1679
+ };
1680
+ for await (const event of turn.events) {
1681
+ if (event.type === "text_delta")
1682
+ textParts.push(event.text);
1683
+ if (event.type === "status" && event.tag === "usage_update") {
1684
+ eventBreakdown = event.breakdown ?? eventBreakdown;
1685
+ eventCostUsd = usdCostAmount(event.cost) ?? eventCostUsd;
1686
+ }
1687
+ await emitRuntimeEvent(ctx, event);
1688
+ }
1689
+ const terminal = await turn.result;
1690
+ if (timeout)
1691
+ clearTimeout(timeout);
1692
+ // Read usage before the close/warm-handle paths below can discard state.
1693
+ const postTurnStatus = await readRuntimeStatus(runtime, sessionHandle);
1694
+ const turnUsage = summarizeAcpxTurnUsage({
1695
+ preStatus: preTurnStatus,
1696
+ postStatus: postTurnStatus,
1697
+ eventBreakdown,
1698
+ eventCostUsd,
1699
+ });
1700
+ if (terminal.status === "failed" || terminal.status === "cancelled" || timedOut) {
1701
+ const existing = warmHandles.get(prepared.sessionKey);
1702
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1703
+ await closeWarmHandle({
1704
+ handles: warmHandles,
1705
+ key: prepared.sessionKey,
1706
+ entry: existing,
1707
+ reason: timedOut ? "paperclip timeout cleanup" : `paperclip turn ${terminal.status}`,
1708
+ discardPersistentState: terminal.status === "cancelled" || timedOut,
1709
+ });
1710
+ }
1711
+ else {
1712
+ await runtime.close({
1713
+ handle: sessionHandle,
1714
+ reason: timedOut ? "paperclip timeout cleanup" : `paperclip turn ${terminal.status}`,
1715
+ discardPersistentState: terminal.status === "cancelled" || timedOut,
1716
+ }).catch(() => { });
1717
+ }
1718
+ }
1719
+ else if (prepared.mode === "persistent" && warmIdleMs > 0 && !prepared.processSessionBridge) {
1720
+ const existing = warmHandles.get(prepared.sessionKey);
1721
+ if (existing && !warmHandleMatches(existing, runtime, sessionHandle)) {
1722
+ await runtime.close({
1723
+ handle: sessionHandle,
1724
+ reason: "paperclip duplicate warm handle cleanup",
1725
+ discardPersistentState: false,
1726
+ }).catch(() => { });
1727
+ }
1728
+ else {
1729
+ const entry = {
1730
+ runtime,
1731
+ handle: sessionHandle,
1732
+ fingerprint: prepared.fingerprint,
1733
+ lastUsedAt: now(),
1734
+ };
1735
+ warmHandles.set(prepared.sessionKey, entry);
1736
+ scheduleIdleHandleCleanup({
1737
+ handles: warmHandles,
1738
+ key: prepared.sessionKey,
1739
+ entry,
1740
+ idleMs: warmIdleMs,
1741
+ now,
1742
+ });
1743
+ }
1744
+ }
1745
+ else {
1746
+ const existing = warmHandles.get(prepared.sessionKey);
1747
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1748
+ await closeWarmHandle({
1749
+ handles: warmHandles,
1750
+ key: prepared.sessionKey,
1751
+ entry: existing,
1752
+ reason: "paperclip completed turn cleanup",
1753
+ });
1754
+ }
1755
+ else {
1756
+ await runtime.close({
1757
+ handle: sessionHandle,
1758
+ reason: "paperclip completed turn cleanup",
1759
+ discardPersistentState: false,
1760
+ }).catch(() => { });
1761
+ }
1762
+ }
1763
+ const errorMessage = timedOut
1764
+ ? formatAdapterExecutionTimeoutErrorMessage(prepared.timeoutResolution)
1765
+ : resultErrorMessage(terminal);
1766
+ const terminalStopReason = terminal.status === "failed" ? terminal.error.message : terminal.stopReason;
1767
+ await emitAcpxLog(ctx, {
1768
+ type: terminal.status === "completed" ? "acpx.result" : "acpx.error",
1769
+ summary: terminal.status,
1770
+ stopReason: terminalStopReason,
1771
+ message: errorMessage,
1772
+ });
1773
+ await cleanupRemoteBridges(prepared);
1774
+ return {
1775
+ exitCode: terminal.status === "completed" ? 0 : 1,
1776
+ signal: timedOut ? "SIGTERM" : null,
1777
+ timedOut,
1778
+ errorMessage,
1779
+ errorCode: terminal.status === "failed" ? "acpx_turn_failed" : timedOut ? "acpx_timeout" : null,
1780
+ sessionId: sessionHandle.backendSessionId ?? sessionHandle.runtimeSessionName,
1781
+ sessionParams: buildSessionParams({ prepared, handle: sessionHandle }),
1782
+ sessionDisplayId: sessionHandle.agentSessionId ?? sessionHandle.backendSessionId ?? sessionHandle.runtimeSessionName,
1783
+ ...billingFields,
1784
+ model: prepared.requestedModel || null,
1785
+ ...(turnUsage.usage ? { usage: turnUsage.usage, usageBasis: "per_run" } : {}),
1786
+ costUsd: turnUsage.costUsd,
1787
+ resultJson: {
1788
+ status: terminal.status,
1789
+ stopReason: terminalStopReason,
1790
+ permissionMode: prepared.permissionMode,
1791
+ mode: prepared.mode,
1792
+ requestedModel: prepared.requestedModel || null,
1793
+ requestedThinkingEffort: prepared.requestedThinkingEffort || null,
1794
+ fastMode: prepared.fastMode,
1795
+ ...(turnUsage.usageDetail ? { usage: turnUsage.usageDetail } : {}),
1796
+ ...(turnUsage.cumulativeCostUsd != null
1797
+ ? { cumulativeCostUsd: turnUsage.cumulativeCostUsd }
1798
+ : {}),
1799
+ },
1800
+ summary: textParts.join("").trim() || terminalStopReason || terminal.status,
1801
+ clearSession,
1802
+ };
1803
+ }
1804
+ catch (err) {
1805
+ if (timeout)
1806
+ clearTimeout(timeout);
1807
+ const messageOverride = timedOut
1808
+ ? formatAdapterExecutionTimeoutErrorMessage(prepared.timeoutResolution)
1809
+ : undefined;
1810
+ const cancel = cancelActiveTurn;
1811
+ const preEmitMessage = messageOverride ?? (err instanceof Error ? err.message : String(err));
1812
+ if (cancel)
1813
+ await cancel(preEmitMessage).catch(() => { });
1814
+ await runtime.close({
1815
+ handle: sessionHandle,
1816
+ reason: timedOut ? "paperclip timeout cleanup" : "paperclip error cleanup",
1817
+ discardPersistentState: timedOut,
1818
+ }).catch(() => { });
1819
+ const existing = warmHandles.get(prepared.sessionKey);
1820
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1821
+ clearWarmHandleTimer(existing);
1822
+ warmHandles.delete(prepared.sessionKey);
1823
+ }
1824
+ const { classified, message } = await emitAcpxFailure({
1825
+ ctx,
1826
+ prepared,
1827
+ err,
1828
+ phase: "turn",
1829
+ messageOverride,
1830
+ });
1831
+ await cleanupRemoteBridges(prepared);
1832
+ return {
1833
+ exitCode: 1,
1834
+ signal: timedOut ? "SIGTERM" : null,
1835
+ timedOut,
1836
+ errorMessage: message,
1837
+ errorCode: timedOut ? "acpx_timeout" : classified.errorCode,
1838
+ errorMeta: classified.errorMeta,
1839
+ ...billingFields,
1840
+ model: prepared.requestedModel || null,
1841
+ clearSession: clearSession || timedOut,
1842
+ resultJson: { phase: "turn" },
1843
+ summary: message,
1844
+ };
1845
+ }
1846
+ };
1847
+ }
1848
+ export const execute = createAcpxEngineExecutor();
1849
+ //# sourceMappingURL=execute.js.map