@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607

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 (441) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +292 -50
  6. package/package.json +1 -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__/app-compiler.test.ts +6 -0
  10. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  13. package/src/__tests__/computer-use-tools.test.ts +17 -5
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +28 -29
  16. package/src/__tests__/config-watcher.test.ts +23 -8
  17. package/src/__tests__/connection-model-compat.test.ts +2 -2
  18. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  19. package/src/__tests__/conversation-pairing.test.ts +26 -0
  20. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  21. package/src/__tests__/conversation-queue.test.ts +48 -207
  22. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  23. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  24. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  25. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  26. package/src/__tests__/conversation-title-service.test.ts +14 -10
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  28. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  29. package/src/__tests__/credential-record-retire.test.ts +171 -1
  30. package/src/__tests__/credential-routes.test.ts +212 -3
  31. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  32. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  33. package/src/__tests__/document-sync-tags.test.ts +25 -0
  34. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/git-porcelain.test.ts +47 -0
  38. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  39. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  40. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  42. package/src/__tests__/host-file-proxy.test.ts +11 -23
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  44. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  45. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  47. package/src/__tests__/mcp-health-check.test.ts +8 -11
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  49. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  50. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  51. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  52. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  53. package/src/__tests__/model-intents.test.ts +3 -0
  54. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  55. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  56. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  57. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  58. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  59. package/src/__tests__/resolve-app.test.ts +135 -0
  60. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  61. package/src/__tests__/service-account-token.test.ts +247 -0
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  63. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  64. package/src/__tests__/source-fingerprint.test.ts +184 -0
  65. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  66. package/src/__tests__/subagent-tools.test.ts +3 -2
  67. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  68. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  70. package/src/__tests__/workspace-git-service.test.ts +26 -0
  71. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  72. package/src/acp/__tests__/agent-process.test.ts +258 -4
  73. package/src/acp/__tests__/auth-required.test.ts +96 -0
  74. package/src/acp/__tests__/client-handler.test.ts +90 -2
  75. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  76. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  77. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  78. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  79. package/src/acp/__tests__/session-manager.test.ts +895 -4
  80. package/src/acp/agent-process.test.ts +4 -1
  81. package/src/acp/agent-process.ts +94 -9
  82. package/src/acp/auth-required.ts +50 -1
  83. package/src/acp/auto-install.test.ts +9 -7
  84. package/src/acp/client-handler.ts +16 -3
  85. package/src/acp/model-config.test.ts +166 -0
  86. package/src/acp/model-config.ts +138 -0
  87. package/src/acp/resolve-agent.test.ts +220 -6
  88. package/src/acp/resolve-agent.ts +88 -23
  89. package/src/acp/session-manager.test.ts +196 -3
  90. package/src/acp/session-manager.ts +441 -18
  91. package/src/acp/types.ts +50 -5
  92. package/src/api/events/acp-session-model-update.test.ts +88 -0
  93. package/src/api/events/acp-session-model-update.ts +46 -0
  94. package/src/api/events/app-files-changed.ts +4 -3
  95. package/src/api/events/notification-intent.ts +2 -0
  96. package/src/api/index.ts +6 -0
  97. package/src/apps/app-store.ts +1 -1
  98. package/src/apps/resolve-app.ts +81 -0
  99. package/src/apps/source-fingerprint.ts +266 -0
  100. package/src/bundler/app-compiler.ts +15 -0
  101. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  102. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  103. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  104. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  105. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  106. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  107. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  108. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  109. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  110. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  111. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  112. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  113. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  114. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  115. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  116. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  117. package/src/calls/barge-in-guard.ts +118 -0
  118. package/src/calls/call-constants.ts +24 -4
  119. package/src/calls/call-controller.ts +499 -199
  120. package/src/calls/call-transport.ts +16 -0
  121. package/src/calls/media-stream-output.ts +32 -0
  122. package/src/calls/media-stream-server.ts +82 -30
  123. package/src/calls/media-stream-stt-session.ts +15 -0
  124. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  125. package/src/calls/resolve-call-tts-provider.ts +33 -2
  126. package/src/calls/tts-call-strategy.ts +9 -5
  127. package/src/calls/types.ts +2 -0
  128. package/src/calls/voice-control-protocol.ts +53 -21
  129. package/src/calls/voice-leg-coordinator.ts +189 -0
  130. package/src/calls/voice-progress-cadence.ts +423 -0
  131. package/src/calls/voice-session-bridge.ts +59 -46
  132. package/src/calls/voice-triage-escalate.ts +175 -56
  133. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  134. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  135. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  136. package/src/cli/commands/apps.help.ts +67 -13
  137. package/src/cli/commands/apps.ts +183 -0
  138. package/src/cli/commands/bash.help.ts +8 -13
  139. package/src/cli/commands/bash.ts +7 -25
  140. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  141. package/src/cli/commands/channels/index.help.ts +70 -2
  142. package/src/cli/commands/channels/index.ts +16 -3
  143. package/src/cli/commands/channels/send.ts +128 -0
  144. package/src/cli/commands/credentials.help.ts +6 -2
  145. package/src/cli/commands/credentials.ts +2 -2
  146. package/src/cli/commands/mcp.help.ts +8 -8
  147. package/src/cli/commands/mcp.ts +1 -1
  148. package/src/cli/commands/roadmap.help.ts +5 -1
  149. package/src/cli/commands/roadmap.ts +1 -1
  150. package/src/cli/lib/bundled-marketplace.json +103 -90
  151. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  152. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  153. package/src/config/__tests__/platform-identity.test.ts +255 -0
  154. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  155. package/src/config/acp-agent-write.ts +43 -0
  156. package/src/config/acp-defaults.test.ts +13 -3
  157. package/src/config/acp-defaults.ts +23 -6
  158. package/src/config/acp-schema.test.ts +81 -0
  159. package/src/config/acp-schema.ts +30 -2
  160. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  161. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  162. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  163. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  164. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  165. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  166. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  167. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  168. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  169. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  170. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  171. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  172. package/src/config/default-profile-catalog.ts +1 -1
  173. package/src/config/env.ts +2 -2
  174. package/src/config/feature-flag-registry.json +24 -12
  175. package/src/config/loader.ts +1 -0
  176. package/src/config/memory-v3-gate.test.ts +31 -0
  177. package/src/config/memory-v3-gate.ts +20 -16
  178. package/src/config/platform-identity.ts +201 -0
  179. package/src/config/platform-rehydration.ts +11 -27
  180. package/src/config/schema.ts +2 -2
  181. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  182. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  183. package/src/config/schemas/calls.ts +1 -28
  184. package/src/config/schemas/live-voice.ts +1 -173
  185. package/src/config/schemas/mcp.ts +1 -1
  186. package/src/config/schemas/memory-retrospective.ts +10 -0
  187. package/src/config/schemas/services.ts +8 -0
  188. package/src/config/schemas/skills.ts +0 -14
  189. package/src/config/schemas/voice.ts +150 -0
  190. package/src/credential-execution/process-manager.test.ts +18 -19
  191. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  192. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  193. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  194. package/src/daemon/app-change-notify.ts +45 -0
  195. package/src/daemon/config-watcher.ts +13 -194
  196. package/src/daemon/conversation-process.ts +35 -64
  197. package/src/daemon/conversation-skill-tools.ts +13 -31
  198. package/src/daemon/conversation-store.ts +1 -1
  199. package/src/daemon/conversation-surfaces.ts +10 -8
  200. package/src/daemon/conversation-tool-setup.ts +37 -43
  201. package/src/daemon/conversation.ts +0 -3
  202. package/src/daemon/host-bash-proxy.ts +8 -12
  203. package/src/daemon/host-browser-proxy.ts +15 -1
  204. package/src/daemon/host-file-proxy.ts +8 -10
  205. package/src/daemon/host-transfer-proxy.ts +14 -18
  206. package/src/daemon/lifecycle.ts +0 -5
  207. package/src/daemon/mcp-reload-service.ts +5 -5
  208. package/src/daemon/message-types/sync.ts +1 -0
  209. package/src/daemon/orphan-reaper.ts +1 -1
  210. package/src/daemon/providers-setup.ts +1 -1
  211. package/src/daemon/shutdown-handlers.ts +1 -9
  212. package/src/daemon/skill-memory-refresh.ts +64 -0
  213. package/src/daemon/tool-side-effects.ts +7 -18
  214. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  215. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  216. package/src/desktop/desktop-chrome-policy.ts +42 -0
  217. package/src/desktop/desktop-dependencies.test.ts +102 -0
  218. package/src/desktop/desktop-dependencies.ts +306 -0
  219. package/src/desktop/desktop-feature.test.ts +18 -0
  220. package/src/desktop/desktop-feature.ts +19 -0
  221. package/src/desktop/desktop-panel-config.test.ts +135 -0
  222. package/src/desktop/desktop-panel-config.ts +149 -0
  223. package/src/desktop/desktop-session-manager.test.ts +627 -0
  224. package/src/desktop/desktop-session-manager.ts +814 -0
  225. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  226. package/src/desktop/desktop-stream-bridge.ts +214 -0
  227. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  228. package/src/desktop/desktop-wallpaper.ts +113 -0
  229. package/src/desktop/desktop-window-theme.ts +170 -0
  230. package/src/home/__tests__/feed-writer.test.ts +22 -0
  231. package/src/home/feed-writer.ts +11 -2
  232. package/src/hooks/types.ts +33 -0
  233. package/src/i18n/AGENTS.md +22 -0
  234. package/src/i18n/__tests__/i18n.test.ts +108 -0
  235. package/src/i18n/index.ts +16 -0
  236. package/src/i18n/locales.ts +93 -0
  237. package/src/i18n/messages.ts +61 -0
  238. package/src/i18n/title.ts +25 -0
  239. package/src/inbound/platform-callback-registration.ts +4 -5
  240. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  241. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  242. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  243. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  244. package/src/live-voice/live-voice-session.ts +213 -693
  245. package/src/live-voice/protocol.ts +0 -2
  246. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  247. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  248. package/src/mcp/effective-config.ts +3 -3
  249. package/src/mcp/mcp-header-store.ts +29 -45
  250. package/src/mcp/spec-schema.ts +111 -0
  251. package/src/mcp/startup.ts +8 -4
  252. package/src/mcp/workspace-mcp-config.ts +188 -0
  253. package/src/messaging/provider.ts +10 -3
  254. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  255. package/src/messaging/providers/callback-routing.ts +18 -0
  256. package/src/messaging/providers/channel-transport.ts +56 -0
  257. package/src/messaging/providers/discord/transport.ts +17 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  259. package/src/messaging/providers/slack/adapter.ts +7 -44
  260. package/src/messaging/providers/slack/transport.ts +16 -0
  261. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  262. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  263. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  264. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  265. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  266. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  267. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  268. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  269. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  270. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  271. package/src/notifications/adapters/macos.ts +3 -0
  272. package/src/notifications/assistant-reply-producer.ts +8 -3
  273. package/src/notifications/conversation-pairing.ts +36 -16
  274. package/src/notifications/delivered-post-record.ts +11 -1
  275. package/src/notifications/guardian-feed-projection.ts +125 -5
  276. package/src/notifications/home-feed-side-effect.ts +55 -7
  277. package/src/notifications/schedule-result-producer.ts +66 -22
  278. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  279. package/src/oauth/byo-connection.test.ts +45 -0
  280. package/src/oauth/byo-connection.ts +36 -6
  281. package/src/oauth/connection-resolver.test.ts +73 -0
  282. package/src/oauth/connection-resolver.ts +95 -2
  283. package/src/oauth/seed-providers.ts +165 -0
  284. package/src/outbound-proxy/certs.ts +3 -5
  285. package/src/permissions/checker.test.ts +11 -6
  286. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  287. package/src/persistence/conversation-crud.ts +71 -63
  288. package/src/persistence/conversation-lineage.ts +12 -4
  289. package/src/persistence/conversation-title-placeholders.ts +23 -0
  290. package/src/persistence/conversation-title-service.ts +9 -26
  291. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  292. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  293. package/src/persistence/message-cursor.ts +78 -0
  294. package/src/persistence/message-reads.ts +30 -0
  295. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  296. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  297. package/src/persistence/schema/memory-core.ts +5 -0
  298. package/src/platform/client.test.ts +4 -2
  299. package/src/platform/client.ts +2 -10
  300. package/src/plugin-api/constants.ts +2 -0
  301. package/src/plugin-api/index.ts +1 -0
  302. package/src/plugin-api/types.ts +2 -0
  303. package/src/plugins/defaults/index.ts +2 -0
  304. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  305. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  308. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  309. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  310. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  311. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  312. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  313. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  314. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  315. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  316. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  317. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  318. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  319. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  320. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  321. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  322. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  323. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  324. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  325. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  326. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  327. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  328. package/src/plugins/defaults/memory/startup.ts +6 -1
  329. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  330. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  331. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  332. package/src/plugins/defaults/memory/worker.ts +6 -0
  333. package/src/plugins/mcp-servers.ts +15 -75
  334. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  335. package/src/providers/gemini/client.ts +9 -23
  336. package/src/providers/gemini-thought-signature.test.ts +63 -0
  337. package/src/providers/gemini-thought-signature.ts +44 -0
  338. package/src/providers/inference/auth.ts +2 -3
  339. package/src/providers/inference/connection-availability.ts +27 -8
  340. package/src/providers/inference/resolve-auth.ts +22 -4
  341. package/src/providers/inference/service-account-token.ts +248 -0
  342. package/src/providers/model-catalog.ts +34 -0
  343. package/src/providers/model-intents.ts +10 -5
  344. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  345. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  346. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  347. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  348. package/src/providers/openai/chat-completions-provider.ts +192 -78
  349. package/src/providers/openai/codex-models.ts +0 -4
  350. package/src/providers/openai/google-thought-signature.ts +320 -0
  351. package/src/runtime/AGENTS.md +2 -0
  352. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  353. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  354. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  355. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  356. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  357. package/src/runtime/auth/same-actor.ts +38 -3
  358. package/src/runtime/channel-send.ts +316 -0
  359. package/src/runtime/http-server.ts +160 -151
  360. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  361. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  362. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  363. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  364. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  365. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  366. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  367. package/src/runtime/routes/acp-routes.test.ts +15 -0
  368. package/src/runtime/routes/acp-routes.ts +72 -9
  369. package/src/runtime/routes/app-management-routes.ts +73 -0
  370. package/src/runtime/routes/app-routes.ts +1 -1
  371. package/src/runtime/routes/channel-send-routes.ts +85 -0
  372. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  373. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  374. package/src/runtime/routes/credential-routes.ts +19 -10
  375. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  376. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  377. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  378. package/src/runtime/routes/index.ts +4 -2
  379. package/src/runtime/routes/log-export-routes.ts +2 -0
  380. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  381. package/src/runtime/routes/migration-routes.ts +6 -20
  382. package/src/runtime/routes/platform-routes.ts +18 -15
  383. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  384. package/src/runtime/services/conversation-serializer.ts +16 -3
  385. package/src/runtime/sync/resource-sync-events.ts +2 -1
  386. package/src/schedule/worker.ts +1 -1
  387. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  388. package/src/security/ces-http-record-backend.ts +120 -0
  389. package/src/security/ces-rpc-record-backend.ts +7 -0
  390. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  391. package/src/skills/workspace-skill-md-signature.ts +50 -0
  392. package/src/subagent/consult-context.ts +12 -25
  393. package/src/tools/acp/list-agents.test.ts +3 -1
  394. package/src/tools/acp/spawn.test.ts +146 -11
  395. package/src/tools/acp/spawn.ts +21 -3
  396. package/src/tools/acp/status.test.ts +94 -0
  397. package/src/tools/acp/status.ts +40 -3
  398. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  399. package/src/tools/capability-offer.test.ts +22 -2
  400. package/src/tools/capability-offer.ts +16 -1
  401. package/src/tools/computer-use/definitions.ts +4 -2
  402. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  403. package/src/tools/credentials/metadata-store.ts +100 -0
  404. package/src/tools/host-filesystem/edit.test.ts +2 -1
  405. package/src/tools/host-filesystem/edit.ts +2 -2
  406. package/src/tools/host-filesystem/read.test.ts +2 -1
  407. package/src/tools/host-filesystem/read.ts +2 -2
  408. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  409. package/src/tools/host-filesystem/transfer.ts +2 -2
  410. package/src/tools/host-filesystem/write.test.ts +2 -1
  411. package/src/tools/host-filesystem/write.ts +2 -2
  412. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  413. package/src/tools/host-terminal/host-shell.ts +4 -20
  414. package/src/tools/policy-context.ts +6 -7
  415. package/src/tools/subagent/spawn.ts +5 -8
  416. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  417. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  418. package/src/tools/terminal/safe-env.ts +23 -3
  419. package/src/tools/terminal/sanitized-bash.ts +105 -0
  420. package/src/tools/watch/watch-retro-report.ts +0 -8
  421. package/src/util/__tests__/language-subtag.test.ts +23 -1
  422. package/src/util/ca-bundle.ts +13 -0
  423. package/src/util/language-subtag.ts +16 -0
  424. package/src/util/platform.ts +8 -0
  425. package/src/workspace/git-porcelain.ts +66 -0
  426. package/src/workspace/git-service.ts +5 -54
  427. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  428. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  429. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  430. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  431. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  432. package/src/workspace/migrations/registry.ts +6 -0
  433. package/src/__tests__/app-source-watcher.test.ts +0 -218
  434. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  435. package/src/__tests__/speaker-identification.test.ts +0 -56
  436. package/src/calls/speaker-identification.ts +0 -233
  437. package/src/daemon/app-source-watcher.ts +0 -218
  438. package/src/messaging/providers/whatsapp/client.ts +0 -29
  439. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  440. package/src/util/fs-watcher-error.ts +0 -36
  441. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Tests for POST apps/:id/refresh (apps_refresh) in the shared route table.
