@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
@@ -4,10 +4,11 @@
4
4
  */
5
5
 
6
6
  import { randomUUID } from "node:crypto";
7
+ import { basename } from "node:path";
7
8
 
8
- import { inArray } from "drizzle-orm";
9
+ import { eq, inArray } from "drizzle-orm";
9
10
 
10
- import { findConversation } from "../daemon/conversation-store.js";
11
+ import { findConversation } from "../daemon/conversation-registry.js";
11
12
  import type { ServerMessage } from "../daemon/message-protocol.js";
12
13
  import type { AcpSessionUpdate } from "../daemon/message-types/acp.js";
13
14
  import { getDb } from "../memory/db-connection.js";
@@ -15,11 +16,41 @@ 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";
21
+ import { prepareAgentEnv } from "./prepare-agent-env.js";
22
+ import { formatResolveFailure } from "./resolve-agent.js";
23
+ import { claudeResumeHint } from "./resume-hint.js";
19
24
  import type { AcpAgentConfig, AcpSessionState } from "./types.js";
20
25
 
21
26
  const log = getLogger("acp:session-manager");
22
27
 
28
+ /**
29
+ * The manager's "unknown session id" error. Thrown whenever an operation
30
+ * references an acpSessionId with no in-memory entry (and, for resume, no
31
+ * persisted history row). Callers (acp_steer tool, /v1/acp/:id/steer route)
32
+ * use `instanceof` checks to map this to their transport's not-found shape.
33
+ */
34
+ export class AcpSessionNotFoundError extends Error {
35
+ constructor(public readonly acpSessionId: string) {
36
+ super(`ACP session "${acpSessionId}" not found`);
37
+ this.name = "AcpSessionNotFoundError";
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Wraps failures from the resume-then-steer phase of `steerOrResume` so
43
+ * transport callers can distinguish them (HTTP 424 with the actionable
44
+ * resume hint) from plain steer failures (404). The message mirrors the
45
+ * underlying error's message; the original error rides on `cause`.
46
+ */
47
+ export class AcpResumeError extends Error {
48
+ constructor(cause: unknown) {
49
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
50
+ this.name = "AcpResumeError";
51
+ }
52
+ }
53
+
23
54
  /** Maximum number of update events kept in a session's ring buffer. */
24
55
  const MAX_BUFFER_EVENTS = 200;
25
56
  /** Maximum aggregate JSON size of a session's ring buffer, in bytes. */
@@ -41,11 +72,21 @@ interface SessionEntry {
41
72
  currentPrompt: Promise<unknown> | null;
42
73
  parentConversationId: string;
43
74
  cwd: string;
44
- /** The adapter binary that was spawned. Used to gate resume hints to
45
- * the only adapter (claude-agent-acp) whose CLI accepts `--resume`. */
75
+ /** Resolved adapter command basename (e.g. "claude-agent-acp"). Used to
76
+ * gate resume hints to the only adapter (claude-agent-acp) whose CLI
77
+ * accepts `--resume`. */
46
78
  command: string;
47
79
  }
48
80
 
81
+ /**
82
+ * An `acp_session_history` row that passed resumeFromHistory's validation
83
+ * guards: `cwd` (nullable in the schema for pre-resume-support rows) is
84
+ * guaranteed present.
85
+ */
86
+ type ResumableHistoryRow = typeof acpSessionHistory.$inferSelect & {
87
+ cwd: string;
88
+ };
89
+
49
90
  export class AcpSessionManager {
50
91
  private sessions = new Map<string, SessionEntry>();
51
92
  /**
@@ -55,6 +96,25 @@ export class AcpSessionManager {
55
96
  * `acp_session_history` on terminal transition, then cleared.
56
97
  */
57
98
  private eventBuffers = new Map<string, BufferedAcpUpdate[]>();
99
+ /**
100
+ * In-flight resumes by session id, keyed to the promise of the resume's
101
+ * async body. Reserved SYNCHRONOUSLY before the first await so concurrent
102
+ * resumes of the same id cannot both pass the guards (the loser would
103
+ * overwrite the winner's map entry and leak its child process), and so N
104
+ * concurrent resumes of distinct ids cannot exceed maxConcurrent. The
105
+ * entry lives until the resume settles so `steerOrResume` can await a
106
+ * concurrent caller's resume instead of failing the already-active guard.
107
+ * The spawn path needs no such reservation: its check-then-register is
108
+ * synchronous.
109
+ */
110
+ private pendingResumes = new Map<string, Promise<void>>();
111
+
112
+ /**
113
+ * Set by dispose() (the daemon-shutdown path). Resumes that are mid-await
114
+ * when the manager is disposed re-check this flag before spawning a child
115
+ * process nothing would ever kill.
116
+ */
117
+ private disposed = false;
58
118
 
59
119
  constructor(private readonly maxConcurrent: number) {
60
120
  this.cleanupStaleRunningRows();
@@ -91,6 +151,33 @@ export class AcpSessionManager {
91
151
  }
92
152
  }
93
153
 
154
+ /**
155
+ * Ids that must be treated as live: registered sessions plus in-flight
156
+ * resume reservations (deduped, since a resuming session appears in both maps
157
+ * between registration and settle). Delete guards use this so a history
158
+ * row cannot be removed out from under a resume that is still awaiting
159
+ * env preparation; the later terminal upsert would resurrect it.
160
+ */
161
+ getActiveAndPendingIds(): string[] {
162
+ return [
163
+ ...new Set([...this.sessions.keys(), ...this.pendingResumes.keys()]),
164
+ ];
165
+ }
166
+
167
+ /**
168
+ * Concurrency guard shared by spawn() and resumeFromHistory(). Counts
169
+ * both registered sessions and in-flight resume reservations so the cap
170
+ * holds even while a resume is still awaiting prepareAgentEnv.
171
+ */
172
+ private assertCapacity(): void {
173
+ if (this.getActiveAndPendingIds().length >= this.maxConcurrent) {
174
+ throw new Error(
175
+ `ACP concurrency limit reached (max ${this.maxConcurrent}). ` +
176
+ `Close an existing session before spawning a new one.`,
177
+ );
178
+ }
179
+ }
180
+
94
181
  /**
95
182
  * Spawns a new ACP agent session. Returns the generated acpSessionId.
96
183
  *
@@ -105,12 +192,7 @@ export class AcpSessionManager {
105
192
  parentConversationId: string,
106
193
  sendToVellum: (msg: ServerMessage) => void,
107
194
  ): Promise<{ acpSessionId: string; protocolSessionId: string }> {
108
- if (this.sessions.size >= this.maxConcurrent) {
109
- throw new Error(
110
- `ACP concurrency limit reached (max ${this.maxConcurrent}). ` +
111
- `Close an existing session before spawning a new one.`,
112
- );
113
- }
195
+ this.assertCapacity();
114
196
 
115
197
  const acpSessionId = randomUUID();
116
198
  log.info(
@@ -124,40 +206,103 @@ export class AcpSessionManager {
124
206
  "ACP spawn requested",
125
207
  );
126
208
 
209
+ const entry = this.registerSession({
210
+ acpSessionId,
211
+ agentId,
212
+ agentConfig,
213
+ parentConversationId,
214
+ cwd,
215
+ startedAt: Date.now(),
216
+ sendToVellum,
217
+ });
218
+ const { process: agentProcess, state } = entry;
219
+
220
+ try {
221
+ log.info({ acpSessionId, agentId }, "ACP spawning child process");
222
+ agentProcess.spawn(cwd);
223
+ log.info(
224
+ { acpSessionId, agentId },
225
+ "ACP initializing protocol connection",
226
+ );
227
+ await agentProcess.initialize();
228
+ log.info({ acpSessionId, agentId }, "ACP creating session");
229
+ const acpProtocolSessionId = await agentProcess.createSession(cwd);
230
+ state.acpSessionId = acpProtocolSessionId;
231
+ state.status = "running";
232
+ log.info(
233
+ { acpSessionId, agentId, acpProtocolSessionId },
234
+ "ACP session running",
235
+ );
236
+ } catch (err) {
237
+ log.error({ acpSessionId, agentId, err }, "ACP spawn failed");
238
+ // No prompt has fired yet, so no permissions can be pending.
239
+ this.teardownSession(acpSessionId, entry);
240
+ throw err;
241
+ }
242
+
243
+ this.sendSpawnedEvent(acpSessionId, entry);
244
+
245
+ // Fire prompt in the background — don't await
246
+ entry.currentPrompt = this.firePromptInBackground(
247
+ acpSessionId,
248
+ entry,
249
+ state.acpSessionId,
250
+ task,
251
+ );
252
+
253
+ return { acpSessionId, protocolSessionId: state.acpSessionId };
254
+ }
255
+
256
+ /**
257
+ * Wires up the in-memory plumbing shared by spawn() and
258
+ * resumeFromHistory(): the per-session ring buffer, the buffer-mirroring
259
+ * sender, the client handler, the agent process, and the SessionEntry.
260
+ * Registers the entry in the session map (reserving a concurrency slot
261
+ * before any async work) and returns it. Does NOT start the process.
262
+ */
263
+ private registerSession(opts: {
264
+ acpSessionId: string;
265
+ agentId: string;
266
+ agentConfig: AcpAgentConfig;
267
+ parentConversationId: string;
268
+ cwd: string;
269
+ startedAt: number;
270
+ sendToVellum: (msg: ServerMessage) => void;
271
+ }): SessionEntry {
272
+ const { acpSessionId } = opts;
273
+
127
274
  // Initialize the per-session ring buffer before any update can fire.
128
275
  this.eventBuffers.set(acpSessionId, []);
129
276
 
130
277
  // Wrap the sender so every emitted message is mirrored into the buffer
131
278
  // when it's an `acp_session_update`. The wrapper preserves the original
132
- // call semantics it forwards every message unchanged.
279
+ // call semantics: it forwards every message unchanged.
133
280
  const wrappedSend = (msg: ServerMessage) => {
134
281
  if (msg.type === "acp_session_update") {
135
282
  this.appendToBuffer(acpSessionId, msg);
136
283
  }
137
- sendToVellum(msg);
284
+ opts.sendToVellum(msg);
138
285
  };
139
286
 
140
287
  const clientHandler = new VellumAcpClientHandler(
141
288
  acpSessionId,
142
289
  wrappedSend,
143
- parentConversationId,
290
+ opts.parentConversationId,
144
291
  );
145
292
 
146
293
  const agentProcess = new AcpAgentProcess(
147
- agentId,
148
- agentConfig,
294
+ opts.agentId,
295
+ opts.agentConfig,
149
296
  (_agent) => clientHandler,
150
297
  );
151
298
 
152
- // Reserve a slot in the map before any async work to enforce the
153
- // concurrency limit even when multiple spawn() calls race.
154
299
  const state: AcpSessionState = {
155
300
  id: acpSessionId,
156
- agentId,
157
- acpSessionId: "", // placeholder until createSession resolves
158
- parentConversationId,
301
+ agentId: opts.agentId,
302
+ acpSessionId: "", // placeholder until createSession/resume resolves
303
+ parentConversationId: opts.parentConversationId,
159
304
  status: "initializing",
160
- startedAt: Date.now(),
305
+ startedAt: opts.startedAt,
161
306
  };
162
307
 
163
308
  const entry: SessionEntry = {
@@ -166,54 +311,223 @@ export class AcpSessionManager {
166
311
  clientHandler,
167
312
  sendToVellum: wrappedSend,
168
313
  currentPrompt: null,
169
- parentConversationId,
170
- cwd,
171
- command: agentConfig.command,
314
+ parentConversationId: opts.parentConversationId,
315
+ cwd: opts.cwd,
316
+ command: basename(opts.agentConfig.command),
172
317
  };
173
318
 
174
319
  this.sessions.set(acpSessionId, entry);
320
+ return entry;
321
+ }
322
+
323
+ /**
324
+ * Notifies connected clients that a session is live. Shared by spawn()
325
+ * and the resume path (resumed sessions reuse the spawned event so
326
+ * clients render them; a dedicated acp_session_resumed event is a
327
+ * possible follow-up, not in scope here).
328
+ */
329
+ private sendSpawnedEvent(acpSessionId: string, entry: SessionEntry): void {
330
+ entry.sendToVellum({
331
+ type: "acp_session_spawned",
332
+ acpSessionId,
333
+ agent: entry.state.agentId,
334
+ parentConversationId: entry.parentConversationId,
335
+ });
336
+ }
337
+
338
+ /**
339
+ * Resumes a terminal-state session from its persisted
340
+ * `acp_session_history` row, reattaching to the agent's stored
341
+ * conversation via ACP `session/resume` (preferred: no history replay) or
342
+ * `session/load` (replayed history is suppressed; see
343
+ * VellumAcpClientHandler.beginReplaySuppression).
344
+ *
345
+ * The resumed session reuses the original vellum session id,
346
+ * parentConversationId, and startedAt, and re-seeds its ring buffer from
347
+ * the persisted event log so the terminal upsert after the resumed run
348
+ * merges new events into the original row instead of losing them.
349
+ *
350
+ * Throws with an actionable message when the row is missing, was recorded
351
+ * before resume support (no cwd), the agent cannot be resolved or
352
+ * auto-installed, or the agent advertises neither resume capability.
353
+ */
354
+ async resumeFromHistory(
355
+ acpSessionId: string,
356
+ sendToVellum: (msg: ServerMessage) => void,
357
+ ): Promise<void> {
358
+ if (
359
+ this.sessions.has(acpSessionId) ||
360
+ this.pendingResumes.has(acpSessionId)
361
+ ) {
362
+ throw new Error(`ACP session "${acpSessionId}" is already active`);
363
+ }
364
+ this.assertCapacity();
365
+
366
+ const row = getDb()
367
+ .select()
368
+ .from(acpSessionHistory)
369
+ .where(eq(acpSessionHistory.id, acpSessionId))
370
+ .get();
371
+ if (!row) {
372
+ throw new AcpSessionNotFoundError(acpSessionId);
373
+ }
374
+ if (!row.cwd) {
375
+ throw new Error(
376
+ `ACP session "${acpSessionId}" was recorded before resume support ` +
377
+ `(no working directory persisted) and cannot be resumed. ` +
378
+ `Spawn a new session instead.`,
379
+ );
380
+ }
381
+ if (!row.acpSessionId) {
382
+ throw new Error(
383
+ `ACP session "${acpSessionId}" has no protocol session id ` +
384
+ `persisted and cannot be resumed. Spawn a new session instead.`,
385
+ );
386
+ }
387
+
388
+ // Everything up to here is synchronous. Reserve the id + concurrency
389
+ // slot BEFORE the first await so a concurrent resume of the same id
390
+ // (or a spawn racing the cap) fails the guards above instead of
391
+ // double-registering and leaking the first child process. The
392
+ // reservation holds the resume's promise until it settles so
393
+ // steerOrResume can await a concurrent caller's in-flight resume, and
394
+ // so the delete guards see the id as live while the row's terminal
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.
398
+ const resumePromise = this.performResume(
399
+ acpSessionId,
400
+ row as ResumableHistoryRow,
401
+ sendToVellum,
402
+ );
403
+ this.pendingResumes.set(acpSessionId, resumePromise);
404
+ try {
405
+ await resumePromise;
406
+ } finally {
407
+ this.pendingResumes.delete(acpSessionId);
408
+ }
409
+ }
410
+
411
+ /**
412
+ * The async body of resumeFromHistory, split out so the caller can store
413
+ * its promise in `pendingResumes` synchronously before the first await.
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.
417
+ */
418
+ private async performResume(
419
+ acpSessionId: string,
420
+ row: ResumableHistoryRow,
421
+ sendToVellum: (msg: ServerMessage) => void,
422
+ ): Promise<void> {
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);
442
+
443
+ // The daemon may have shut down while prepareAgentEnv was pending.
444
+ // Registering now would spawn a child process on a disposed manager
445
+ // that nothing would ever kill.
446
+ if (this.disposed) {
447
+ throw new Error(
448
+ `ACP session manager is disposed; cannot resume session "${acpSessionId}"`,
449
+ );
450
+ }
451
+
452
+ const entry = this.registerSession({
453
+ acpSessionId,
454
+ agentId: row.agentId,
455
+ agentConfig,
456
+ parentConversationId: row.parentConversationId,
457
+ cwd: row.cwd,
458
+ startedAt: row.startedAt,
459
+ sendToVellum,
460
+ });
461
+
462
+ log.info(
463
+ { acpSessionId, agentId: row.agentId, cwd: row.cwd },
464
+ "ACP resume from history requested",
465
+ );
466
+ const { process: agentProcess, state } = entry;
467
+
468
+ // Re-seed the ring buffer from the persisted event log, routed through
469
+ // appendToBuffer so the count/byte caps still apply. The terminal
470
+ // upsert then persists the merged (old + new) log.
471
+ try {
472
+ const persisted = JSON.parse(row.eventLogJson) as unknown;
473
+ if (Array.isArray(persisted)) {
474
+ for (const update of persisted) {
475
+ this.appendToBuffer(acpSessionId, update as AcpSessionUpdate);
476
+ }
477
+ }
478
+ } catch (err) {
479
+ log.warn(
480
+ { acpSessionId, err },
481
+ "Failed to re-seed ACP event buffer from persisted history",
482
+ );
483
+ }
175
484
 
176
485
  try {
177
- log.info({ acpSessionId, agentId }, "ACP spawning child process");
178
- agentProcess.spawn(cwd);
179
486
  log.info(
180
- { acpSessionId, agentId },
181
- "ACP initializing protocol connection",
487
+ { acpSessionId, agentId: row.agentId },
488
+ "ACP spawning child process for resume",
182
489
  );
490
+ agentProcess.spawn(row.cwd);
183
491
  await agentProcess.initialize();
184
- log.info({ acpSessionId, agentId }, "ACP creating session");
185
- const acpProtocolSessionId = await agentProcess.createSession(cwd);
186
- state.acpSessionId = acpProtocolSessionId;
492
+ if (agentProcess.supportsSessionResume) {
493
+ // session/resume reattaches without replaying history.
494
+ await agentProcess.resumeSession(row.acpSessionId, row.cwd);
495
+ } else if (agentProcess.supportsLoadSession) {
496
+ // session/load replays the full history as session/update
497
+ // notifications before resolving; suppress forwarding so the
498
+ // conversation and ring buffer don't receive duplicates.
499
+ entry.clientHandler.beginReplaySuppression();
500
+ try {
501
+ await agentProcess.loadSession(row.acpSessionId, row.cwd);
502
+ } finally {
503
+ entry.clientHandler.endReplaySuppression();
504
+ }
505
+ } else {
506
+ throw new Error(
507
+ `ACP agent "${row.agentId}" does not support session resume`,
508
+ );
509
+ }
510
+ state.acpSessionId = row.acpSessionId;
187
511
  state.status = "running";
188
512
  log.info(
189
- { acpSessionId, agentId, acpProtocolSessionId },
190
- "ACP session running",
513
+ {
514
+ acpSessionId,
515
+ agentId: row.agentId,
516
+ protocolSessionId: row.acpSessionId,
517
+ },
518
+ "ACP session resumed",
191
519
  );
192
520
  } catch (err) {
193
- log.error({ acpSessionId, agentId, err }, "ACP spawn failed");
194
- // Kill the orphaned child process and remove the reserved slot.
195
- agentProcess.kill();
196
- this.sessions.delete(acpSessionId);
197
- this.eventBuffers.delete(acpSessionId);
521
+ log.error(
522
+ { acpSessionId, agentId: row.agentId, err },
523
+ "ACP resume failed",
524
+ );
525
+ // No prompt has fired yet, so no permissions can be pending.
526
+ this.teardownSession(acpSessionId, entry);
198
527
  throw err;
199
528
  }
200
529
 
201
- wrappedSend({
202
- type: "acp_session_spawned",
203
- acpSessionId,
204
- agent: agentId,
205
- parentConversationId,
206
- });
207
-
208
- // Fire prompt in the background — don't await
209
- entry.currentPrompt = this.firePromptInBackground(
210
- acpSessionId,
211
- entry,
212
- state.acpSessionId,
213
- task,
214
- );
215
-
216
- return { acpSessionId, protocolSessionId: state.acpSessionId };
530
+ this.sendSpawnedEvent(acpSessionId, entry);
217
531
  }
218
532
 
219
533
  /**
@@ -225,7 +539,7 @@ export class AcpSessionManager {
225
539
  async steer(acpSessionId: string, instruction: string): Promise<void> {
226
540
  const entry = this.sessions.get(acpSessionId);
227
541
  if (!entry) {
228
- throw new Error(`ACP session "${acpSessionId}" not found`);
542
+ throw new AcpSessionNotFoundError(acpSessionId);
229
543
  }
230
544
 
231
545
  if (entry.state.status !== "running") {
@@ -258,22 +572,123 @@ export class AcpSessionManager {
258
572
  );
259
573
  }
260
574
 
575
+ /**
576
+ * Steers a session, transparently resuming it from persisted history
577
+ * first when it is no longer in memory (it completed, or the daemon
578
+ * restarted). The resume and the instruction prompt are atomic from the
579
+ * caller's perspective: a successfully resumed session immediately gets
580
+ * the instruction fired, so it never sits running-idle with no in-flight
581
+ * prompt (and therefore no teardown owner). If the post-resume steer
582
+ * fails, the freshly resumed session is closed (process killed, terminal
583
+ * row persisted, maps cleared) instead of being leaked.
584
+ *
585
+ * When a concurrent caller's resume of the same id is already in flight,
586
+ * this call awaits that resume and then retries the plain steer, so both
587
+ * callers' instructions land on the single resumed session.
588
+ *
589
+ * Error contract for transport callers (acp_steer tool, steer route):
590
+ * - `AcpSessionNotFoundError`: no in-memory session AND no history row.
591
+ * - `AcpResumeError`: the resume (own or a concurrent caller's awaited
592
+ * one, or the steer immediately after an own resume) failed; the
593
+ * message carries the actionable hint.
594
+ * - any other error: the plain steer on an in-memory session failed.
595
+ */
596
+ async steerOrResume(
597
+ acpSessionId: string,
598
+ instruction: string,
599
+ sendToVellum: (msg: ServerMessage) => void,
600
+ ): Promise<{ resumed: boolean }> {
601
+ try {
602
+ await this.steer(acpSessionId, instruction);
603
+ return { resumed: false };
604
+ } catch (err) {
605
+ // Fall through to the in-flight-resume handling both when the session
606
+ // is entirely unknown and when a concurrent resume has already
607
+ // registered its entry but is still initializing: steer rejects with a
608
+ // plain not-running error in that window, yet the resume reservation
609
+ // is live and the retry below will land once it settles.
610
+ if (
611
+ !(err instanceof AcpSessionNotFoundError) &&
612
+ !this.pendingResumes.has(acpSessionId)
613
+ ) {
614
+ throw err;
615
+ }
616
+ }
617
+
618
+ // Another caller's resume of this id may already be in flight (the
619
+ // session is not in memory yet, but its slot is reserved). Await that
620
+ // resume and retry the plain steer once instead of failing
621
+ // resumeFromHistory's already-active guard, which would surface a
622
+ // misleading resume error and drop this instruction.
623
+ const inFlightResume = this.pendingResumes.get(acpSessionId);
624
+ if (inFlightResume) {
625
+ try {
626
+ await inFlightResume;
627
+ } catch (err) {
628
+ throw new AcpResumeError(err);
629
+ }
630
+ // The resumed session is owned by the concurrent caller (its own
631
+ // post-resume steer handles teardown on failure), so a failure here
632
+ // propagates as a plain steer error without closing the session.
633
+ await this.steer(acpSessionId, instruction);
634
+ return { resumed: true };
635
+ }
636
+
637
+ try {
638
+ await this.resumeFromHistory(acpSessionId, sendToVellum);
639
+ } catch (err) {
640
+ // A missing history row keeps its not-found shape; everything else
641
+ // (legacy row without cwd, resolver failure, capability missing)
642
+ // is a resume failure with an actionable message.
643
+ if (err instanceof AcpSessionNotFoundError) throw err;
644
+ throw new AcpResumeError(err);
645
+ }
646
+
647
+ try {
648
+ await this.steer(acpSessionId, instruction);
649
+ } catch (err) {
650
+ // Tear down the just-resumed session rather than leaving it
651
+ // running-idle with no prompt handler to own its cleanup.
652
+ try {
653
+ this.close(acpSessionId);
654
+ } catch (closeErr) {
655
+ log.warn(
656
+ { acpSessionId, err: closeErr },
657
+ "Failed to close ACP session after post-resume steer failure",
658
+ );
659
+ }
660
+ throw new AcpResumeError(err);
661
+ }
662
+ return { resumed: true };
663
+ }
664
+
261
665
  /**
262
666
  * Cancels an ongoing prompt in the specified session.
263
667
  *
264
- * The session's in-flight `prompt()` will reject in response, and the
265
- * catch handler in `firePromptInBackground` performs the terminal
266
- * persistence + teardown. We just flip the status here so that handler
267
- * preserves "cancelled" instead of overwriting with "failed".
668
+ * When a prompt is in flight, its `prompt()` call rejects in response
669
+ * and the catch handler in `firePromptInBackground` performs the
670
+ * terminal persistence + teardown; we just flip the status here so that
671
+ * handler preserves "cancelled" instead of overwriting with "failed".
672
+ *
673
+ * When NO prompt is in flight there is no handler to own cleanup, so
674
+ * cancel persists and tears down the session itself. (Sessions normally
675
+ * always have a prompt in flight, but a cancel can race the window in
676
+ * steer() between clearing the old prompt and firing the new one.)
268
677
  */
269
678
  async cancel(acpSessionId: string): Promise<void> {
270
679
  const entry = this.sessions.get(acpSessionId);
271
680
  if (!entry) {
272
- throw new Error(`ACP session "${acpSessionId}" not found`);
681
+ throw new AcpSessionNotFoundError(acpSessionId);
273
682
  }
274
683
  await entry.process.cancel(entry.state.acpSessionId);
275
684
  entry.state.status = "cancelled";
276
685
  entry.state.completedAt = Date.now();
686
+ // Re-check the map after the await: the in-flight prompt's handler may
687
+ // have already torn the session down while process.cancel was pending.
688
+ if (!entry.currentPrompt && this.sessions.get(acpSessionId) === entry) {
689
+ this.persistTerminal(acpSessionId, entry);
690
+ this.teardownSession(acpSessionId, entry);
691
+ }
277
692
  }
278
693
 
279
694
  /**
@@ -288,7 +703,7 @@ export class AcpSessionManager {
288
703
  close(acpSessionId: string): void {
289
704
  const entry = this.sessions.get(acpSessionId);
290
705
  if (!entry) {
291
- throw new Error(`ACP session "${acpSessionId}" not found`);
706
+ throw new AcpSessionNotFoundError(acpSessionId);
292
707
  }
293
708
  if (
294
709
  entry.state.status === "running" ||
@@ -335,7 +750,7 @@ export class AcpSessionManager {
335
750
  if (acpSessionId) {
336
751
  const entry = this.sessions.get(acpSessionId);
337
752
  if (!entry) {
338
- throw new Error(`ACP session "${acpSessionId}" not found`);
753
+ throw new AcpSessionNotFoundError(acpSessionId);
339
754
  }
340
755
  return entry.state;
341
756
  }
@@ -368,15 +783,18 @@ export class AcpSessionManager {
368
783
 
369
784
  /**
370
785
  * Persists the session's final state + buffered event log to
371
- * `acp_session_history`, then frees the buffer entry. Best-effort: a DB
372
- * failure is logged but does not propagate, since the session has already
373
- * reached a terminal state and clients have been notified.
786
+ * `acp_session_history`, then frees the buffer entry. Upserts on id:
787
+ * resumed runs reuse the original vellum session id, so their terminal
788
+ * write must update the existing row (status, event log, etc.) instead of
789
+ * being silently skipped. Best-effort: a DB failure is logged but does
790
+ * not propagate, since the session has already reached a terminal state
791
+ * and clients have been notified.
374
792
  */
375
793
  private persistTerminal(acpSessionId: string, entry: SessionEntry): void {
376
794
  const buffer = this.eventBuffers.get(acpSessionId) ?? [];
377
795
  // Serialize only the wire-shaped updates — drop the byte-size accounting
378
796
  // metadata so persisted rows match the protocol shape clients receive.
379
- const wireUpdates = buffer.map((buffered) => buffered.update);
797
+ const eventLogJson = JSON.stringify(buffer.map((b) => b.update));
380
798
  try {
381
799
  getDb()
382
800
  .insert(acpSessionHistory)
@@ -390,9 +808,20 @@ export class AcpSessionManager {
390
808
  status: entry.state.status,
391
809
  stopReason: entry.state.stopReason ?? null,
392
810
  error: entry.state.error ?? null,
393
- eventLogJson: JSON.stringify(wireUpdates),
811
+ eventLogJson,
812
+ cwd: entry.cwd,
813
+ })
814
+ .onConflictDoUpdate({
815
+ target: acpSessionHistory.id,
816
+ set: {
817
+ status: entry.state.status,
818
+ completedAt: entry.state.completedAt ?? null,
819
+ stopReason: entry.state.stopReason ?? null,
820
+ error: entry.state.error ?? null,
821
+ eventLogJson,
822
+ cwd: entry.cwd,
823
+ },
394
824
  })
395
- .onConflictDoNothing()
396
825
  .run();
397
826
  } catch (err) {
398
827
  log.error(
@@ -451,10 +880,12 @@ export class AcpSessionManager {
451
880
  const agentLabel = current.state.agentId;
452
881
  const responseText = current.clientHandler.responseText;
453
882
  const sessionId = current.state.acpSessionId;
454
- const resumeHint =
455
- current.command === "claude-agent-acp"
456
- ? `\n\nTo resume: cd ${current.cwd} && claude --resume ${sessionId}`
457
- : "";
883
+ const hint = claudeResumeHint(
884
+ current.command,
885
+ current.cwd,
886
+ sessionId,
887
+ );
888
+ const resumeHint = hint ? `\n\n${hint}` : "";
458
889
  const notifyMessage = `[ACP agent "${agentLabel}" completed]\n\n${responseText}${resumeHint}`;
459
890
  const parentConversation = findConversation(
460
891
  current.parentConversationId,
@@ -517,9 +948,12 @@ export class AcpSessionManager {
517
948
  }
518
949
 
519
950
  /**
520
- * Kills all processes on shutdown.
951
+ * Kills all processes on shutdown. Also flags the manager as disposed so
952
+ * resumes that are mid-await when shutdown happens abort before spawning
953
+ * a child process nothing would ever kill.
521
954
  */
522
955
  dispose(): void {
956
+ this.disposed = true;
523
957
  this.closeAll();
524
958
  }
525
959
  }