@evermore.work/adapter-acpx-local 2026.509.0-canary.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 (65) hide show
  1. package/dist/cli/format-event.d.ts +2 -0
  2. package/dist/cli/format-event.d.ts.map +1 -0
  3. package/dist/cli/format-event.js +128 -0
  4. package/dist/cli/format-event.js.map +1 -0
  5. package/dist/cli/format-event.test.d.ts +2 -0
  6. package/dist/cli/format-event.test.d.ts.map +1 -0
  7. package/dist/cli/format-event.test.js +88 -0
  8. package/dist/cli/format-event.test.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.d.ts.map +1 -0
  11. package/dist/cli/index.js +2 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.d.ts +22 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +51 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/server/config-schema.d.ts +3 -0
  18. package/dist/server/config-schema.d.ts.map +1 -0
  19. package/dist/server/config-schema.js +73 -0
  20. package/dist/server/config-schema.js.map +1 -0
  21. package/dist/server/execute.d.ts +19 -0
  22. package/dist/server/execute.d.ts.map +1 -0
  23. package/dist/server/execute.js +1205 -0
  24. package/dist/server/execute.js.map +1 -0
  25. package/dist/server/execute.test.d.ts +2 -0
  26. package/dist/server/execute.test.d.ts.map +1 -0
  27. package/dist/server/execute.test.js +371 -0
  28. package/dist/server/execute.test.js.map +1 -0
  29. package/dist/server/index.d.ts +6 -0
  30. package/dist/server/index.d.ts.map +1 -0
  31. package/dist/server/index.js +6 -0
  32. package/dist/server/index.js.map +1 -0
  33. package/dist/server/session-codec.d.ts +3 -0
  34. package/dist/server/session-codec.d.ts.map +1 -0
  35. package/dist/server/session-codec.js +48 -0
  36. package/dist/server/session-codec.js.map +1 -0
  37. package/dist/server/skills.d.ts +4 -0
  38. package/dist/server/skills.d.ts.map +1 -0
  39. package/dist/server/skills.js +86 -0
  40. package/dist/server/skills.js.map +1 -0
  41. package/dist/server/test.d.ts +3 -0
  42. package/dist/server/test.d.ts.map +1 -0
  43. package/dist/server/test.js +267 -0
  44. package/dist/server/test.js.map +1 -0
  45. package/dist/server/test.test.d.ts +2 -0
  46. package/dist/server/test.test.d.ts.map +1 -0
  47. package/dist/server/test.test.js +38 -0
  48. package/dist/server/test.test.js.map +1 -0
  49. package/dist/ui/build-config.d.ts +3 -0
  50. package/dist/ui/build-config.d.ts.map +1 -0
  51. package/dist/ui/build-config.js +152 -0
  52. package/dist/ui/build-config.js.map +1 -0
  53. package/dist/ui/index.d.ts +3 -0
  54. package/dist/ui/index.d.ts.map +1 -0
  55. package/dist/ui/index.js +3 -0
  56. package/dist/ui/index.js.map +1 -0
  57. package/dist/ui/parse-stdout.d.ts +3 -0
  58. package/dist/ui/parse-stdout.d.ts.map +1 -0
  59. package/dist/ui/parse-stdout.js +148 -0
  60. package/dist/ui/parse-stdout.js.map +1 -0
  61. package/dist/ui/parse-stdout.test.d.ts +2 -0
  62. package/dist/ui/parse-stdout.test.d.ts.map +1 -0
  63. package/dist/ui/parse-stdout.test.js +106 -0
  64. package/dist/ui/parse-stdout.test.js.map +1 -0
  65. package/package.json +57 -0