3
+ *
4
+ * `mock.module` is process-global in Bun. Stubs delegate to the real
5
+ * implementations unless this file's tests are running.
6
+ */
7
+
8
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import {
12
+ afterAll,
13
+ afterEach,
14
+ beforeAll,
15
+ beforeEach,
16
+ describe,
17
+ expect,
18
+ mock,
19
+ test,
20
+ } from "bun:test";
21
+
22
+ import type { CompileResult } from "../../../bundler/app-compiler.js";
23
+
24
+ let mockActive = false;
25
+ beforeAll(() => {
26
+ mockActive = true;
27
+ });
28
+ afterAll(() => {
29
+ mockActive = false;
30
+ });
31
+
32
+ const realCompiler = { ...(await import("../../../bundler/app-compiler.js")) };
33
+ const realNotify = {
34
+ ...(await import("../../../daemon/app-change-notify.js")),
35
+ };
36
+
37
+ const compileApp = mock(
38
+ async (
39
+ _appDir: Parameters<typeof realCompiler.compileApp>[0],
40
+ ): Promise<CompileResult> => ({
41
+ ok: true,
42
+ errors: [],
43
+ warnings: [],
44
+ durationMs: 11,
45
+ }),
46
+ );
47
+ const notifyAppSurfacesChanged = mock(
48
+ (..._args: Parameters<typeof realNotify.notifyAppSurfacesChanged>) => {},
49
+ );
50
+
51
+ mock.module("../../../bundler/app-compiler.js", () => ({
52
+ ...realCompiler,
53
+ compileApp: (...args: Parameters<typeof realCompiler.compileApp>) => {
54
+ if (!mockActive) {
55
+ return realCompiler.compileApp(...args);
56
+ }
57
+ return compileApp(...args);
58
+ },
59
+ }));
60
+
61
+ mock.module("../../../daemon/app-change-notify.js", () => ({
62
+ ...realNotify,
63
+ notifyAppSurfacesChanged: (
64
+ ...args: Parameters<typeof realNotify.notifyAppSurfacesChanged>
65
+ ) => {
66
+ if (!mockActive) {
67
+ return realNotify.notifyAppSurfacesChanged(...args);
68
+ }
69
+ return notifyAppSurfacesChanged(...args);
70
+ },
71
+ }));
72
+
73
+ const { createApp } = await import("../../../apps/app-store.js");
74
+ const { getWorkspacePluginsDir } = await import("../../../util/platform.js");
75
+ const { ROUTES } = await import("../app-management-routes.js");
76
+
77
+ function findHandler(operationId: string) {
78
+ const route = ROUTES.find((r) => r.operationId === operationId);
79
+ if (!route) {
80
+ throw new Error(`Route ${operationId} not found`);
81
+ }
82
+ return route.handler;
83
+ }
84
+
85
+ const handleRefreshApp = findHandler("apps_refresh");
86
+
87
+ let workspaceDir: string;
88
+ let previousWorkspaceDir: string | undefined;
89
+
90
+ beforeEach(() => {
91
+ previousWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
92
+ workspaceDir = join(
93
+ tmpdir(),
94
+ `vellum-apps-refresh-${Date.now()}-${Math.random().toString(36).slice(2)}`,
95
+ );
96
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
97
+ compileApp.mockClear();
98
+ notifyAppSurfacesChanged.mockClear();
99
+ compileApp.mockResolvedValue({
100
+ ok: true,
101
+ errors: [],
102
+ warnings: [],
103
+ durationMs: 11,
104
+ });
105
+ });
106
+
107
+ afterEach(() => {
108
+ rmSync(workspaceDir, { recursive: true, force: true });
109
+ if (previousWorkspaceDir === undefined) {
110
+ delete process.env.VELLUM_WORKSPACE_DIR;
111
+ } else {
112
+ process.env.VELLUM_WORKSPACE_DIR = previousWorkspaceDir;
113
+ }
114
+ });
115
+
116
+ describe("apps_refresh route", () => {
117
+ test("is registered on the shared app-management ROUTES table", () => {
118
+ expect(typeof handleRefreshApp).toBe("function");
119
+ });
120
+
121
+ test("compiles a workspace app and notifies surfaces", async () => {
122
+ const created = createApp({
123
+ name: "Budget",
124
+ schemaJson: "{}",
125
+ htmlDefinition: "<h1>Budget</h1>",
126
+ });
127
+
128
+ const result = await handleRefreshApp({
129
+ pathParams: { id: created.id },
130
+ });
131
+
132
+ expect(compileApp).toHaveBeenCalled();
133
+ expect(notifyAppSurfacesChanged).toHaveBeenCalledWith(created.id, {
134
+ fileChange: true,
135
+ });
136
+ expect(result).toEqual({
137
+ ok: true,
138
+ appId: created.id,
139
+ name: "Budget",
140
+ compiled: true,
141
+ compile_duration_ms: 11,
142
+ });
143
+ });
144
+
145
+ test("compiles a plugin app in place", async () => {
146
+ const pluginName = `charts-${Math.random().toString(36).slice(2, 8)}`;
147
+ const pluginDir = join(getWorkspacePluginsDir(), pluginName);
148
+ mkdirSync(join(pluginDir, "apps", "viewer"), { recursive: true });
149
+ writeFileSync(
150
+ join(pluginDir, "package.json"),
151
+ JSON.stringify({ name: pluginName, version: "1.0.0" }),
152
+ );
153
+ writeFileSync(join(pluginDir, "apps", "viewer", "index.html"), "<h1></h1>");
154
+ const pluginAppId = `plugins~${pluginName}~viewer`;
155
+
156
+ const result = await handleRefreshApp({
157
+ pathParams: { id: pluginAppId },
158
+ });
159
+
160
+ expect(compileApp).toHaveBeenCalledWith(
161
+ join(pluginDir, "apps", "viewer"),
162
+ );
163
+ expect(notifyAppSurfacesChanged).toHaveBeenCalledWith(pluginAppId, {
164
+ fileChange: true,
165
+ });
166
+ expect(result).toMatchObject({
167
+ ok: true,
168
+ appId: pluginAppId,
169
+ name: "viewer",
170
+ compiled: true,
171
+ });
172
+ });
173
+
174
+ test("returns compile errors without throwing", async () => {
175
+ const created = createApp({
176
+ name: "Budget",
177
+ schemaJson: "{}",
178
+ htmlDefinition: "<h1>Budget</h1>",
179
+ });
180
+ compileApp.mockImplementationOnce(async () => ({
181
+ ok: false,
182
+ errors: [{ text: "Could not resolve foo" }],
183
+ warnings: [],
184
+ durationMs: 4,
185
+ }));
186
+
187
+ const result = await handleRefreshApp({
188
+ pathParams: { id: created.id },
189
+ });
190
+
191
+ expect(result).toMatchObject({
192
+ compiled: false,
193
+ compile_errors: [{ text: "Could not resolve foo" }],
194
+ });
195
+ expect(notifyAppSurfacesChanged).toHaveBeenCalled();
196
+ });
197
+
198
+ test("throws not found for an unknown app", async () => {
199
+ await expect(
200
+ handleRefreshApp({ pathParams: { id: "missing" } }),
201
+ ).rejects.toMatchObject({
202
+ statusCode: 404,
203
+ });
204
+ });
205
+ });
@@ -0,0 +1,127 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // The route is a thin door over the send function: it validates the body
4
+ // against its declared schema and maps the function's refusals onto route
5
+ // errors, so the CLI and the messaging tool share one seam.
6
+
7
+ const sendCalls: Array<Record<string, unknown>> = [];
8
+ let sendImpl: (params: Record<string, unknown>) => Promise<unknown> = async (
9
+ params,
10
+ ) => ({
11
+ channel: params.channel,
12
+ chatId: "C123",
13
+ messageIds: ["1700000000.000100"],
14
+ lastMessageId: "1700000000.000100",
15
+ recordedIn: "home-1",
16
+ });
17
+
18
+ class ChannelNotAddressableError extends Error {}
19
+ class ChannelSendFailedError extends Error {}
20
+
21
+ mock.module("../../channel-send.js", () => ({
22
+ ChannelNotAddressableError,
23
+ ChannelSendFailedError,
24
+ sendChannelText: async (params: Record<string, unknown>) => {
25
+ sendCalls.push(params);
26
+ return sendImpl(params);
27
+ },
28
+ }));
29
+
30
+ const { ROUTES } = await import("../channel-send-routes.js");
31
+ const { BadGatewayError, BadRequestError } = await import("../errors.js");
32
+
33
+ const route = ROUTES.find((r) => r.operationId === "channels_send_post")!;
34
+
35
+ beforeEach(() => {
36
+ sendCalls.length = 0;
37
+ });
38
+
39
+ describe("POST channels/send", () => {
40
+ test("is registered as a chat write for actor principals", () => {
41
+ expect(route).toBeDefined();
42
+ expect(route.endpoint).toBe("channels/send");
43
+ expect(route.method).toBe("POST");
44
+ expect(route.policy?.requiredScopes).toEqual(["chat.write"]);
45
+ });
46
+
47
+ test("hands a valid body to the send function and returns its result", async () => {
48
+ const body = {
49
+ channel: "slack",
50
+ target: { kind: "chat", chatId: "C123", threadId: "1690000000.000001" },
51
+ text: "hello",
52
+ renderRichly: true,
53
+ };
54
+ const result = await route.handler({ body });
55
+ expect(sendCalls).toEqual([body]);
56
+ expect(result).toMatchObject({
57
+ channel: "slack",
58
+ messageIds: ["1700000000.000100"],
59
+ recordedIn: "home-1",
60
+ });
61
+ });
62
+
63
+ test("names no sending turn, so a caller cannot claim a send was its own chat's", async () => {
64
+ await route.handler({
65
+ body: {
66
+ channel: "slack",
67
+ target: { kind: "chat", chatId: "C123" },
68
+ text: "hello",
69
+ sender: {
70
+ conversationId: "conv-A",
71
+ executionChannel: "slack",
72
+ requesterChatId: "C123",
73
+ },
74
+ },
75
+ });
76
+ expect(sendCalls).toHaveLength(1);
77
+ expect(sendCalls[0]).not.toHaveProperty("sender");
78
+ });
79
+
80
+ test("rejects a malformed body before calling the send function", async () => {
81
+ await expect(
82
+ route.handler({
83
+ body: { channel: "slack", target: { kind: "chat" }, text: "" },
84
+ }),
85
+ ).rejects.toBeInstanceOf(BadRequestError);
86
+ await expect(
87
+ route.handler({
88
+ body: {
89
+ channel: "not-a-channel",
90
+ target: { kind: "chat", chatId: "C1" },
91
+ text: "hi",
92
+ },
93
+ }),
94
+ ).rejects.toBeInstanceOf(BadRequestError);
95
+ expect(sendCalls).toEqual([]);
96
+ });
97
+
98
+ test("maps the send function's refusals onto route errors", async () => {
99
+ sendImpl = async () => {
100
+ throw new ChannelNotAddressableError(
101
+ 'Channel "email" is not addressable',
102
+ );
103
+ };
104
+ await expect(
105
+ route.handler({
106
+ body: {
107
+ channel: "email",
108
+ target: { kind: "chat", chatId: "x" },
109
+ text: "hi",
110
+ },
111
+ }),
112
+ ).rejects.toBeInstanceOf(BadRequestError);
113
+
114
+ sendImpl = async () => {
115
+ throw new ChannelSendFailedError("not acknowledged");
116
+ };
117
+ await expect(
118
+ route.handler({
119
+ body: {
120
+ channel: "slack",
121
+ target: { kind: "chat", chatId: "C1" },
122
+ text: "hi",
123
+ },
124
+ }),
125
+ ).rejects.toBeInstanceOf(BadGatewayError);
126
+ });
127
+ });
@@ -51,7 +51,12 @@ mock.module("../../../persistence/embeddings/embedding-backend.js", () => ({
51
51
  }));
