@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -6,6 +6,7 @@
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { basename } from "node:path";
8
8
 
9
+ import type { SessionConfigOption } from "@agentclientprotocol/sdk";
9
10
  import { eq, inArray } from "drizzle-orm";
10
11
 
11
12
  import type { AcpSessionUpdateEvent } from "../api/events/acp-session-update.js";
@@ -26,17 +27,27 @@ import { AcpAgentProcess } from "./agent-process.js";
26
27
  import {
27
28
  ACP_AUTH_RECOVERY_GUIDANCE,
28
29
  ACP_CLAUDE_AUTH_REQUIRED_CODE,
30
+ AcpAuthRequiredError,
29
31
  CLAUDE_ACP_COMMAND,
30
32
  isAcpAuthRequired,
31
33
  isClaudeAuthFailureMessage,
34
+ requestErrorReason,
32
35
  } from "./auth-required.js";
33
36
  import { resolveAgentWithAutoInstall } from "./auto-install.js";
34
37
  import { VellumAcpClientHandler } from "./client-handler.js";
35
38
  import { deriveFailureError } from "./failure-error.js";
39
+ import type { AcpModelInfo } from "./model-config.js";
40
+ import { deriveModelInfo, resolveAcpModel } from "./model-config.js";
36
41
  import { prepareAgentEnv } from "./prepare-agent-env.js";
37
42
  import { formatResolveFailure } from "./resolve-agent.js";
38
43
  import { claudeResumeHint } from "./resume-hint.js";
39
- import type { AcpAgentConfig, AcpSessionState } from "./types.js";
44
+ import {
45
+ ACP_LIVE_STATUSES,
46
+ type AcpAgentConfig,
47
+ AcpConfigOptionRefusedError,
48
+ type AcpSessionState,
49
+ isLiveAcpStatus,
50
+ } from "./types.js";
40
51
 
41
52
  const log = getLogger("acp:session-manager");
42
53
 
@@ -46,7 +57,9 @@ const log = getLogger("acp:session-manager");
46
57
  * and the marker promises a repair only the Connect Claude flow can perform.
47
58
  * Checks both auth-failure shapes (see `auth-required.ts`) against both the
48
59
  * raw rejection and the derived failure message, since `deriveFailureError`
49
- * may replace one with the other.
60
+ * may replace one with the other. The raw side reads both the rejection's own
61
+ * message and its decoded reason, because an adapter's words travel in either
62
+ * depending on how it raised the error.
50
63
  */
