@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
@@ -124,11 +124,22 @@ const spawnMock = mock(
124
124
  _cwd: string,
125
125
  _parentConversationId: string,
126
126
  _sendToVellum: (msg: unknown) => void,
127
- _parentToolUseId?: string,
128
- ) => ({
129
- acpSessionId: "acp-session-test",
130
- protocolSessionId: "proto-session-test",
131
- }),
127
+ _options?: { parentToolUseId?: string; model?: string },
128
+ ): Promise<{
129
+ acpSessionId: string;
130
+ protocolSessionId: string;
131
+ requestedModel?: string;
132
+ effectiveModel?: string;
133
+ modelWarning?: string;
134
+ }> => {
135
+ const requestedModel = _options?.model?.trim() || undefined;
136
+ return {
137
+ acpSessionId: "acp-session-test",
138
+ protocolSessionId: "proto-session-test",
139
+ requestedModel,
140
+ effectiveModel: "opus",
141
+ };
142
+ },
132
143
  );
133
144
 
134
145
  // Spread the real module's exports so transitive importers that pull other
@@ -201,6 +212,8 @@ describe("executeAcpSpawn - happy path", () => {
201
212
  const payload = JSON.parse(result.content);
202
213
  expect(payload.acpSessionId).toBe("acp-session-test");
203
214
  expect(payload.status).toBe("running");
215
+ expect(payload.requestedModel).toBeNull();
216
+ expect(payload.effectiveModel).toBe("opus");
204
217
  // The real adapter binary is spawned (no `bun x` wrapper).
205
218
  const agentConfigArg = spawnMock.mock.calls[0][1] as { command: string };
206
219
  expect(agentConfigArg.command).toBe("claude-agent-acp");
@@ -214,8 +227,10 @@ describe("executeAcpSpawn - happy path", () => {
214
227
 
215
228
  expect(result.isError).toBe(false);
216
229
  expect(spawnMock).toHaveBeenCalledTimes(1);
217
- // parentToolUseId is the 7th positional arg to spawn().
218
- expect(spawnMock.mock.calls[0][6]).toBe("toolu_abc123");
230
+ // The options bag is the 7th positional arg to spawn().
231
+ expect(spawnMock.mock.calls[0][6]).toEqual({
232
+ parentToolUseId: "toolu_abc123",
233
+ });
219
234
  });
220
235
 
221
236
  test("default-profile fallback when user config is empty", async () => {
@@ -272,7 +287,7 @@ describe("executeAcpSpawn — input validation", () => {
272
287
  expect(result.isError).toBe(true);
273
288
  expect(result.content).toContain("claude-agent-acp is not on PATH");
274
289
  expect(result.content).toContain(
275
- "bun add -g @agentclientprotocol/claude-agent-acp",
290
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
276
291
  );
277
292
  expect(execFileMock).not.toHaveBeenCalled();
278
293
  expect(spawnMock).not.toHaveBeenCalled();
@@ -309,7 +324,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
309
324
  expect(spawnMock).toHaveBeenCalledTimes(1);
310
325
  const payload = JSON.parse(result.content);
311
326
  expect(payload.message).toContain(
312
- "Installed @agentclientprotocol/claude-agent-acp automatically.",
327
+ "Installed @agentclientprotocol/claude-agent-acp@0.75.1 automatically.",
313
328
  );
314
329
  // The real binary was spawned with cwd = the project dir and token
315
330
  // injected (trusted-binary config, no resolution at spawn).
@@ -330,7 +345,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
330
345
  expect(args).toEqual([
331
346
  "add",
332
347
  "--global",
333
- "@agentclientprotocol/claude-agent-acp",
348
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
334
349
  ]);
335
350
  });
336
351
 
@@ -419,7 +434,7 @@ describe("executeAcpSpawn: sandboxed bun auto-install on missing binary", () =>
419
434
  expect(result.isError).toBe(true);
420
435
  expect(result.content).toContain("claude-agent-acp is not on PATH");
421
436
  expect(result.content).toContain(
422
- "bun add -g @agentclientprotocol/claude-agent-acp",
437
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
423
438
  );
424
439
  expect(result.content).toContain("auto-install failed");
425
440
  expect(result.content).toContain("EACCES");
@@ -458,6 +473,126 @@ describe("executeAcpSpawn — per-agent resume hint", () => {
458
473
  });
459
474
  });
460
475
 
476
+ describe("executeAcpSpawn - model selection", () => {
477
+ test("threads the requested model into manager.spawn's options", async () => {
478
+ const result = await executeAcpSpawn(
479
+ { agent: "claude", task: "do something", model: "opus" },
480
+ { ...makeContext(), toolUseId: "toolu_model" },
481
+ );
482
+
483
+ expect(result.isError).toBe(false);
484
+ expect(spawnMock.mock.calls[0][6]).toEqual({
485
+ parentToolUseId: "toolu_model",
486
+ model: "opus",
487
+ });
488
+ const payload = JSON.parse(result.content);
489
+ expect(payload.requestedModel).toBe("opus");
490
+ expect(payload.effectiveModel).toBe("opus");
491
+ expect(payload.message).toContain(
492
+ 'The top-level ACP session reports "opus" as its effective model.',
493
+ );
494
+ });
495
+
496
+ test("reports the manager-normalized requested model", async () => {
497
+ const result = await executeAcpSpawn(
498
+ { agent: "claude", task: "do something", model: " opus " },
499
+ makeContext(),
500
+ );
501
+
502
+ expect(spawnMock.mock.calls[0][6]).toEqual({
503
+ parentToolUseId: undefined,
504
+ model: " opus ",
505
+ });
506
+ expect(JSON.parse(result.content).requestedModel).toBe("opus");
507
+ });
508
+
509
+ test("a null model is treated as omitted", async () => {
510
+ const result = await executeAcpSpawn(
511
+ { agent: "claude", task: "do something", model: null },
512
+ makeContext(),
513
+ );
514
+
515
+ expect(result.isError).toBe(false);
516
+ const options = spawnMock.mock.calls[0][6] as { model?: string };
517
+ expect(options.model).toBeUndefined();
518
+ });
519
+
520
+ test("a modelWarning from the spawn is relayed in the result message", async () => {
521
+ spawnMock.mockImplementationOnce(async () => ({
522
+ acpSessionId: "acp-session-test",
523
+ protocolSessionId: "proto-session-test",
524
+ requestedModel: "nope",
525
+ effectiveModel: "opus",
526
+ modelWarning: "Unknown model: nope",
527
+ }));
528
+
529
+ const result = await executeAcpSpawn(
530
+ { agent: "claude", task: "do something", model: "nope" },
531
+ makeContext(),
532
+ );
533
+
534
+ expect(result.isError).toBe(false);
535
+ const payload = JSON.parse(result.content);
536
+ expect(payload.message).toContain(
537
+ "The requested model was not applied: Unknown model: nope",
538
+ );
539
+ expect(payload.requestedModel).toBe("nope");
540
+ expect(payload.effectiveModel).toBe("opus");
541
+ });
542
+
543
+ test("the note relays a no-selector warning without calling it a refusal", async () => {
544
+ spawnMock.mockImplementationOnce(async () => ({
545
+ acpSessionId: "acp-session-test",
546
+ protocolSessionId: "proto-session-test",
547
+ requestedModel: "opus",
548
+ modelWarning:
549
+ 'Agent "claude" does not support model selection, so the session is running on the agent\'s own model.',
550
+ }));
551
+
552
+ const result = await executeAcpSpawn(
553
+ { agent: "claude", task: "do something", model: "opus" },
554
+ makeContext(),
555
+ );
556
+
557
+ const payload = JSON.parse(result.content);
558
+ expect(payload.message).toContain("does not support model selection");
559
+ expect(payload.message).not.toContain("refused");
560
+ });
561
+
562
+ test("no model note when the spawn reports no warning", async () => {
563
+ const result = await executeAcpSpawn(
564
+ { agent: "claude", task: "do something", model: "opus" },
565
+ makeContext(),
566
+ );
567
+
568
+ expect(result.isError).toBe(false);
569
+ const payload = JSON.parse(result.content);
570
+ expect(payload.message).not.toContain(
571
+ "The requested model was not applied",
572
+ );
573
+ });
574
+
575
+ test("reports when the adapter does not identify the effective model", async () => {
576
+ spawnMock.mockImplementationOnce(async () => ({
577
+ acpSessionId: "acp-session-test",
578
+ protocolSessionId: "proto-session-test",
579
+ requestedModel: "sonnet",
580
+ }));
581
+
582
+ const result = await executeAcpSpawn(
583
+ { agent: "claude", task: "do something", model: "sonnet" },
584
+ makeContext(),
585
+ );
586
+
587
+ const payload = JSON.parse(result.content);
588
+ expect(payload.requestedModel).toBe("sonnet");
589
+ expect(payload.effectiveModel).toBeNull();
590
+ expect(payload.message).toContain(
591
+ "The top-level ACP session did not report an effective model.",
592
+ );
593
+ });
594
+ });
595
+
461
596
  // ---------------------------------------------------------------------------
462
597
  // executeAcpSpawn — CLAUDE_CODE_OAUTH_TOKEN env injection + preflight
463
598
  //
@@ -37,6 +37,7 @@ export const acpSpawnInputSchema = z.looseObject({
37
37
  agent: nullAsOmitted(z.string()),
38
38
  task: nullAsOmitted(z.string()),
39
39
  cwd: nullAsOmitted(z.string()),
40
+ model: nullAsOmitted(z.string()),
40
41
  });
41
42
 
42
43
  /**
@@ -66,6 +67,7 @@ export async function executeAcpSpawn(
66
67
  }
67
68
  const agent = parsedInput.data.agent || "claude";
68
69
  const task = parsedInput.data.task;
70
+ const model = parsedInput.data.model;
69
71
 
70
72
  if (!task) {
71
73
  return { content: '"task" is required.', isError: true };
@@ -124,14 +126,20 @@ export async function executeAcpSpawn(
124
126
  // Recheck: the auto-install and the agent-env preparation above are both
125
127
  // awaits, and this is the point a long-lived subprocess starts.
126
128
  throwIfCancelled(context);
127
- const { acpSessionId, protocolSessionId } = await manager.spawn(
129
+ const {
130
+ acpSessionId,
131
+ protocolSessionId,
132
+ requestedModel,
133
+ effectiveModel,
134
+ modelWarning,
135
+ } = await manager.spawn(
128
136
  agent,
129
137
  agentConfig,
130
138
  task,
131
139
  cwd,
132
140
  context.conversationId,
133
141
  sendToClient,
134
- context.toolUseId,
142
+ { parentToolUseId: context.toolUseId, model },
135
143
  // The manager rechecks after its own protocol handshake and session
136
144
  // creation, so a turn stopped in that window tears the child process
137
145
  // down instead of handing it the task.
@@ -150,16 +158,26 @@ export async function executeAcpSpawn(
150
158
  const installNote = autoInstalledPackage
151
159
  ? ` Installed ${autoInstalledPackage} automatically.`
152
160
  : "";
161
+ // Relayed verbatim: the warning already distinguishes an adapter with no
162
+ // selector from one that refused the value.
163
+ const modelNote = modelWarning
164
+ ? ` The requested model was not applied: ${modelWarning}`
165
+ : "";
166
+ const effectiveModelNote = effectiveModel
167
+ ? ` The top-level ACP session reports "${effectiveModel}" as its effective model.`
168
+ : " The top-level ACP session did not report an effective model.";
153
169
  const payload = JSON.stringify({
154
170
  acpSessionId,
155
171
  protocolSessionId,
156
172
  agent,
157
173
  cwd,
174
+ requestedModel: requestedModel ?? null,
175
+ effectiveModel: effectiveModel ?? null,
158
176
  status: "running",
159
177
  message:
160
178
  `ACP agent "${agent}" spawned (session: ${protocolSessionId}). ` +
161
179
  `Results stream back via SSE. You will be notified when it completes.` +
162
- `${installNote}${resumeHint}`,
180
+ `${installNote}${modelNote}${effectiveModelNote}${resumeHint}`,
163
181
  });
164
182
 
165
183
  return { content: payload, isError: false };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tests for the `acp_status` tool's projection of session state.
3
+ *
4
+ * The state the manager tracks feeds three surfaces with different budgets:
5
+ * the HTTP route, the SSE picker, and this tool, whose output is spent on
6
+ * LLM context. Only the fields an agent can act on belong here.
7
+ */
8
+
9
+ import { describe, expect, mock, test } from "bun:test";
10
+
11
+ import type { AcpSessionState } from "../../acp/types.js";
12
+ import type { ToolContext } from "../types.js";
13
+
14
+ const RUNNING_STATE: AcpSessionState = {
15
+ id: "acp-1",
16
+ agentId: "claude",
17
+ acpSessionId: "proto-1",
18
+ parentConversationId: "conv-1",
19
+ status: "running",
20
+ startedAt: 1234,
21
+ task: "refactor the parser",
22
+ model: "claude-opus-4-5",
23
+ availableModels: [
24
+ { value: "sonnet", label: "Sonnet" },
25
+ { value: "opus", label: "Opus", description: "Most capable" },
26
+ ],
27
+ };
28
+
29
+ let statusResult: AcpSessionState | AcpSessionState[] = RUNNING_STATE;
30
+
31
+ const realAcpModule = await import("../../acp/index.js");
32
+ mock.module("../../acp/index.js", () => ({
33
+ ...realAcpModule,
34
+ getAcpSessionManager: () => ({ getStatus: () => statusResult }),
35
+ }));
36
+
37
+ const { executeAcpStatus } = await import("./status.js");
38
+
39
+ const context = {
40
+ conversationId: "conv-1",
41
+ workingDir: "/tmp",
42
+ } as unknown as ToolContext;
43
+
44
+ describe("executeAcpStatus", () => {
45
+ test("a single session reports its model but not the picker", async () => {
46
+ statusResult = RUNNING_STATE;
47
+
48
+ const result = await executeAcpStatus({ acp_session_id: "acp-1" }, context);
49
+
50
+ expect(result.isError).toBe(false);
51
+ const payload = JSON.parse(result.content);
52
+ expect(payload).toEqual({
53
+ id: "acp-1",
54
+ agentId: "claude",
55
+ acpSessionId: "proto-1",
56
+ parentConversationId: "conv-1",
57
+ status: "running",
58
+ startedAt: 1234,
59
+ task: "refactor the parser",
60
+ model: "claude-opus-4-5",
61
+ });
62
+ expect(result.content).not.toContain("Most capable");
63
+ });
64
+
65
+ test("the listing drops the picker from every entry", async () => {
66
+ statusResult = [
67
+ RUNNING_STATE,
68
+ { ...RUNNING_STATE, id: "acp-2", model: "sonnet" },
69
+ ];
70
+
71
+ const result = await executeAcpStatus({}, context);
72
+
73
+ const payload = JSON.parse(result.content) as Array<
74
+ Record<string, unknown>
75
+ >;
76
+ expect(payload).toHaveLength(2);
77
+ expect(payload.map((entry) => entry.model)).toEqual([
78
+ "claude-opus-4-5",
79
+ "sonnet",
80
+ ]);
81
+ for (const entry of payload) {
82
+ expect(entry).not.toHaveProperty("availableModels");
83
+ }
84
+ });
85
+
86
+ test("an empty listing says so rather than rendering an empty array", async () => {
87
+ statusResult = [];
88
+
89
+ const result = await executeAcpStatus({}, context);
90
+
91
+ expect(result.content).toBe("No ACP sessions found.");
92
+ expect(result.isError).toBe(false);
93
+ });
94
+ });
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { getAcpSessionManager } from "../../acp/index.js";
4
+ import type { AcpSessionState } from "../../acp/types.js";
4
5
  import {
5
6
  invalidToolInputResult,
6
7
  nullAsOmitted,
@@ -16,6 +17,40 @@ export const acpStatusInputSchema = z.looseObject({
16
17
  acp_session_id: nullAsOmitted(z.string()),
17
18
  });
18
19
 
20
+ /**
21
+ * The session fields worth spending LLM context on. An allowlist rather than
22
+ * an omission: `availableModels` is a picker for the human surfaces (HTTP and
23
+ * SSE) that says nothing the agent can act on, and a field added to
24
+ * `AcpSessionState` for those surfaces should not reach this one by default.
25
+ * `model` stays: which model a session is running on is answerable.
26
+ */
27
+ function projectSession(state: AcpSessionState) {
28
+ return {
29
+ id: state.id,
30
+ agentId: state.agentId,
31
+ acpSessionId: state.acpSessionId,
32
+ parentConversationId: state.parentConversationId,
33
+ status: state.status,
34
+ startedAt: state.startedAt,
35
+ completedAt: state.completedAt,
36
+ error: state.error,
37
+ stopReason: state.stopReason,
38
+ task: state.task,
39
+ parentToolUseId: state.parentToolUseId,
40
+ authErrorCode: state.authErrorCode,
41
+ authErrorCredential: state.authErrorCredential,
42
+ latestUsage: state.latestUsage,
43
+ model: state.model,
44
+ };
45
+ }
46
+
47
+ /** Projects either shape `getStatus` answers with. */
48
+ function projectStatus(status: AcpSessionState | AcpSessionState[]): unknown {
49
+ return Array.isArray(status)
50
+ ? status.map(projectSession)
51
+ : projectSession(status);
52
+ }
53
+
19
54
  export async function executeAcpStatus(
20
55
  input: Record<string, unknown>,
21
56
  _context: ToolContext,
@@ -29,9 +64,8 @@ export async function executeAcpStatus(
29
64
 
30
65
  try {
31
66
  if (acpSessionId) {
32
- const state = manager.getStatus(acpSessionId);
33
67
  return {
34
- content: JSON.stringify(state),
68
+ content: JSON.stringify(projectStatus(manager.getStatus(acpSessionId))),
35
69
  isError: false,
36
70
  };
37
71
  }
@@ -42,7 +76,10 @@ export async function executeAcpStatus(
42
76
  return { content: "No ACP sessions found.", isError: false };
43
77
  }
44
78
 
45
- return { content: JSON.stringify(allStates), isError: false };
79
+ return {
80
+ content: JSON.stringify(projectStatus(allStates)),
81
+ isError: false,
82
+ };
46
83
  } catch (err) {
47
84
  const msg = err instanceof Error ? err.message : String(err);
48
85
  return { content: msg, isError: true };
@@ -5,6 +5,7 @@
5
5
  * the proxy resolver, which forwards the call to the connected client.
6
6
  */
7
7
 
8
+ import { formatDesktopAppRequired } from "../capability-offer.js";
8
9
  import { throwIfCancelled } from "../shared/abort.js";
9
10
  import type { ToolContext, ToolExecutionResult } from "../types.js";
10
11
 
@@ -32,7 +33,7 @@ export function forwardAppControlProxyTool(
32
33
  }
33
34
  if (!context.proxyToolResolver) {
34
35
  return Promise.resolve({
35
- content: `Cannot execute ${toolName}: no proxy resolver available. This tool requires a connected client.`,
36
+ content: formatDesktopAppRequired("apps"),
36
37
  isError: true,
37
38
  });
38
39
  }
@@ -262,7 +262,7 @@ export const EXTRACT_LINKS_EXPRESSION = `
262
262
  (() => {
263
263
  const anchors = Array.from(document.querySelectorAll('a[href]'));
264
264
  return anchors.slice(0, 200).map(a => ({
265
- text: (a.textContent || '').trim().slice(0, 80),
265
+ text: Array.from((a.textContent || '').trim()).slice(0, 80).join(''),
266
266
  href: a.href,
267
267
  }));
268
268
  })()
@@ -14,6 +14,8 @@
14
14
  * a later PR is a direct drop-in replacement.
15
15
  */
16
16
 
17
+ import { safeStringSlice } from "../../../util/unicode.js";
18
+
17
19
  // ── Types ─────────────────────────────────────────────────────────────
18
20
 
19
21
  /**
@@ -288,7 +290,11 @@ function extractAttrs(node: RawAxNode): Record<string, string> {
288
290
  if (!PROPERTY_ALLOWLIST.has(name)) {
289
291
  continue;
290
292
  }
291
- attrs[name] = stringifyAxValue(prop.value?.value).slice(0, MAX_VALUE_CHARS);
293
+ attrs[name] = safeStringSlice(
294
+ stringifyAxValue(prop.value?.value),
295
+ 0,
296
+ MAX_VALUE_CHARS,
297
+ );
292
298
  }
293
299
  return attrs;
294
300
  }
@@ -346,12 +352,12 @@ export function transformAxTree(
346
352
  }
347
353
 
348
354
  const rawName = typeof node.name?.value === "string" ? node.name.value : "";
349
- const name = rawName.trim().slice(0, MAX_NAME_CHARS);
355
+ const name = safeStringSlice(rawName.trim(), 0, MAX_NAME_CHARS);
350
356
 
351
357
  const rawValue = node.value?.value;
352
358
  const value =
353
359
  typeof rawValue === "string" && rawValue.length > 0
354
- ? rawValue.slice(0, MAX_VALUE_CHARS)
360
+ ? safeStringSlice(rawValue, 0, MAX_VALUE_CHARS)
355
361
  : undefined;
356
362
 
357
363
  const attrs = extractAttrs(node);
@@ -25,6 +25,7 @@
25
25
  * value.
26
26
  */
27
27
 
28
+ import { safeStringSlice } from "../../../../util/unicode.js";
28
29
  import type { CdpWsTransport } from "./ws-transport.js";
29
30
 
30
31
  /**
@@ -336,7 +337,7 @@ function parseJsonText(text: string, endpoint: string): unknown {
336
337
  } catch (err) {
337
338
  throw new DevToolsDiscoveryError(
338
339
  "invalid_response",
339
- `Expected JSON from ${endpoint} but got: ${text.slice(0, 200)}`,
340
+ `Expected JSON from ${endpoint} but got: ${safeStringSlice(text, 0, 200)}`,
340
341
  err,
341
342
  );
342
343
  }
@@ -1,5 +1,6 @@
1
1
  import type { HostBrowserProxy } from "../../../daemon/host-browser-proxy.js";
2
2
  import { getLogger } from "../../../util/logger.js";
3
+ import { safeStringSlice } from "../../../util/unicode.js";
3
4
  import type { CdpErrorCode } from "./errors.js";
4
5
  import { CdpError } from "./errors.js";
5
6
  import type { CdpClientKind, ScopedCdpClient, TabInfo } from "./types.js";
@@ -121,7 +122,7 @@ export class ExtensionCdpClient implements ScopedCdpClient {
121
122
  // and mask the extension path as the true failing hop.
122
123
  throw new CdpError(
123
124
  "cdp_error",
124
- result.content.slice(0, 200) || `CDP error for ${method}`,
125
+ safeStringSlice(result.content, 0, 200) || `CDP error for ${method}`,
125
126
  {
126
127
  cdpMethod: method,
127
128
  cdpParams: params,
@@ -164,7 +165,7 @@ export class ExtensionCdpClient implements ScopedCdpClient {
164
165
  } catch (err) {
165
166
  throw new CdpError(
166
167
  "transport_error",
167
- `Non-JSON content from host_browser_result: ${result.content.slice(0, 200)}`,
168
+ `Non-JSON content from host_browser_result: ${safeStringSlice(result.content, 0, 200)}`,
168
169
  {
169
170
  cdpMethod: method,
170
171
  cdpParams: params,
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { getLogger } from "../../util/logger.js";
9
9
  import { truncate } from "../../util/truncate.js";
10
+ import { safeStringSlice } from "../../util/unicode.js";
10
11
  import type {
11
12
  ExtractedCredential,
12
13
  NetworkRecordedEntry,
@@ -457,7 +458,7 @@ export class NetworkRecorder {
457
458
 
458
459
  entry.response.body =
459
460
  body.length > MAX_BODY_SIZE
460
- ? body.slice(0, MAX_BODY_SIZE) + "...[truncated]"
461
+ ? safeStringSlice(body, 0, MAX_BODY_SIZE) + "...[truncated]"
461
462
  : body;
462
463
  }
463
464
  } catch {
@@ -4,11 +4,26 @@ import {
4
4
  appendLoggedInBrowserOffer,
5
5
  CHROME_WEB_STORE_INSTALL_URL,
6
6
  DESKTOP_APP_DOWNLOAD_URL,
7
+ formatDesktopAppRequired,
7
8
  formatLoggedInBrowserOffer,
8
9
  isPhoneSurface,
9
10
  shouldOfferLoggedInBrowser,
10
11
  } from "./capability-offer.js";
11
12
 
13
+ describe("formatDesktopAppRequired", () => {
14
+ test("offers a direct desktop-app download and retry step", () => {
15
+ const message = formatDesktopAppRequired("shell");
16
+
17
+ expect(message).toContain(
18
+ "The Vellum desktop app is needed to run commands on your computer",
19
+ );
20
+ expect(message).toContain(DESKTOP_APP_DOWNLOAD_URL);
21
+ expect(message).toContain("Open it, then retry.");
22
+ expect(message).not.toContain("client");
23
+ expect(message).not.toContain("daemon");
24
+ });
25
+ });
26
+
12
27
  describe("isPhoneSurface", () => {
13
28
  test("is true for iOS transport and mobile client OS", () => {
14
29
  expect(isPhoneSurface({ transportInterface: "ios" })).toBe(true);
@@ -49,7 +64,9 @@ describe("shouldOfferLoggedInBrowser", () => {
49
64
  expect(shouldOfferLoggedInBrowser('Invalid browser_mode "bogus"')).toBe(
50
65
  false,
51
66
  );
52
- expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(false);
67
+ expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(
68
+ false,
69
+ );
53
70
  });
54
71
  });
55
72
 
@@ -71,6 +88,7 @@ describe("formatLoggedInBrowserOffer", () => {
71
88
  });
72
89
  expect(offer).toContain("no in-app browser");
73
90
  expect(offer).toContain("Do not describe a browser panel");
91
+ expect(offer).toContain("Mac or Windows PC");
74
92
  expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
75
93
  expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
76
94
  });
@@ -78,7 +96,9 @@ describe("formatLoggedInBrowserOffer", () => {
78
96
 
79
97
  describe("appendLoggedInBrowserOffer", () => {
80
98
  test("appends the offer once", () => {
81
- const once = appendLoggedInBrowserOffer("Error: Navigation failed: timeout");
99
+ const once = appendLoggedInBrowserOffer(
100
+ "Error: Navigation failed: timeout",
101
+ );
82
102
  expect(once).toContain("Error: Navigation failed: timeout");
83
103
  expect(once).toContain(DESKTOP_APP_DOWNLOAD_URL);
84
104
  const twice = appendLoggedInBrowserOffer(once);
@@ -7,7 +7,22 @@ export const DESKTOP_APP_DOWNLOAD_URL = "https://www.vellum.ai/downloads";
7
7
  export const CHROME_WEB_STORE_INSTALL_URL =
8
8
  "https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne";
9
9
 
10
- export const DESKTOP_APP_INSTALL_HINT = `Install the Vellum desktop app: ${DESKTOP_APP_DOWNLOAD_URL}`;
10
+ export const DESKTOP_APP_INSTALL_HINT = `Download the Vellum desktop app: ${DESKTOP_APP_DOWNLOAD_URL}. Open it, then retry.`;
11
+
12
+ export type DesktopAppCapability = "files" | "screen" | "shell" | "apps";
13
+
14
+ const DESKTOP_APP_CAPABILITY_ACTION: Record<DesktopAppCapability, string> = {
15
+ files: "access files on your computer",
16
+ screen: "view or control your screen",
17
+ shell: "run commands on your computer",
18
+ apps: "control apps on your computer",
19
+ };
20
+
21
+ export function formatDesktopAppRequired(
22
+ capability: DesktopAppCapability,
23
+ ): string {
24
+ return `Error: The Vellum desktop app is needed to ${DESKTOP_APP_CAPABILITY_ACTION[capability]}. ${DESKTOP_APP_INSTALL_HINT}`;
25
+ }
11
26
 
12
27
  /** User-facing install step for status userActions and command-failure hints. */
13
28
  export const CHROME_EXTENSION_INSTALL_HINT = `Install the Vellum Assistant Chrome extension from the Chrome Web Store: ${CHROME_WEB_STORE_INSTALL_URL}`;