52
52
 
53
53
  import { BACKUP_PROFILE_KEYS } from "../../../config/default-profile-names.js";
54
- import { getConfig, loadRawConfig } from "../../../config/loader.js";
54
+ import {
55
+ getConfig,
56
+ loadConfig,
57
+ loadRawConfig,
58
+ } from "../../../config/loader.js";
59
+ import { AssistantConfigSchema } from "../../../config/schema.js";
55
60
  import { LLMConfigBase } from "../../../config/schemas/llm.js";
56
61
  import type { ConversationCreateType } from "../../../persistence/conversation-types.js";
57
62
  import {
@@ -1940,3 +1945,105 @@ describe("ingress URL writes through the generic config routes", () => {
1940
1945
  expect(ingress.lastTunnel).toEqual(LAST_TUNNEL);
1941
1946
  });
1942
1947
  });
1948
+
1949
+ describe("config writes to a per-agent acp entry", () => {
1950
+ const patchRoute = ROUTES.find((r) => r.operationId === "config_patch")!;
1951
+ const setRoute = ROUTES.find((r) => r.operationId === "config_set")!;
1952
+
1953
+ beforeEach(() => {
1954
+ rawConfigFixture = {
1955
+ acp: {
1956
+ agents: {
1957
+ claude: { command: "claude-agent-acp", args: [], model: "opus" },
1958
+ },
1959
+ },
1960
+ };
1961
+ seedRawConfig();
1962
+ });
1963
+
1964
+ function agentEntry(
1965
+ raw: Record<string, unknown>,
1966
+ id = "claude",
1967
+ ): Record<string, unknown> {
1968
+ const acp = raw.acp as Record<string, unknown>;
1969
+ const agents = acp.agents as Record<string, Record<string, unknown>>;
1970
+ return agents[id]!;
1971
+ }
1972
+
1973
+ test("a nulled per-agent model is dropped on both write paths", async () => {
1974
+ await patchRoute.handler({
1975
+ body: { acp: { agents: { claude: { model: null } } } },
1976
+ });
1977
+
1978
+ const patched = agentEntry(loadRawConfig());
1979
+ expect("model" in patched).toBe(false);
1980
+ expect(patched.command).toBe("claude-agent-acp");
1981
+ expect(AssistantConfigSchema.safeParse(loadRawConfig()).success).toBe(true);
1982
+
1983
+ seedRawConfig();
1984
+ await setRoute.handler({
1985
+ body: { path: "acp.agents.claude.model", value: null },
1986
+ });
1987
+
1988
+ expect("model" in agentEntry(loadRawConfig())).toBe(false);
1989
+ expect(AssistantConfigSchema.safeParse(loadRawConfig()).success).toBe(true);
1990
+ expect(loadConfig().acp.agents.claude?.model).toBeUndefined();
1991
+ });
1992
+
1993
+ test("a nulled per-agent command is dropped on both write paths", async () => {
1994
+ await setRoute.handler({
1995
+ body: { path: "acp.agents.claude.command", value: null },
1996
+ });
1997
+
1998
+ expect("command" in agentEntry(loadRawConfig())).toBe(false);
1999
+ expect(AssistantConfigSchema.safeParse(loadRawConfig()).success).toBe(true);
2000
+ expect(loadConfig().acp.agents.claude?.command).toBeUndefined();
2001
+
2002
+ seedRawConfig();
2003
+ await patchRoute.handler({
2004
+ body: { acp: { agents: { claude: { command: null } } } },
2005
+ });
2006
+
2007
+ const patched = agentEntry(loadRawConfig());
2008
+ expect("command" in patched).toBe(false);
2009
+ expect(patched.model).toBe("opus");
2010
+ expect(AssistantConfigSchema.safeParse(loadRawConfig()).success).toBe(true);
2011
+ expect(loadConfig().acp.agents.claude?.command).toBeUndefined();
2012
+ });
2013
+
2014
+ test("a nulled command on an id with no bundled profile is dropped too", async () => {
2015
+ rawConfigFixture = {
2016
+ acp: { agents: { mine: { command: "my-acp", args: [] } } },
2017
+ };
2018
+ seedRawConfig();
2019
+
2020
+ await setRoute.handler({
2021
+ body: { path: "acp.agents.mine.command", value: null },
2022
+ });
2023
+
2024
+ expect("command" in agentEntry(loadRawConfig(), "mine")).toBe(false);
2025
+ const result = AssistantConfigSchema.safeParse(loadRawConfig());
2026
+ expect(result.success).toBe(false);
2027
+ if (result.success) {
2028
+ return;
2029
+ }
2030
+ expect(result.error.issues.map((issue) => issue.path)).toEqual([
2031
+ ["acp", "agents", "mine", "command"],
2032
+ ]);
2033
+ expect(result.error.issues[0]?.message).toContain("acp.agents.mine");
2034
+ });
2035
+
2036
+ test("a model set on a bare bundled entry survives the next load", async () => {
2037
+ rawConfigFixture = {};
2038
+ seedRawConfig();
2039
+
2040
+ await setRoute.handler({
2041
+ body: { path: "acp.agents.claude.model", value: "sonnet" },
2042
+ });
2043
+
2044
+ const entry = agentEntry(loadRawConfig());
2045
+ expect("command" in entry).toBe(false);
2046
+ expect(AssistantConfigSchema.safeParse(loadRawConfig()).success).toBe(true);
2047
+ expect(loadConfig().acp.agents.claude?.model).toBe("sonnet");
2048
+ });
2049
+ });
@@ -377,7 +377,7 @@ describe("GET config/llm/default-provider", () => {
377
377
  expect(availability(result).message).toContain("Vellum-managed");
378
378
  });
