@vellumai/assistant 0.12.1 → 0.12.2-staging.1

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 (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -194,7 +194,10 @@ describe("AcpAgentProcess.createSession auth_required handling", () => {
194
194
  },
195
195
  });
196
196
 
197
- await expect(proc.createSession("/tmp")).resolves.toBe("session-1");
197
+ await expect(proc.createSession("/tmp")).resolves.toEqual({
198
+ sessionId: "session-1",
199
+ configOptions: [],
200
+ });
198
201
  expect(authenticateCalls()).toBe(1);
199
202
  });
200
203
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { type ChildProcess, spawn } from "node:child_process";
9
+ import { basename } from "node:path";
9
10
  import { Readable, Writable } from "node:stream";
10
11
 
11
12
  import type {
@@ -14,14 +15,25 @@ import type {
14
15
  AuthMethodEnvVar,
15
16
  Client,
16
17
  InitializeResponse,
18
+ LoadSessionResponse,
17
19
  NewSessionResponse,
18
20
  PromptResponse,
21
+ ResumeSessionResponse,
22
+ SessionConfigOption,
23
+ SetSessionConfigOptionRequest,
19
24
  } from "@agentclientprotocol/sdk";
20
25
  import * as acp from "@agentclientprotocol/sdk";
21
26
 
22
27
  import { getLogger } from "../util/logger.js";
23
- import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
24
- import type { AcpAgentConfig } from "./types.js";
28
+ import { safeStringSlice } from "../util/unicode.js";
29
+ import {
30
+ AcpAuthRequiredError,
31
+ CLAUDE_ACP_COMMAND,
32
+ isAcpAuthRequired,
33
+ isClaudeAuthFailureMessage,
34
+ requestErrorReason,
35
+ } from "./auth-required.js";
36
+ import { type AcpAgentConfig, AcpConfigOptionRefusedError } from "./types.js";
25
37
 
26
38
  const log = getLogger("acp");
27
39
 
@@ -32,6 +44,16 @@ const log = getLogger("acp");
32
44
  */
33
45
  const STDERR_RETENTION_BYTES = 4096;
34
46
 
47
+ /**
48
+ * Normalizes the SDK's optional-and-nullable config-option field to an array,
49
+ * so every session call hands its caller the same shape.
50
+ */
51
+ function normalizeConfigOptions(
52
+ configOptions: SessionConfigOption[] | null | undefined,
53
+ ): SessionConfigOption[] {
54
+ return configOptions ?? [];
55
+ }
56
+
35
57
  function isEnvVarMethod(
36
58
  method: AuthMethod,
37
59
  ): method is AuthMethodEnvVar & { type: "env_var" } {
@@ -142,7 +164,7 @@ export class AcpAgentProcess {
142
164
  // deriveFailureError reads from the end, so dropping the head is lossless.
143
165
  const line =
144
166
  text.length > STDERR_RETENTION_BYTES
145
- ? text.slice(-STDERR_RETENTION_BYTES)
167
+ ? safeStringSlice(text, text.length - STDERR_RETENTION_BYTES)
146
168
  : text;
147
169
  this.stderrSeq += 1;
148
170
  this.stderrRing.push({ seq: this.stderrSeq, text: line });
@@ -355,16 +377,21 @@ export class AcpAgentProcess {
355
377
 
356
378
  /**
357
379
  * Creates a new ACP session in the specified working directory.
358
- * Returns the session ID.
380
+ * Returns the session ID and the config options the agent reported.
359
381
  */
360
- async createSession(cwd: string): Promise<string> {
382
+ async createSession(
383
+ cwd: string,
384
+ ): Promise<{ sessionId: string; configOptions: SessionConfigOption[] }> {
361
385
  log.info({ agentId: this.agentId, cwd }, "Creating ACP session");
362
386
 
363
387
  const result: NewSessionResponse = await this.withAuthRetry(() =>
364
388
  this.requireConnection().newSession({ cwd, mcpServers: [] }),
365
389
  );
366
390
 
367
- return result.sessionId;
391
+ return {
392
+ sessionId: result.sessionId,
393
+ configOptions: normalizeConfigOptions(result.configOptions),
394
+ };
368
395
  }
369
396
 
370
397
  /**
@@ -375,12 +402,17 @@ export class AcpAgentProcess {
375
402
  * callers should suppress forwarding of those replayed updates (see
376
403
  * VellumAcpClientHandler.beginReplaySuppression).
377
404
  */
378
- async loadSession(sessionId: string, cwd: string): Promise<void> {
405
+ async loadSession(
406
+ sessionId: string,
407
+ cwd: string,
408
+ ): Promise<{ configOptions: SessionConfigOption[] }> {
379
409
  log.info({ agentId: this.agentId, sessionId, cwd }, "Loading ACP session");
380
410
 
381
- await this.withAuthRetry(() =>
411
+ const result: LoadSessionResponse = await this.withAuthRetry(() =>
382
412
  this.requireConnection().loadSession({ sessionId, cwd, mcpServers: [] }),
383
413
  );
414
+
415
+ return { configOptions: normalizeConfigOptions(result.configOptions) };
384
416
  }
385
417
 
386
418
  /**
@@ -390,16 +422,70 @@ export class AcpAgentProcess {
390
422
  * preferred when the agent advertises the capability
391
423
  * (see supportsSessionResume).
392
424
  */
393
- async resumeSession(sessionId: string, cwd: string): Promise<void> {
425
+ async resumeSession(
426
+ sessionId: string,
427
+ cwd: string,
428
+ ): Promise<{ configOptions: SessionConfigOption[] }> {
394
429
  log.info({ agentId: this.agentId, sessionId, cwd }, "Resuming ACP session");
395
430
 
396
- await this.withAuthRetry(() =>
431
+ const result: ResumeSessionResponse = await this.withAuthRetry(() =>
397
432
  this.requireConnection().resumeSession({
398
433
  sessionId,
399
434
  cwd,
400
435
  mcpServers: [],
401
436
  }),
402
437
  );
438
+
439
+ return { configOptions: normalizeConfigOptions(result.configOptions) };
440
+ }
441
+
442
+ /**
443
+ * Sets one session config option (e.g. the model selector) via
444
+ * `session/set_config_option`. The agent answers with the full refreshed
445
+ * option set, which is returned to the caller.
446
+ *
447
+ * A boolean value sends the `type: "boolean"` request variant; a string
448
+ * sends the value-id variant.
449
+ */
450
+ async setConfigOption(
451
+ sessionId: string,
452
+ configId: string,
453
+ value: string | boolean,
454
+ ): Promise<SessionConfigOption[]> {
455
+ log.info(
456
+ { agentId: this.agentId, sessionId, configId, value },
457
+ "Setting ACP session config option",
458
+ );
459
+
460
+ const request: SetSessionConfigOptionRequest =
461
+ typeof value === "boolean"
462
+ ? { sessionId, configId, type: "boolean", value }
463
+ : { sessionId, configId, value };
464
+
465
+ const response = await this.withAuthRetry(async () => {
466
+ try {
467
+ return await this.requireConnection().setSessionConfigOption(request);
468
+ } catch (err) {
469
+ // Wrapped here, at the request itself, so `withAuthRetry` still sees
470
+ // an auth_required answer and a caller cannot confuse the adapter's
471
+ // refusal with a transport or authentication failure. Claude's
472
+ // expired-token failure travels as a generic error whose reason is
473
+ // the only signal, so under the Claude adapter it is left for the
474
+ // caller to classify.
475
+ if (err instanceof acp.RequestError && !isAcpAuthRequired(err)) {
476
+ const reason = requestErrorReason(err);
477
+ const claudeAuthFailure =
478
+ basename(this.config.command) === CLAUDE_ACP_COMMAND &&
479
+ isClaudeAuthFailureMessage(reason);
480
+ if (!claudeAuthFailure) {
481
+ throw new AcpConfigOptionRefusedError(reason);
482
+ }
483
+ }
484
+ throw err;
485
+ }
486
+ });
487
+
488
+ return normalizeConfigOptions(response.configOptions);
403
489
  }
404
490
 
405
491
  /**
@@ -8,7 +8,8 @@
8
8
  * 2. Credentials present but rejected (the expired/revoked case): the CLI
9
9
  * surfaces the API 401 as an error message it authors, and the adapter
10
10
  * relays it as a generic -32603 internal error, NOT as `auth_required`.
11
- * {@link isClaudeAuthFailureMessage}.
11
+ * {@link isClaudeAuthFailureMessage}, over the text
12
+ * {@link requestErrorReason} decodes out of the rejection.
12
13
  *
13
14
  * A positive detection becomes {@link ACP_CLAUDE_AUTH_REQUIRED_CODE} on the
14
15
  * `acp_auth_required` event, which the client turns into the inline
@@ -81,6 +82,54 @@ export function isClaudeAuthFailureMessage(
81
82
  );
82
83
  }
83
84
 
85
+ /** Messages the SDK's `RequestError` factories build when given no text. */
86
+ const GENERIC_RPC_MESSAGES: ReadonlySet<string> = new Set([
87
+ "Parse error",
88
+ "Invalid request",
89
+ "Invalid params",
90
+ "Internal error",
91
+ "Request cancelled",
92
+ "Authentication required",
93
+ "Resource not found",
94
+ ]);
95
+
96
+ /**
97
+ * The sentence behind a JSON-RPC rejection. An adapter that throws a plain
98
+ * Error reaches the client as a bare "Internal error" whose real text the
99
+ * agent-side SDK moved into `data.details`. An adapter that raises
100
+ * `RequestError.internalError(data, text)` keeps its words in the message and
101
+ * only supplemental context in `data`, so a specific message wins. Behind a
102
+ * generic message such as "Invalid params", `data.details` or the serialized
103
+ * payload supplies the reason instead. Duck-typed like
104
+ * {@link isAcpAuthRequired}:
105
+ * `message` is read as a property, so a rejection that arrives as a plain
106
+ * object off the wire decodes the same as an `Error` instance.
107
+ */
108
+ export function requestErrorReason(err: unknown): string {
109
+ if (typeof err !== "object" || err === null) {
110
+ return String(err);
111
+ }
112
+ const { data, message } = err as { data?: unknown; message?: unknown };
113
+ const own = typeof message === "string" ? message : String(err);
114
+ if (data == null) {
115
+ return own;
116
+ }
117
+ if (
118
+ typeof message === "string" &&
119
+ message.length > 0 &&
120
+ !GENERIC_RPC_MESSAGES.has(message)
121
+ ) {
122
+ return message;
123
+ }
124
+ const details = (data as { details?: unknown }).details;
125
+ if (typeof details === "string" && details.length > 0) {
126
+ return details;
127
+ }
128
+ // JSON.stringify answers undefined for a value it cannot represent.
129
+ const serialized: string | undefined = JSON.stringify(data);
130
+ return serialized ?? own;
131
+ }
132
+
84
133
  /** The adapter whose auth failures the Connect Claude flow can repair. */
85
134
  export const CLAUDE_ACP_COMMAND = "claude-agent-acp";
86
135
 
@@ -78,7 +78,7 @@ describe("ensureAdapterInstalled", () => {
78
78
  expect(args).toEqual([
79
79
  "add",
80
80
  "--global",
81
- "@agentclientprotocol/claude-agent-acp",
81
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
82
82
  ]);
83
83
  });
84
84
 
@@ -198,8 +198,8 @@ describe("ensureAdapterInstalled", () => {
198
198
  (call) => (call[1] as string[])[2],
199
199
  );
200
200
  expect(installedPackages.sort()).toEqual([
201
- "@agentclientprotocol/claude-agent-acp",
202
- "@agentclientprotocol/codex-acp",
201
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
202
+ "@agentclientprotocol/codex-acp@1.10.0",
203
203
  ]);
204
204
  });
205
205
  });
@@ -232,7 +232,7 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
232
232
  // The resolved command is the REAL binary, not a `bun x` wrapper.
233
233
  expect(result.resolved.agent.command).toBe("claude-agent-acp");
234
234
  expect(result.autoInstalledPackage).toBe(
235
- "@agentclientprotocol/claude-agent-acp",
235
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
236
236
  );
237
237
  expect(result.failureMessage).toBeUndefined();
238
238
  expect(execFileMock).toHaveBeenCalledTimes(1);
@@ -264,12 +264,14 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
264
264
  return;
265
265
  }
266
266
  expect(result.resolved.agent.command).toBe("codex-acp");
267
- expect(result.autoInstalledPackage).toBe("@agentclientprotocol/codex-acp");
267
+ expect(result.autoInstalledPackage).toBe(
268
+ "@agentclientprotocol/codex-acp@1.10.0",
269
+ );
268
270
  expect(execFileMock).toHaveBeenCalledTimes(1);
269
271
  expect(execFileMock.mock.calls[0][1]).toEqual([
270
272
  "add",
271
273
  "--global",
272
- "@agentclientprotocol/codex-acp",
274
+ "@agentclientprotocol/codex-acp@1.10.0",
273
275
  ]);
274
276
  });
275
277
 
@@ -295,7 +297,7 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
295
297
  expect(result.resolved.ok).toBe(false);
296
298
  expect(result.failureMessage).toContain("claude-agent-acp is not on PATH");
297
299
  expect(result.failureMessage).toContain(
298
- "bun add -g @agentclientprotocol/claude-agent-acp",
300
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
299
301
  );
300
302
  expect(result.failureMessage).toContain("network is down");
301
303
  for (const call of execFileMock.mock.calls) {
@@ -21,6 +21,7 @@ import type {
21
21
  ReleaseTerminalResponse,
22
22
  RequestPermissionRequest,
23
23
  RequestPermissionResponse,
24
+ SessionConfigOption,
24
25
  SessionNotification,
25
26
  TerminalOutputRequest,
26
27
  TerminalOutputResponse,
@@ -103,6 +104,9 @@ export class VellumAcpClientHandler implements Client {
103
104
  private readonly acpSessionId: string,
104
105
  private readonly sendToVellum: (msg: AssistantEvent) => void,
105
106
  private readonly parentConversationId: string,
107
+ private readonly onConfigOptions?: (
108
+ configOptions: SessionConfigOption[],
109
+ ) => void,
106
110
  ) {}
107
111
 
108
112
  /** Forwards an update to Vellum, stamping a contiguous per-session `seq`. */
@@ -182,7 +186,12 @@ export class VellumAcpClientHandler implements Client {
182
186
  async sessionUpdate(params: SessionNotification): Promise<void> {
183
187
  const update = params.update;
184
188
 
185
- if (this.suppressForwarding) {
189
+ // Config options are current state rather than transcript history, so they
190
+ // are applied even while a replay is being suppressed.
191
+ if (
192
+ this.suppressForwarding &&
193
+ update.sessionUpdate !== "config_option_update"
194
+ ) {
186
195
  log.debug(
187
196
  { acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
188
197
  "Dropping replayed session update during suppression",
@@ -286,10 +295,14 @@ export class VellumAcpClientHandler implements Client {
286
295
  break;
287
296
  }
288
297
 
298
+ case "config_option_update": {
299
+ this.onConfigOptions?.(update.configOptions);
300
+ break;
301
+ }
302
+
289
303
  default: {
290
304
  // Other update types (available_commands_update, current_mode_update,
291
- // config_option_update, session_info_update) are not forwarded to
292
- // Vellum.
305
+ // session_info_update) are not forwarded to Vellum.
293
306
  log.debug(
294
307
  {
295
308
  acpSessionId: this.acpSessionId,
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Tests for the pure ACP model-config helpers: flattening an adapter's model
3
+ * `SessionConfigOption` into publishable model info, and the spawn-time
4
+ * precedence ladder.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import type { SessionConfigOption } from "@agentclientprotocol/sdk";
10
+
11
+ import {
12
+ MODEL_OPTION_MODELS,
13
+ modelOption,
14
+ } from "./__tests__/helpers/acp-model-option.js";
15
+ import { deriveModelInfo, resolveAcpModel } from "./model-config.js";
16
+
17
+ const flatModelOption = modelOption("sonnet");
18
+
19
+ describe("deriveModelInfo", () => {
20
+ test("flattens flat options and reports the current value", () => {
21
+ expect(deriveModelInfo([flatModelOption])).toEqual({
22
+ model: "sonnet",
23
+ modelConfigId: "model",
24
+ availableModels: MODEL_OPTION_MODELS,
25
+ });
26
+ });
27
+
28
+ test("flattens grouped options and carries the group name", () => {
29
+ const grouped: SessionConfigOption = {
30
+ type: "select",
31
+ id: "model",
32
+ name: "Model",
33
+ category: "model",
34
+ currentValue: "opus",
35
+ options: [
36
+ {
37
+ group: "anthropic",
38
+ name: "Anthropic",
39
+ options: [
40
+ { value: "opus", name: "Opus" },
41
+ { value: "haiku", name: "Haiku", description: "Fast" },
42
+ ],
43
+ },
44
+ {
45
+ group: "aliases",
46
+ name: "Aliases",
47
+ options: [{ value: "best", name: "Best available" }],
48
+ },
49
+ ],
50
+ };
51
+
52
+ expect(deriveModelInfo([grouped])).toEqual({
53
+ model: "opus",
54
+ modelConfigId: "model",
55
+ availableModels: [
56
+ { value: "opus", label: "Opus", group: "Anthropic" },
57
+ {
58
+ value: "haiku",
59
+ label: "Haiku",
60
+ description: "Fast",
61
+ group: "Anthropic",
62
+ },
63
+ { value: "best", label: "Best available", group: "Aliases" },
64
+ ],
65
+ });
66
+ });
67
+
68
+ test("matches on id alone when no category is reported", () => {
69
+ const uncategorized: SessionConfigOption = {
70
+ type: "select",
71
+ id: "model",
72
+ name: "Model",
73
+ currentValue: "haiku",
74
+ options: [{ value: "haiku", name: "Haiku" }],
75
+ };
76
+
77
+ const info = deriveModelInfo([uncategorized]);
78
+
79
+ expect(info.modelConfigId).toBe("model");
80
+ expect(info.model).toBe("haiku");
81
+ });
82
+
83
+ test("matches on category alone when the id differs", () => {
84
+ const byCategory: SessionConfigOption = {
85
+ type: "select",
86
+ id: "llm",
87
+ name: "LLM",
88
+ category: "model",
89
+ currentValue: "gpt-5",
90
+ options: [{ value: "gpt-5", name: "GPT-5" }],
91
+ };
92
+
93
+ expect(deriveModelInfo([byCategory]).modelConfigId).toBe("llm");
94
+ });
95
+
96
+ test("ignores a boolean option even when its category is model", () => {
97
+ const booleanOption: SessionConfigOption = {
98
+ type: "boolean",
99
+ id: "model",
100
+ name: "Model",
101
+ category: "model",
102
+ currentValue: true,
103
+ };
104
+
105
+ expect(deriveModelInfo([booleanOption])).toEqual({ availableModels: [] });
106
+ });
107
+
108
+ test("skips non-model select options and picks the first match", () => {
109
+ const mode: SessionConfigOption = {
110
+ type: "select",
111
+ id: "mode",
112
+ name: "Mode",
113
+ category: "mode",
114
+ currentValue: "ask",
115
+ options: [{ value: "ask", name: "Ask" }],
116
+ };
117
+
118
+ expect(deriveModelInfo([mode, flatModelOption]).modelConfigId).toBe(
119
+ "model",
120
+ );
121
+ });
122
+
123
+ test("returns no model info for an empty array", () => {
124
+ expect(deriveModelInfo([])).toEqual({ availableModels: [] });
125
+ });
126
+
127
+ test("returns no model info for null", () => {
128
+ expect(deriveModelInfo(null)).toEqual({ availableModels: [] });
129
+ });
130
+
131
+ test("returns no model info for undefined", () => {
132
+ expect(deriveModelInfo(undefined)).toEqual({ availableModels: [] });
133
+ });
134
+ });
135
+
136
+ describe("resolveAcpModel", () => {
137
+ test("prefers the requested model over the per-agent one", () => {
138
+ expect(
139
+ resolveAcpModel({ requestedModel: "opus", agentModel: "haiku" }),
140
+ ).toBe("opus");
141
+ });
142
+
143
+ test("falls back to the per-agent model", () => {
144
+ expect(resolveAcpModel({ agentModel: "haiku" })).toBe("haiku");
145
+ });
146
+
147
+ test("returns undefined when every rung is unset", () => {
148
+ expect(resolveAcpModel({})).toBeUndefined();
149
+ });
150
+
151
+ test("treats blank and whitespace-only values as unset", () => {
152
+ expect(
153
+ resolveAcpModel({ requestedModel: "\t\n", agentModel: "haiku" }),
154
+ ).toBe("haiku");
155
+ });
156
+
157
+ test("returns undefined when every rung is whitespace", () => {
158
+ expect(
159
+ resolveAcpModel({ requestedModel: " ", agentModel: " " }),
160
+ ).toBeUndefined();
161
+ });
162
+
163
+ test("trims the value it returns", () => {
164
+ expect(resolveAcpModel({ requestedModel: " opus " })).toBe("opus");
165
+ });
166
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Pure helpers for ACP session model selection.
3
+ *
4
+ * ACP 0.25.0 has no `session/set_model`: a model is one `SessionConfigOption`
5
+ * among the set an adapter reports from `session/new`, `session/load`,
6
+ * `session/resume`, and `session/set_config_option`. `deriveModelInfo` turns
7
+ * that raw set into the trio the daemon publishes and persists (current model,
8
+ * selectable options, and the config id to write back through
9
+ * `setSessionConfigOption`), flattening the grouped `options` variant so
10
+ * callers never branch on it.
11
+ *
12
+ * The model option is identified by `type === "select"` and `id === "model"`
13
+ * or `category === "model"` only. Option `name` is a human label an adapter
14
+ * may localize or restyle at will, so it is never matched against.
15
+ *
16
+ * `resolveAcpModel` is the precedence ladder a spawn or a resume walks before
17
+ * it talks to the adapter: an explicit request beats the per-agent default from
18
+ * `acp.agents.<id>.model`, and nothing named there leaves the adapter on its
19
+ * own. Values are adapter-reported aliases (`opus`, `sonnet`), never Assistant
20
+ * catalog ids, and are passed through unvalidated: only the adapter knows what
21
+ * it accepts.
22
+ *
23
+ * Both helpers are synchronous and side-effect free.
24
+ */
25
+
26
+ import type {
27
+ SessionConfigOption,
28
+ SessionConfigSelectGroup,
29
+ SessionConfigSelectOption,
30
+ } from "@agentclientprotocol/sdk";
31
+ import type { z } from "zod";
32
+
33
+ import type { AcpSessionModelUpdateEventSchema } from "../api/events/acp-session-model-update.js";
34
+
35
+ /**
36
+ * A selectable model as reported by the adapter, flattened out of its group.
37
+ * Derived from the event schema so the published wire shape and the shape the
38
+ * daemon carries in memory cannot drift apart.
39
+ */
40
+ export type AcpModelOption = z.infer<
41
+ typeof AcpSessionModelUpdateEventSchema
42
+ >["availableModels"][number];
43
+
44
+ /** What an adapter's config-option set says about the session's model. */
45
+ export type AcpModelInfo = {
46
+ model?: string;
47
+ availableModels: AcpModelOption[];
48
+ modelConfigId?: string;
49
+ };
50
+
51
+ type ResolveAcpModelInput = {
52
+ requestedModel?: string;
53
+ agentModel?: string;
54
+ };
55
+
56
+ type ModelSelectOption = Extract<SessionConfigOption, { type: "select" }>;
57
+
58
+ function isGroup(
59
+ entry: SessionConfigSelectOption | SessionConfigSelectGroup,
60
+ ): entry is SessionConfigSelectGroup {
61
+ return "group" in entry;
62
+ }
63
+
64
+ function toModelOption(
65
+ option: SessionConfigSelectOption,
66
+ group?: string,
67
+ ): AcpModelOption {
68
+ return {
69
+ value: option.value,
70
+ label: option.name,
71
+ ...(option.description ? { description: option.description } : {}),
72
+ ...(group ? { group } : {}),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * The adapter's model selector, if it advertises one. `category` is UX-only
78
+ * per the ACP spec, so a `model` id counts too; non-select options never do.
79
+ */
80
+ function findModelConfigOption(
81
+ configOptions: SessionConfigOption[] | null | undefined,
82
+ ): ModelSelectOption | undefined {
83
+ return configOptions?.find(
84
+ (option): option is ModelSelectOption =>
85
+ option.type === "select" &&
86
+ (option.id === "model" || option.category === "model"),
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Extract current model, selectable models, and the config id to write back.
92
+ * Returns `{ availableModels: [] }` when the adapter advertises no model
93
+ * selector, which is how the whole feature degrades to invisible.
94
+ */
95
+ export function deriveModelInfo(
96
+ configOptions: SessionConfigOption[] | null | undefined,
97
+ ): AcpModelInfo {
98
+ const modelOption = findModelConfigOption(configOptions);
99
+
100
+ if (!modelOption) {
101
+ return { availableModels: [] };
102
+ }
103
+
104
+ const availableModels: AcpModelOption[] = [];
105
+ for (const entry of modelOption.options) {
106
+ if (isGroup(entry)) {
107
+ for (const option of entry.options) {
108
+ availableModels.push(toModelOption(option, entry.name));
109
+ }
110
+ continue;
111
+ }
112
+ availableModels.push(toModelOption(entry));
113
+ }
114
+
115
+ return {
116
+ ...(modelOption.currentValue ? { model: modelOption.currentValue } : {}),
117
+ availableModels,
118
+ modelConfigId: modelOption.id,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Pick the model a session should start on. Blank and whitespace-only values
124
+ * count as unset so an empty config field never pins anything.
125
+ */
126
+ export function resolveAcpModel(
127
+ input: ResolveAcpModelInput,
128
+ ): string | undefined {
129
+ const ladder = [input.requestedModel, input.agentModel];
130
+
131
+ for (const candidate of ladder) {
132
+ const trimmed = candidate?.trim();
133
+ if (trimmed) {
134
+ return trimmed;
135
+ }
136
+ }
137
+ return undefined;
138
+ }