@@ -0,0 +1,1205 @@
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 { readAdapterExecutionTarget, adapterExecutionTargetSessionIdentity } from "@evermore.work/adapter-utils/execution-target";
7
+ import { DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE, applyEvermoreWorkspaceEnv, asNumber, asString, buildInvocationEnvForLogs, buildEvermoreEnv, ensureAbsoluteDirectory, ensurePathInEnv, joinPromptSections, materializeEvermoreSkillCopy, parseObject, readEvermoreRuntimeSkillEntries, readEvermoreIssueWorkModeFromContext, renderEvermoreWakePrompt, renderTemplate, resolveEvermoreDesiredSkillNames, shapeEvermoreWorkspaceEnvForExecution, stringifyEvermoreWakePayload, } from "@evermore.work/adapter-utils/server-utils";
8
+ import { shellQuote } from "@evermore.work/adapter-utils/ssh";
9
+ import { createAcpRuntime, createAgentRegistry, createRuntimeStore, isAcpRuntimeError, } from "acpx/runtime";
10
+ import { DEFAULT_ACPX_LOCAL_AGENT, DEFAULT_ACPX_LOCAL_MODE, DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACPX_LOCAL_PERMISSION_MODE, DEFAULT_ACPX_LOCAL_TIMEOUT_SEC, DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS, } from "../index.js";
11
+ const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
12
+ const WRAPPER_CLEANUP_RETENTION_MS = 15 * 60 * 1000;
13
+ const EVERMORE_MANAGED_CODEX_SKILLS_MANIFEST = ".evermore-managed-skills.json";
14
+ const defaultWarmHandles = new Map();
15
+ function stableJson(value) {
16
+ if (Array.isArray(value))
17
+ return `[${value.map(stableJson).join(",")}]`;
18
+ if (value && typeof value === "object") {
19
+ return `{${Object.entries(value)
20
+ .sort(([a], [b]) => a.localeCompare(b))
21
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
22
+ .join(",")}}`;
23
+ }
24
+ return JSON.stringify(value);
25
+ }
26
+ function shortHash(value) {
27
+ return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 16);
28
+ }
29
+ function defaultEvermoreInstanceDir() {
30
+ const home = process.env.EVERMORE_HOME?.trim() || path.join(os.homedir(), ".evermore");
31
+ const instanceId = process.env.EVERMORE_INSTANCE_ID?.trim() || "default";
32
+ return path.join(home, "instances", instanceId);
33
+ }
34
+ function defaultStateDir(companyId, agentId) {
35
+ return path.join(defaultEvermoreInstanceDir(), "companies", companyId, "acpx-local", "agents", agentId);
36
+ }
37
+ function resolveManagedCodexHomeDir(companyId) {
38
+ return path.join(defaultEvermoreInstanceDir(), "companies", companyId, "codex-home");
39
+ }
40
+ function packageRootDir() {
41
+ return path.resolve(__moduleDir, "../..");
42
+ }
43
+ function resolveBuiltInAgentCommand(agent) {
44
+ const binName = agent === "claude"
45
+ ? "claude-agent-acp"
46
+ : agent === "codex"
47
+ ? "codex-acp"
48
+ : null;
49
+ if (!binName)
50
+ return null;
51
+ return path.join(packageRootDir(), "node_modules", ".bin", binName);
52
+ }
53
+ function normalizeAgent(config) {
54
+ const agent = asString(config.agent, DEFAULT_ACPX_LOCAL_AGENT).trim();
55
+ return agent || DEFAULT_ACPX_LOCAL_AGENT;
56
+ }
57
+ async function pathExists(candidate) {
58
+ return fs.access(candidate).then(() => true).catch(() => false);
59
+ }
60
+ async function ensureParentDir(target) {
61
+ await fs.mkdir(path.dirname(target), { recursive: true });
62
+ }
63
+ async function writeFileAtomically(input) {
64
+ await ensureParentDir(input.target);
65
+ const tempPath = `${input.target}.tmp-${process.pid}-${randomUUID()}`;
66
+ const handle = await fs.open(tempPath, "wx", input.mode);
67
+ try {
68
+ await handle.writeFile(input.contents, "utf8");
69
+ await handle.close();
70
+ await fs.rename(tempPath, input.target);
71
+ await fs.chmod(input.target, input.mode).catch(() => { });
72
+ }
73
+ catch (err) {
74
+ await handle.close().catch(() => { });
75
+ await fs.rm(tempPath, { force: true }).catch(() => { });
76
+ throw err;
77
+ }
78
+ }
79
+ async function ensureSymlink(target, source) {
80
+ const resolvedSource = path.resolve(source);
81
+ const existing = await fs.lstat(target).catch(() => null);
82
+ if (!existing) {
83
+ await ensureParentDir(target);
84
+ await fs.symlink(resolvedSource, target);
85
+ return;
86
+ }
87
+ if (!existing.isSymbolicLink()) {
88
+ await fs.rm(target, { recursive: true, force: true });
89
+ await fs.symlink(resolvedSource, target);
90
+ return;
91
+ }
92
+ const linkedPath = await fs.readlink(target).catch(() => null);
93
+ if (!linkedPath)
94
+ return;
95
+ const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
96
+ if (resolvedLinkedPath === resolvedSource)
97
+ return;
98
+ await fs.unlink(target);
99
+ await fs.symlink(resolvedSource, target);
100
+ }
101
+ async function ensureCopiedFile(target, source) {
102
+ if (await pathExists(target))
103
+ return;
104
+ await ensureParentDir(target);
105
+ await fs.copyFile(source, target);
106
+ }
107
+ async function prepareManagedCodexHome(input) {
108
+ const { sourceHome, targetHome, onLog } = input;
109
+ if (path.resolve(sourceHome) === path.resolve(targetHome))
110
+ return targetHome;
111
+ await fs.mkdir(targetHome, { recursive: true });
112
+ const authJson = path.join(sourceHome, "auth.json");
113
+ if (await pathExists(authJson))
114
+ await ensureSymlink(path.join(targetHome, "auth.json"), authJson);
115
+ for (const name of ["config.json", "config.toml", "instructions.md"]) {
116
+ const source = path.join(sourceHome, name);
117
+ if (await pathExists(source))
118
+ await ensureCopiedFile(path.join(targetHome, name), source);
119
+ }
120
+ await onLog("stdout", `[evermore] Using Evermore-managed ACPX Codex home "${targetHome}" (seeded from "${sourceHome}").\n`);
121
+ return targetHome;
122
+ }
123
+ async function hashPathContents(candidate, hash, relativePath, seenDirectories) {
124
+ const stat = await fs.lstat(candidate);
125
+ if (stat.isSymbolicLink()) {
126
+ hash.update(`symlink-skipped:${relativePath}\n`);
127
+ return;
128
+ }
129
+ if (stat.isDirectory()) {
130
+ const realDir = await fs.realpath(candidate).catch(() => candidate);
131
+ hash.update(`dir:${relativePath}\n`);
132
+ if (seenDirectories.has(realDir)) {
133
+ hash.update("loop\n");
134
+ return;
135
+ }
136
+ seenDirectories.add(realDir);
137
+ const entries = await fs.readdir(candidate, { withFileTypes: true });
138
+ entries.sort((left, right) => left.name.localeCompare(right.name));
139
+ for (const entry of entries) {
140
+ const childRelativePath = relativePath.length > 0 ? `${relativePath}/${entry.name}` : entry.name;
141
+ await hashPathContents(path.join(candidate, entry.name), hash, childRelativePath, seenDirectories);
142
+ }
143
+ return;
144
+ }
145
+ if (stat.isFile()) {
146
+ hash.update(`file:${relativePath}\n`);
147
+ hash.update(await fs.readFile(candidate));
148
+ hash.update("\n");
149
+ return;
150
+ }
151
+ hash.update(`other:${relativePath}:${stat.mode}\n`);
152
+ }
153
+ async function buildSkillSetKey(input) {
154
+ const hash = createHash("sha256");
155
+ hash.update(`evermore-acpx-${input.label}-skills:v1\n`);
156
+ const sorted = [...input.skills].sort((left, right) => left.runtimeName.localeCompare(right.runtimeName));
157
+ for (const entry of sorted) {
158
+ hash.update(`skill:${entry.key}:${entry.runtimeName}\n`);
159
+ await hashPathContents(entry.source, hash, entry.runtimeName, new Set());
160
+ }
161
+ return hash.digest("hex");
162
+ }
163
+ async function resolveSelectedRuntimeSkills(config) {
164
+ const allSkills = await readEvermoreRuntimeSkillEntries(config, __moduleDir);
165
+ const desiredSkillNames = resolveEvermoreDesiredSkillNames(config, allSkills);
166
+ const desiredSet = new Set(desiredSkillNames);
167
+ return {
168
+ allSkills,
169
+ selectedSkills: allSkills.filter((entry) => desiredSet.has(entry.key)),
170
+ desiredSkillNames,
171
+ };
172
+ }
173
+ async function prepareClaudeSkillRuntime(input) {
174
+ const { selectedSkills, desiredSkillNames } = await resolveSelectedRuntimeSkills(input.config);
175
+ const skillSetKey = await buildSkillSetKey({ skills: selectedSkills, label: "claude" });
176
+ const bundleRoot = path.join(input.stateDir, "runtime-skills", "claude", skillSetKey);
177
+ const skillsHome = path.join(bundleRoot, ".claude", "skills");
178
+ await fs.mkdir(skillsHome, { recursive: true });
179
+ for (const entry of selectedSkills) {
180
+ const target = path.join(skillsHome, entry.runtimeName);
181
+ try {
182
+ const result = await materializeEvermoreSkillCopy(entry.source, target);
183
+ if (result.skippedSymlinks.length > 0) {
184
+ await input.onLog("stdout", `[evermore] Materialized ACPX Claude skill "${entry.runtimeName}" into ${skillsHome} and skipped ${result.skippedSymlinks.length} symlink(s).\n`);
185
+ }
186
+ }
187
+ catch (err) {
188
+ await input.onLog("stderr", `[evermore] Failed to materialize ACPX Claude skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
189
+ }
190
+ }
191
+ const selectedNames = selectedSkills.map((entry) => entry.runtimeName).sort();
192
+ const promptInstructions = selectedSkills.length > 0
193
+ ? [
194
+ "Evermore has materialized selected runtime skills for this ACPX Claude session.",
195
+ `Skill root: ${skillsHome}`,
196
+ selectedNames.length > 0 ? `Selected skills: ${selectedNames.join(", ")}` : "",
197
+ "When a task calls for one of these skills, read its SKILL.md from that root and follow it.",
198
+ ].filter(Boolean).join("\n")
199
+ : "";
200
+ return {
201
+ identity: {
202
+ mode: "claude",
203
+ skillSetKey,
204
+ desiredSkillNames,
205
+ selectedSkills: selectedNames,
206
+ skillRoot: selectedSkills.length > 0 ? skillsHome : null,
207
+ },
208
+ promptInstructions,
209
+ commandNotes: selectedSkills.length > 0
210
+ ? [`Materialized ${selectedSkills.length} Evermore skill(s) for ACPX Claude at ${skillsHome}.`]
211
+ : [],
212
+ };
213
+ }
214
+ async function readManagedCodexSkillsManifest(skillsHome) {
215
+ const manifestPath = path.join(skillsHome, EVERMORE_MANAGED_CODEX_SKILLS_MANIFEST);
216
+ try {
217
+ const raw = JSON.parse(await fs.readFile(manifestPath, "utf8"));
218
+ const parsed = parseObject(raw);
219
+ const skills = Array.isArray(parsed.managedSkillNames)
220
+ ? parsed.managedSkillNames.filter((value) => typeof value === "string" && value.trim().length > 0)
221
+ : [];
222
+ return new Set(skills);
223
+ }
224
+ catch {
225
+ return new Set();
226
+ }
227
+ }
228
+ async function writeManagedCodexSkillsManifest(skillsHome, skillNames) {
229
+ const managedSkillNames = Array.from(new Set(skillNames)).sort();
230
+ await fs.writeFile(path.join(skillsHome, EVERMORE_MANAGED_CODEX_SKILLS_MANIFEST), `${JSON.stringify({ version: 1, managedSkillNames }, null, 2)}\n`, "utf8");
231
+ }
232
+ async function removeSkillTarget(target) {
233
+ const existing = await fs.lstat(target).catch(() => null);
234
+ if (!existing)
235
+ return false;
236
+ await fs.rm(target, { recursive: true, force: true });
237
+ return true;
238
+ }
239
+ async function reconcileManagedCodexSkills(input) {
240
+ const desired = new Set(input.selectedSkills.map((entry) => entry.runtimeName));
241
+ const managed = await readManagedCodexSkillsManifest(input.skillsHome);
242
+ const availableByRuntimeName = new Map(input.allSkills.map((entry) => [entry.runtimeName, entry]));
243
+ for (const name of managed) {
244
+ if (desired.has(name))
245
+ continue;
246
+ if (await removeSkillTarget(path.join(input.skillsHome, name))) {
247
+ await input.onLog("stdout", `[evermore] Revoked ACPX Codex skill "${name}" from ${input.skillsHome}\n`);
248
+ }
249
+ }
250
+ for (const entry of input.allSkills) {
251
+ if (desired.has(entry.runtimeName) || managed.has(entry.runtimeName))
252
+ continue;
253
+ const target = path.join(input.skillsHome, entry.runtimeName);
254
+ const existing = await fs.lstat(target).catch(() => null);
255
+ if (!existing?.isSymbolicLink())
256
+ continue;
257
+ const linkedPath = await fs.readlink(target).catch(() => null);
258
+ if (!linkedPath)
259
+ continue;
260
+ const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
261
+ if (resolvedLinkedPath !== path.resolve(entry.source))
262
+ continue;
263
+ if (await removeSkillTarget(target)) {
264
+ await input.onLog("stdout", `[evermore] Revoked legacy ACPX Codex skill "${entry.runtimeName}" from ${input.skillsHome}\n`);
265
+ }
266
+ }
267
+ for (const name of managed) {
268
+ if (desired.has(name) || availableByRuntimeName.has(name))
269
+ continue;
270
+ if (await removeSkillTarget(path.join(input.skillsHome, name))) {
271
+ await input.onLog("stdout", `[evermore] Revoked unavailable ACPX Codex skill "${name}" from ${input.skillsHome}\n`);
272
+ }
273
+ }
274
+ }
275
+ async function prepareCodexSkillRuntime(input) {
276
+ const envConfig = parseObject(input.config.env);
277
+ const configuredCodexHome = typeof envConfig.CODEX_HOME === "string" && envConfig.CODEX_HOME.trim().length > 0
278
+ ? path.resolve(envConfig.CODEX_HOME.trim())
279
+ : null;
280
+ const sourceCodexHome = typeof process.env.CODEX_HOME === "string" && process.env.CODEX_HOME.trim().length > 0
281
+ ? path.resolve(process.env.CODEX_HOME.trim())
282
+ : path.join(os.homedir(), ".codex");
283
+ const managedCodexHome = resolveManagedCodexHomeDir(input.companyId);
284
+ const effectiveCodexHome = configuredCodexHome ??
285
+ await prepareManagedCodexHome({
286
+ companyId: input.companyId,
287
+ sourceHome: sourceCodexHome,
288
+ targetHome: managedCodexHome,
289
+ onLog: input.onLog,
290
+ });
291
+ const { allSkills, selectedSkills, desiredSkillNames } = await resolveSelectedRuntimeSkills(input.config);
292
+ const skillSetKey = await buildSkillSetKey({ skills: selectedSkills, label: "codex" });
293
+ const skillsHome = path.join(effectiveCodexHome, "skills");
294
+ await fs.mkdir(skillsHome, { recursive: true });
295
+ await reconcileManagedCodexSkills({
296
+ skillsHome,
297
+ allSkills,
298
+ selectedSkills,
299
+ onLog: input.onLog,
300
+ });
301
+ for (const entry of selectedSkills) {
302
+ const target = path.join(skillsHome, entry.runtimeName);
303
+ try {
304
+ const result = await materializeEvermoreSkillCopy(entry.source, target);
305
+ if (result.skippedSymlinks.length > 0) {
306
+ await input.onLog("stdout", `[evermore] Materialized ACPX Codex skill "${entry.runtimeName}" into ${skillsHome} and skipped ${result.skippedSymlinks.length} symlink(s).\n`);
307
+ }
308
+ }
309
+ catch (err) {
310
+ await input.onLog("stderr", `[evermore] Failed to inject ACPX Codex skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
311
+ }
312
+ }
313
+ await writeManagedCodexSkillsManifest(skillsHome, selectedSkills.map((entry) => entry.runtimeName));
314
+ input.env.CODEX_HOME = effectiveCodexHome;
315
+ return {
316
+ identity: {
317
+ mode: "codex",
318
+ skillSetKey,
319
+ desiredSkillNames,
320
+ selectedSkills: selectedSkills.map((entry) => entry.runtimeName).sort(),
321
+ codexHome: effectiveCodexHome,
322
+ skillsHome,
323
+ },
324
+ commandNotes: [`Prepared ACPX Codex skill home at ${skillsHome}.`],
325
+ };
326
+ }
327
+ function normalizeMode(config) {
328
+ return asString(config.mode, DEFAULT_ACPX_LOCAL_MODE) === "oneshot" ? "oneshot" : "persistent";
329
+ }
330
+ function normalizePermissionMode(config) {
331
+ const value = asString(config.permissionMode, DEFAULT_ACPX_LOCAL_PERMISSION_MODE).trim();
332
+ if (value === "approve-reads" || value === "deny-all")
333
+ return value;
334
+ if (value === "default")
335
+ return "approve-reads";
336
+ return "approve-all";
337
+ }
338
+ function normalizeNonInteractivePermissions(config) {
339
+ return asString(config.nonInteractivePermissions, DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS) === "fail"
340
+ ? "fail"
341
+ : "deny";
342
+ }
343
+ function normalizeRequestedThinkingEffort(config) {
344
+ return (asString(config.modelReasoningEffort, "") ||
345
+ asString(config.reasoningEffort, "") ||
346
+ asString(config.thinkingEffort, "") ||
347
+ asString(config.effort, "")).trim();
348
+ }
349
+ function isCompatibleSession(params, runtime) {
350
+ if (asString(params.configFingerprint, "") !== runtime.fingerprint)
351
+ return false;
352
+ if (asString(params.sessionKey, "") !== runtime.sessionKey)
353
+ return false;
354
+ if (asString(params.agent, "") !== runtime.acpxAgent)
355
+ return false;
356
+ if (asString(params.mode, "") !== runtime.mode)
357
+ return false;
358
+ const savedCwd = asString(params.cwd, "");
359
+ if (!savedCwd || path.resolve(savedCwd) !== path.resolve(runtime.cwd))
360
+ return false;
361
+ const savedRemote = parseObject(params.remoteExecution);
362
+ return stableJson(savedRemote) === stableJson(runtime.remoteExecutionIdentity ?? {});
363
+ }
364
+ function buildSessionParams(input) {
365
+ const { prepared, handle } = input;
366
+ return {
367
+ sessionKey: prepared.sessionKey,
368
+ runtimeSessionName: handle.runtimeSessionName,
369
+ acpxRecordId: handle.acpxRecordId,
370
+ acpSessionId: handle.backendSessionId,
371
+ agentSessionId: handle.agentSessionId,
372
+ agent: prepared.acpxAgent,
373
+ cwd: prepared.cwd,
374
+ mode: prepared.mode,
375
+ stateDir: prepared.stateDir,
376
+ configFingerprint: prepared.fingerprint,
377
+ ...(prepared.requestedModel ? { model: prepared.requestedModel } : {}),
378
+ ...(prepared.requestedThinkingEffort ? { thinkingEffort: prepared.requestedThinkingEffort } : {}),
379
+ ...(prepared.fastMode ? { fastMode: true } : {}),
380
+ skills: prepared.skillsIdentity,
381
+ ...(prepared.workspaceId ? { workspaceId: prepared.workspaceId } : {}),
382
+ ...(prepared.workspaceRepoUrl ? { repoUrl: prepared.workspaceRepoUrl } : {}),
383
+ ...(prepared.workspaceRepoRef ? { repoRef: prepared.workspaceRepoRef } : {}),
384
+ ...(prepared.remoteExecutionIdentity ? { remoteExecution: prepared.remoteExecutionIdentity } : {}),
385
+ };
386
+ }
387
+ async function writeAgentWrapper(input) {
388
+ const wrappersDir = path.join(input.stateDir, "wrappers");
389
+ await fs.mkdir(wrappersDir, { recursive: true });
390
+ const envLines = Object.entries(input.env)
391
+ .filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
392
+ .sort(([left], [right]) => left.localeCompare(right))
393
+ .map(([key, value]) => `${key}=${shellQuote(value)}`);
394
+ const wrapperHash = shortHash({
395
+ agent: input.acpxAgent,
396
+ command: input.agentCommandShell,
397
+ env: envLines,
398
+ });
399
+ const wrapperPath = path.join(wrappersDir, `${input.acpxAgent}-${wrapperHash}.sh`);
400
+ const envFilePath = path.join(wrappersDir, `${input.acpxAgent}-${wrapperHash}.env`);
401
+ const script = [
402
+ "#!/usr/bin/env bash",
403
+ "set -euo pipefail",
404
+ `env_file=${shellQuote(envFilePath)}`,
405
+ "if [[ -f \"$env_file\" ]]; then",
406
+ " set -a",
407
+ " source \"$env_file\"",
408
+ " set +a",
409
+ "fi",
410
+ `exec ${input.agentCommandShell} "$@"`,
411
+ "",
412
+ ].join("\n");
413
+ await writeFileAtomically({
414
+ target: envFilePath,
415
+ contents: `${envLines.join("\n")}\n`,
416
+ mode: 0o600,
417
+ });
418
+ await writeFileAtomically({
419
+ target: wrapperPath,
420
+ contents: script,
421
+ mode: 0o700,
422
+ });
423
+ await cleanupStaleAgentWrappers({
424
+ wrappersDir,
425
+ currentFileNames: new Set([path.basename(wrapperPath), path.basename(envFilePath)]),
426
+ });
427
+ return { wrapperPath, envFilePath };
428
+ }
429
+ async function cleanupStaleAgentWrappers(input) {
430
+ const wrappers = await fs.readdir(input.wrappersDir).catch(() => []);
431
+ const now = Date.now();
432
+ await Promise.all(wrappers.map(async (name) => {
433
+ const isManagedWrapperFile = name.endsWith(".sh") || name.endsWith(".env");
434
+ if (!isManagedWrapperFile || input.currentFileNames.has(name))
435
+ return;
436
+ const wrapperPath = path.join(input.wrappersDir, name);
437
+ const stats = await fs.stat(wrapperPath).catch(() => null);
438
+ if (!stats || now - stats.mtimeMs < WRAPPER_CLEANUP_RETENTION_MS)
439
+ return;
440
+ await fs.rm(wrapperPath, { force: true });
441
+ }));
442
+ }
443
+ async function buildRuntime(input) {
444
+ const { runId, agent, config, context, authToken } = input.ctx;
445
+ const workspaceContext = parseObject(context.evermoreWorkspace);
446
+ const workspaceCwd = asString(workspaceContext.cwd, "");
447
+ const workspaceSource = asString(workspaceContext.source, "");
448
+ const workspaceStrategy = asString(workspaceContext.strategy, "");
449
+ const workspaceId = asString(workspaceContext.workspaceId, "");
450
+ const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
451
+ const workspaceRepoRef = asString(workspaceContext.repoRef, "");
452
+ const workspaceBranch = asString(workspaceContext.branchName, "");
453
+ const workspaceWorktreePath = asString(workspaceContext.worktreePath, "");
454
+ const agentHome = asString(workspaceContext.agentHome, "");
455
+ const configuredCwd = asString(config.cwd, "");
456
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
457
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
458
+ const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
459
+ const executionTarget = readAdapterExecutionTarget({
460
+ executionTarget: input.ctx.executionTarget,
461
+ legacyRemoteExecution: input.ctx.executionTransport?.remoteExecution,
462
+ });
463
+ const remoteExecutionIdentity = adapterExecutionTargetSessionIdentity(executionTarget);
464
+ const effectiveExecutionCwd = remoteExecutionIdentity && typeof remoteExecutionIdentity.remoteCwd === "string"
465
+ ? remoteExecutionIdentity.remoteCwd
466
+ : cwd;
467
+ const shapedWorkspaceEnv = shapeEvermoreWorkspaceEnvForExecution({
468
+ workspaceCwd: effectiveWorkspaceCwd,
469
+ workspaceWorktreePath,
470
+ executionTargetIsRemote: remoteExecutionIdentity !== null,
471
+ executionCwd: effectiveExecutionCwd,
472
+ });
473
+ await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
474
+ const acpxAgent = normalizeAgent(config);
475
+ const mode = normalizeMode(config);
476
+ const permissionMode = normalizePermissionMode(config);
477
+ const nonInteractivePermissions = normalizeNonInteractivePermissions(config);
478
+ const requestedModel = asString(config.model, "").trim();
479
+ const requestedThinkingEffort = normalizeRequestedThinkingEffort(config);
480
+ const fastMode = acpxAgent === "codex" && config.fastMode === true;
481
+ const timeoutSec = asNumber(config.timeoutSec, DEFAULT_ACPX_LOCAL_TIMEOUT_SEC);
482
+ const stateDir = path.resolve(asString(config.stateDir, "") || defaultStateDir(agent.companyId, agent.id));
483
+ await fs.mkdir(stateDir, { recursive: true });
484
+ const envConfig = parseObject(config.env);
485
+ const hasExplicitApiKey = typeof envConfig.EVERMORE_API_KEY === "string" && envConfig.EVERMORE_API_KEY.trim().length > 0;
486
+ const env = { ...buildEvermoreEnv(agent), EVERMORE_RUN_ID: runId };
487
+ const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim()) ||
488
+ (typeof context.issueId === "string" && context.issueId.trim()) ||
489
+ "";
490
+ const wakeReason = typeof context.wakeReason === "string" ? context.wakeReason.trim() : "";
491
+ const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim()) ||
492
+ (typeof context.commentId === "string" && context.commentId.trim()) ||
493
+ "";
494
+ const approvalId = typeof context.approvalId === "string" ? context.approvalId.trim() : "";
495
+ const approvalStatus = typeof context.approvalStatus === "string" ? context.approvalStatus.trim() : "";
496
+ const linkedIssueIds = Array.isArray(context.issueIds)
497
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
498
+ : [];
499
+ const wakePayloadJson = stringifyEvermoreWakePayload(context.evermoreWake);
500
+ const issueWorkMode = readEvermoreIssueWorkModeFromContext(context);
501
+ if (wakeTaskId)
502
+ env.EVERMORE_TASK_ID = wakeTaskId;
503
+ if (issueWorkMode)
504
+ env.EVERMORE_ISSUE_WORK_MODE = issueWorkMode;
505
+ if (wakeReason)
506
+ env.EVERMORE_WAKE_REASON = wakeReason;
507
+ if (wakeCommentId)
508
+ env.EVERMORE_WAKE_COMMENT_ID = wakeCommentId;
509
+ if (approvalId)
510
+ env.EVERMORE_APPROVAL_ID = approvalId;
511
+ if (approvalStatus)
512
+ env.EVERMORE_APPROVAL_STATUS = approvalStatus;
513
+ if (linkedIssueIds.length > 0)
514
+ env.EVERMORE_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
515
+ if (wakePayloadJson)
516
+ env.EVERMORE_WAKE_PAYLOAD_JSON = wakePayloadJson;
517
+ applyEvermoreWorkspaceEnv(env, {
518
+ workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
519
+ workspaceSource,
520
+ workspaceStrategy,
521
+ workspaceId,
522
+ workspaceRepoUrl,
523
+ workspaceRepoRef,
524
+ workspaceBranch,
525
+ workspaceWorktreePath: shapedWorkspaceEnv.workspaceWorktreePath,
526
+ agentHome,
527
+ });
528
+ for (const [key, value] of Object.entries(envConfig)) {
529
+ if (typeof value === "string")
530
+ env[key] = value;
531
+ }
532
+ if (!hasExplicitApiKey && authToken)
533
+ env.EVERMORE_API_KEY = authToken;
534
+ let skillPromptInstructions = "";
535
+ let skillsIdentity = { mode: "unsupported" };
536
+ const skillCommandNotes = [];
537
+ if (acpxAgent === "claude") {
538
+ const preparedSkills = await prepareClaudeSkillRuntime({
539
+ stateDir,
540
+ config,
541
+ onLog: input.ctx.onLog,
542
+ });
543
+ skillPromptInstructions = preparedSkills.promptInstructions;
544
+ skillsIdentity = preparedSkills.identity;
545
+ skillCommandNotes.push(...preparedSkills.commandNotes);
546
+ }
547
+ else if (acpxAgent === "codex") {
548
+ const preparedSkills = await prepareCodexSkillRuntime({
549
+ companyId: agent.companyId,
550
+ config,
551
+ env,
552
+ onLog: input.ctx.onLog,
553
+ });
554
+ skillsIdentity = preparedSkills.identity;
555
+ skillCommandNotes.push(...preparedSkills.commandNotes);
556
+ }
557
+ else {
558
+ const desired = resolveEvermoreDesiredSkillNames(config, await readEvermoreRuntimeSkillEntries(config, __moduleDir));
559
+ skillsIdentity = { mode: "custom_unsupported", desiredSkillNames: desired };
560
+ if (desired.length > 0) {
561
+ skillCommandNotes.push("Selected Evermore skills are tracked only; ACPX custom commands do not expose a runtime skill contract yet.");
562
+ }
563
+ }
564
+ const configuredCommand = asString(config.agentCommand, "").trim();
565
+ const builtInCommand = resolveBuiltInAgentCommand(acpxAgent);
566
+ const agentCommand = configuredCommand || builtInCommand || null;
567
+ const agentCommandShell = configuredCommand || (builtInCommand ? shellQuote(builtInCommand) : "");
568
+ const wrapper = agentCommand
569
+ ? await writeAgentWrapper({
570
+ stateDir,
571
+ acpxAgent,
572
+ agentCommandShell,
573
+ env,
574
+ })
575
+ : null;
576
+ const wrapperPath = wrapper?.wrapperPath ?? null;
577
+ const overrides = wrapperPath ? { [acpxAgent]: wrapperPath } : undefined;
578
+ const agentRegistry = createAgentRegistry({ overrides });
579
+ const fingerprint = shortHash({
580
+ acpxAgent,
581
+ agentCommand: agentCommand ?? acpxAgent,
582
+ cwd: path.resolve(cwd),
583
+ mode,
584
+ permissionMode,
585
+ nonInteractivePermissions,
586
+ requestedModel,
587
+ requestedThinkingEffort,
588
+ fastMode,
589
+ remoteExecutionIdentity,
590
+ skillsIdentity,
591
+ skillPromptInstructions,
592
+ });
593
+ const taskKey = asString(input.ctx.runtime.taskKey, "") || wakeTaskId || workspaceId || "default";
594
+ const sessionKey = `evermore:${agent.companyId}:${agent.id}:${taskKey}:${fingerprint}`;
595
+ const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
596
+ const loggedEnv = buildInvocationEnvForLogs(env, {
597
+ runtimeEnv,
598
+ includeRuntimeKeys: ["HOME"],
599
+ resolvedCommand: wrapperPath ?? agentCommand ?? acpxAgent,
600
+ });
601
+ return {
602
+ acpxAgent,
603
+ mode,
604
+ cwd,
605
+ workspaceId,
606
+ workspaceRepoUrl,
607
+ workspaceRepoRef,
608
+ env,
609
+ loggedEnv,
610
+ stateDir,
611
+ permissionMode,
612
+ nonInteractivePermissions,
613
+ requestedModel,
614
+ requestedThinkingEffort,
615
+ fastMode,
616
+ timeoutSec,
617
+ sessionKey,
618
+ fingerprint,
619
+ agentCommand,
620
+ agentRegistry,
621
+ remoteExecutionIdentity,
622
+ skillPromptInstructions,
623
+ skillsIdentity: {
624
+ ...skillsIdentity,
625
+ commandNotes: skillCommandNotes,
626
+ },
627
+ };
628
+ }
629
+ function sessionConfigOptions(prepared) {
630
+ const options = [];
631
+ if (prepared.requestedModel)
632
+ options.push({ key: "model", value: prepared.requestedModel });
633
+ if (prepared.requestedThinkingEffort) {
634
+ options.push({
635
+ key: prepared.acpxAgent === "codex" ? "reasoning_effort" : "effort",
636
+ value: prepared.requestedThinkingEffort,
637
+ });
638
+ }
639
+ if (prepared.fastMode) {
640
+ options.push({ key: "service_tier", value: "fast" }, { key: "features.fast_mode", value: "true" });
641
+ }
642
+ return options;
643
+ }
644
+ async function applySessionConfigOptions(input) {
645
+ const options = sessionConfigOptions(input.prepared);
646
+ if (options.length === 0)
647
+ return;
648
+ if (!input.runtime.setConfigOption) {
649
+ const message = "ACPX runtime does not expose session config controls; upgrade ACPX or remove configured model, effort, and fast mode overrides.";
650
+ await input.onLog("stderr", `[evermore] ${message}\n`);
651
+ throw new Error(message);
652
+ }
653
+ for (const option of options) {
654
+ await input.runtime.setConfigOption({
655
+ handle: input.handle,
656
+ key: option.key,
657
+ value: option.value,
658
+ });
659
+ await input.onLog("stdout", `[evermore] Applied ACPX ${input.prepared.acpxAgent} config ${option.key}=${option.value}\n`);
660
+ }
661
+ }
662
+ async function buildPrompt(ctx, resumedSession) {
663
+ const { agent, runId, config, context, onLog } = ctx;
664
+ const promptTemplate = asString(config.promptTemplate, DEFAULT_EVERMORE_AGENT_PROMPT_TEMPLATE);
665
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
666
+ const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
667
+ let instructionsPrefix = "";
668
+ const commandNotes = [];
669
+ if (instructionsFilePath) {
670
+ try {
671
+ const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
672
+ instructionsPrefix =
673
+ `${instructionsContents}\n\n` +
674
+ `The above agent instructions were loaded from ${instructionsFilePath}. ` +
675
+ `Resolve any relative file references from ${instructionsDir}.\n\n`;
676
+ commandNotes.push(`Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to the ACPX prompt (relative references from ${instructionsDir}).`);
677
+ }
678
+ catch (err) {
679
+ const reason = err instanceof Error ? err.message : String(err);
680
+ await onLog("stderr", `[evermore] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
681
+ commandNotes.push(`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read.`);
682
+ }
683
+ }
684
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
685
+ const templateData = {
686
+ agentId: agent.id,
687
+ companyId: agent.companyId,
688
+ runId,
689
+ company: { id: agent.companyId },
690
+ agent,
691
+ run: { id: runId, source: "on_demand" },
692
+ context,
693
+ };
694
+ const renderedBootstrapPrompt = !resumedSession && bootstrapPromptTemplate.trim().length > 0
695
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
696
+ : "";
697
+ const wakePrompt = renderEvermoreWakePrompt(context.evermoreWake, { resumedSession });
698
+ const shouldUseResumeDeltaPrompt = resumedSession && wakePrompt.length > 0;
699
+ const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
700
+ const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
701
+ const sessionHandoffNote = asString(context.evermoreSessionHandoffMarkdown, "").trim();
702
+ const taskContextNote = asString(context.evermoreTaskMarkdown, "").trim();
703
+ const prompt = joinPromptSections([
704
+ promptInstructionsPrefix,
705
+ renderedBootstrapPrompt,
706
+ wakePrompt,
707
+ sessionHandoffNote,
708
+ taskContextNote,
709
+ renderedPrompt,
710
+ ]);
711
+ return {
712
+ prompt,
713
+ commandNotes,
714
+ promptMetrics: {
715
+ promptChars: prompt.length,
716
+ instructionsChars: promptInstructionsPrefix.length,
717
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
718
+ wakePromptChars: wakePrompt.length,
719
+ sessionHandoffChars: sessionHandoffNote.length,
720
+ taskContextChars: taskContextNote.length,
721
+ heartbeatPromptChars: renderedPrompt.length,
722
+ },
723
+ };
724
+ }
725
+ async function emitAcpxLog(ctx, payload) {
726
+ await ctx.onLog("stdout", `${JSON.stringify(payload)}\n`);
727
+ }
728
+ async function emitRuntimeEvent(ctx, event) {
729
+ if (event.type === "text_delta") {
730
+ await emitAcpxLog(ctx, {
731
+ type: "acpx.text_delta",
732
+ text: event.text,
733
+ channel: event.stream === "thought" ? "thought" : "output",
734
+ tag: event.tag,
735
+ });
736
+ return;
737
+ }
738
+ if (event.type === "tool_call") {
739
+ await emitAcpxLog(ctx, {
740
+ type: "acpx.tool_call",
741
+ name: event.title ?? "acp_tool",
742
+ toolCallId: event.toolCallId,
743
+ status: event.status,
744
+ text: event.text,
745
+ tag: event.tag,
746
+ });
747
+ return;
748
+ }
749
+ if (event.type === "status") {
750
+ await emitAcpxLog(ctx, {
751
+ type: "acpx.status",
752
+ text: event.text,
753
+ tag: event.tag,
754
+ used: event.used,
755
+ size: event.size,
756
+ });
757
+ return;
758
+ }
759
+ if (event.type === "done") {
760
+ await emitAcpxLog(ctx, {
761
+ type: "acpx.result",
762
+ summary: event.stopReason ?? "completed",
763
+ stopReason: event.stopReason,
764
+ });
765
+ return;
766
+ }
767
+ if (event.type === "error") {
768
+ await emitAcpxLog(ctx, {
769
+ type: "acpx.error",
770
+ message: event.message,
771
+ code: event.code,
772
+ retryable: event.retryable,
773
+ });
774
+ }
775
+ }
776
+ function resultErrorMessage(result) {
777
+ if (result.status !== "failed")
778
+ return null;
779
+ return result.error.message;
780
+ }
781
+ function classifyError(err) {
782
+ const message = err instanceof Error ? err.message : String(err);
783
+ const maybeCode = err && typeof err === "object" && typeof err.code === "string"
784
+ ? err.code
785
+ : null;
786
+ const acpCode = isAcpRuntimeError(err) || (maybeCode?.startsWith("ACP_") ?? false) ? maybeCode : null;
787
+ const lower = message.toLowerCase();
788
+ const authLike = lower.includes("auth") || lower.includes("login") || lower.includes("credential");
789
+ if (authLike) {
790
+ return {
791
+ errorCode: "acpx_auth_required",
792
+ errorMeta: { category: "auth", ...(acpCode ? { acpCode } : {}) },
793
+ };
794
+ }
795
+ if (acpCode) {
796
+ return {
797
+ errorCode: "acpx_protocol_error",
798
+ errorMeta: { category: "protocol", acpCode },
799
+ };
800
+ }
801
+ return {
802
+ errorCode: "acpx_runtime_error",
803
+ errorMeta: { category: "runtime" },
804
+ };
805
+ }
806
+ function isResumeFailure(err) {
807
+ const message = err instanceof Error ? err.message : String(err);
808
+ return /resume|load|not found|no session|unknown session|conversation/i.test(message);
809
+ }
810
+ async function cleanupIdleHandles(input) {
811
+ if (input.idleMs <= 0)
812
+ return;
813
+ const stale = [];
814
+ for (const entry of input.handles.entries()) {
815
+ if (input.now - entry[1].lastUsedAt >= input.idleMs)
816
+ stale.push(entry);
817
+ }
818
+ for (const [key, entry] of stale) {
819
+ await closeWarmHandle({
820
+ handles: input.handles,
821
+ key,
822
+ entry,
823
+ reason: "evermore idle cleanup",
824
+ });
825
+ }
826
+ }
827
+ function clearWarmHandleTimer(entry) {
828
+ if (!entry.cleanupTimer)
829
+ return;
830
+ clearTimeout(entry.cleanupTimer);
831
+ entry.cleanupTimer = undefined;
832
+ }
833
+ async function closeWarmHandle(input) {
834
+ if (input.handles.get(input.key) === input.entry) {
835
+ input.handles.delete(input.key);
836
+ }
837
+ clearWarmHandleTimer(input.entry);
838
+ await input.entry.runtime.close({
839
+ handle: input.entry.handle,
840
+ reason: input.reason,
841
+ discardPersistentState: input.discardPersistentState ?? false,
842
+ }).catch(() => { });
843
+ }
844
+ function scheduleIdleHandleCleanup(input) {
845
+ clearWarmHandleTimer(input.entry);
846
+ if (input.idleMs <= 0)
847
+ return;
848
+ const delayMs = Math.max(1, input.entry.lastUsedAt + input.idleMs - input.now());
849
+ input.entry.cleanupTimer = setTimeout(() => {
850
+ void (async () => {
851
+ const current = input.handles.get(input.key);
852
+ if (current !== input.entry)
853
+ return;
854
+ const idleForMs = input.now() - input.entry.lastUsedAt;
855
+ if (idleForMs < input.idleMs) {
856
+ scheduleIdleHandleCleanup(input);
857
+ return;
858
+ }
859
+ await closeWarmHandle({
860
+ handles: input.handles,
861
+ key: input.key,
862
+ entry: input.entry,
863
+ reason: "evermore idle cleanup",
864
+ });
865
+ })();
866
+ }, delayMs);
867
+ input.entry.cleanupTimer.unref?.();
868
+ }
869
+ function warmHandleMatches(entry, runtime, handle) {
870
+ return entry?.runtime === runtime && entry.handle === handle;
871
+ }
872
+ export function createAcpxLocalExecutor(deps = {}) {
873
+ const createRuntime = deps.createRuntime ?? createAcpRuntime;
874
+ const now = deps.now ?? (() => Date.now());
875
+ const warmHandles = deps.warmHandles ?? defaultWarmHandles;
876
+ return async function executeAcpxLocal(ctx) {
877
+ const prepared = await buildRuntime({ ctx });
878
+ const warmIdleMs = asNumber(ctx.config.warmHandleIdleMs, DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS);
879
+ await cleanupIdleHandles({ handles: warmHandles, now: now(), idleMs: warmIdleMs });
880
+ const previousParams = parseObject(ctx.runtime.sessionParams);
881
+ const canResume = isCompatibleSession(previousParams, prepared);
882
+ const resumeSessionId = canResume ? asString(previousParams.acpSessionId, "") || undefined : undefined;
883
+ const cached = canResume ? warmHandles.get(prepared.sessionKey) : undefined;
884
+ const runtimeOptions = {
885
+ cwd: prepared.cwd,
886
+ sessionStore: createRuntimeStore({ stateDir: prepared.stateDir }),
887
+ agentRegistry: prepared.agentRegistry,
888
+ permissionMode: prepared.permissionMode,
889
+ nonInteractivePermissions: prepared.nonInteractivePermissions,
890
+ timeoutMs: prepared.timeoutSec > 0 ? prepared.timeoutSec * 1000 : undefined,
891
+ };
892
+ const runtime = cached?.runtime ?? createRuntime(runtimeOptions);
893
+ if (cached)
894
+ clearWarmHandleTimer(cached);
895
+ if (!canResume && asString(previousParams.runtimeSessionName, "")) {
896
+ await ctx.onLog("stdout", `[evermore] ACPX session "${asString(previousParams.runtimeSessionName, "")}" does not match the current agent/cwd/mode/runtime identity; starting fresh in "${prepared.cwd}".\n`);
897
+ }
898
+ let handle = cached?.handle ?? null;
899
+ let resumedSession = Boolean(handle ?? resumeSessionId);
900
+ let clearSession = false;
901
+ try {
902
+ if (!handle) {
903
+ try {
904
+ handle = await runtime.ensureSession({
905
+ sessionKey: prepared.sessionKey,
906
+ agent: prepared.acpxAgent,
907
+ mode: prepared.mode,
908
+ cwd: prepared.cwd,
909
+ resumeSessionId,
910
+ });
911
+ }
912
+ catch (err) {
913
+ if (!resumeSessionId || !isResumeFailure(err))
914
+ throw err;
915
+ clearSession = true;
916
+ resumedSession = false;
917
+ await ctx.onLog("stdout", `[evermore] ACPX resume session "${resumeSessionId}" is unavailable; retrying with a fresh session.\n`);
918
+ handle = await runtime.ensureSession({
919
+ sessionKey: prepared.sessionKey,
920
+ agent: prepared.acpxAgent,
921
+ mode: prepared.mode,
922
+ cwd: prepared.cwd,
923
+ });
924
+ }
925
+ }
926
+ }
927
+ catch (err) {
928
+ const classified = classifyError(err);
929
+ const message = err instanceof Error ? err.message : String(err);
930
+ await emitAcpxLog(ctx, { type: "acpx.error", message, ...classified.errorMeta });
931
+ return {
932
+ exitCode: 1,
933
+ signal: null,
934
+ timedOut: false,
935
+ errorMessage: message,
936
+ ...classified,
937
+ provider: "acpx",
938
+ model: prepared.requestedModel || null,
939
+ clearSession,
940
+ resultJson: { phase: "ensure_session" },
941
+ summary: message,
942
+ };
943
+ }
944
+ if (!handle) {
945
+ return {
946
+ exitCode: 1,
947
+ signal: null,
948
+ timedOut: false,
949
+ errorMessage: "ACPX did not return a runtime session handle.",
950
+ errorCode: "acpx_runtime_error",
951
+ provider: "acpx",
952
+ model: prepared.requestedModel || null,
953
+ resultJson: { phase: "ensure_session" },
954
+ summary: "ACPX did not return a runtime session handle.",
955
+ };
956
+ }
957
+ const sessionHandle = handle;
958
+ try {
959
+ await applySessionConfigOptions({
960
+ runtime,
961
+ handle: sessionHandle,
962
+ prepared,
963
+ onLog: ctx.onLog,
964
+ });
965
+ }
966
+ catch (err) {
967
+ const classified = classifyError(err);
968
+ const message = err instanceof Error ? err.message : String(err);
969
+ await emitAcpxLog(ctx, { type: "acpx.error", message, ...classified.errorMeta });
970
+ await runtime.close({
971
+ handle: sessionHandle,
972
+ reason: "evermore config cleanup",
973
+ discardPersistentState: false,
974
+ }).catch(() => { });
975
+ const existing = warmHandles.get(prepared.sessionKey);
976
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
977
+ clearWarmHandleTimer(existing);
978
+ warmHandles.delete(prepared.sessionKey);
979
+ }
980
+ return {
981
+ exitCode: 1,
982
+ signal: null,
983
+ timedOut: false,
984
+ errorMessage: message,
985
+ ...classified,
986
+ provider: "acpx",
987
+ model: prepared.requestedModel || null,
988
+ clearSession,
989
+ resultJson: {
990
+ phase: "configure_session",
991
+ agent: prepared.acpxAgent,
992
+ requestedModel: prepared.requestedModel || null,
993
+ requestedThinkingEffort: prepared.requestedThinkingEffort || null,
994
+ fastMode: prepared.fastMode,
995
+ },
996
+ summary: message,
997
+ };
998
+ }
999
+ const { prompt, promptMetrics, commandNotes } = await buildPrompt(ctx, resumedSession);
1000
+ const runPrompt = joinPromptSections([prepared.skillPromptInstructions, prompt]);
1001
+ await emitAcpxLog(ctx, {
1002
+ type: "acpx.session",
1003
+ agent: prepared.acpxAgent,
1004
+ sessionId: sessionHandle.backendSessionId,
1005
+ acpSessionId: sessionHandle.backendSessionId,
1006
+ agentSessionId: sessionHandle.agentSessionId,
1007
+ runtimeSessionName: sessionHandle.runtimeSessionName,
1008
+ mode: prepared.mode,
1009
+ permissionMode: prepared.permissionMode,
1010
+ model: prepared.requestedModel || null,
1011
+ thinkingEffort: prepared.requestedThinkingEffort || null,
1012
+ fastMode: prepared.fastMode,
1013
+ });
1014
+ if (ctx.onMeta) {
1015
+ await ctx.onMeta({
1016
+ adapterType: "acpx_local",
1017
+ command: prepared.agentCommand ?? prepared.acpxAgent,
1018
+ cwd: prepared.cwd,
1019
+ commandNotes: [
1020
+ `ACPX runtime embedded in Evermore with ${prepared.mode} session mode.`,
1021
+ `Effective ACPX permission mode: ${prepared.permissionMode}.`,
1022
+ ...(prepared.requestedModel ? [`Requested ACPX model: ${prepared.requestedModel}.`] : []),
1023
+ ...(prepared.requestedThinkingEffort ? [`Requested ACPX thinking effort: ${prepared.requestedThinkingEffort}.`] : []),
1024
+ ...(prepared.fastMode ? ["Requested ACPX Codex fast mode."] : []),
1025
+ ...(Array.isArray(prepared.skillsIdentity.commandNotes)
1026
+ ? prepared.skillsIdentity.commandNotes.filter((note) => typeof note === "string")
1027
+ : []),
1028
+ ...commandNotes,
1029
+ ],
1030
+ env: prepared.loggedEnv,
1031
+ prompt: runPrompt,
1032
+ promptMetrics,
1033
+ context: ctx.context,
1034
+ });
1035
+ }
1036
+ let cancelActiveTurn = null;
1037
+ let controller = null;
1038
+ let timeout = null;
1039
+ let timedOut = false;
1040
+ const textParts = [];
1041
+ try {
1042
+ const timeoutMs = prepared.timeoutSec > 0 ? prepared.timeoutSec * 1000 : undefined;
1043
+ controller = new AbortController();
1044
+ if (timeoutMs) {
1045
+ timeout = setTimeout(() => {
1046
+ timedOut = true;
1047
+ controller?.abort();
1048
+ void cancelActiveTurn?.(`Timed out after ${prepared.timeoutSec}s`).catch(() => { });
1049
+ }, timeoutMs);
1050
+ }
1051
+ const turn = runtime.startTurn({
1052
+ handle: sessionHandle,
1053
+ text: runPrompt,
1054
+ mode: "prompt",
1055
+ requestId: ctx.runId,
1056
+ timeoutMs,
1057
+ signal: controller?.signal,
1058
+ });
1059
+ cancelActiveTurn = async (reason) => {
1060
+ await turn.cancel({ reason });
1061
+ };
1062
+ for await (const event of turn.events) {
1063
+ if (event.type === "text_delta")
1064
+ textParts.push(event.text);
1065
+ await emitRuntimeEvent(ctx, event);
1066
+ }
1067
+ const terminal = await turn.result;
1068
+ if (timeout)
1069
+ clearTimeout(timeout);
1070
+ if (terminal.status === "failed" || terminal.status === "cancelled" || timedOut) {
1071
+ const existing = warmHandles.get(prepared.sessionKey);
1072
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1073
+ await closeWarmHandle({
1074
+ handles: warmHandles,
1075
+ key: prepared.sessionKey,
1076
+ entry: existing,
1077
+ reason: timedOut ? "evermore timeout cleanup" : `evermore turn ${terminal.status}`,
1078
+ discardPersistentState: terminal.status === "cancelled" || timedOut,
1079
+ });
1080
+ }
1081
+ else {
1082
+ await runtime.close({
1083
+ handle: sessionHandle,
1084
+ reason: timedOut ? "evermore timeout cleanup" : `evermore turn ${terminal.status}`,
1085
+ discardPersistentState: terminal.status === "cancelled" || timedOut,
1086
+ }).catch(() => { });
1087
+ }
1088
+ }
1089
+ else if (prepared.mode === "persistent" && warmIdleMs > 0) {
1090
+ const existing = warmHandles.get(prepared.sessionKey);
1091
+ if (existing && !warmHandleMatches(existing, runtime, sessionHandle)) {
1092
+ await runtime.close({
1093
+ handle: sessionHandle,
1094
+ reason: "evermore duplicate warm handle cleanup",
1095
+ discardPersistentState: false,
1096
+ }).catch(() => { });
1097
+ }
1098
+ else {
1099
+ const entry = {
1100
+ runtime,
1101
+ handle: sessionHandle,
1102
+ fingerprint: prepared.fingerprint,
1103
+ lastUsedAt: now(),
1104
+ };
1105
+ warmHandles.set(prepared.sessionKey, entry);
1106
+ scheduleIdleHandleCleanup({
1107
+ handles: warmHandles,
1108
+ key: prepared.sessionKey,
1109
+ entry,
1110
+ idleMs: warmIdleMs,
1111
+ now,
1112
+ });
1113
+ }
1114
+ }
1115
+ else {
1116
+ const existing = warmHandles.get(prepared.sessionKey);
1117
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1118
+ await closeWarmHandle({
1119
+ handles: warmHandles,
1120
+ key: prepared.sessionKey,
1121
+ entry: existing,
1122
+ reason: "evermore completed turn cleanup",
1123
+ });
1124
+ }
1125
+ else {
1126
+ await runtime.close({
1127
+ handle: sessionHandle,
1128
+ reason: "evermore completed turn cleanup",
1129
+ discardPersistentState: false,
1130
+ }).catch(() => { });
1131
+ }
1132
+ }
1133
+ const errorMessage = timedOut
1134
+ ? `Timed out after ${prepared.timeoutSec}s`
1135
+ : resultErrorMessage(terminal);
1136
+ const terminalStopReason = terminal.status === "failed" ? terminal.error.message : terminal.stopReason;
1137
+ await emitAcpxLog(ctx, {
1138
+ type: terminal.status === "completed" ? "acpx.result" : "acpx.error",
1139
+ summary: terminal.status,
1140
+ stopReason: terminalStopReason,
1141
+ message: errorMessage,
1142
+ });
1143
+ return {
1144
+ exitCode: terminal.status === "completed" ? 0 : 1,
1145
+ signal: timedOut ? "SIGTERM" : null,
1146
+ timedOut,
1147
+ errorMessage,
1148
+ errorCode: terminal.status === "failed" ? "acpx_turn_failed" : timedOut ? "acpx_timeout" : null,
1149
+ sessionId: sessionHandle.backendSessionId ?? sessionHandle.runtimeSessionName,
1150
+ sessionParams: buildSessionParams({ prepared, handle: sessionHandle }),
1151
+ sessionDisplayId: sessionHandle.agentSessionId ?? sessionHandle.backendSessionId ?? sessionHandle.runtimeSessionName,
1152
+ provider: "acpx",
1153
+ model: prepared.requestedModel || null,
1154
+ billingType: "unknown",
1155
+ costUsd: null,
1156
+ resultJson: {
1157
+ status: terminal.status,
1158
+ stopReason: terminalStopReason,
1159
+ permissionMode: prepared.permissionMode,
1160
+ mode: prepared.mode,
1161
+ requestedModel: prepared.requestedModel || null,
1162
+ requestedThinkingEffort: prepared.requestedThinkingEffort || null,
1163
+ fastMode: prepared.fastMode,
1164
+ },
1165
+ summary: textParts.join("").trim() || terminalStopReason || terminal.status,
1166
+ clearSession,
1167
+ };
1168
+ }
1169
+ catch (err) {
1170
+ if (timeout)
1171
+ clearTimeout(timeout);
1172
+ const classified = classifyError(err);
1173
+ const message = timedOut ? `Timed out after ${prepared.timeoutSec}s` : err instanceof Error ? err.message : String(err);
1174
+ const cancel = cancelActiveTurn;
1175
+ if (cancel)
1176
+ await cancel(message).catch(() => { });
1177
+ await runtime.close({
1178
+ handle: sessionHandle,
1179
+ reason: timedOut ? "evermore timeout cleanup" : "evermore error cleanup",
1180
+ discardPersistentState: timedOut,
1181
+ }).catch(() => { });
1182
+ const existing = warmHandles.get(prepared.sessionKey);
1183
+ if (warmHandleMatches(existing, runtime, sessionHandle) && existing) {
1184
+ clearWarmHandleTimer(existing);
1185
+ warmHandles.delete(prepared.sessionKey);
1186
+ }
1187
+ await emitAcpxLog(ctx, { type: "acpx.error", message, ...classified.errorMeta });
1188
+ return {
1189
+ exitCode: 1,
1190
+ signal: timedOut ? "SIGTERM" : null,
1191
+ timedOut,
1192
+ errorMessage: message,
1193
+ errorCode: timedOut ? "acpx_timeout" : classified.errorCode,
1194
+ errorMeta: classified.errorMeta,
1195
+ provider: "acpx",
1196
+ model: prepared.requestedModel || null,
1197
+ clearSession: clearSession || timedOut,
1198
+ resultJson: { phase: "turn" },
1199
+ summary: message,
1200
+ };
1201
+ }
1202
+ };
1203
+ }
1204
+ export const execute = createAcpxLocalExecutor();
1205
+ //# sourceMappingURL=execute.js.map