@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea

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 (570) hide show
  1. package/Dockerfile +20 -4
  2. package/bun.lock +2 -2
  3. package/docker-entrypoint.sh +4 -2
  4. package/docker-init-apt-root.sh +3 -1
  5. package/docker-kata-apt-env.sh +3 -1
  6. package/docker-kata-runtime-family.sh +12 -0
  7. package/docs/architecture/memory.md +1 -1
  8. package/examples/plugins/echo/README.md +61 -66
  9. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  10. package/examples/plugins/echo/hooks/stop.ts +16 -0
  11. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  12. package/examples/plugins/echo/package.json +1 -2
  13. package/examples/plugins/echo/src/emit.ts +19 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  15. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  16. package/openapi.yaml +3378 -335
  17. package/package.json +2 -2
  18. package/scripts/generate-openapi.ts +68 -41
  19. package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  21. package/src/__tests__/agent-loop.test.ts +37 -87
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  23. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  24. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  25. package/src/__tests__/anthropic-provider.test.ts +95 -2
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  30. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  31. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  32. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  33. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  34. package/src/__tests__/btw-routes.test.ts +62 -3
  35. package/src/__tests__/build-persisted-content.test.ts +184 -0
  36. package/src/__tests__/catalog-files.test.ts +1 -1
  37. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  38. package/src/__tests__/channel-approvals.test.ts +1 -1
  39. package/src/__tests__/clawhub-files.test.ts +1 -1
  40. package/src/__tests__/compaction-circuit.test.ts +258 -0
  41. package/src/__tests__/compaction-direct.test.ts +132 -0
  42. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  43. package/src/__tests__/config-watcher.test.ts +1 -1
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
  48. package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
  49. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  50. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  51. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  52. package/src/__tests__/conversation-pairing.test.ts +4 -31
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
  55. package/src/__tests__/conversation-queue.test.ts +2 -0
  56. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  57. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  58. package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
  59. package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
  60. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  61. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  62. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  63. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  64. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  65. package/src/__tests__/conversation-title-service.test.ts +135 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
  67. package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  71. package/src/__tests__/db-acp-history.test.ts +101 -0
  72. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  73. package/src/__tests__/dm-persistence.test.ts +5 -1
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +304 -0
  76. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  77. package/src/__tests__/file-write-tool.test.ts +63 -0
  78. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  79. package/src/__tests__/gemini-image-service.test.ts +13 -0
  80. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  81. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  82. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  83. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  84. package/src/__tests__/heartbeat-service.test.ts +1 -0
  85. package/src/__tests__/helpers/mock-provider.ts +110 -0
  86. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  87. package/src/__tests__/history-repair-hook.test.ts +1 -0
  88. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  89. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  90. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  91. package/src/__tests__/identity-routes.test.ts +248 -7
  92. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  93. package/src/__tests__/injector-background-turn.test.ts +3 -9
  94. package/src/__tests__/injector-chain.test.ts +139 -275
  95. package/src/__tests__/injector-disk-pressure.test.ts +75 -41
  96. package/src/__tests__/injector-document-comments.test.ts +3 -3
  97. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  98. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  99. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  100. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  101. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  102. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  103. package/src/__tests__/llm-usage-store.test.ts +223 -1
  104. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  105. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  106. package/src/__tests__/native-web-search.test.ts +191 -0
  107. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  108. package/src/__tests__/openai-image-service.test.ts +17 -0
  109. package/src/__tests__/openai-provider.test.ts +31 -1
  110. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
  111. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  112. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  113. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  114. package/src/__tests__/plugin-api-shim.test.ts +3 -6
  115. package/src/__tests__/plugin-bootstrap.test.ts +14 -40
  116. package/src/__tests__/plugin-registry.test.ts +3 -76
  117. package/src/__tests__/plugin-types.test.ts +0 -193
  118. package/src/__tests__/process-message-display-content.test.ts +6 -2
  119. package/src/__tests__/reaction-persistence.test.ts +1 -1
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  121. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  122. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  123. package/src/__tests__/schedule-routes.test.ts +603 -2
  124. package/src/__tests__/schedule-store.test.ts +41 -0
  125. package/src/__tests__/schedule-tools.test.ts +35 -0
  126. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  127. package/src/__tests__/server-history-render.test.ts +314 -1
  128. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  129. package/src/__tests__/skillssh-files.test.ts +1 -1
  130. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  132. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  133. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  134. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  135. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  136. package/src/__tests__/system-prompt.test.ts +20 -0
  137. package/src/__tests__/task-scheduler.test.ts +162 -1
  138. package/src/__tests__/terminal-tools.test.ts +6 -1
  139. package/src/__tests__/title-generate-hook.test.ts +319 -0
  140. package/src/__tests__/tool-error-hook.test.ts +278 -0
  141. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  142. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  143. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  144. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  145. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  146. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  147. package/src/__tests__/usage-routes.test.ts +285 -1
  148. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  149. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  150. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  151. package/src/acp/__tests__/agent-process.test.ts +161 -0
  152. package/src/acp/__tests__/client-handler.test.ts +40 -0
  153. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  154. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  155. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  156. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  157. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  158. package/src/acp/agent-process.ts +61 -1
  159. package/src/acp/auto-install.test.ts +196 -0
  160. package/src/acp/auto-install.ts +177 -0
  161. package/src/acp/client-handler.ts +31 -0
  162. package/src/acp/feature-gate.test.ts +48 -0
  163. package/src/acp/feature-gate.ts +34 -0
  164. package/src/acp/prepare-agent-env.ts +83 -29
  165. package/src/acp/resolve-agent.test.ts +320 -7
  166. package/src/acp/resolve-agent.ts +182 -18
  167. package/src/acp/resume-hint.ts +25 -0
  168. package/src/acp/session-manager.ts +495 -73
  169. package/src/acp/types.ts +8 -0
  170. package/src/agent/compaction-circuit.ts +60 -102
  171. package/src/agent/loop.ts +362 -485
  172. package/src/api/events/assistant-thinking-delta.ts +33 -0
  173. package/src/api/events/tool-output-chunk.ts +45 -0
  174. package/src/api/events/tool-use-preview-start.ts +32 -0
  175. package/src/api/events/trace-event.ts +69 -0
  176. package/src/api/index.ts +48 -13
  177. package/src/api/responses/conversation-message.ts +374 -0
  178. package/src/approvals/guardian-request-resolvers.ts +1 -1
  179. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  180. package/src/background-wake/next-wake.ts +1 -0
  181. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  182. package/src/cli/commands/notifications.ts +112 -60
  183. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  184. package/src/config/acp-defaults.test.ts +10 -0
  185. package/src/config/acp-defaults.ts +6 -0
  186. package/src/config/assistant-feature-flags.ts +22 -11
  187. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  188. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  189. package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
  190. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  191. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  192. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  193. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  194. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  195. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  196. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  197. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  198. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  199. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  200. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  201. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  202. package/src/config/bundled-tool-registry.ts +2 -0
  203. package/src/config/feature-flag-cache.ts +3 -3
  204. package/src/config/feature-flag-registry.json +48 -7
  205. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  207. package/src/config/schemas/heartbeat.ts +9 -0
  208. package/src/config/schemas/llm.ts +1 -0
  209. package/src/config/schemas/memory-v2.ts +8 -0
  210. package/src/config/schemas/memory-v3.ts +8 -0
  211. package/src/config/schemas/platform.ts +8 -0
  212. package/src/config/seed-inference-profiles.ts +2 -2
  213. package/src/config/skills.ts +13 -0
  214. package/src/context/compactor.ts +1 -1
  215. package/src/context/strip-injections.ts +128 -0
  216. package/src/context/token-estimator.ts +23 -0
  217. package/src/context/tool-result-truncation.ts +0 -23
  218. package/src/context/window-manager.ts +5 -7
  219. package/src/credential-execution/executable-discovery.ts +16 -0
  220. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  221. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  222. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  223. package/src/daemon/assistant-attachments.ts +1 -1
  224. package/src/daemon/config-watcher.ts +2 -2
  225. package/src/daemon/context-overflow-reducer.ts +0 -1
  226. package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
  227. package/src/daemon/conversation-agent-loop.ts +301 -997
  228. package/src/daemon/conversation-history.ts +5 -4
  229. package/src/daemon/conversation-lifecycle.ts +3 -4
  230. package/src/daemon/conversation-messaging.ts +7 -6
  231. package/src/daemon/conversation-process.ts +11 -16
  232. package/src/daemon/conversation-registry.ts +159 -0
  233. package/src/daemon/conversation-runtime-assembly.ts +218 -398
  234. package/src/daemon/conversation-slash.ts +6 -25
  235. package/src/daemon/conversation-store.ts +9 -90
  236. package/src/daemon/conversation-surfaces.ts +222 -4
  237. package/src/daemon/conversation-tool-setup.ts +2 -29
  238. package/src/daemon/conversation-workspace.ts +17 -0
  239. package/src/daemon/conversation.ts +32 -20
  240. package/src/daemon/external-plugins-bootstrap.ts +17 -18
  241. package/src/daemon/handlers/config-a2a.ts +51 -36
  242. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  243. package/src/daemon/handlers/config-telegram.ts +16 -2
  244. package/src/daemon/handlers/conversations.ts +3 -1
  245. package/src/daemon/handlers/shared.ts +156 -84
  246. package/src/daemon/handlers/skills.ts +42 -10
  247. package/src/daemon/lifecycle.ts +25 -0
  248. package/src/daemon/message-types/apps.ts +1 -29
  249. package/src/daemon/message-types/messages.ts +9 -57
  250. package/src/daemon/message-types/skills.ts +2 -0
  251. package/src/daemon/message-types/surfaces.ts +136 -3
  252. package/src/daemon/now-scratchpad.ts +21 -0
  253. package/src/daemon/orphan-reaper.test.ts +210 -0
  254. package/src/daemon/orphan-reaper.ts +240 -0
  255. package/src/daemon/overflow-reduction-loop.ts +230 -0
  256. package/src/daemon/persist-unsendable-image.ts +117 -0
  257. package/src/daemon/process-message.ts +1 -3
  258. package/src/daemon/server.ts +2 -0
  259. package/src/daemon/trace-emitter.ts +6 -4
  260. package/src/daemon/trust-context.ts +19 -0
  261. package/src/daemon/wake-target-adapter.ts +3 -1
  262. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  263. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  264. package/src/heartbeat/heartbeat-service.ts +26 -0
  265. package/src/home/home-greeting-cache.ts +24 -1
  266. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  267. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  268. package/src/ipc/gateway-client.test.ts +2 -2
  269. package/src/ipc/gateway-client.ts +3 -3
  270. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  271. package/src/ipc/skill-routes/memory.ts +4 -2
  272. package/src/media/gemini-image-service.ts +15 -0
  273. package/src/media/openai-image-service.ts +14 -0
  274. package/src/media/types.ts +34 -0
  275. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  276. package/src/memory/auth-fallback-events-store.ts +94 -0
  277. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  278. package/src/memory/conversation-title-service.ts +65 -41
  279. package/src/memory/db-init.ts +6 -0
  280. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  281. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  282. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  283. package/src/memory/jobs-store.ts +33 -0
  284. package/src/memory/jobs-worker.ts +32 -5
  285. package/src/memory/llm-usage-store.ts +224 -50
  286. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  287. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  288. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  289. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  290. package/src/memory/migrations/index.ts +3 -0
  291. package/src/memory/pkb/autoinject.ts +61 -0
  292. package/src/memory/pkb/context.ts +50 -0
  293. package/src/memory/pkb/types.ts +14 -0
  294. package/src/memory/schedule-attribution-sql.ts +104 -0
  295. package/src/memory/schema/acp.ts +4 -0
  296. package/src/memory/schema/infrastructure.ts +16 -0
  297. package/src/memory/usage-grouped-buckets.ts +6 -1
  298. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
  299. package/src/memory/v2/consolidation-job.ts +14 -5
  300. package/src/notifications/conversation-pairing.ts +8 -15
  301. package/src/notifications/decision-engine.ts +6 -3
  302. package/src/notifications/home-feed-side-effect.ts +12 -1
  303. package/src/permissions/prompter.ts +4 -0
  304. package/src/plugin-api/constants.ts +4 -0
  305. package/src/plugin-api/index.ts +7 -5
  306. package/src/plugin-api/types.ts +151 -1
  307. package/src/plugins/defaults/compaction/compact.ts +59 -0
  308. package/src/plugins/defaults/compaction/package.json +1 -1
  309. package/src/plugins/defaults/compaction/register.ts +8 -19
  310. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  311. package/src/plugins/defaults/empty-response/register.ts +8 -13
  312. package/src/plugins/defaults/index.ts +2 -18
  313. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  314. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  315. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  316. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
  317. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  318. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +16 -0
  319. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  321. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +48 -12
  322. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  323. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  324. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  325. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +104 -32
  326. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +96 -30
  328. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
  329. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
  330. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
  331. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  332. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  333. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  334. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  335. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  336. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
  337. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
  338. package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
  339. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  340. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  341. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
  342. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  343. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  344. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +51 -45
  345. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  346. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +61 -46
  347. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
  348. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  349. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
  350. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  351. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  352. package/src/plugins/defaults/title-generate/package.json +1 -1
  353. package/src/plugins/defaults/title-generate/register.ts +18 -18
  354. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  355. package/src/plugins/defaults/tool-error/package.json +1 -1
  356. package/src/plugins/defaults/tool-error/register.ts +9 -21
  357. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  358. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  359. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  360. package/src/plugins/external-api.ts +2 -2
  361. package/src/plugins/pipeline.ts +6 -305
  362. package/src/plugins/registry.ts +10 -55
  363. package/src/plugins/types.ts +62 -797
  364. package/src/plugins/user-loader.ts +30 -127
  365. package/src/proactive-artifact/aux-message-injector.ts +4 -4
  366. package/src/proactive-artifact/job.test.ts +8 -13
  367. package/src/prompts/__tests__/system-prompt.test.ts +42 -0
  368. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  369. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  370. package/src/prompts/templates/system-sections.ts +15 -0
  371. package/src/providers/anthropic/client.ts +37 -29
  372. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  373. package/src/providers/openai/chat-completions-provider.ts +44 -0
  374. package/src/providers/openrouter/client.ts +1 -0
  375. package/src/providers/placeholder-sentinels.ts +35 -0
  376. package/src/runtime/__tests__/agent-wake.test.ts +10 -6
  377. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  378. package/src/runtime/agent-wake.ts +2 -5
  379. package/src/runtime/assistant-event-hub.ts +37 -7
  380. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  381. package/src/runtime/channel-approvals.ts +1 -1
  382. package/src/runtime/http-router.ts +16 -21
  383. package/src/runtime/http-types.ts +16 -70
  384. package/src/runtime/interactive-ui.ts +1 -1
  385. package/src/runtime/pending-interactions.ts +1 -0
  386. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  387. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  388. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  389. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  390. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  391. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  392. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  393. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  394. package/src/runtime/routes/acp-routes.test.ts +89 -25
  395. package/src/runtime/routes/acp-routes.ts +81 -29
  396. package/src/runtime/routes/app-management-routes.ts +6 -117
  397. package/src/runtime/routes/app-routes.ts +13 -15
  398. package/src/runtime/routes/approval-routes.ts +1 -1
  399. package/src/runtime/routes/attachment-routes.ts +26 -15
  400. package/src/runtime/routes/avatar-routes.ts +26 -0
  401. package/src/runtime/routes/browser-routes.ts +1 -1
  402. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  403. package/src/runtime/routes/btw-routes.ts +29 -23
  404. package/src/runtime/routes/consolidation-routes.ts +120 -20
  405. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  406. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  407. package/src/runtime/routes/conversation-query-routes.ts +3 -1
  408. package/src/runtime/routes/conversation-routes.ts +372 -185
  409. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  410. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  411. package/src/runtime/routes/documents-routes.ts +4 -0
  412. package/src/runtime/routes/domain-routes.ts +51 -37
  413. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  414. package/src/runtime/routes/events-routes.ts +28 -34
  415. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  416. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  417. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  418. package/src/runtime/routes/host-cu-routes.ts +1 -1
  419. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  420. package/src/runtime/routes/identity-routes.ts +224 -18
  421. package/src/runtime/routes/image-generation-routes.ts +40 -2
  422. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  423. package/src/runtime/routes/index.ts +2 -0
  424. package/src/runtime/routes/integrations/a2a.ts +12 -10
  425. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  426. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  427. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  428. package/src/runtime/routes/integrations/telegram.ts +6 -0
  429. package/src/runtime/routes/integrations/twilio.ts +42 -0
  430. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  431. package/src/runtime/routes/log-export-routes.ts +8 -0
  432. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  433. package/src/runtime/routes/memory-v3-routes.ts +66 -34
  434. package/src/runtime/routes/oauth-apps.ts +66 -12
  435. package/src/runtime/routes/oauth-providers.ts +44 -5
  436. package/src/runtime/routes/platform-routes.ts +81 -5
  437. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  438. package/src/runtime/routes/playground/force-compact.ts +1 -1
  439. package/src/runtime/routes/playground/helpers.ts +1 -1
  440. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  441. package/src/runtime/routes/schedule-routes.ts +152 -42
  442. package/src/runtime/routes/secret-routes.ts +14 -2
  443. package/src/runtime/routes/skills-routes.ts +43 -14
  444. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  445. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  446. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  447. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  448. package/src/runtime/routes/tts-routes.ts +35 -0
  449. package/src/runtime/routes/types.ts +66 -8
  450. package/src/runtime/routes/usage-routes.ts +47 -39
  451. package/src/runtime/routes/webhook-routes.ts +41 -2
  452. package/src/runtime/routes/work-items-routes.ts +2 -4
  453. package/src/runtime/routes/workspace-routes.ts +4 -0
  454. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  455. package/src/runtime/services/analyze-conversation.ts +2 -2
  456. package/src/runtime/services/conversation-serializer.ts +1 -1
  457. package/src/schedule/schedule-store.ts +20 -1
  458. package/src/schedule/schedule-usage-store.ts +83 -0
  459. package/src/schedule/scheduler.ts +12 -5
  460. package/src/signals/cancel.ts +2 -4
  461. package/src/skills/catalog-files.ts +2 -2
  462. package/src/skills/catalog-install.ts +3 -0
  463. package/src/skills/categories-cache.ts +118 -0
  464. package/src/skills/clawhub-files.ts +1 -2
  465. package/src/skills/skillssh-files.ts +1 -2
  466. package/src/subagent/manager.ts +17 -5
  467. package/src/telemetry/types.ts +29 -1
  468. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  469. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  470. package/src/tools/acp/context.ts +20 -0
  471. package/src/tools/acp/list-agents.test.ts +7 -1
  472. package/src/tools/acp/spawn.test.ts +158 -55
  473. package/src/tools/acp/spawn.ts +47 -72
  474. package/src/tools/acp/steer.test.ts +105 -8
  475. package/src/tools/acp/steer.ts +48 -17
  476. package/src/tools/apps/executors.ts +13 -8
  477. package/src/tools/executor.ts +1 -53
  478. package/src/tools/filesystem/write.ts +34 -0
  479. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  480. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  481. package/src/tools/network/web-search-error.test.ts +248 -0
  482. package/src/tools/network/web-search-error.ts +267 -0
  483. package/src/tools/network/web-search.ts +207 -48
  484. package/src/tools/schedule/create.ts +2 -0
  485. package/src/tools/subagent/spawn.ts +2 -4
  486. package/src/tools/terminal/safe-env.ts +10 -1
  487. package/src/tools/ui-surface/definitions.ts +34 -5
  488. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  489. package/src/tts/provider-catalog.ts +76 -1
  490. package/src/util/mutex.ts +47 -0
  491. package/src/workspace/git-service.ts +1 -42
  492. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  493. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  494. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  495. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  496. package/src/workspace/migrations/registry.ts +6 -0
  497. package/docs/plugins.md +0 -836
  498. package/examples/plugins/echo/register.ts +0 -184
  499. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  500. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  501. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  502. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  503. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  504. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  505. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  506. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  507. package/src/__tests__/pipeline-runner.test.ts +0 -564
  508. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  509. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  510. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  511. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  512. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  513. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  514. package/src/gallery/default-gallery.ts +0 -1359
  515. package/src/gallery/gallery-manifest.ts +0 -28
  516. package/src/home/feature-gate.ts +0 -22
  517. package/src/memory/v3/provider-blocks.ts +0 -16
  518. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  519. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  520. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  521. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  522. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  523. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  524. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  525. package/src/plugins/defaults/injectors/package.json +0 -15
  526. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  527. package/src/plugins/defaults/llm-call/register.ts +0 -45
  528. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  529. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  530. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  531. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  532. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  533. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  534. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  535. package/src/plugins/defaults/persistence/package.json +0 -15
  536. package/src/plugins/defaults/persistence/register.ts +0 -38
  537. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  538. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  539. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  540. package/src/plugins/defaults/token-estimate/package.json +0 -15
  541. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  542. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  543. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  544. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  545. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  546. package/src/plugins/defaults/tool-execute/package.json +0 -15
  547. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  548. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  549. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  550. package/src/skills/category-inference.ts +0 -111
  551. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  552. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  553. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  554. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  555. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  556. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  557. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  558. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +0 -0
  559. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  560. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  561. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  562. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  563. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  564. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  565. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  566. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  567. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  568. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  569. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  570. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { inArray } from "drizzle-orm";
