@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.2

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 (62) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/openapi.yaml +6 -7
  3. package/package.json +1 -1
  4. package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
  5. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  6. package/src/__tests__/app-executors.test.ts +132 -0
  7. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  8. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  9. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
  10. package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
  11. package/src/__tests__/conversation-agent-loop.test.ts +48 -96
  12. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  13. package/src/__tests__/conversation-provider-retry-repair.test.ts +52 -49
  14. package/src/__tests__/memory-retrieval-hook.test.ts +68 -30
  15. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  16. package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
  17. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
  18. package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
  19. package/src/acp/auto-install.test.ts +130 -46
  20. package/src/acp/auto-install.ts +86 -31
  21. package/src/acp/prepare-agent-env.ts +8 -9
  22. package/src/acp/resolve-agent.test.ts +26 -123
  23. package/src/acp/resolve-agent.ts +26 -85
  24. package/src/acp/resume-hint.ts +1 -3
  25. package/src/acp/session-manager.ts +32 -20
  26. package/src/acp/types.ts +0 -8
  27. package/src/agent/loop.ts +42 -26
  28. package/src/config/bundled-skills/acp/SKILL.md +14 -14
  29. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  30. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  31. package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
  32. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  33. package/src/config/bundled-tool-registry.ts +2 -0
  34. package/src/config/call-site-defaults.ts +0 -2
  35. package/src/config/feature-flag-registry.json +1 -1
  36. package/src/config/schemas/call-site-catalog.ts +0 -14
  37. package/src/config/schemas/llm.ts +0 -2
  38. package/src/context/compactor.ts +22 -4
  39. package/src/daemon/conversation-agent-loop.ts +15 -57
  40. package/src/daemon/conversation.ts +23 -3
  41. package/src/daemon/disk-pressure-policy.ts +0 -1
  42. package/src/daemon/lifecycle.ts +0 -7
  43. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  44. package/src/plugins/defaults/compaction/manager-store.ts +13 -0
  45. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
  46. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +24 -25
  47. package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
  48. package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
  49. package/src/runtime/routes/plugins-routes.ts +10 -9
  50. package/src/tools/acp/list-agents.test.ts +2 -2
  51. package/src/tools/acp/spawn.test.ts +103 -225
  52. package/src/tools/acp/spawn.ts +10 -120
  53. package/src/tools/apps/executors.ts +153 -42
  54. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  55. package/src/proactive-artifact/decision.test.ts +0 -226
  56. package/src/proactive-artifact/decision.ts +0 -165
  57. package/src/proactive-artifact/index.ts +0 -7
  58. package/src/proactive-artifact/job.test.ts +0 -962
  59. package/src/proactive-artifact/job.ts +0 -372
  60. package/src/proactive-artifact/message-copy.ts +0 -58
  61. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  62. package/src/proactive-artifact/trigger-state.ts +0 -123
@@ -13,8 +13,7 @@ afterAll(() => {
13
13
  setOverridesForTesting({});
14
14
  });
15
15
 
16
- const { resolveAcpAgent, listAcpAgents, adapterCommandOf, runsViaBunx } =
17
- await import("./resolve-agent.js");
16
+ const { resolveAcpAgent, listAcpAgents } = await import("./resolve-agent.js");
18
17
 
