@vellumai/assistant 0.8.8 → 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 (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -34,6 +34,7 @@ export type AcpClientFactory = (agent: Agent) => Client;
34
34
  export class AcpAgentProcess {
35
35
  private proc: ChildProcess | null = null;
36
36
  private connection: acp.ClientSideConnection | null = null;
37
+ private initializeResponse: InitializeResponse | null = null;
37
38
 
38
39
  constructor(
39
40
  public readonly agentId: string,
@@ -99,7 +100,7 @@ export class AcpAgentProcess {
99
100
 
100
101
  log.info({ agentId: this.agentId }, "Initializing ACP connection");
101
102
 
102
- return this.connection.initialize({
103
+ const response = await this.connection.initialize({
103
104
  protocolVersion: acp.PROTOCOL_VERSION,
104
105
  clientInfo: { name: "vellum", version: "1.0.0" },
105
106
  clientCapabilities: {
@@ -107,6 +108,28 @@ export class AcpAgentProcess {
107
108
  terminal: true,
108
109
  },
109
110
  });
111
+
112
+ this.initializeResponse = response;
113
+ return response;
114
+ }
115
+
116
+ /**
117
+ * Whether the agent advertised support for `session/load` at initialize.
118
+ * Returns false before initialize() resolves.
119
+ */
120
+ get supportsLoadSession(): boolean {
121
+ return this.initializeResponse?.agentCapabilities?.loadSession === true;
122
+ }
123
+
124
+ /**
125
+ * Whether the agent advertised support for `session/resume` at initialize.
126
+ * Returns false before initialize() resolves.
127
+ */
128
+ get supportsSessionResume(): boolean {
129
+ return (
130
+ this.initializeResponse?.agentCapabilities?.sessionCapabilities?.resume !=
131
+ null
132
+ );
110
133
  }
111
134
 
112
135
  /**
@@ -128,6 +151,41 @@ export class AcpAgentProcess {
128
151
  return result.sessionId;
129
152
  }
130
153
 
154
+ /**
155
+ * Loads a previously persisted ACP session via `session/load`.
156
+ *
157
+ * Per the ACP spec, the agent replays the session's conversation history
158
+ * as `session/update` notifications before the load response resolves;
159
+ * callers should suppress forwarding of those replayed updates (see
160
+ * VellumAcpClientHandler.beginReplaySuppression).
161
+ */
162
+ async loadSession(sessionId: string, cwd: string): Promise<void> {
163
+ if (!this.connection) {
164
+ throw new Error(`ACP agent "${this.agentId}" is not spawned`);
165
+ }
166
+
167
+ log.info({ agentId: this.agentId, sessionId, cwd }, "Loading ACP session");
168
+
169
+ await this.connection.loadSession({ sessionId, cwd, mcpServers: [] });
170
+ }
171
+
172
+ /**
173
+ * Resumes a previously persisted ACP session via `session/resume`.
174
+ *
175
+ * Unlike `session/load`, resume performs no history replay, so it is
176
+ * preferred when the agent advertises the capability
177
+ * (see supportsSessionResume).
178
+ */
179
+ async resumeSession(sessionId: string, cwd: string): Promise<void> {
180
+ if (!this.connection) {
181
+ throw new Error(`ACP agent "${this.agentId}" is not spawned`);
182
+ }
183
+
184
+ log.info({ agentId: this.agentId, sessionId, cwd }, "Resuming ACP session");
185
+
186
+ await this.connection.resumeSession({ sessionId, cwd, mcpServers: [] });
187
+ }
188
+
131
189
  /**
132
190
  * Sends a prompt to an existing ACP session.
133
191
  * Returns the prompt response (includes stopReason).
@@ -175,6 +233,7 @@ export class AcpAgentProcess {
175
233
  this.proc = null;
176
234
  }
177
235
  this.connection = null;
236
+ this.initializeResponse = null;
178
237
  }
179
238
 
180
239
  /**
@@ -205,5 +264,6 @@ export class AcpAgentProcess {
205
264
 
206
265
  this.proc = null;
207
266
  this.connection = null;
267
+ this.initializeResponse = null;
208
268
  }
209
269
  }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Tests for the sandboxed ACP adapter auto-installer.
3
+ *
4
+ * `execFile` is stubbed via the shared `installExecFileStub` helper: a
5
+ * process-global `mock.module("node:child_process", ...)` driven by per-call
6
+ * scripted responses keyed on `${command} ${args[0]}`. `Bun.which` is stubbed
7
+ * via `installWhichStub` so each test controls whether `bun` is on PATH and
8
+ * whether the adapter binary resolves after the install.
9
+ *
10
+ * The security-critical assertions live here: the installer must be `bun`
11
+ * (never `npm`), run in a fresh temp dir (NOT the task cwd), with the ACP
12
+ * secrets stripped from its env and the registry pinned to the public npm
13
+ * registry.
14
+ */
15
+
16
+ import { tmpdir } from "node:os";
17
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
18
+
19
+ import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
20
+ import { installExecFileStub } from "./__tests__/helpers/exec-file-stub.js";
21
+ import { installWhichStub } from "./__tests__/helpers/which-stub.js";
22
+
23
+ const { execScripts, execFileMock, reset } = installExecFileStub();
24
+ const config = await installAcpConfigStub();
25
+ const which = installWhichStub();
26
+
27
+ /** Fixed resolved `bun` path so script keys are predictable. */
28
+ const BUN_BIN = "/usr/local/bin/bun";
29
+ /** Key the exec stub uses for the global install. */
30
+ const BUN_ADD_KEY = `${BUN_BIN} add`;
31
+
32
+ afterAll(() => {
33
+ which.restore();
34
+ });
35
+
36
+ // Spread the real module so other test files that load logger consumers
37
+ // (e.g. `truncateForLog` importers) after this process-global mock still
38
+ // resolve every named export.
39
+ const realLogger = await import("../util/logger.js");
40
+ mock.module("../util/logger.js", () => ({
41
+ ...realLogger,
42
+ getLogger: () =>
43
+ new Proxy({} as Record<string, unknown>, {
44
+ get: () => () => {},
45
+ }),
46
+ }));
47
+
48
+ const {
49
+ ensureAdapterInstalled,
50
+ resolveAgentWithAutoInstall,
51
+ _resetAdapterInstallCacheForTests,
52
+ } = await import("./auto-install.js");
53
+
54
+ /** Latest call's execFile options ({ cwd, env, ... }). */
55
+ function lastInstallOptions(): { cwd?: string; env?: NodeJS.ProcessEnv } {
56
+ const call = execFileMock.mock.calls.at(-1)!;
57
+ return call[2] as { cwd?: string; env?: NodeJS.ProcessEnv };
58
+ }
59
+
60
+ beforeEach(() => {
61
+ reset();
62
+ _resetAdapterInstallCacheForTests();
63
+ config.setConfig({ agents: {} });
64
+ // Default: bun on PATH, nothing else.
65
+ which.setWhich({ bun: BUN_BIN });
66
+ });
67
+
68
+ describe("ensureAdapterInstalled", () => {
69
+ test("known command: runs `bun add --global <pkg>` and reports installed", async () => {
70
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
71
+
72
+ const result = await ensureAdapterInstalled("claude-agent-acp");
73
+
74
+ expect(result).toEqual({ installed: true });
75
+ expect(execFileMock).toHaveBeenCalledTimes(1);
76
+ const [command, args] = execFileMock.mock.calls[0];
77
+ expect(command).toBe(BUN_BIN);
78
+ expect(args).toEqual([
79
+ "add",
80
+ "--global",
81
+ "@agentclientprotocol/claude-agent-acp",
82
+ ]);
83
+ });
84
+
85
+ test("installer never invokes npm", async () => {
86
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
87
+
88
+ await ensureAdapterInstalled("claude-agent-acp");
89
+
90
+ for (const call of execFileMock.mock.calls) {
91
+ expect(call[0]).not.toBe("npm");
92
+ }
93
+ });
94
+
95
+ test("runs in a fresh temp dir (NOT the task cwd), token-free env, public registry", async () => {
96
+ // Seed the secrets on the ambient env so we can assert they are stripped.
97
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "should-not-leak";
98
+ process.env.GEMINI_API_KEY = "should-not-leak-either";
99
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
100
+
101
+ try {
102
+ await ensureAdapterInstalled("claude-agent-acp");
103
+ } finally {
104
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
105
+ delete process.env.GEMINI_API_KEY;
106
+ }
107
+
108
+ const { cwd, env } = lastInstallOptions();
109
+ // Sandboxed cwd: a temp dir under the OS temp root, never the process cwd.
110
+ expect(cwd).toBeDefined();
111
+ expect(cwd!.startsWith(tmpdir())).toBe(true);
112
+ expect(cwd).not.toBe(process.cwd());
113
+ expect(cwd).toContain("vellum-acp-install-");
114
+
115
+ // Secrets stripped, registry pinned.
116
+ expect(env).toBeDefined();
117
+ expect(env!.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
118
+ expect(env!.GEMINI_API_KEY).toBeUndefined();
119
+ expect(env!.BUN_CONFIG_REGISTRY).toBe("https://registry.npmjs.org/");
120
+ });
121
+
122
+ test("unknown command: never invokes a package manager (security allowlist)", async () => {
123
+ const result = await ensureAdapterInstalled("some-arbitrary-binary");
124
+
125
+ expect(result.installed).toBe(false);
126
+ expect(result.error).toBeUndefined();
127
+ expect(execFileMock).not.toHaveBeenCalled();
128
+ });
129
+
130
+ test("bun absent: no install attempted, reports not installed", async () => {
131
+ which.setWhich({}); // bun not on PATH
132
+
133
+ const result = await ensureAdapterInstalled("claude-agent-acp");
134
+
135
+ expect(result.installed).toBe(false);
136
+ expect(result.error).toBeUndefined();
137
+ expect(execFileMock).not.toHaveBeenCalled();
138
+ });
139
+
140
+ test("install failure: reports the error and does not install", async () => {
141
+ execScripts.set(BUN_ADD_KEY, {
142
+ error: new Error("EACCES: permission denied"),
143
+ });
144
+
145
+ const result = await ensureAdapterInstalled("codex-acp");
146
+
147
+ expect(result.installed).toBe(false);
148
+ expect(result.error).toContain("EACCES");
149
+ });
150
+
151
+ test("failed install is retried on the next call", async () => {
152
+ execScripts.set(BUN_ADD_KEY, { error: new Error("network down") });
153
+ const first = await ensureAdapterInstalled("claude-agent-acp");
154
+ expect(first.installed).toBe(false);
155
+
156
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
157
+ const second = await ensureAdapterInstalled("claude-agent-acp");
158
+ expect(second.installed).toBe(true);
159
+ expect(execFileMock).toHaveBeenCalledTimes(2);
160
+ });
161
+
162
+ test("successful install is cached for the process lifetime", async () => {
163
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
164
+
165
+ await ensureAdapterInstalled("claude-agent-acp");
166
+ await ensureAdapterInstalled("claude-agent-acp");
167
+
168
+ expect(execFileMock).toHaveBeenCalledTimes(1);
169
+ });
170
+
171
+ test("concurrent calls dedupe to exactly one install", async () => {
172
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
173
+
174
+ const [a, b, c] = await Promise.all([
175
+ ensureAdapterInstalled("claude-agent-acp"),
176
+ ensureAdapterInstalled("claude-agent-acp"),
177
+ ensureAdapterInstalled("claude-agent-acp"),
178
+ ]);
179
+
180
+ expect(a.installed).toBe(true);
181
+ expect(b.installed).toBe(true);
182
+ expect(c.installed).toBe(true);
183
+ expect(execFileMock).toHaveBeenCalledTimes(1);
184
+ });
185
+
186
+ test("different commands install independently", async () => {
187
+ execScripts.set(BUN_ADD_KEY, { stdout: "" });
188
+
189
+ const [claude, codex] = await Promise.all([
190
+ ensureAdapterInstalled("claude-agent-acp"),
191
+ ensureAdapterInstalled("codex-acp"),
192
+ ]);
193
+
194
+ expect(claude.installed).toBe(true);
195
+ expect(codex.installed).toBe(true);
196
+ expect(execFileMock).toHaveBeenCalledTimes(2);
197
+ const installedPackages = execFileMock.mock.calls.map(
198
+ (call) => (call[1] as string[])[2],
199
+ );
200
+ expect(installedPackages.sort()).toEqual([
201
+ "@agentclientprotocol/claude-agent-acp",
202
+ "@zed-industries/codex-acp",
203
+ ]);
204
+ });
205
+ });
206
+
207
+ describe("resolveAgentWithAutoInstall - resolution order", () => {
208
+ test("binary missing + bun present: installs then resolves to the real binary", async () => {
209
+ let installed = false;
210
+ which.setWhich((cmd) => {
211
+ if (cmd === "bun") return BUN_BIN;
212
+ if (cmd === "claude-agent-acp" && installed) {
213
+ return "/usr/local/bin/claude-agent-acp";
214
+ }
215
+ return null;
216
+ });
217
+ execScripts.set(BUN_ADD_KEY, {
218
+ stdout: "",
219
+ onCall: () => {
220
+ installed = true;
221
+ },
222
+ });
223
+
224
+ const result = await resolveAgentWithAutoInstall("claude");
225
+
226
+ expect(result.resolved.ok).toBe(true);
227
+ if (!result.resolved.ok) return;
228
+ // The resolved command is the REAL binary, not a `bun x` wrapper.
229
+ expect(result.resolved.agent.command).toBe("claude-agent-acp");
230
+ expect(result.autoInstalledPackage).toBe(
231
+ "@agentclientprotocol/claude-agent-acp",
232
+ );
233
+ expect(result.failureMessage).toBeUndefined();
234
+ expect(execFileMock).toHaveBeenCalledTimes(1);
235
+ expect(execFileMock.mock.calls[0][0]).toBe(BUN_BIN);
236
+ });
237
+
238
+ test("binary missing + bun absent: no install, plain failure with the hint", async () => {
239
+ which.setWhich({}); // neither bun nor the adapter on PATH
240
+
241
+ const result = await resolveAgentWithAutoInstall("claude");
242
+
243
+ expect(result.resolved.ok).toBe(false);
244
+ expect(result.autoInstalledPackage).toBeUndefined();
245
+ // No augmented failure message (the plain binary_not_found hint is
246
+ // surfaced by the caller via formatResolveFailure).
247
+ expect(result.failureMessage).toBeUndefined();
248
+ expect(execFileMock).not.toHaveBeenCalled();
249
+ });
250
+
251
+ test("install fails: failure message carries the hint and install error, never npm", async () => {
252
+ which.setWhich({ bun: BUN_BIN });
253
+ execScripts.set(BUN_ADD_KEY, { error: new Error("network is down") });
254
+
255
+ const result = await resolveAgentWithAutoInstall("claude");
256
+
257
+ expect(result.resolved.ok).toBe(false);
258
+ expect(result.failureMessage).toContain("claude-agent-acp is not on PATH");
259
+ expect(result.failureMessage).toContain(
260
+ "bun add -g @agentclientprotocol/claude-agent-acp",
261
+ );
262
+ expect(result.failureMessage).toContain("network is down");
263
+ for (const call of execFileMock.mock.calls) {
264
+ expect(call[0]).not.toBe("npm");
265
+ }
266
+ });
267
+
268
+ test("codex unaffected: already on PATH resolves with no install", async () => {
269
+ which.setWhich((cmd) =>
270
+ cmd === "codex-acp" ? "/usr/local/bin/codex-acp" : null,
271
+ );
272
+
273
+ const result = await resolveAgentWithAutoInstall("codex");
274
+
275
+ expect(result.resolved.ok).toBe(true);
276
+ if (!result.resolved.ok) return;
277
+ expect(result.resolved.agent.command).toBe("codex-acp");
278
+ expect(execFileMock).not.toHaveBeenCalled();
279
+ });
280
+ });
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Sandboxed auto-install for known ACP adapter binaries.
3
+ *
4
+ * When a spawn fails preflight with `binary_not_found`, callers (the
5
+ * `acp_spawn` tool and the `/v1/acp/spawn` route) call
6
+ * `resolveAgentWithAutoInstall(agentId)`, which performs a ONE-TIME global
7
+ * install of the mapped adapter package via `bun`, then re-resolves and
8
+ * continues. After the install the adapter is a normal trusted binary on
9
+ * PATH, and the session manager spawns it the usual way (project cwd, token
10
+ * injected only at spawn).
11
+ *
12
+ * Security boundaries (this is the ATL-808 fix):
13
+ * - Only commands present in `DEFAULT_AGENT_NPM_PACKAGES` are ever
14
+ * installed. The package names are vendored constants, NOT user input.
15
+ * - The install runs `bun` with cwd = a FRESH empty daemon-owned temp dir,
16
+ * never the untrusted task project dir. A clean cwd has no project-local
17
+ * `node_modules/.bin`, `bunfig.toml`, or `.npmrc`, so none of the
18
+ * cwd-based package-resolution hijacks apply.
19
+ * - The installer env is a SANITIZED copy of `process.env` with
20
+ * `CLAUDE_CODE_OAUTH_TOKEN` and `GEMINI_API_KEY` stripped, so the secret
21
+ * is never in scope during package resolution, and `BUN_CONFIG_REGISTRY`
22
+ * forced to the public npm registry so a redirected registry cannot serve
23
+ * a malicious package.
24
+ * - The token is injected ONLY later, at spawn time, on the real installed
25
+ * binary (see `prepare-agent-env.ts`). `prepareAgentEnv` is never called
26
+ * here.
27
+ */
28
+
29
+ import { execFile } from "node:child_process";
30
+ import { mkdtemp, rm } from "node:fs/promises";
31
+ import { tmpdir } from "node:os";
32
+ import { join } from "node:path";
33
+
34
+ import { DEFAULT_AGENT_NPM_PACKAGES } from "../config/acp-defaults.js";
35
+ import { getLogger } from "../util/logger.js";
36
+ import {
37
+ resolveAcpAgent,
38
+ type ResolveAcpAgentResult,
39
+ } from "./resolve-agent.js";
40
+
41
+ const log = getLogger("acp:auto-install");
42
+
43
+ /** Per-install timeout for the global install. Generous: cold caches are slow. */
44
+ const BUN_INSTALL_TIMEOUT_MS = 120_000;
45
+
46
+ /**
47
+ * The trusted public npm registry. Forced via `BUN_CONFIG_REGISTRY` on the
48
+ * installer env so a stray `.npmrc`/`bunfig.toml` in the ambient environment
49
+ * cannot redirect package downloads to an attacker-controlled registry.
50
+ */
51
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
52
+
53
+ export interface AdapterInstallResult {
54
+ installed: boolean;
55
+ error?: string;
56
+ }
57
+
58
+ /**
59
+ * Per-command install promises. Concurrent spawns for the same missing
60
+ * binary dedupe to a single global install; successful results are cached for
61
+ * the process lifetime. Failed installs are evicted so a later spawn can
62
+ * retry (e.g. after the user fixes network or install permissions).
63
+ */
64
+ const installPromises = new Map<string, Promise<AdapterInstallResult>>();
65
+
66
+ /**
67
+ * Run `execFile` with an AbortController-driven timeout. Returns the stdout
68
+ * on success; throws on error or timeout. The optional `cwd`/`env` let the
69
+ * installer run in a sandboxed working directory with a sanitized env.
70
+ */
71
+ function execFileWithTimeout(
72
+ command: string,
73
+ args: string[],
74
+ timeoutMs: number,
75
+ options?: { cwd?: string; env?: NodeJS.ProcessEnv },
76
+ ): Promise<string> {
77
+ return new Promise((resolve, reject) => {
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
80
+ execFile(
81
+ command,
82
+ args,
83
+ {
84
+ signal: controller.signal,
85
+ encoding: "utf8",
86
+ cwd: options?.cwd,
87
+ env: options?.env,
88
+ },
89
+ (err, stdout) => {
90
+ clearTimeout(timer);
91
+ if (err) {
92
+ reject(err);
93
+ return;
94
+ }
95
+ resolve(stdout);
96
+ },
97
+ );
98
+ });
99
+ }
100
+
101
+ /**
102
+ * A copy of `process.env` safe to hand to the package installer: the ACP
103
+ * secrets are stripped so they can never leak into a resolved package's
104
+ * lifecycle/runtime, and the registry is pinned to the trusted public one.
105
+ */
106
+ function sanitizedInstallEnv(): NodeJS.ProcessEnv {
107
+ const env: NodeJS.ProcessEnv = { ...process.env };
108
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
109
+ delete env.GEMINI_API_KEY;
110
+ env.BUN_CONFIG_REGISTRY = PUBLIC_NPM_REGISTRY;
111
+ return env;
112
+ }
113
+
114
+ /**
115
+ * Install the npm-registry package mapped to `command` via a sandboxed `bun`
116
+ * global install, if (and only if) the command is a known adapter binary and
117
+ * `bun` is on PATH. Unknown commands, or hosts without `bun`, resolve to
118
+ * `{ installed: false }` without ever invoking a package manager (see the
119
+ * security boundary note in the module doc).
120
+ */
121
+ export function ensureAdapterInstalled(
122
+ command: string,
123
+ ): Promise<AdapterInstallResult> {
124
+ const packageName = DEFAULT_AGENT_NPM_PACKAGES[command];
125
+ if (!packageName) {
126
+ return Promise.resolve({ installed: false });
127
+ }
128
+
129
+ const bunPath = Bun.which("bun");
130
+ if (!bunPath) {
131
+ return Promise.resolve({ installed: false });
132
+ }
133
+
134
+ const inFlight = installPromises.get(command);
135
+ if (inFlight) return inFlight;
136
+
137
+ const promise = runInstall(bunPath, command, packageName).then((result) => {
138
+ if (!result.installed) installPromises.delete(command);
139
+ return result;
140
+ });
141
+ installPromises.set(command, promise);
142
+ return promise;
143
+ }
144
+
145
+ async function runInstall(
146
+ bunPath: string,
147
+ command: string,
148
+ packageName: string,
149
+ ): Promise<AdapterInstallResult> {
150
+ log.info({ command, packageName }, "Auto-installing missing ACP adapter");
151
+ // Fresh empty dir guaranteed to have no project-local node_modules,
152
+ // bunfig.toml, or .npmrc - this neutralizes the cwd-based resolution
153
+ // hijacks the untrusted task dir would otherwise enable.
154
+ const installDir = await mkdtemp(join(tmpdir(), "vellum-acp-install-"));
155
+ try {
156
+ await execFileWithTimeout(
157
+ bunPath,
158
+ // `bun add --global` installs AND links the package bin into bun's
159
+ // global bin dir (on PATH in every image).
160
+ ["add", "--global", packageName],
161
+ BUN_INSTALL_TIMEOUT_MS,
162
+ { cwd: installDir, env: sanitizedInstallEnv() },
163
+ );
164
+ log.info({ command, packageName }, "ACP adapter auto-install succeeded");
165
+ return { installed: true };
166
+ } catch (err) {
167
+ const error = err instanceof Error ? err.message : String(err);
168
+ log.warn(
169
+ { err, command, packageName },
170
+ "ACP adapter auto-install failed (falling back to install hint)",
171
+ );
172
+ return { installed: false, error };
173
+ } finally {
174
+ await rm(installDir, { recursive: true, force: true }).catch(() => {});
175
+ }
176
+ }
177
+
178
+ export interface ResolveWithAutoInstallResult {
179
+ /** The final resolver outcome (post-install re-resolve when applicable). */
180
+ resolved: ResolveAcpAgentResult;
181
+ /** Set when a missing adapter binary was silently installed via bun. */
182
+ autoInstalledPackage?: string;
183
+ /**
184
+ * Set when the auto-install itself failed: the original install hint
185
+ * augmented with the install failure reason. Callers should surface this
186
+ * instead of re-deriving a message from `resolved`.
187
+ */
188
+ failureMessage?: string;
189
+ }
190
+
191
+ /**
192
+ * Resolve an ACP agent id, silently auto-installing the mapped adapter
193
+ * package when (and only when) the failure is a missing allowlisted binary.
194
+ * Shared by the `acp_spawn` tool and the `/v1/acp/spawn` route so the
195
+ * resolve-install-re-resolve flow has a single implementation; callers map
196
+ * the result to their transport (tool error result vs. HTTP error class).
197
+ */
198
+ export async function resolveAgentWithAutoInstall(
199
+ agentId: string,
200
+ ): Promise<ResolveWithAutoInstallResult> {
201
+ const resolved = resolveAcpAgent(agentId);
202
+ if (resolved.ok || resolved.reason !== "binary_not_found") {
203
+ return { resolved };
204
+ }
205
+
206
+ const { command, hint } = resolved;
207
+ const install = await ensureAdapterInstalled(command);
208
+ if (install.installed) {
209
+ const retried = resolveAcpAgent(agentId);
210
+ if (retried.ok) {
211
+ log.info(
212
+ { agentId, command },
213
+ "Auto-installed missing ACP adapter binary",
214
+ );
215
+ return {
216
+ resolved: retried,
217
+ autoInstalledPackage: DEFAULT_AGENT_NPM_PACKAGES[command],
218
+ };
219
+ }
220
+ } else if (install.error) {
221
+ return {
222
+ resolved,
223
+ failureMessage: `${command} is not on PATH. ${hint} (auto-install failed: ${install.error})`,
224
+ };
225
+ }
226
+ return { resolved };
227
+ }
228
+
229
+ /** @internal: exposed for tests only. */
230
+ export function _resetAdapterInstallCacheForTests(): void {
231
+ installPromises.clear();
232
+ }
@@ -51,6 +51,7 @@ interface TerminalState {
51
51
  export class VellumAcpClientHandler implements Client {
52
52
  private terminals = new Map<string, TerminalState>();
53
53
  private accumulatedText = "";
54
+ private suppressForwarding = false;
54
55
  /** Tracks pending ACP permission requestIds for cleanup on session close. */
55
56
  readonly pendingRequestIds = new Set<string>();
56
57
 
@@ -65,8 +66,38 @@ export class VellumAcpClientHandler implements Client {
65
66
  private readonly parentConversationId: string,
66
67
  ) {}
67
68
 
69
+ /**
70
+ * Begins suppressing session updates from being forwarded to Vellum.
71
+ *
72
+ * Per the ACP spec, `session/load` replays the entire conversation history
73
+ * as `session/update` notifications before the load response resolves. The
74
+ * parent conversation already received those events during the original
75
+ * run, so re-forwarding them would duplicate them into the conversation and
76
+ * the ring buffer. Callers wrap `loadSession` in
77
+ * beginReplaySuppression()/endReplaySuppression() to drop the replay.
78
+ * `session/resume` performs no replay, which is why it is preferred when
79
+ * the agent supports it.
80
+ */
81
+ beginReplaySuppression(): void {
82
+ this.suppressForwarding = true;
83
+ }
84
+
85
+ /** Ends replay suppression; subsequent updates flow normally. */
86
+ endReplaySuppression(): void {
87
+ this.suppressForwarding = false;
88
+ }
89
+
68
90
  async sessionUpdate(params: SessionNotification): Promise<void> {
69
91
  const update = params.update;
92
+
93
+ if (this.suppressForwarding) {
94
+ log.debug(
95
+ { acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
96
+ "Dropping replayed session update during suppression",
97
+ );
98
+ return;
99
+ }
100
+
70
101
  log.debug(
71
102
  { acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
72
103
  "ACP session update received",