8
+ import { eq, inArray } from "drizzle-orm";
9
9
 
10
- import { findConversation } from "../daemon/conversation-store.js";
10
+ import { findConversation } from "../daemon/conversation-registry.js";
11
11
  import type { ServerMessage } from "../daemon/message-protocol.js";
12
12
  import type { AcpSessionUpdate } from "../daemon/message-types/acp.js";
13
13
  import { getDb } from "../memory/db-connection.js";
@@ -16,10 +16,43 @@ import * as pendingInteractions from "../runtime/pending-interactions.js";
16
16
  import { getLogger } from "../util/logger.js";
17
17
  import { AcpAgentProcess } from "./agent-process.js";
18
18
  import { VellumAcpClientHandler } from "./client-handler.js";
19
+ import { prepareAgentEnv } from "./prepare-agent-env.js";
20
+ import {
21
+ adapterCommandOf,
22
+ formatResolveFailure,
23
+ resolveAcpAgent,
24
+ } from "./resolve-agent.js";
25
+ import { claudeResumeHint } from "./resume-hint.js";
19
26
  import type { AcpAgentConfig, AcpSessionState } from "./types.js";
20
27
 
21
28
  const log = getLogger("acp:session-manager");
22
29
 
30
+ /**
31
+ * The manager's "unknown session id" error. Thrown whenever an operation
32
+ * references an acpSessionId with no in-memory entry (and, for resume, no
33
+ * persisted history row). Callers (acp_steer tool, /v1/acp/:id/steer route)
34
+ * use `instanceof` checks to map this to their transport's not-found shape.
35
+ */
36
+ export class AcpSessionNotFoundError extends Error {
37
+ constructor(public readonly acpSessionId: string) {
38
+ super(`ACP session "${acpSessionId}" not found`);
39
+ this.name = "AcpSessionNotFoundError";
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Wraps failures from the resume-then-steer phase of `steerOrResume` so
45
+ * transport callers can distinguish them (HTTP 424 with the actionable
46
+ * resume hint) from plain steer failures (404). The message mirrors the
47
+ * underlying error's message; the original error rides on `cause`.
48
+ */
49
+ export class AcpResumeError extends Error {
50
+ constructor(cause: unknown) {
51
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
52
+ this.name = "AcpResumeError";
53
+ }
54
+ }
55
+
23
56
  /** Maximum number of update events kept in a session's ring buffer. */
24
57
  const MAX_BUFFER_EVENTS = 200;
25
58
  /** Maximum aggregate JSON size of a session's ring buffer, in bytes. */
@@ -41,11 +74,22 @@ interface SessionEntry {
41
74
  currentPrompt: Promise<unknown> | null;
42
75
  parentConversationId: string;
43
76
  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`. */
77
+ /** Canonical adapter command for the spawned config (e.g.
78
+ * "claude-agent-acp" even when the adapter runs via `bun x`). Used to
79
+ * gate resume hints to the only adapter (claude-agent-acp) whose CLI
80
+ * accepts `--resume`. */
46
81
  command: string;
47
82
  }
48
83
 
84
+ /**
85
+ * An `acp_session_history` row that passed resumeFromHistory's validation
86
+ * guards: `cwd` (nullable in the schema for pre-resume-support rows) is
87
+ * guaranteed present.
88
+ */
89
+ type ResumableHistoryRow = typeof acpSessionHistory.$inferSelect & {
90
+ cwd: string;
91
+ };
92
+
49
93
  export class AcpSessionManager {
50
94
  private sessions = new Map<string, SessionEntry>();
51
95
  /**
@@ -55,6 +99,25 @@ export class AcpSessionManager {
55
99
  * `acp_session_history` on terminal transition, then cleared.
56
100
  */
57
101
  private eventBuffers = new Map<string, BufferedAcpUpdate[]>();
102
+ /**
103
+ * In-flight resumes by session id, keyed to the promise of the resume's
104
+ * async body. Reserved SYNCHRONOUSLY before the first await so concurrent
105
+ * resumes of the same id cannot both pass the guards (the loser would
106
+ * overwrite the winner's map entry and leak its child process), and so N
107
+ * concurrent resumes of distinct ids cannot exceed maxConcurrent. The
108
+ * entry lives until the resume settles so `steerOrResume` can await a
109
+ * concurrent caller's resume instead of failing the already-active guard.
110
+ * The spawn path needs no such reservation: its check-then-register is
111
+ * synchronous.
112
+ */
113
+ private pendingResumes = new Map<string, Promise<void>>();
114
+
115
+ /**
116
+ * Set by dispose() (the daemon-shutdown path). Resumes that are mid-await
117
+ * when the manager is disposed re-check this flag before spawning a child
118
+ * process nothing would ever kill.
119
+ */
120
+ private disposed = false;
58
121
 
59
122
  constructor(private readonly maxConcurrent: number) {
60
123
  this.cleanupStaleRunningRows();
@@ -91,6 +154,33 @@ export class AcpSessionManager {
91
154
  }
92
155
  }
93
156
 
157
+ /**
158
+ * Ids that must be treated as live: registered sessions plus in-flight
159
+ * resume reservations (deduped, since a resuming session appears in both maps
160
+ * between registration and settle). Delete guards use this so a history
161
+ * row cannot be removed out from under a resume that is still awaiting
162
+ * env preparation; the later terminal upsert would resurrect it.
163
+ */
164
+ getActiveAndPendingIds(): string[] {
165
+ return [
166
+ ...new Set([...this.sessions.keys(), ...this.pendingResumes.keys()]),
167
+ ];
168
+ }
169
+
170
+ /**
171
+ * Concurrency guard shared by spawn() and resumeFromHistory(). Counts
172
+ * both registered sessions and in-flight resume reservations so the cap
173
+ * holds even while a resume is still awaiting prepareAgentEnv.
174
+ */
175
+ private assertCapacity(): void {
176
+ if (this.getActiveAndPendingIds().length >= this.maxConcurrent) {
177
+ throw new Error(
178
+ `ACP concurrency limit reached (max ${this.maxConcurrent}). ` +
179
+ `Close an existing session before spawning a new one.`,
180
+ );
181
+ }
182
+ }
183
+
94
184
  /**
95
185
  * Spawns a new ACP agent session. Returns the generated acpSessionId.
96
186
  *
@@ -105,12 +195,7 @@ export class AcpSessionManager {
105
195
  parentConversationId: string,
106
196
  sendToVellum: (msg: ServerMessage) => void,
107
197
  ): 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
- }
198
+ this.assertCapacity();
114
199
 
115
200
  const acpSessionId = randomUUID();
116
201
  log.info(
@@ -124,40 +209,103 @@ export class AcpSessionManager {
124
209
  "ACP spawn requested",
125
210
  );
126
211
 
212
+ const entry = this.registerSession({
213
+ acpSessionId,
214
+ agentId,
215
+ agentConfig,
216
+ parentConversationId,
217
+ cwd,
218
+ startedAt: Date.now(),
219
+ sendToVellum,
220
+ });
221
+ const { process: agentProcess, state } = entry;
222
+
223
+ try {
224
+ log.info({ acpSessionId, agentId }, "ACP spawning child process");
225
+ agentProcess.spawn(cwd);
226
+ log.info(
227
+ { acpSessionId, agentId },
228
+ "ACP initializing protocol connection",
229
+ );
230
+ await agentProcess.initialize();
231
+ log.info({ acpSessionId, agentId }, "ACP creating session");
232
+ const acpProtocolSessionId = await agentProcess.createSession(cwd);
233
+ state.acpSessionId = acpProtocolSessionId;
234
+ state.status = "running";
235
+ log.info(
236
+ { acpSessionId, agentId, acpProtocolSessionId },
237
+ "ACP session running",
238
+ );
239
+ } catch (err) {
240
+ log.error({ acpSessionId, agentId, err }, "ACP spawn failed");
241
+ // No prompt has fired yet, so no permissions can be pending.
242
+ this.teardownSession(acpSessionId, entry);
243
+ throw err;
244
+ }
245
+
246
+ this.sendSpawnedEvent(acpSessionId, entry);
247
+
248
+ // Fire prompt in the background — don't await
249
+ entry.currentPrompt = this.firePromptInBackground(
250
+ acpSessionId,
251
+ entry,
252
+ state.acpSessionId,
253
+ task,
254
+ );
255
+
256
+ return { acpSessionId, protocolSessionId: state.acpSessionId };
257
+ }
258
+
259
+ /**
260
+ * Wires up the in-memory plumbing shared by spawn() and
261
+ * resumeFromHistory(): the per-session ring buffer, the buffer-mirroring
262
+ * sender, the client handler, the agent process, and the SessionEntry.
263
+ * Registers the entry in the session map (reserving a concurrency slot
264
+ * before any async work) and returns it. Does NOT start the process.
265
+ */
266
+ private registerSession(opts: {
267
+ acpSessionId: string;
268
+ agentId: string;
269
+ agentConfig: AcpAgentConfig;
270
+ parentConversationId: string;
271
+ cwd: string;
272
+ startedAt: number;
273
+ sendToVellum: (msg: ServerMessage) => void;
274
+ }): SessionEntry {
275
+ const { acpSessionId } = opts;
276
+
127
277
  // Initialize the per-session ring buffer before any update can fire.
128
278
  this.eventBuffers.set(acpSessionId, []);
129
279
 
130
280
  // Wrap the sender so every emitted message is mirrored into the buffer
131
281
  // when it's an `acp_session_update`. The wrapper preserves the original
132
- // call semantics it forwards every message unchanged.
282
+ // call semantics: it forwards every message unchanged.
133
283
  const wrappedSend = (msg: ServerMessage) => {
134
284
  if (msg.type === "acp_session_update") {
135
285
  this.appendToBuffer(acpSessionId, msg);
136
286
  }
137
- sendToVellum(msg);
287
+ opts.sendToVellum(msg);
138
288
  };
139
289
 
140
290
  const clientHandler = new VellumAcpClientHandler(
141
291
  acpSessionId,
142
292
  wrappedSend,
143
- parentConversationId,
293
+ opts.parentConversationId,
144
294
  );
145
295
 
146
296
  const agentProcess = new AcpAgentProcess(
147
- agentId,
148
- agentConfig,
297
+ opts.agentId,
298
+ opts.agentConfig,
149
299
  (_agent) => clientHandler,
150
300
  );
151
301
 
152
- // Reserve a slot in the map before any async work to enforce the
153
- // concurrency limit even when multiple spawn() calls race.
154
302
  const state: AcpSessionState = {
155
303
  id: acpSessionId,
156
- agentId,
157
- acpSessionId: "", // placeholder until createSession resolves
158
- parentConversationId,
304
+ agentId: opts.agentId,
305
+ acpSessionId: "", // placeholder until createSession/resume resolves
306
+ parentConversationId: opts.parentConversationId,
159
307
  status: "initializing",
160
- startedAt: Date.now(),
308
+ startedAt: opts.startedAt,
161
309
  };
162
310
 
163
311
  const entry: SessionEntry = {
@@ -166,54 +314,208 @@ export class AcpSessionManager {
166
314
  clientHandler,
167
315
  sendToVellum: wrappedSend,
168
316
  currentPrompt: null,
169
- parentConversationId,
170
- cwd,
171
- command: agentConfig.command,
317
+ parentConversationId: opts.parentConversationId,
318
+ cwd: opts.cwd,
319
+ command: adapterCommandOf(opts.agentConfig),
172
320
  };
173
321
 
174
322
  this.sessions.set(acpSessionId, entry);
323
+ return entry;
324
+ }
325
+
326
+ /**
327
+ * Notifies connected clients that a session is live. Shared by spawn()
328
+ * and the resume path (resumed sessions reuse the spawned event so
329
+ * clients render them; a dedicated acp_session_resumed event is a
330
+ * possible follow-up, not in scope here).
331
+ */
332
+ private sendSpawnedEvent(acpSessionId: string, entry: SessionEntry): void {
333
+ entry.sendToVellum({
334
+ type: "acp_session_spawned",
335
+ acpSessionId,
336
+ agent: entry.state.agentId,
337
+ parentConversationId: entry.parentConversationId,
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Resumes a terminal-state session from its persisted
343
+ * `acp_session_history` row, reattaching to the agent's stored
344
+ * conversation via ACP `session/resume` (preferred: no history replay) or
345
+ * `session/load` (replayed history is suppressed; see
346
+ * VellumAcpClientHandler.beginReplaySuppression).
347
+ *
348
+ * The resumed session reuses the original vellum session id,
349
+ * parentConversationId, and startedAt, and re-seeds its ring buffer from
350
+ * the persisted event log so the terminal upsert after the resumed run
351
+ * merges new events into the original row instead of losing them.
352
+ *
353
+ * Throws with an actionable message when the row is missing, was recorded
354
+ * before resume support (no cwd), the agent cannot be resolved, or the
355
+ * agent advertises neither resume capability.
356
+ */
357
+ async resumeFromHistory(
358
+ acpSessionId: string,
359
+ sendToVellum: (msg: ServerMessage) => void,
360
+ ): Promise<void> {
361
+ if (
362
+ this.sessions.has(acpSessionId) ||
363
+ this.pendingResumes.has(acpSessionId)
364
+ ) {
365
+ throw new Error(`ACP session "${acpSessionId}" is already active`);
366
+ }
367
+ this.assertCapacity();
368
+
369
+ const row = getDb()
370
+ .select()
371
+ .from(acpSessionHistory)
372
+ .where(eq(acpSessionHistory.id, acpSessionId))
373
+ .get();
374
+ if (!row) {
375
+ throw new AcpSessionNotFoundError(acpSessionId);
376
+ }
377
+ if (!row.cwd) {
378
+ throw new Error(
379
+ `ACP session "${acpSessionId}" was recorded before resume support ` +
380
+ `(no working directory persisted) and cannot be resumed. ` +
381
+ `Spawn a new session instead.`,
382
+ );
383
+ }
384
+ if (!row.acpSessionId) {
385
+ throw new Error(
386
+ `ACP session "${acpSessionId}" has no protocol session id ` +
387
+ `persisted and cannot be resumed. Spawn a new session instead.`,
388
+ );
389
+ }
390
+
391
+ const resolved = resolveAcpAgent(row.agentId);
392
+ if (!resolved.ok) {
393
+ throw new Error(formatResolveFailure(row.agentId, resolved));
394
+ }
395
+
396
+ // Everything up to here is synchronous. Reserve the id + concurrency
397
+ // slot BEFORE the first await so a concurrent resume of the same id
398
+ // (or a spawn racing the cap) fails the guards above instead of
399
+ // double-registering and leaking the first child process. The
400
+ // reservation holds the resume's promise until it settles so
401
+ // steerOrResume can await a concurrent caller's in-flight resume, and
402
+ // so the delete guards see the id as live while the row's terminal
403
+ // status still reflects the previous run.
404
+ const resumePromise = this.performResume(
405
+ acpSessionId,
406
+ row as ResumableHistoryRow,
407
+ resolved.agent,
408
+ sendToVellum,
409
+ );
410
+ this.pendingResumes.set(acpSessionId, resumePromise);
411
+ try {
412
+ await resumePromise;
413
+ } finally {
414
+ this.pendingResumes.delete(acpSessionId);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * The async body of resumeFromHistory, split out so the caller can store
420
+ * its promise in `pendingResumes` synchronously before the first await.
421
+ * All guards and row validation have already passed.
422
+ */
423
+ private async performResume(
424
+ acpSessionId: string,
425
+ row: ResumableHistoryRow,
426
+ agent: AcpAgentConfig,
427
+ sendToVellum: (msg: ServerMessage) => void,
428
+ ): Promise<void> {
429
+ const agentConfig = await prepareAgentEnv(agent);
430
+
431
+ // The daemon may have shut down while prepareAgentEnv was pending.
432
+ // Registering now would spawn a child process on a disposed manager
433
+ // that nothing would ever kill.
434
+ if (this.disposed) {
435
+ throw new Error(
436
+ `ACP session manager is disposed; cannot resume session "${acpSessionId}"`,
437
+ );
438
+ }
439
+
440
+ const entry = this.registerSession({
441
+ acpSessionId,
442
+ agentId: row.agentId,
443
+ agentConfig,
444
+ parentConversationId: row.parentConversationId,
445
+ cwd: row.cwd,
446
+ startedAt: row.startedAt,
447
+ sendToVellum,
448
+ });
449
+
450
+ log.info(
451
+ { acpSessionId, agentId: row.agentId, cwd: row.cwd },
452
+ "ACP resume from history requested",
453
+ );
454
+ const { process: agentProcess, state } = entry;
455
+
456
+ // Re-seed the ring buffer from the persisted event log, routed through
457
+ // appendToBuffer so the count/byte caps still apply. The terminal
458
+ // upsert then persists the merged (old + new) log.
459
+ try {
460
+ const persisted = JSON.parse(row.eventLogJson) as unknown;
461
+ if (Array.isArray(persisted)) {
462
+ for (const update of persisted) {
463
+ this.appendToBuffer(acpSessionId, update as AcpSessionUpdate);
464
+ }
465
+ }
466
+ } catch (err) {
467
+ log.warn(
468
+ { acpSessionId, err },
469
+ "Failed to re-seed ACP event buffer from persisted history",
470
+ );
471
+ }
175
472
 
176
473
  try {
177
- log.info({ acpSessionId, agentId }, "ACP spawning child process");
178
- agentProcess.spawn(cwd);
179
474
  log.info(
180
- { acpSessionId, agentId },
181
- "ACP initializing protocol connection",
475
+ { acpSessionId, agentId: row.agentId },
476
+ "ACP spawning child process for resume",
182
477
  );
478
+ agentProcess.spawn(row.cwd);
183
479
  await agentProcess.initialize();
184
- log.info({ acpSessionId, agentId }, "ACP creating session");
185
- const acpProtocolSessionId = await agentProcess.createSession(cwd);
186
- state.acpSessionId = acpProtocolSessionId;
480
+ if (agentProcess.supportsSessionResume) {
481
+ // session/resume reattaches without replaying history.
482
+ await agentProcess.resumeSession(row.acpSessionId, row.cwd);
483
+ } else if (agentProcess.supportsLoadSession) {
484
+ // session/load replays the full history as session/update
485
+ // notifications before resolving; suppress forwarding so the
486
+ // conversation and ring buffer don't receive duplicates.
487
+ entry.clientHandler.beginReplaySuppression();
488
+ try {
489
+ await agentProcess.loadSession(row.acpSessionId, row.cwd);
490
+ } finally {
491
+ entry.clientHandler.endReplaySuppression();
492
+ }
493
+ } else {
494
+ throw new Error(
495
+ `ACP agent "${row.agentId}" does not support session resume`,
496
+ );
497
+ }
498
+ state.acpSessionId = row.acpSessionId;
187
499
  state.status = "running";
188
500
  log.info(
189
- { acpSessionId, agentId, acpProtocolSessionId },
190
- "ACP session running",
501
+ {
502
+ acpSessionId,
503
+ agentId: row.agentId,
504
+ protocolSessionId: row.acpSessionId,
505
+ },
506
+ "ACP session resumed",
191
507
  );
192
508
  } 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);
509
+ log.error(
510
+ { acpSessionId, agentId: row.agentId, err },
511
+ "ACP resume failed",
512
+ );
513
+ // No prompt has fired yet, so no permissions can be pending.
514
+ this.teardownSession(acpSessionId, entry);
198
515
  throw err;
199
516
  }
200
517
 
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 };
518
+ this.sendSpawnedEvent(acpSessionId, entry);
217
519
  }
218
520
 
219
521
  /**
@@ -225,7 +527,7 @@ export class AcpSessionManager {
225
527
  async steer(acpSessionId: string, instruction: string): Promise<void> {
226
528
  const entry = this.sessions.get(acpSessionId);
227
529
  if (!entry) {
228
- throw new Error(`ACP session "${acpSessionId}" not found`);
530
+ throw new AcpSessionNotFoundError(acpSessionId);
229
531
  }
230
532
 
231
533
  if (entry.state.status !== "running") {
@@ -258,22 +560,123 @@ export class AcpSessionManager {
258
560
  );
259
561
  }
260
562
 
563
+ /**
564
+ * Steers a session, transparently resuming it from persisted history
565
+ * first when it is no longer in memory (it completed, or the daemon
566
+ * restarted). The resume and the instruction prompt are atomic from the
567
+ * caller's perspective: a successfully resumed session immediately gets
568
+ * the instruction fired, so it never sits running-idle with no in-flight
569
+ * prompt (and therefore no teardown owner). If the post-resume steer
570
+ * fails, the freshly resumed session is closed (process killed, terminal
571
+ * row persisted, maps cleared) instead of being leaked.
572
+ *
573
+ * When a concurrent caller's resume of the same id is already in flight,
574
+ * this call awaits that resume and then retries the plain steer, so both
575
+ * callers' instructions land on the single resumed session.
576
+ *
577
+ * Error contract for transport callers (acp_steer tool, steer route):
578
+ * - `AcpSessionNotFoundError`: no in-memory session AND no history row.
579
+ * - `AcpResumeError`: the resume (own or a concurrent caller's awaited
580
+ * one, or the steer immediately after an own resume) failed; the
581
+ * message carries the actionable hint.
582
+ * - any other error: the plain steer on an in-memory session failed.
583
+ */
584
+ async steerOrResume(
585
+ acpSessionId: string,
586
+ instruction: string,
587
+ sendToVellum: (msg: ServerMessage) => void,
588
+ ): Promise<{ resumed: boolean }> {
589
+ try {
590
+ await this.steer(acpSessionId, instruction);
591
+ return { resumed: false };
592
+ } catch (err) {
593
+ // Fall through to the in-flight-resume handling both when the session
594
+ // is entirely unknown and when a concurrent resume has already
595
+ // registered its entry but is still initializing: steer rejects with a
596
+ // plain not-running error in that window, yet the resume reservation
597
+ // is live and the retry below will land once it settles.
598
+ if (
599
+ !(err instanceof AcpSessionNotFoundError) &&
600
+ !this.pendingResumes.has(acpSessionId)
601
+ ) {
602
+ throw err;
603
+ }
604
+ }
605
+
606
+ // Another caller's resume of this id may already be in flight (the
607
+ // session is not in memory yet, but its slot is reserved). Await that
608
+ // resume and retry the plain steer once instead of failing
609
+ // resumeFromHistory's already-active guard, which would surface a
610
+ // misleading resume error and drop this instruction.
611
+ const inFlightResume = this.pendingResumes.get(acpSessionId);
612
+ if (inFlightResume) {
613
+ try {
614
+ await inFlightResume;
615
+ } catch (err) {
616
+ throw new AcpResumeError(err);
617
+ }
618
+ // The resumed session is owned by the concurrent caller (its own
619
+ // post-resume steer handles teardown on failure), so a failure here
620
+ // propagates as a plain steer error without closing the session.
621
+ await this.steer(acpSessionId, instruction);
622
+ return { resumed: true };
623
+ }
624
+
625
+ try {
626
+ await this.resumeFromHistory(acpSessionId, sendToVellum);
627
+ } catch (err) {
628
+ // A missing history row keeps its not-found shape; everything else
629
+ // (legacy row without cwd, resolver failure, capability missing)
630
+ // is a resume failure with an actionable message.
631
+ if (err instanceof AcpSessionNotFoundError) throw err;
632
+ throw new AcpResumeError(err);
633
+ }
634
+
635
+ try {
636
+ await this.steer(acpSessionId, instruction);
637
+ } catch (err) {
638
+ // Tear down the just-resumed session rather than leaving it
639
+ // running-idle with no prompt handler to own its cleanup.
640
+ try {
641
+ this.close(acpSessionId);
642
+ } catch (closeErr) {
643
+ log.warn(
644
+ { acpSessionId, err: closeErr },
645
+ "Failed to close ACP session after post-resume steer failure",
646
+ );
647
+ }
648
+ throw new AcpResumeError(err);
649
+ }
650
+ return { resumed: true };
651
+ }
652
+
261
653
  /**
262
654
  * Cancels an ongoing prompt in the specified session.
263
655
  *
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".
656
+ * When a prompt is in flight, its `prompt()` call rejects in response
657
+ * and the catch handler in `firePromptInBackground` performs the
658
+ * terminal persistence + teardown; we just flip the status here so that
659
+ * handler preserves "cancelled" instead of overwriting with "failed".
660
+ *
661
+ * When NO prompt is in flight there is no handler to own cleanup, so
662
+ * cancel persists and tears down the session itself. (Sessions normally
663
+ * always have a prompt in flight, but a cancel can race the window in
664
+ * steer() between clearing the old prompt and firing the new one.)
268
665
  */
269
666
  async cancel(acpSessionId: string): Promise<void> {
270
667
  const entry = this.sessions.get(acpSessionId);
271
668
  if (!entry) {
272
- throw new Error(`ACP session "${acpSessionId}" not found`);
669
+ throw new AcpSessionNotFoundError(acpSessionId);
273
670
  }
274
671
  await entry.process.cancel(entry.state.acpSessionId);
275
672
  entry.state.status = "cancelled";
276
673
  entry.state.completedAt = Date.now();
674
+ // Re-check the map after the await: the in-flight prompt's handler may
675
+ // have already torn the session down while process.cancel was pending.
676
+ if (!entry.currentPrompt && this.sessions.get(acpSessionId) === entry) {
677
+ this.persistTerminal(acpSessionId, entry);
678
+ this.teardownSession(acpSessionId, entry);
679
+ }
277
680
  }
278
681
 
279
682
  /**
@@ -288,7 +691,7 @@ export class AcpSessionManager {
288
691
  close(acpSessionId: string): void {
289
692
  const entry = this.sessions.get(acpSessionId);
290
693
  if (!entry) {
291
- throw new Error(`ACP session "${acpSessionId}" not found`);
694
+ throw new AcpSessionNotFoundError(acpSessionId);
292
695
  }
293
696
  if (
294
697
  entry.state.status === "running" ||
@@ -335,7 +738,7 @@ export class AcpSessionManager {
335
738
  if (acpSessionId) {
336
739
  const entry = this.sessions.get(acpSessionId);
337
740
  if (!entry) {
338
- throw new Error(`ACP session "${acpSessionId}" not found`);
741
+ throw new AcpSessionNotFoundError(acpSessionId);
339
742
  }
340
743
  return entry.state;
341
744
  }
@@ -368,15 +771,18 @@ export class AcpSessionManager {
368
771
 
369
772
  /**
370
773
  * 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.
774
+ * `acp_session_history`, then frees the buffer entry. Upserts on id:
775
+ * resumed runs reuse the original vellum session id, so their terminal
776
+ * write must update the existing row (status, event log, etc.) instead of
777
+ * being silently skipped. Best-effort: a DB failure is logged but does
778
+ * not propagate, since the session has already reached a terminal state
779
+ * and clients have been notified.
374
780
  */
375
781
  private persistTerminal(acpSessionId: string, entry: SessionEntry): void {
376
782
  const buffer = this.eventBuffers.get(acpSessionId) ?? [];
377
783
  // Serialize only the wire-shaped updates — drop the byte-size accounting
378
784
  // metadata so persisted rows match the protocol shape clients receive.
379
- const wireUpdates = buffer.map((buffered) => buffered.update);
785
+ const eventLogJson = JSON.stringify(buffer.map((b) => b.update));
380
786
  try {
381
787
  getDb()
382
788
  .insert(acpSessionHistory)
@@ -390,9 +796,20 @@ export class AcpSessionManager {
390
796
  status: entry.state.status,
391
797
  stopReason: entry.state.stopReason ?? null,
392
798
  error: entry.state.error ?? null,
393
- eventLogJson: JSON.stringify(wireUpdates),
799
+ eventLogJson,
800
+ cwd: entry.cwd,
801
+ })
802
+ .onConflictDoUpdate({
803
+ target: acpSessionHistory.id,
804
+ set: {
805
+ status: entry.state.status,
806
+ completedAt: entry.state.completedAt ?? null,
807
+ stopReason: entry.state.stopReason ?? null,
808
+ error: entry.state.error ?? null,
809
+ eventLogJson,
810
+ cwd: entry.cwd,
811
+ },
394
812
  })
395
- .onConflictDoNothing()
396
813
  .run();
397
814
  } catch (err) {
398
815
  log.error(
@@ -451,10 +868,12 @@ export class AcpSessionManager {
451
868
  const agentLabel = current.state.agentId;
452
869
  const responseText = current.clientHandler.responseText;
453
870
  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
- : "";
871
+ const hint = claudeResumeHint(
872
+ current.command,
873
+ current.cwd,
874
+ sessionId,
875
+ );
876
+ const resumeHint = hint ? `\n\n${hint}` : "";
458
877
  const notifyMessage = `[ACP agent "${agentLabel}" completed]\n\n${responseText}${resumeHint}`;
459
878
  const parentConversation = findConversation(
460
879
  current.parentConversationId,
@@ -517,9 +936,12 @@ export class AcpSessionManager {
517
936
  }
518
937
 
519
938
  /**
520
- * Kills all processes on shutdown.
939
+ * Kills all processes on shutdown. Also flags the manager as disposed so
940
+ * resumes that are mid-await when shutdown happens abort before spawning
941
+ * a child process nothing would ever kill.
521
942
  */
522
943
  dispose(): void {
944
+ this.disposed = true;
523
945
  this.closeAll();
524
946
  }
525
947
  }