19
18
  beforeEach(() => {
20
19
  config.setConfig({});
@@ -217,7 +216,7 @@ describe("resolveAcpAgent", () => {
217
216
  if (result.ok) return;
218
217
  expect(result.reason).toBe("binary_not_found");
219
218
  if (result.reason !== "binary_not_found") return;
220
- expect(result.hint).toBe("npm i -g @agentclientprotocol/claude-agent-acp");
219
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
221
220
  expect(result.command).toBe("claude-agent-acp");
222
221
  });
223
222
 
@@ -257,7 +256,7 @@ describe("resolveAcpAgent", () => {
257
256
  if (result.ok) return;
258
257
  expect(result.reason).toBe("binary_not_found");
259
258
  if (result.reason !== "binary_not_found") return;
260
- expect(result.hint).toBe("npm i -g @zed-industries/codex-acp");
259
+ expect(result.hint).toBe("bun add -g @zed-industries/codex-acp");
261
260
  });
262
261
 
263
262
  test("binary preflight honors agent.env.PATH override (matches spawn env)", () => {
@@ -300,7 +299,7 @@ describe("resolveAcpAgent", () => {
300
299
  expect(result.agent.args).toEqual(["--verbose"]);
301
300
  });
302
301
 
303
- test("direct resolution sets adapterCommand to the command basename", () => {
302
+ test("resolves a full-path command directly without rewriting it", () => {
304
303
  config.setConfig({
305
304
  agents: {
306
305
  custom: { command: "/opt/bin/claude-agent-acp", args: [] },
@@ -310,117 +309,38 @@ describe("resolveAcpAgent", () => {
310
309
  const direct = resolveAcpAgent("claude");
311
310
  expect(direct.ok).toBe(true);
312
311
  if (!direct.ok) return;
313
- expect(direct.agent.adapterCommand).toBe("claude-agent-acp");
314
- expect(runsViaBunx(direct.agent)).toBe(false);
312
+ expect(direct.agent.command).toBe("claude-agent-acp");
315
313
 
316
314
  const fullPath = resolveAcpAgent("custom");
317
315
  expect(fullPath.ok).toBe(true);
318
316
  if (!fullPath.ok) return;
319
- expect(fullPath.agent.adapterCommand).toBe("claude-agent-acp");
317
+ expect(fullPath.agent.command).toBe("/opt/bin/claude-agent-acp");
320
318
  });
321
319
  });
322
320
 
323
321
  // ---------------------------------------------------------------------------
324
- // resolveAcpAgent - bunx fallback for missing binaries
322
+ // resolveAcpAgent - missing binaries are never run from the task cwd
325
323
  // ---------------------------------------------------------------------------
326
324
 
327
- describe("resolveAcpAgent - bunx fallback", () => {
328
- test("binary missing + bun present: rewrites to `bun x --bun <pkg>` with adapterCommand preserved", () => {
325
+ describe("resolveAcpAgent - missing binary", () => {
326
+ test("binary missing + bun present: still binary_not_found (no bunx rewrite)", () => {
327
+ // bun on PATH no longer makes a missing adapter "runnable" at resolve
328
+ // time: the sandboxed install happens separately, never as a `bun x`
329
+ // rewrite in the untrusted cwd.
329
330
  config.setConfig({ agents: {} });
330
331
  which.setWhich({ bun: "/usr/local/bin/bun" });
331
332
 
332
333
  const result = resolveAcpAgent("claude");
333
334
 
334
- expect(result.ok).toBe(true);
335
- if (!result.ok) return;
336
- expect(result.agent.command).toBe("bun");
337
- expect(result.agent.args).toEqual([
338
- "x",
339
- "--bun",
340
- "@agentclientprotocol/claude-agent-acp",
341
- ]);
342
- expect(result.agent.adapterCommand).toBe("claude-agent-acp");
343
- expect(runsViaBunx(result.agent)).toBe(true);
344
- expect(adapterCommandOf(result.agent)).toBe("claude-agent-acp");
345
- });
346
-
347
- test("bunx rewrite appends the original args after the package (gemini --acp)", () => {
348
- config.setConfig({ agents: {} });
349
- which.setWhich({ bun: "/usr/local/bin/bun" });
350
-
351
- const result = resolveAcpAgent("gemini");
352
-
353
- expect(result.ok).toBe(true);
354
- if (!result.ok) return;
355
- expect(result.agent.command).toBe("bun");
356
- expect(result.agent.args).toEqual([
357
- "x",
358
- "--bun",
359
- "@google/gemini-cli",
360
- "--acp",
361
- ]);
362
- expect(result.agent.adapterCommand).toBe("gemini");
363
- });
364
-
365
- test("bunx rewrite leaves env unchanged", () => {
366
- config.setConfig({
367
- agents: {
368
- claude: {
369
- command: "claude-agent-acp",
370
- args: [],
371
- env: { KEEP_ME: "yes" },
372
- },
373
- },
374
- });
375
- which.setWhich({ bun: "/usr/local/bin/bun" });
376
-
377
- const result = resolveAcpAgent("claude");
378
-
379
- expect(result.ok).toBe(true);
380
- if (!result.ok) return;
381
- expect(result.agent.command).toBe("bun");
382
- expect(result.agent.env).toEqual({ KEEP_ME: "yes" });
383
- });
384
-
385
- test("bun lookup honors agent.env.PATH override (matches spawn env)", () => {
386
- config.setConfig({
387
- agents: {
388
- claude: {
389
- command: "claude-agent-acp",
390
- args: [],
391
- env: { PATH: "/opt/custom/bin" },
392
- },
393
- },
394
- });
395
- which.setWhich((cmd, options) =>
396
- cmd === "bun" && options?.PATH === "/opt/custom/bin"
397
- ? "/opt/custom/bin/bun"
398
- : null,
399
- );
400
-
401
- const result = resolveAcpAgent("claude");
402
-
403
- expect(result.ok).toBe(true);
404
- if (!result.ok) return;
405
- expect(result.agent.command).toBe("bun");
406
- });
407
-
408
- test("user-config command without a package mapping is NOT rewritten even when bun is present", () => {
409
- config.setConfig({
410
- agents: {
411
- custom: { command: "unknown-binary", args: [] },
412
- },
413
- });
414
- which.setWhich({ bun: "/usr/local/bin/bun" });
415
-
416
- const result = resolveAcpAgent("custom");
417
-
418
335
  expect(result.ok).toBe(false);
419
336
  if (result.ok) return;
420
337
  expect(result.reason).toBe("binary_not_found");
338
+ if (result.reason !== "binary_not_found") return;
339
+ expect(result.command).toBe("claude-agent-acp");
340
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
421
341
  });
422
342
 
423
- test("binary missing + bun missing: binary_not_found with the npm hint", () => {
343
+ test("binary missing + bun missing: binary_not_found with the bun hint", () => {
424
344
  config.setConfig({ agents: {} });
425
345
  which.setWhich({});
426
346
 
@@ -430,26 +350,7 @@ describe("resolveAcpAgent - bunx fallback", () => {
430
350
  if (result.ok) return;
431
351
  expect(result.reason).toBe("binary_not_found");
432
352
  if (result.reason !== "binary_not_found") return;
433
- expect(result.hint).toBe("npm i -g @agentclientprotocol/claude-agent-acp");
434
- });
435
- });
436
-
437
- // ---------------------------------------------------------------------------
438
- // adapterCommandOf - fallback for plain configs
439
- // ---------------------------------------------------------------------------
440
-
441
- describe("adapterCommandOf", () => {
442
- test("falls back to the command basename for configs without adapterCommand", () => {
443
- expect(adapterCommandOf({ command: "/opt/bin/claude-agent-acp" })).toBe(
444
- "claude-agent-acp",
445
- );
446
- expect(adapterCommandOf({ command: "codex-acp" })).toBe("codex-acp");
447
- });
448
-
449
- test("prefers an explicit adapterCommand over the command", () => {
450
- expect(
451
- adapterCommandOf({ command: "bun", adapterCommand: "claude-agent-acp" }),
452
- ).toBe("claude-agent-acp");
353
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
453
354
  });
454
355
  });
455
356
 
@@ -523,18 +424,20 @@ describe("listAcpAgents", () => {
523
424
  expect(codex?.source).toBe("default");
524
425
  });
525
426
 
526
- test("missing binaries with bun present are listed available (bunx fallback)", () => {
427
+ test("missing binaries are listed unavailable even when bun is present", () => {
428
+ // bun presence no longer makes a missing adapter available: it is
429
+ // installed on demand at spawn time, not run via `bun x` from the cwd.
527
430
  config.setConfig({ agents: {} });
528
431
  which.setWhich({ bun: "/usr/local/bin/bun" });
529
432
 
530
433
  const result = listAcpAgents();
531
434
 
532
435
  for (const entry of result.agents) {
533
- expect(entry.available).toBe(true);
534
- expect(entry.unavailableReason).toBeUndefined();
535
- expect(entry.setupHint).toBeUndefined();
436
+ expect(entry.available).toBe(false);
437
+ expect(entry.unavailableReason).toBeDefined();
438
+ expect(entry.setupHint).toContain("bun add -g");
536
439
  }
537
- // The catalog keeps the canonical adapter commands, not the rewrite.
440
+ // The catalog keeps the canonical adapter commands.
538
441
  expect(result.agents.map((a) => a.command)).toEqual([
539
442
  "claude-agent-acp",
540
443
  "codex-acp",
@@ -551,7 +454,7 @@ describe("listAcpAgents", () => {
551
454
  const codex = result.agents.find((a) => a.id === "codex");
552
455
  expect(codex?.available).toBe(false);
553
456
  expect(codex?.unavailableReason).toBe("'codex-acp' is not on PATH");
554
- expect(codex?.setupHint).toBe("npm i -g @zed-industries/codex-acp");
457
+ expect(codex?.setupHint).toBe("bun add -g @zed-industries/codex-acp");
555
458
  });
556
459
 
557
460
  test("unavailable gemini surfaces the @google/gemini-cli install hint", () => {
@@ -566,7 +469,7 @@ describe("listAcpAgents", () => {
566
469
  const gemini = result.agents.find((a) => a.id === "gemini");
567
470
  expect(gemini?.available).toBe(false);
568
471
  expect(gemini?.unavailableReason).toBe("'gemini' is not on PATH");
569
- expect(gemini?.setupHint).toBe("npm i -g @google/gemini-cli");
472
+ expect(gemini?.setupHint).toBe("bun add -g @google/gemini-cli");
570
473
  });
571
474
 
572
475
  test("aliases are resolution sugar, not catalog entries", () => {
@@ -13,22 +13,20 @@
13
13
  * acp_list_agents, and the `/v1/acp/spawn` HTTP route) get a single source
14
14
  * of truth and matching actionable hints.
15
15
  *
16
- * When the adapter binary is NOT on PATH but `bun` is and the command has a
17
- * vendored entry in `DEFAULT_AGENT_NPM_PACKAGES`, the resolver rewrites the
18
- * agent to run via `bun x --bun <package>` instead of failing. bunx fetches
19
- * the package into its cache on first use, so platform-hosted assistants
20
- * whose image ships bun (but no node and no npm) work out of the box with
21
- * no global install. The original command is preserved as `adapterCommand`
22
- * so downstream consumers (env injection, resume hints, version probe) keep
23
- * gating on the canonical adapter identity.
16
+ * The resolver NEVER fetches or runs packages in the (untrusted) task cwd.
17
+ * When the adapter binary is missing, resolution simply fails with
18
+ * `binary_not_found`; the `resolveAgentWithAutoInstall` flow in
19
+ * `auto-install.ts` then performs a one-time sandboxed `bun` global install
20
+ * (clean temp cwd, secrets stripped from the installer env) and re-resolves.
21
+ * The resolved command is therefore ALWAYS the real adapter binary on PATH,
22
+ * so downstream gates (env injection, resume hints) key off the command
23
+ * basename directly.
24
24
  *
25
25
  * `listAcpAgents()` exposes the merged catalog with availability info for
26
26
  * the `acp_list_agents` tool — same merge semantics, plus per-entry
27
- * `available` / `setupHint` derived from the same binary-or-bunx resolution.
27
+ * `available` / `setupHint` derived from the same binary resolution.
28
28
  */
29
29
 
30
- import { basename } from "node:path";
31
-
32
30
  import {
33
31
  DEFAULT_ACP_AGENT_PROFILES,
34
32
  DEFAULT_AGENT_NPM_PACKAGES,
@@ -44,48 +42,8 @@ import { isAcpEnabled } from "./feature-gate.js";
44
42
  */
45
43
  type AcpAgentSource = "config" | "default";
46
44
 
47
- /**
48
- * A resolver-produced agent config, ready to spawn. `adapterCommand` carries
49
- * the canonical adapter identity (e.g. "claude-agent-acp") even when the
50
- * spawn command was rewritten to run via `bun x`, so consumers that gate
51
- * behavior on the adapter (env injection in `prepare-agent-env.ts`, resume
52
- * hints, the version probe in `tools/acp/spawn.ts`) stay correct for
53
- * bunx-resolved agents.
54
- */
55
- export interface ResolvedAcpAgent extends AcpAgentConfig {
56
- adapterCommand: string;
57
- }
58
-
59
- /**
60
- * Canonical adapter identity for a (possibly rewritten) agent config.
61
- * Resolver-produced configs carry `adapterCommand` explicitly; plain configs
62
- * that never went through the resolver fall back to the command basename so
63
- * user-supplied agent configs keep working.
64
- */
65
- export function adapterCommandOf(config: {
66
- command: string;
67
- adapterCommand?: string;
68
- }): string {
69
- return config.adapterCommand ?? basename(config.command);
70
- }
71
-
72
- /**
73
- * Whether the config was rewritten by the resolver to run through `bun x`
74
- * (the only path where the canonical adapter identity diverges from the
75
- * actual spawn command).
76
- */
77
- export function runsViaBunx(config: {
78
- command: string;
79
- adapterCommand?: string;
80
- }): boolean {
81
- return (
82
- config.adapterCommand !== undefined &&
83
- config.adapterCommand !== basename(config.command)
84
- );
85
- }
86
-
87
45
  export type ResolveAcpAgentResult =
88
- | { ok: true; agent: ResolvedAcpAgent }
46
+ | { ok: true; agent: AcpAgentConfig }
89
47
  | ResolveAcpAgentFailure;
90
48
 
91
49
  export type ResolveAcpAgentFailure =
@@ -146,7 +104,7 @@ export const ACP_DISABLED_HINT =
146
104
  function installHintFor(command: string): string {
147
105
  const pkg = DEFAULT_AGENT_NPM_PACKAGES[command];
148
106
  return pkg
149
- ? `npm i -g ${pkg}`
107
+ ? `bun add -g ${pkg}`
150
108
  : `Install '${command}' and ensure it is on PATH.`;
151
109
  }
152
110
 
@@ -154,8 +112,7 @@ function installHintFor(command: string): string {
154
112
  * Resolve a binary using the same PATH the spawn will see. `AcpAgentProcess`
155
113
  * spawns with `{ ...process.env, ...config.env }`, so a per-agent `env.PATH`
156
114
  * override wins over the assistant's PATH. Mirror that here so a config that
157
- * relies on a custom PATH to locate the binary (or `bun`, for the bunx
158
- * rewrite) doesn't fail preflight.
115
+ * relies on a custom PATH to locate the binary doesn't fail preflight.
159
116
  */
160
117
  function whichOnAgentPath(
161
118
  agent: AcpAgentConfig,
@@ -166,34 +123,17 @@ function whichOnAgentPath(
166
123
  }
167
124
 
168
125
  /**
169
- * Resolve an agent config to its runnable form, or `null` when it cannot
170
- * spawn. The ONLY place the bunx rewrite happens:
126
+ * Resolve an agent config to its runnable form, or `null` when its `command`
127
+ * is not on PATH. Resolution is pure preflight: the binary is used directly
128
+ * when present, and otherwise the agent cannot spawn (callers fall back to
129
+ * the sandboxed `bun` global install in `auto-install.ts`, then re-resolve).
171
130
  *
172
- * 1. Binary on PATH → use it directly (`adapterCommand` = command basename).
173
- * 2. Binary missing, command has a vendored npm package mapping, and `bun`
174
- * is on PATH → rewrite to `bun x --bun <package> <original args>`. bunx
175
- * fetches the package on first use, preserving the
176
- * install-latest-on-first-use design with no global install.
177
- * 3. Otherwise → null (callers fall back to npm auto-install or surface the
178
- * install hint).
179
- *
180
- * Security boundary (mirrors `auto-install.ts`): only commands present in
181
- * `DEFAULT_AGENT_NPM_PACKAGES` are ever rewritten. The package names are
182
- * vendored constants, NOT user input: an arbitrary command from user config
183
- * must never be turned into a `bun x <attacker-controlled-name>` execution.
131
+ * Crucially, this never fetches or executes anything in the task cwd: a
132
+ * missing binary returns `null` rather than running a package manager from
133
+ * the (untrusted) project directory.
184
134
  */
185
- function resolveRunnableAgent(agent: AcpAgentConfig): ResolvedAcpAgent | null {
186
- if (whichOnAgentPath(agent, agent.command)) {
187
- return { ...agent, adapterCommand: basename(agent.command) };
188
- }
189
- const packageName = DEFAULT_AGENT_NPM_PACKAGES[agent.command];
190
- if (!packageName || !whichOnAgentPath(agent, "bun")) return null;
191
- return {
192
- ...agent,
193
- command: "bun",
194
- args: ["x", "--bun", packageName, ...agent.args],
195
- adapterCommand: agent.command,
196
- };
135
+ function resolveRunnableAgent(agent: AcpAgentConfig): AcpAgentConfig | null {
136
+ return whichOnAgentPath(agent, agent.command) ? agent : null;
197
137
  }
198
138
 
199
139
  /**
@@ -271,8 +211,8 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
271
211
  * Order of checks:
272
212
  * 1. ACP must be enabled (feature flag or config; see `isAcpEnabled`).
273
213
  * 2. The id must resolve to an agent (user config wins; falls back to defaults).
274
- * 3. The agent must be runnable: its `command` on PATH, or the bunx rewrite
275
- * applies (see `resolveRunnableAgent`).
214
+ * 3. The agent must be runnable: its `command` on PATH (see
215
+ * `resolveRunnableAgent`).
276
216
  *
277
217
  * Each failure mode carries an actionable hint so callers can surface a
278
218
  * single user-facing message without re-deriving the remediation.
@@ -330,8 +270,9 @@ export function listAcpAgents(): {
330
270
  const agents: AcpAgentEntry[] = mergedAgentIds(userAgents).map((id) => {
331
271
  // Non-null: ids come from `mergedAgentIds` so the lookup always resolves.
332
272
  const { agent, source } = lookupAgent(userAgents, id)!;
333
- // Same binary-or-bunx resolution as `resolveAcpAgent`: an agent whose
334
- // binary is missing but would spawn via `bun x` IS available.
273
+ // Same binary preflight as `resolveAcpAgent`: available iff the command
274
+ // is on PATH. A missing binary is auto-installed at spawn time, but the
275
+ // catalog reflects what is runnable right now.
335
276
  const available = resolveRunnableAgent(agent) !== null;
336
277
  const entry: AcpAgentEntry = {
337
278
  id,
@@ -5,9 +5,7 @@
5
5
  * adapter binary). Other adapters resume differently or not at all, so the
6
6
  * hint is gated by the resolved adapter, not the agent id - this stays
7
7
  * correct when a user aliases an id to a different binary. Callers pass the
8
- * CANONICAL adapter command (`adapterCommandOf` in `resolve-agent.ts`), not
9
- * the raw spawn command, so the hint still fires when the claude adapter is
10
- * run via `bun x` (spawn command "bun").
8
+ * resolved command basename, which is always the real adapter binary.
11
9
  *
12
10
  * Used by the `acp_spawn` tool's success payload and the session manager's
13
11
  * completion notification so both surfaces render identical copy. Returns
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { randomUUID } from "node:crypto";
7
+ import { basename } from "node:path";
7
8
 
8
9
  import { eq, inArray } from "drizzle-orm";
9
10
 
@@ -15,13 +16,10 @@ import { acpSessionHistory } from "../memory/schema.js";
15
16
  import * as pendingInteractions from "../runtime/pending-interactions.js";
16
17
  import { getLogger } from "../util/logger.js";
17
18
  import { AcpAgentProcess } from "./agent-process.js";
19
+ import { resolveAgentWithAutoInstall } from "./auto-install.js";
18
20
  import { VellumAcpClientHandler } from "./client-handler.js";
19
21
  import { prepareAgentEnv } from "./prepare-agent-env.js";
20
- import {
21
- adapterCommandOf,
22
- formatResolveFailure,
23
- resolveAcpAgent,
24
- } from "./resolve-agent.js";
22
+ import { formatResolveFailure } from "./resolve-agent.js";
25
23
  import { claudeResumeHint } from "./resume-hint.js";
26
24
  import type { AcpAgentConfig, AcpSessionState } from "./types.js";
27
25
 
@@ -74,8 +72,7 @@ interface SessionEntry {
74
72
  currentPrompt: Promise<unknown> | null;
75
73
  parentConversationId: string;
76
74
  cwd: string;
77
- /** Canonical adapter command for the spawned config (e.g.
78
- * "claude-agent-acp" even when the adapter runs via `bun x`). Used to
75
+ /** Resolved adapter command basename (e.g. "claude-agent-acp"). Used to
79
76
  * gate resume hints to the only adapter (claude-agent-acp) whose CLI
80
77
  * accepts `--resume`. */
81
78
  command: string;
@@ -316,7 +313,7 @@ export class AcpSessionManager {
316
313
  currentPrompt: null,
317
314
  parentConversationId: opts.parentConversationId,
318
315
  cwd: opts.cwd,
319
- command: adapterCommandOf(opts.agentConfig),
316
+ command: basename(opts.agentConfig.command),
320
317
  };
321
318
 
322
319
  this.sessions.set(acpSessionId, entry);
@@ -351,8 +348,8 @@ export class AcpSessionManager {
351
348
  * merges new events into the original row instead of losing them.
352
349
  *
353
350
  * Throws with an actionable message when the row is missing, was recorded
354
- * before resume support (no cwd), the agent cannot be resolved, or the
355
- * agent advertises neither resume capability.
351
+ * before resume support (no cwd), the agent cannot be resolved or
352
+ * auto-installed, or the agent advertises neither resume capability.
356
353
  */
357
354
  async resumeFromHistory(
358
355
  acpSessionId: string,
@@ -388,11 +385,6 @@ export class AcpSessionManager {
388
385
  );
389
386
  }
390
387
 
391
- const resolved = resolveAcpAgent(row.agentId);
392
- if (!resolved.ok) {
393
- throw new Error(formatResolveFailure(row.agentId, resolved));
394
- }
395
-
396
388
  // Everything up to here is synchronous. Reserve the id + concurrency
397
389
  // slot BEFORE the first await so a concurrent resume of the same id
398
390
  // (or a spawn racing the cap) fails the guards above instead of
@@ -400,11 +392,12 @@ export class AcpSessionManager {
400
392
  // reservation holds the resume's promise until it settles so
401
393
  // steerOrResume can await a concurrent caller's in-flight resume, and
402
394
  // so the delete guards see the id as live while the row's terminal
403
- // status still reflects the previous run.
395
+ // status still reflects the previous run. Agent resolution (which may
396
+ // auto-install a missing adapter) is therefore deferred into
397
+ // performResume, after the slot is reserved.
404
398
  const resumePromise = this.performResume(
405
399
  acpSessionId,
406
400
  row as ResumableHistoryRow,
407
- resolved.agent,
408
401
  sendToVellum,
409
402
  );
410
403
  this.pendingResumes.set(acpSessionId, resumePromise);
@@ -418,15 +411,34 @@ export class AcpSessionManager {
418
411
  /**
419
412
  * The async body of resumeFromHistory, split out so the caller can store
420
413
  * its promise in `pendingResumes` synchronously before the first await.
421
- * All guards and row validation have already passed.
414
+ * All guards and row validation have already passed; this resolves the
415
+ * adapter (auto-installing a missing allowlisted binary), prepares its
416
+ * env, and reattaches.
422
417
  */
423
418
  private async performResume(
424
419
  acpSessionId: string,
425
420
  row: ResumableHistoryRow,
426
- agent: AcpAgentConfig,
427
421
  sendToVellum: (msg: ServerMessage) => void,
428
422
  ): Promise<void> {
429
- const agentConfig = await prepareAgentEnv(agent);
423
+ // Resolve the adapter, silently auto-installing a missing allowlisted
424
+ // binary via the same sandboxed `bun` path as spawn (see
425
+ // acp/auto-install.ts). A fresh container, or a session created under
426
+ // the old bunx fallback, may have no adapter on PATH; resume installs it
427
+ // transparently instead of failing with binary_not_found. The token is
428
+ // NEVER in scope here — resolveAgentWithAutoInstall strips it from the
429
+ // installer env; it is injected only by prepareAgentEnv below, at spawn
430
+ // time, on the real installed binary.
431
+ const { resolved, failureMessage } = await resolveAgentWithAutoInstall(
432
+ row.agentId,
433
+ );
434
+ if (failureMessage) {
435
+ throw new Error(failureMessage);
436
+ }
437
+ if (!resolved.ok) {
438
+ throw new Error(formatResolveFailure(row.agentId, resolved));
439
+ }
440
+
441
+ const agentConfig = await prepareAgentEnv(resolved.agent);
430
442
 
431
443
  // The daemon may have shut down while prepareAgentEnv was pending.
432
444
  // Registering now would spawn a child process on a disposed manager
package/src/acp/types.ts CHANGED
@@ -12,14 +12,6 @@ export interface AcpAgentConfig {
12
12
  args: string[];
13
13
  description?: string;
14
14
  env?: Record<string, string>;
15
- /**
16
- * Canonical adapter identity (e.g. "claude-agent-acp"), set by the
17
- * resolver. It survives the bunx rewrite, where `command` becomes "bun"
18
- * and the adapter package moves into `args`. Optional because plain user
19
- * configs that bypass the resolver never set it; consumers fall back to
20
- * the command basename via `adapterCommandOf` in `resolve-agent.ts`.
21
- */
22
- adapterCommand?: string;
23
15
  }
24
16
 
25
17
  /**