379
379
 
380
- test("service_account connection → unsupported_auth even with a stored credential", async () => {
380
+ test("service_account connection with invalid credential → missing_credential", async () => {
381
381
  seedConnection({
382
382
  name: "gemini-personal",
383
383
  provider: "gemini",
@@ -390,7 +390,7 @@ describe("GET config/llm/default-provider", () => {
390
390
  setConfig("llm", { defaultProvider: { provider: "gemini" } });
391
391
 
392
392
  const result = await get();
393
- expect(availability(result).status).toBe("unsupported_auth");
393
+ expect(availability(result).status).toBe("missing_credential");
394
394
  expect(availability(result).message).toContain("service-account");
395
395
  });
396
396
 
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The dictation cleanup pass rewrites what the user actually said, so a
3
+ * rewrite the provider cut off at the token cap must never become the
4
+ * payload: the raw transcript is sent instead.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import {
10
+ computeMaxTokens,
11
+ resolveCleanedDictation,
12
+ } from "../diagnostics-routes.js";
13
+
14
+ const SPOKEN =
15
+ "Hello Quill can you generate a list of 20 open HR coordinator or HR " +
16
+ "related roles that are remote and pay about 50 to 60 K per year";
17
+
18
+ describe("resolveCleanedDictation", () => {
19
+ test("accepts an ordinary cleanup", () => {
20
+ const cleaned =
21
+ "Hello Quill, can you generate a list of 20 open HR coordinator or HR " +
22
+ "related roles that are remote and pay about 50 to 60K per year?";
23
+ expect(resolveCleanedDictation(SPOKEN, cleaned, "end_turn")).toEqual({
24
+ text: cleaned,
25
+ rejected: null,
26
+ });
27
+ });
28
+
29
+ test("trims surrounding whitespace off an accepted cleanup", () => {
30
+ const { text } = resolveCleanedDictation(
31
+ "so um I think we should ship the thing on Tuesday you know",
32
+ " I think we should ship the thing on Tuesday. ",
33
+ "end_turn",
34
+ );
35
+ expect(text).toBe("I think we should ship the thing on Tuesday.");
36
+ });
37
+
38
+ test("rejects a rewrite cut off by max_tokens and keeps the raw words", () => {
39
+ expect(
40
+ resolveCleanedDictation(
41
+ SPOKEN,
42
+ "Hello Quill, can you create a list",
43
+ "max_tokens",
44
+ ),
45
+ ).toEqual({ text: SPOKEN, rejected: "truncated" });
46
+ });
47
+
48
+ test("rejects a max_tokens rewrite even when it kept most of the words", () => {
49
+ const nearlyComplete = SPOKEN.slice(0, SPOKEN.length - 4);
50
+ const { rejected } = resolveCleanedDictation(
51
+ SPOKEN,
52
+ nearlyComplete,
53
+ "max_tokens",
54
+ );
55
+ expect(rejected).toBe("truncated");
56
+ });
57
+
58
+ test("rejects the token-cap stop under every provider's name for it", () => {
59
+ for (const stopReason of ["length", "max_output_tokens", "MAX_TOKENS "]) {
60
+ expect(
61
+ resolveCleanedDictation(SPOKEN, "Hello Quill, can you", stopReason)
62
+ .rejected,
63
+ ).toBe("truncated");
64
+ }
65
+ });
66
+
67
+ test("a missing stop reason is not treated as a truncation", () => {
68
+ const cleaned = "Hello Quill, can you generate a list?";
69
+ expect(resolveCleanedDictation(SPOKEN, cleaned, undefined)).toEqual({
70
+ text: cleaned,
71
+ rejected: null,
72
+ });
73
+ });
74
+
75
+ test("a much shorter rewrite is the model's call, not a rejection", () => {
76
+ // The prompt asks for fillers and vague phrasing to go, so a concise
77
+ // rewrite of a rambling utterance is a correct cleanup.
78
+ const spoken =
79
+ "um you know I just kind of wanted to say like thank you so much you know";
80
+ expect(
81
+ resolveCleanedDictation(spoken, "Thank you so much.", "end_turn"),
82
+ ).toEqual({ text: "Thank you so much.", rejected: null });
83
+ });
84
+
85
+ test("an empty rewrite falls back to raw without flagging a rejection", () => {
86
+ // The model returned nothing usable; there is no rewrite to distrust.
87
+ expect(resolveCleanedDictation(SPOKEN, " ", "end_turn")).toEqual({
88
+ text: SPOKEN,
89
+ rejected: null,
90
+ });
91
+ });
92
+ });
93
+
94
+ describe("computeMaxTokens", () => {
95
+ test("budgets room for the transcript, reasoning, and JSON scaffolding", () => {
96
+ // The tool call carries the whole transcript back in `text`, so the
97
+ // budget has to clear the input's own token count with room to spare.
98
+ const estimatedInputTokens = Math.ceil(SPOKEN.length / 3);
99
+ expect(computeMaxTokens(SPOKEN.length)).toBeGreaterThan(
100
+ estimatedInputTokens * 2,
101
+ );
102
+ });
103
+
104
+ test("holds a floor for very short transcripts", () => {
105
+ expect(computeMaxTokens(5)).toBe(512);
106
+ });
107
+ });
@@ -1,19 +1,24 @@
1
1
  /**
2
- * `internal_mcp_add` writes only the transport. Policy fields (enabled,
3
- * risk, tool caps, allow/block lists) are not persisted.
2
+ * `internal_mcp_add` writes only the transport into workspace mcp.json.
3
+ * Policy fields (enabled, risk, tool caps, allow/block lists) are not
4
+ * persisted.
4
5
  */
5
6
 
6
7
  import { beforeEach, describe, expect, mock, test } from "bun:test";
7
8
 
8
- let raw: Record<string, unknown> = {};
9
- let saved: Record<string, unknown> | undefined;
9
+ let stored: { servers: Record<string, { transport: Record<string, unknown> }> } =
10
+ { servers: {} };
11
+ let saved:
12
+ | { servers: Record<string, { transport: Record<string, unknown> }> }
13
+ | undefined;
10
14
 
11
- const actualLoader = await import("../../../config/loader.js");
12
- mock.module("../../../config/loader.js", () => ({
13
- ...actualLoader,
14
- loadRawConfig: () => raw,
15
- saveRawConfig: (next: Record<string, unknown>) => {
15
+ mock.module("../../../mcp/workspace-mcp-config.js", () => ({
16
+ loadWorkspaceMcpConfig: () => stored,
17
+ saveWorkspaceMcpConfig: (next: {
18
+ servers: Record<string, { transport: Record<string, unknown> }>;
19
+ }) => {
16
20
  saved = next;
21
+ stored = next;
17
22
  },
18
23
  }));
19
24
 
@@ -26,13 +31,12 @@ const { ROUTES } = await import("../mcp-auth-routes.js");
26
31
  const addRoute = ROUTES.find((r) => r.operationId === "internal_mcp_add")!;
27
32
 
28
33
  function addedEntry(): Record<string, unknown> {
29
- const mcp = saved?.mcp as { servers: Record<string, unknown> };
30
- return mcp.servers["srv"] as Record<string, unknown>;
34
+ return saved!.servers["srv"] as Record<string, unknown>;
31
35
  }
32
36
 
33
37
  describe("internal_mcp_add persisted shape", () => {
34
38
  beforeEach(() => {
35
- raw = {};
39
+ stored = { servers: {} };
36
40
  saved = undefined;
37
41
  });
38
42
 
@@ -262,6 +262,21 @@ describe("POST /v1/acp/spawn", () => {
262
262
  // Guard runs before the approval gate — no prompt is surfaced.
263
263
  expect(confirmationRequests).toHaveLength(0);
264
264
  });
265
+
266
+ test("rejects a non-string model before the approval gate", async () => {
267
+ const handler = getSpawnHandler();
268
+ await expect(
269
+ handler({
270
+ body: {
271
+ agent: "claude",
272
+ task: "do a thing",
273
+ conversationId: "conv-1",
274
+ model: 42,
275
+ },
276
+ }),
277
+ ).rejects.toThrow("model must be a string when provided");
278
+ expect(confirmationRequests).toHaveLength(0);
279
+ });
265
280
  });
266
281
 
267
282
  // ---------------------------------------------------------------------------