51
64
  function claudeAuthRequiredCode(
52
65
  err: unknown,
@@ -59,6 +72,7 @@ function claudeAuthRequiredCode(
59
72
  const rawMessage = err instanceof Error ? err.message : String(err);
60
73
  return isAcpAuthRequired(err) ||
61
74
  isClaudeAuthFailureMessage(rawMessage) ||
75
+ isClaudeAuthFailureMessage(requestErrorReason(err)) ||
62
76
  isClaudeAuthFailureMessage(failureMessage)
63
77
  ? ACP_CLAUDE_AUTH_REQUIRED_CODE
64
78
  : undefined;
@@ -125,6 +139,28 @@ interface SessionEntry {
125
139
  * rather than needing a sweep to retire it. Absent for agents that use no
126
140
  * Claude credential. */
127
141
  credentialDigest?: string;
142
+ /** Config id of the adapter's model selector, absent when it advertises
143
+ * none. Both the id to write a model back through and the flag that says
144
+ * this session has a model to publish at all. */
145
+ modelConfigId?: string;
146
+ }
147
+
148
+ /** What a spawn or resume pin did about the model it was asked for. */
149
+ interface ModelPinResult {
150
+ /** False when the session is not on the model asked for: the adapter
151
+ * refused it, or advertises no selector at all. */
152
+ applied: boolean;
153
+ /** Message to relay, present only when the caller named the model. */
154
+ warning?: string;
155
+ }
156
+
157
+ /**
158
+ * The sentence a caller who named a model gets when the session has no model
159
+ * selector to put it through, whether the adapter never advertised one or its
160
+ * answer to the pin dropped the one it had.
161
+ */
162
+ function noModelSelectionWarning(agentId: string): string {
163
+ return `Agent "${agentId}" does not support model selection, so the session is running on the agent's own model.`;
128
164
  }
129
165
 
130
166
  /**
@@ -149,6 +185,10 @@ export interface AcpCancellationOptions {
149
185
 
150
186
  export class AcpSessionManager {
151
187
  private sessions = new Map<string, SessionEntry>();
188
+ /** Scopes process-local model revisions across assistant restarts. */
189
+ private readonly modelRevisionEpoch = randomUUID();
190
+ /** Orders model snapshots within this assistant process. */
191
+ private modelRevision = 0;
152
192
  /**
153
193
  * Per-session ring buffer of wire-shaped update events forwarded to
154
194
  * clients. Bounded by event count and aggregate JSON byte size; oldest
@@ -201,7 +241,7 @@ export class AcpSessionManager {
201
241
  stopReason: "daemon_restarted",
202
242
  completedAt: Date.now(),
203
243
  })
204
- .where(inArray(acpSessionHistory.status, ["running", "initializing"]))
244
+ .where(inArray(acpSessionHistory.status, [...ACP_LIVE_STATUSES]))
205
245
  .run();
206
246
  } catch (err) {
207
247
  log.error(
@@ -243,6 +283,12 @@ export class AcpSessionManager {
243
283
  *
244
284
  * The prompt is fired in the background — results stream via sessionUpdate
245
285
  * callbacks and completion/error messages are sent when the prompt finishes.
286
+ *
287
+ * `requestedModel` is the normalized explicit request this spawn acted on.
288
+ * `effectiveModel` is the model the adapter reports for the live session.
289
+ * `modelWarning` comes back when the adapter refused the model the session
290
+ * was asked for: the caller relays the reason rather than treating the spawn
291
+ * as failed.
246
292
  */
247
293
  async spawn(
248
294
  agentId: string,
@@ -251,11 +297,17 @@ export class AcpSessionManager {
251
297
  cwd: string,
252
298
  parentConversationId: string,
253
299
  sendToVellum: (msg: AssistantEvent) => void,
254
- parentToolUseId?: string,
255
- opts?: AcpCancellationOptions,
256
- ): Promise<{ acpSessionId: string; protocolSessionId: string }> {
300
+ options?: { parentToolUseId?: string; model?: string },
301
+ cancellation?: AcpCancellationOptions,
302
+ ): Promise<{
303
+ acpSessionId: string;
304
+ protocolSessionId: string;
305
+ requestedModel?: string;
306
+ effectiveModel?: string;
307
+ modelWarning?: string;
308
+ }> {
257
309
  this.assertCapacity();
258
- opts?.signal?.throwIfAborted();
310
+ cancellation?.signal?.throwIfAborted();
259
311
 
260
312
  const acpSessionId = randomUUID();
261
313
  log.info(
@@ -277,11 +329,20 @@ export class AcpSessionManager {
277
329
  cwd,
278
330
  startedAt: Date.now(),
279
331
  sendToVellum,
280
- parentToolUseId,
332
+ parentToolUseId: options?.parentToolUseId,
281
333
  task,
282
334
  });
283
335
  const { process: agentProcess, state } = entry;
284
336
 
337
+ const requestedModel = options?.model?.trim() || undefined;
338
+ // Resolved before the adapter is asked for anything, so the ladder is
339
+ // walked once against the config the spawn was made under.
340
+ const resolvedModel = resolveAcpModel({
341
+ requestedModel,
342
+ agentModel: agentConfig.model,
343
+ });
344
+
345
+ let configOptions: SessionConfigOption[] = [];
285
346
  try {
286
347
  log.info({ acpSessionId, agentId }, "ACP spawning child process");
287
348
  agentProcess.spawn(cwd);
@@ -291,7 +352,9 @@ export class AcpSessionManager {
291
352
  );
292
353
  await agentProcess.initialize();
293
354
  log.info({ acpSessionId, agentId }, "ACP creating session");
294
- const acpProtocolSessionId = await agentProcess.createSession(cwd);
355
+ const created = await agentProcess.createSession(cwd);
356
+ const acpProtocolSessionId = created.sessionId;
357
+ configOptions = created.configOptions;
295
358
  state.acpSessionId = acpProtocolSessionId;
296
359
  state.status = "running";
297
360
  log.info(
@@ -317,16 +380,47 @@ export class AcpSessionManager {
317
380
  // awaits, and the child process is already running. A turn stopped in that
318
381
  // window must not be told a session started, nor have the agent handed the
319
382
  // task, so the process is torn down and the abort let out instead.
320
- if (opts?.signal?.aborted) {
383
+ if (cancellation?.signal?.aborted) {
321
384
  log.info(
322
385
  { acpSessionId, agentId },
323
386
  "ACP spawn cancelled during setup; tearing the session down",
324
387
  );
325
388
  this.teardownSession(acpSessionId, entry);
326
- opts.signal.throwIfAborted();
389
+ cancellation.signal.throwIfAborted();
390
+ }
391
+
392
+ let modelWarning: string | undefined;
393
+ try {
394
+ ({ warning: modelWarning } = await this.pinSessionModel(
395
+ entry,
396
+ configOptions,
397
+ requestedModel,
398
+ resolvedModel,
399
+ ));
400
+ } catch (err) {
401
+ log.error(
402
+ { acpSessionId, agentId, err },
403
+ "ACP spawn failed while applying the model",
404
+ );
405
+ const failure = this.classifyPinFailure(err, agentId, agentConfig);
406
+ // No prompt has fired yet, so no permissions can be pending.
407
+ this.teardownSession(acpSessionId, entry);
408
+ throw failure;
409
+ }
410
+
411
+ // Recheck: the model pin is an await too, and everything below it either
412
+ // announces the session or hands the agent the task.
413
+ if (cancellation?.signal?.aborted) {
414
+ log.info(
415
+ { acpSessionId, agentId },
416
+ "ACP spawn cancelled during the model pin; tearing the session down",
417
+ );
418
+ this.teardownSession(acpSessionId, entry);
419
+ cancellation.signal.throwIfAborted();
327
420
  }
328
421
 
329
422
  this.sendSpawnedEvent(acpSessionId, entry);
423
+ this.sendModelEvent(acpSessionId, entry);
330
424
 
331
425
  // Fire prompt in the background, do not await
332
426
  entry.currentPrompt = this.firePromptInBackground(
@@ -336,7 +430,179 @@ export class AcpSessionManager {
336
430
  task,
337
431
  );
338
432
 
339
- return { acpSessionId, protocolSessionId: state.acpSessionId };
433
+ return {
434
+ acpSessionId,
435
+ protocolSessionId: state.acpSessionId,
436
+ requestedModel,
437
+ effectiveModel: state.model,
438
+ ...(modelWarning ? { modelWarning } : {}),
439
+ };
440
+ }
441
+
442
+ /** Records what a config-option set says about the session's model. */
443
+ private applyModelInfo(
444
+ entry: SessionEntry,
445
+ configOptions: SessionConfigOption[],
446
+ ): void {
447
+ this.recordModelInfo(entry, deriveModelInfo(configOptions));
448
+ }
449
+
450
+ /**
451
+ * Records the model state a `session/new`, `session/load`, or
452
+ * `session/resume` response reports. The response is authoritative when it
453
+ * names a selector, or when nothing has announced one yet; otherwise the
454
+ * selector a `config_option_update` announced while the call was open
455
+ * stands, because an omitted optional field is not a removal.
456
+ */
457
+ private applyOpeningModelInfo(
458
+ entry: SessionEntry,
459
+ configOptions: SessionConfigOption[],
460
+ ): void {
461
+ const info = deriveModelInfo(configOptions);
462
+ if (info.modelConfigId || entry.modelConfigId === undefined) {
463
+ this.recordModelInfo(entry, info);
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Writes what the adapter reports onto the entry. `modelConfigId` doubles
469
+ * as the "this adapter has a model selector" flag, so state is left
470
+ * untouched when there is none: an adapter that reports no selector cannot
471
+ * wipe a model another response already named.
472
+ */
473
+ private recordModelInfo(entry: SessionEntry, info: AcpModelInfo): void {
474
+ entry.modelConfigId = info.modelConfigId;
475
+ if (info.modelConfigId) {
476
+ entry.state.model = info.model;
477
+ entry.state.availableModels = info.availableModels;
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Records what the opening response says about the model, then puts the
483
+ * session on `resolvedModel` if that is somewhere else. The value crosses
484
+ * the wire unvalidated: the adapter's own resolver accepts aliases and full
485
+ * ids and rejects everything else, and duplicating that judgement here
486
+ * would refuse values it would have taken. A refusal leaves the run on the
487
+ * adapter's default and comes back as the message to relay, because a
488
+ * session that is live and working is worth more than one that never
489
+ * started over a model name.
490
+ *
491
+ * An answer that carries no model selector is not a pin that landed: the
492
+ * session is left with no model rather than one only the superseded
493
+ * response ever named, and the empty picker goes out so no client is left
494
+ * holding a selector the answer retired.
495
+ *
496
+ * A caller who named the model is owed the news when it was not applied,
497
+ * whether the adapter has no selector or refused the value; an inherited
498
+ * rung (the per-agent config model) is only logged, so a value the caller
499
+ * never named does not surface as a warning on every spawn.
500
+ */
501
+ /**
502
+ * A model pin that fails for authentication is the same failure a session
503
+ * that could not open: Claude refused the configured credential, so it is
504
+ * written down before teardown and surfaced in the shape the spawn boundary
505
+ * recognises, whether the adapter answered with a structured
506
+ * `auth_required` or with Claude's message-shaped 401.
507
+ */
508
+ private classifyPinFailure(
509
+ err: unknown,
510
+ agentId: string,
511
+ agentConfig: { command: string; credentialDigest?: string },
512
+ ): unknown {
513
+ const message = requestErrorReason(err);
514
+ const authCode = claudeAuthRequiredCode(err, message, {
515
+ command: basename(agentConfig.command),
516
+ });
517
+ if (authCode === undefined && !isAcpAuthRequired(err)) {
518
+ return err;
519
+ }
520
+ noteClaudeTokenRefused(agentConfig.credentialDigest, Date.now());
521
+ return isAcpAuthRequired(err)
522
+ ? err
523
+ : new AcpAuthRequiredError(agentId, message);
524
+ }
525
+
526
+ private async pinSessionModel(
527
+ entry: SessionEntry,
528
+ configOptions: SessionConfigOption[],
529
+ requestedModel: string | undefined,
530
+ resolvedModel: string | undefined,
531
+ options?: { keepReportedModel?: boolean },
532
+ ): Promise<ModelPinResult> {
533
+ const { state } = entry;
534
+ this.applyOpeningModelInfo(entry, configOptions);
535
+
536
+ // A resume reattaches a session the adapter restored from its own
537
+ // transcript, so any model it has named by now is the model the run stays
538
+ // on, whether the opening reply carried it or a replayed
539
+ // `config_option_update` did.
540
+ if (options?.keepReportedModel && state.model) {
541
+ log.info(
542
+ { acpSessionId: state.id, agentId: state.agentId, model: state.model },
543
+ "ACP agent reported the resumed session's model; leaving it there",
544
+ );
545
+ return { applied: true };
546
+ }
547
+
548
+ if (!resolvedModel || resolvedModel === state.model) {
549
+ return { applied: true };
550
+ }
551
+ if (!entry.modelConfigId) {
552
+ log.info(
553
+ { acpSessionId: state.id, agentId: state.agentId, resolvedModel },
554
+ "ACP agent advertises no model selector; running on its own model",
555
+ );
556
+ return {
557
+ applied: false,
558
+ ...(requestedModel
559
+ ? { warning: noModelSelectionWarning(state.agentId) }
560
+ : {}),
561
+ };
562
+ }
563
+
564
+ try {
565
+ const refreshed = await entry.process.setConfigOption(
566
+ state.acpSessionId,
567
+ entry.modelConfigId,
568
+ resolvedModel,
569
+ );
570
+ this.applyModelInfo(entry, refreshed);
571
+ if (!entry.modelConfigId) {
572
+ // Published, not just recorded: a `config_option_update` that arrived
573
+ // while the round trip was open has already put its selector in front
574
+ // of clients, and a session no client holds yet drops the frame.
575
+ this.clearVanishedModelSelector(state.id, entry);
576
+ log.warn(
577
+ { acpSessionId: state.id, agentId: state.agentId, resolvedModel },
578
+ "ACP agent dropped its model selector while applying the model; running on its own model",
579
+ );
580
+ return {
581
+ applied: false,
582
+ ...(requestedModel
583
+ ? { warning: noModelSelectionWarning(state.agentId) }
584
+ : {}),
585
+ };
586
+ }
587
+ return { applied: true };
588
+ } catch (err) {
589
+ // Only the adapter's own answer is a refusal. A closed connection, an
590
+ // exited process, or a failed authentication is the caller's failure
591
+ // to tear down, not a model to fall back from.
592
+ if (!(err instanceof AcpConfigOptionRefusedError)) {
593
+ throw err;
594
+ }
595
+ log.warn(
596
+ { acpSessionId: state.id, agentId: state.agentId, resolvedModel, err },
597
+ "ACP agent refused the requested model; running on its own model",
598
+ );
599
+ return {
600
+ applied: false,
601
+ ...(requestedModel
602
+ ? { warning: err instanceof Error ? err.message : String(err) }
603
+ : {}),
604
+ };
605
+ }
340
606
  }
341
607
 
342
608
  /**
@@ -362,10 +628,20 @@ export class AcpSessionManager {
362
628
  // Initialize the per-session ring buffer before any update can fire.
363
629
  this.eventBuffers.set(acpSessionId, []);
364
630
 
631
+ // The map is the evidence, as everywhere else in this class: an id whose
632
+ // entry is no longer this one was cancelled and resumed, so this
633
+ // process's late frames must not reach the buffer, the state, or the
634
+ // clients the replacement now owns. `entry` is declared below and read
635
+ // only from inside closures the process cannot fire until it exists.
636
+ const ownsSession = () => this.sessions.get(acpSessionId) === entry;
637
+
365
638
  // Wrap the sender so every emitted message is mirrored into the buffer
366
639
  // when it's an `acp_session_update`. The wrapper preserves the original
367
640
  // call semantics: it forwards every message unchanged.
368
641
  const wrappedSend = (msg: AssistantEvent) => {
642
+ if (!ownsSession()) {
643
+ return;
644
+ }
369
645
  if (msg.type === "acp_session_update") {
370
646
  this.appendToBuffer(acpSessionId, msg);
371
647
  } else if (msg.type === "acp_session_usage") {
@@ -386,10 +662,15 @@ export class AcpSessionManager {
386
662
  opts.sendToVellum(msg);
387
663
  };
388
664
 
665
+ // The callback runs once the agent is connected and sending
666
+ // notifications, which is after agentProcess is constructed just below.
389
667
  const clientHandler = new VellumAcpClientHandler(
390
668
  acpSessionId,
391
669
  wrappedSend,
392
670
  opts.parentConversationId,
671
+ (configOptions) => {
672
+ this.applyUnsolicitedConfigOptions(acpSessionId, entry, configOptions);
673
+ },
393
674
  );
394
675
 
395
676
  const agentProcess = new AcpAgentProcess(
@@ -444,6 +725,98 @@ export class AcpSessionManager {
444
725
  });
445
726
  }
446
727
 
728
+ /**
729
+ * Publishes the session's model selection. Silent for adapters with no
730
+ * model selector: the client renders nothing rather than an empty picker.
731
+ * A separate frame from `acp_session_spawned`, which carries a fixed subset
732
+ * older clients parse.
733
+ */
734
+ private sendModelEvent(acpSessionId: string, entry: SessionEntry): void {
735
+ if (!entry.modelConfigId) {
736
+ return;
737
+ }
738
+ this.sendModelSnapshot(acpSessionId, entry);
739
+ }
740
+
741
+ /** Publishes model state with the same revision exposed by session reads. */
742
+ private sendModelSnapshot(acpSessionId: string, entry: SessionEntry): void {
743
+ const modelRevision = ++this.modelRevision;
744
+ entry.state.modelRevisionEpoch = this.modelRevisionEpoch;
745
+ entry.state.modelRevision = modelRevision;
746
+ entry.sendToVellum({
747
+ type: "acp_session_model_update",
748
+ acpSessionId,
749
+ modelRevisionEpoch: this.modelRevisionEpoch,
750
+ modelRevision,
751
+ model: entry.state.model,
752
+ availableModels: entry.state.availableModels ?? [],
753
+ });
754
+ }
755
+
756
+ /**
757
+ * Whether `entry` is still this manager's live entry for `acpSessionId`.
758
+ * A notification that lands after close, cancellation, or prompt completion
759
+ * has nothing left to update: the terminal row is already written, so
760
+ * applying it would leave history on one model and clients on another.
761
+ */
762
+ private isEntryLive(acpSessionId: string, entry: SessionEntry): boolean {
763
+ return (
764
+ this.sessions.get(acpSessionId) === entry &&
765
+ isLiveAcpStatus(entry.state.status)
766
+ );
767
+ }
768
+
769
+ /**
770
+ * Drops the live model snapshot a vanished selector took with it and
771
+ * publishes the empty picker. `applyModelInfo` keeps the model already
772
+ * recorded for adapters that never had a selector, which here would leave
773
+ * the client holding options the session cannot reach. Returns whether it
774
+ * fired, so callers skip the publish that assumes a model is still there.
775
+ *
776
+ * A published picker is what says the selector was there to vanish: only an
777
+ * adapter that advertised one ever populates it, so an adapter that never
778
+ * had one publishes nothing.
779
+ */
780
+ private clearVanishedModelSelector(
781
+ acpSessionId: string,
782
+ entry: SessionEntry,
783
+ ): boolean {
784
+ if (entry.modelConfigId || entry.state.availableModels === undefined) {
785
+ return false;
786
+ }
787
+ entry.state.model = undefined;
788
+ entry.state.availableModels = [];
789
+ this.sendModelSnapshot(acpSessionId, entry);
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * Takes in a `config_option_update` the adapter sent unprompted, which is
795
+ * how a `/model` the user typed straight into the transcript reaches the
796
+ * daemon: record what it reports, publish it, and drop the live snapshot
797
+ * when the selector it announced has vanished.
798
+ *
799
+ * A session past its terminal transition takes nothing from a late
800
+ * notification at all: its history row is already written, so mutating
801
+ * state or emitting here would leave clients and history disagreeing about
802
+ * the same run.
803
+ */
804
+ private applyUnsolicitedConfigOptions(
805
+ acpSessionId: string,
806
+ entry: SessionEntry,
807
+ configOptions: SessionConfigOption[],
808
+ ): void {
809
+ if (!this.isEntryLive(acpSessionId, entry)) {
810
+ return;
811
+ }
812
+ this.applyModelInfo(entry, configOptions);
813
+
814
+ if (this.clearVanishedModelSelector(acpSessionId, entry)) {
815
+ return;
816
+ }
817
+ this.sendModelEvent(acpSessionId, entry);
818
+ }
819
+
447
820
  /**
448
821
  * Resumes a terminal-state session from its persisted
449
822
  * `acp_session_history` row, reattaching to the agent's stored
@@ -623,6 +996,7 @@ export class AcpSessionManager {
623
996
  // Seed before the child process spawns so no live update can fire first.
624
997
  entry.clientHandler.seedSeq(maxSeq);
625
998
 
999
+ let configOptions: SessionConfigOption[] = [];
626
1000
  try {
627
1001
  log.info(
628
1002
  { acpSessionId, agentId: row.agentId },
@@ -632,14 +1006,20 @@ export class AcpSessionManager {
632
1006
  await agentProcess.initialize();
633
1007
  if (agentProcess.supportsSessionResume) {
634
1008
  // session/resume reattaches without replaying history.
635
- await agentProcess.resumeSession(row.acpSessionId, row.cwd);
1009
+ ({ configOptions } = await agentProcess.resumeSession(
1010
+ row.acpSessionId,
1011
+ row.cwd,
1012
+ ));
636
1013
  } else if (agentProcess.supportsLoadSession) {
637
1014
  // session/load replays the full history as session/update
638
1015
  // notifications before resolving; suppress forwarding so the
639
1016
  // conversation and ring buffer don't receive duplicates.
640
1017
  entry.clientHandler.beginReplaySuppression();
641
1018
  try {
642
- await agentProcess.loadSession(row.acpSessionId, row.cwd);
1019
+ ({ configOptions } = await agentProcess.loadSession(
1020
+ row.acpSessionId,
1021
+ row.cwd,
1022
+ ));
643
1023
  } finally {
644
1024
  entry.clientHandler.endReplaySuppression();
645
1025
  }
@@ -668,7 +1048,43 @@ export class AcpSessionManager {
668
1048
  throw err;
669
1049
  }
670
1050
 
1051
+ // The ladder a spawn walks, for the case the adapter reports no model of
1052
+ // its own; a reattached session that comes back naming one keeps it.
1053
+ const resolvedModel = resolveAcpModel({ agentModel: agentConfig.model });
1054
+ let applied: boolean;
1055
+ try {
1056
+ ({ applied } = await this.pinSessionModel(
1057
+ entry,
1058
+ configOptions,
1059
+ undefined,
1060
+ resolvedModel,
1061
+ { keepReportedModel: true },
1062
+ ));
1063
+ } catch (err) {
1064
+ log.error(
1065
+ { acpSessionId, agentId: row.agentId, err },
1066
+ "ACP resume failed while applying the model",
1067
+ );
1068
+ const failure = this.classifyPinFailure(err, row.agentId, agentConfig);
1069
+ this.teardownSession(acpSessionId, entry);
1070
+ throw failure;
1071
+ }
1072
+ if (resolvedModel && !applied) {
1073
+ // State keeps the adapter's own answer, so the model event, the status
1074
+ // projection and the panel all name the model the run is really on.
1075
+ log.warn(
1076
+ {
1077
+ acpSessionId,
1078
+ agentId: row.agentId,
1079
+ resolvedModel,
1080
+ model: state.model,
1081
+ },
1082
+ "ACP resume could not apply the configured model; running on the agent's own model",
1083
+ );
1084
+ }
1085
+
671
1086
  this.sendSpawnedEvent(acpSessionId, entry);
1087
+ this.sendModelEvent(acpSessionId, entry);
672
1088
  }
673
1089
 
674
1090
  /**
@@ -929,10 +1345,7 @@ export class AcpSessionManager {
929
1345
  if (!entry) {
930
1346
  throw new AcpSessionNotFoundError(acpSessionId);
931
1347
  }
932
- if (
933
- entry.state.status === "running" ||
934
- entry.state.status === "initializing"
935
- ) {
1348
+ if (isLiveAcpStatus(entry.state.status)) {
936
1349
  entry.state.status = "cancelled";
937
1350
  entry.state.completedAt = Date.now();
938
1351
  }
@@ -942,6 +1355,13 @@ export class AcpSessionManager {
942
1355
 
943
1356
  /**
944
1357
  * Denies pending ACP permissions, kills the process, and removes the session.
1358
+ *
1359
+ * The permissions and the process belong to `entry`, so they are always
1360
+ * torn down; the map slot and its buffer belong to whichever entry holds
1361
+ * the id, so they are only cleared while that is still this one. A caller
1362
+ * whose entry the map has since replaced (a cancel that persisted a
1363
+ * resumable row, then a resume of the same id, while an await was pending)
1364
+ * would otherwise evict the live session that took its place.
945
1365
  */
946
1366
  private teardownSession(acpSessionId: string, entry: SessionEntry): void {
947
1367
  for (const requestId of entry.clientHandler.pendingRequestIds) {
@@ -951,6 +1371,9 @@ export class AcpSessionManager {
951
1371
  }
952
1372
  }
953
1373
  entry.process.kill();
1374
+ if (this.sessions.get(acpSessionId) !== entry) {
1375
+ return;
1376
+ }
954
1377
  this.sessions.delete(acpSessionId);
955
1378
  // Free the buffer in case persistTerminal hasn't already (e.g. close()
956
1379
  // before terminal transition).