@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

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 (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { eq, inArray } from "drizzle-orm";
8
+ import { inArray } from "drizzle-orm";
9
9
 
10
- import { findConversation } from "../daemon/conversation-registry.js";
10
+ import { findConversation } from "../daemon/conversation-store.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,43 +16,10 @@ 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";
26
19
  import type { AcpAgentConfig, AcpSessionState } from "./types.js";
27
20
 
28
21
  const log = getLogger("acp:session-manager");
29
22
 
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
-
56
23
  /** Maximum number of update events kept in a session's ring buffer. */
57
24
  const MAX_BUFFER_EVENTS = 200;
58
25
  /** Maximum aggregate JSON size of a session's ring buffer, in bytes. */
@@ -74,22 +41,11 @@ interface SessionEntry {
74
41
  currentPrompt: Promise<unknown> | null;
75
42
  parentConversationId: string;
76
43
  cwd: string;
77
- /** Canonical adapter command for the spawned config (e.g.
78
- * "claude-agent-acp" even when the adapter runs via `bun x`). Used to
79
- * gate resume hints to the only adapter (claude-agent-acp) whose CLI
80
- * accepts `--resume`. */
44
+ /** The adapter binary that was spawned. Used to gate resume hints to
45
+ * the only adapter (claude-agent-acp) whose CLI accepts `--resume`. */
81
46
  command: string;
82
47
  }
83
48
 
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
-
93
49
  export class AcpSessionManager {
94
50
  private sessions = new Map<string, SessionEntry>();
95
51
  /**
@@ -99,25 +55,6 @@ export class AcpSessionManager {
99
55
  * `acp_session_history` on terminal transition, then cleared.
100
56
  */
101
57
  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;
121
58
 
122
59
  constructor(private readonly maxConcurrent: number) {
123
60
  this.cleanupStaleRunningRows();
@@ -154,33 +91,6 @@ export class AcpSessionManager {
154
91
  }
155
92
  }
156
93
 
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
-
184
94
  /**
185
95
  * Spawns a new ACP agent session. Returns the generated acpSessionId.
186
96
  *
@@ -195,7 +105,12 @@ export class AcpSessionManager {
195
105
  parentConversationId: string,
196
106
  sendToVellum: (msg: ServerMessage) => void,
197
107
  ): Promise<{ acpSessionId: string; protocolSessionId: string }> {
198
- this.assertCapacity();
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
+ }
199
114
 
200
115
  const acpSessionId = randomUUID();
201
116
  log.info(
@@ -209,103 +124,40 @@ export class AcpSessionManager {
209
124
  "ACP spawn requested",
210
125
  );
211
126
 
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
-
277
127
  // Initialize the per-session ring buffer before any update can fire.
278
128
  this.eventBuffers.set(acpSessionId, []);
279
129
 
280
130
  // Wrap the sender so every emitted message is mirrored into the buffer
281
131
  // when it's an `acp_session_update`. The wrapper preserves the original
282
- // call semantics: it forwards every message unchanged.
132
+ // call semantics it forwards every message unchanged.
283
133
  const wrappedSend = (msg: ServerMessage) => {
284
134
  if (msg.type === "acp_session_update") {
285
135
  this.appendToBuffer(acpSessionId, msg);
286
136
  }
287
- opts.sendToVellum(msg);
137
+ sendToVellum(msg);
288
138
  };
289
139
 
290
140
  const clientHandler = new VellumAcpClientHandler(
291
141
  acpSessionId,
292
142
  wrappedSend,
293
- opts.parentConversationId,
143
+ parentConversationId,
294
144
  );
295
145
 
296
146
  const agentProcess = new AcpAgentProcess(
297
- opts.agentId,
298
- opts.agentConfig,
147
+ agentId,
148
+ agentConfig,
299
149
  (_agent) => clientHandler,
300
150
  );
301
151
 
152
+ // Reserve a slot in the map before any async work to enforce the
153
+ // concurrency limit even when multiple spawn() calls race.
302
154
  const state: AcpSessionState = {
303
155
  id: acpSessionId,
304
- agentId: opts.agentId,
305
- acpSessionId: "", // placeholder until createSession/resume resolves
306
- parentConversationId: opts.parentConversationId,
156
+ agentId,
157
+ acpSessionId: "", // placeholder until createSession resolves
158
+ parentConversationId,
307
159
  status: "initializing",
308
- startedAt: opts.startedAt,
160
+ startedAt: Date.now(),
309
161
  };
310
162
 
311
163
  const entry: SessionEntry = {
@@ -314,208 +166,54 @@ export class AcpSessionManager {
314
166
  clientHandler,
315
167
  sendToVellum: wrappedSend,
316
168
  currentPrompt: null,
317
- parentConversationId: opts.parentConversationId,
318
- cwd: opts.cwd,
319
- command: adapterCommandOf(opts.agentConfig),
169
+ parentConversationId,
170
+ cwd,
171
+ command: agentConfig.command,
320
172
  };
321
173
 
322
174
  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
- }
472
175
 
473
176
  try {
177
+ log.info({ acpSessionId, agentId }, "ACP spawning child process");
178
+ agentProcess.spawn(cwd);
474
179
  log.info(
475
- { acpSessionId, agentId: row.agentId },
476
- "ACP spawning child process for resume",
180
+ { acpSessionId, agentId },
181
+ "ACP initializing protocol connection",
477
182
  );
478
- agentProcess.spawn(row.cwd);
479
183
  await agentProcess.initialize();
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;
184
+ log.info({ acpSessionId, agentId }, "ACP creating session");
185
+ const acpProtocolSessionId = await agentProcess.createSession(cwd);
186
+ state.acpSessionId = acpProtocolSessionId;
499
187
  state.status = "running";
500
188
  log.info(
501
- {
502
- acpSessionId,
503
- agentId: row.agentId,
504
- protocolSessionId: row.acpSessionId,
505
- },
506
- "ACP session resumed",
189
+ { acpSessionId, agentId, acpProtocolSessionId },
190
+ "ACP session running",
507
191
  );
508
192
  } catch (err) {
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);
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);
515
198
  throw err;
516
199
  }
517
200
 
518
- this.sendSpawnedEvent(acpSessionId, entry);
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 };
519
217
  }
520
218
 
521
219
  /**
@@ -527,7 +225,7 @@ export class AcpSessionManager {
527
225
  async steer(acpSessionId: string, instruction: string): Promise<void> {
528
226
  const entry = this.sessions.get(acpSessionId);
529
227
  if (!entry) {
530
- throw new AcpSessionNotFoundError(acpSessionId);
228
+ throw new Error(`ACP session "${acpSessionId}" not found`);
531
229
  }
532
230
 
533
231
  if (entry.state.status !== "running") {
@@ -560,123 +258,22 @@ export class AcpSessionManager {
560
258
  );
561
259
  }
562
260
 
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
-
653
261
  /**
654
262
  * Cancels an ongoing prompt in the specified session.
655
263
  *
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.)
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".
665
268
  */
666
269
  async cancel(acpSessionId: string): Promise<void> {
667
270
  const entry = this.sessions.get(acpSessionId);
668
271
  if (!entry) {
669
- throw new AcpSessionNotFoundError(acpSessionId);
272
+ throw new Error(`ACP session "${acpSessionId}" not found`);
670
273
  }
671
274
  await entry.process.cancel(entry.state.acpSessionId);
672
275
  entry.state.status = "cancelled";
673
276
  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
- }
680
277
  }
681
278
 
682
279
  /**
@@ -691,7 +288,7 @@ export class AcpSessionManager {
691
288
  close(acpSessionId: string): void {
692
289
  const entry = this.sessions.get(acpSessionId);
693
290
  if (!entry) {
694
- throw new AcpSessionNotFoundError(acpSessionId);
291
+ throw new Error(`ACP session "${acpSessionId}" not found`);
695
292
  }
696
293
  if (
697
294
  entry.state.status === "running" ||
@@ -738,7 +335,7 @@ export class AcpSessionManager {
738
335
  if (acpSessionId) {
739
336
  const entry = this.sessions.get(acpSessionId);
740
337
  if (!entry) {
741
- throw new AcpSessionNotFoundError(acpSessionId);
338
+ throw new Error(`ACP session "${acpSessionId}" not found`);
742
339
  }
743
340
  return entry.state;
744
341
  }
@@ -771,18 +368,15 @@ export class AcpSessionManager {
771
368
 
772
369
  /**
773
370
  * Persists the session's final state + buffered event log to
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.
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.
780
374
  */
781
375
  private persistTerminal(acpSessionId: string, entry: SessionEntry): void {
782
376
  const buffer = this.eventBuffers.get(acpSessionId) ?? [];
783
377
  // Serialize only the wire-shaped updates — drop the byte-size accounting
784
378
  // metadata so persisted rows match the protocol shape clients receive.
785
- const eventLogJson = JSON.stringify(buffer.map((b) => b.update));
379
+ const wireUpdates = buffer.map((buffered) => buffered.update);
786
380
  try {
787
381
  getDb()
788
382
  .insert(acpSessionHistory)
@@ -796,20 +390,9 @@ export class AcpSessionManager {
796
390
  status: entry.state.status,
797
391
  stopReason: entry.state.stopReason ?? null,
798
392
  error: entry.state.error ?? null,
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
- },
393
+ eventLogJson: JSON.stringify(wireUpdates),
812
394
  })
395
+ .onConflictDoNothing()
813
396
  .run();
814
397
  } catch (err) {
815
398
  log.error(
@@ -868,12 +451,10 @@ export class AcpSessionManager {
868
451
  const agentLabel = current.state.agentId;
869
452
  const responseText = current.clientHandler.responseText;
870
453
  const sessionId = current.state.acpSessionId;
871
- const hint = claudeResumeHint(
872
- current.command,
873
- current.cwd,
874
- sessionId,
875
- );
876
- const resumeHint = hint ? `\n\n${hint}` : "";
454
+ const resumeHint =
455
+ current.command === "claude-agent-acp"
456
+ ? `\n\nTo resume: cd ${current.cwd} && claude --resume ${sessionId}`
457
+ : "";
877
458
  const notifyMessage = `[ACP agent "${agentLabel}" completed]\n\n${responseText}${resumeHint}`;
878
459
  const parentConversation = findConversation(
879
460
  current.parentConversationId,
@@ -936,12 +517,9 @@ export class AcpSessionManager {
936
517
  }
937
518
 
938
519
  /**
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.
520
+ * Kills all processes on shutdown.
942
521
  */
943
522
  dispose(): void {
944
- this.disposed = true;
945
523
  this.closeAll();
946
524
  }
947
525
  }