@vellumai/assistant 0.11.3 → 0.11.4-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 (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -14,6 +14,7 @@ import type {
14
14
  ConversationStreamingMode,
15
15
  SttBoundaryId,
16
16
  SttProviderId,
17
+ SttTurnDetectionMode,
17
18
  TelephonySttMode,
18
19
  } from "../../stt/types.js";
19
20
  import { baseLanguageSubtag } from "../../util/language-subtag.js";
@@ -87,6 +88,14 @@ interface SttProviderEntry {
87
88
  */
88
89
  readonly conversationStreamingMode: ConversationStreamingMode;
89
90
 
91
+ /**
92
+ * Whether the provider decides end-of-turn itself, in-band with the audio
93
+ * it transcribes, or leaves the boundary to the session's local silence
94
+ * timer. A live-voice session reads this to decide whether to arm its
95
+ * provider turn-end path; it never names a provider directly.
96
+ */
97
+ readonly turnDetection: SttTurnDetectionMode;
98
+
90
99
  /**
91
100
  * Whether the provider can attribute transcribed speech to distinct
92
101
  * speakers (speaker diarization). When `true`, callers may opt in to
@@ -105,8 +114,9 @@ interface SttProviderEntry {
105
114
  *
106
115
  * - `"manual"`: the provider accepts an explicit language parameter and
107
116
  * defaults to English when omitted; a language picker is meaningful.
108
- * - `"auto"`: the provider detects language natively and accepts no
109
- * language parameter; a picker would be a no-op, so clients must hide it.
117
+ * - `"auto"`: the provider takes no language parameter, either because it
118
+ * detects the language natively or because its model is monolingual; a
119
+ * picker would be a no-op, so clients must hide it.
110
120
  */
111
121
  readonly languageSelection: "manual" | "auto";
112
122
 
@@ -118,6 +128,17 @@ interface SttProviderEntry {
118
128
  // Catalog data
119
129
  // ---------------------------------------------------------------------------
120
130
 
131
+ /**
132
+ * Shared by every provider that authenticates against a Deepgram account.
133
+ * The key is the same one, so the instructions must not drift apart.
134
+ */
135
+ const DEEPGRAM_CREDENTIALS_GUIDE: SttCredentialsGuide = {
136
+ description:
137
+ "Sign in to the Deepgram console, navigate to API Keys, and create a new key.",
138
+ url: "https://console.deepgram.com/",
139
+ linkLabel: "Open Deepgram Console",
140
+ };
141
+
121
142
  /**
122
143
  * Provider catalog entries, keyed by provider ID.
123
144
  *
@@ -147,14 +168,41 @@ const CATALOG: ReadonlyMap<SttProviderId, SttProviderEntry> = new Map<
147
168
  ]),
148
169
  telephonyMode: "realtime-ws",
149
170
  conversationStreamingMode: "realtime-ws",
171
+ turnDetection: "none",
150
172
  supportsDiarization: true,
151
173
  languageSelection: "manual",
152
- credentialsGuide: {
153
- description:
154
- "Sign in to the Deepgram console, navigate to API Keys, and create a new key.",
155
- url: "https://console.deepgram.com/",
156
- linkLabel: "Open Deepgram Console",
157
- },
174
+ credentialsGuide: DEEPGRAM_CREDENTIALS_GUIDE,
175
+ },
176
+ ],
177
+ [
178
+ "deepgram-flux",
179
+ {
180
+ id: "deepgram-flux",
181
+ displayName: "Deepgram Flux",
182
+ subtitle:
183
+ "Conversational speech-to-text with model-native turn detection. Uses your Deepgram API key.",
184
+ setupMode: "api-key",
185
+ setupHint:
186
+ "Enter your Deepgram API key. Flux shares the same key as Deepgram.",
187
+ // Shared with the `deepgram` provider: Flux is a model on the same
188
+ // account, not a separate credential.
189
+ credentialProvider: "deepgram",
190
+ // Streaming only: Flux has no batch endpoint.
191
+ supportedBoundaries: new Set<SttBoundaryId>(["daemon-streaming"]),
192
+ // Telephony is out of scope for the spike. Nothing reroutes a call to
193
+ // another provider, so a Flux-configured assistant does not transcribe
194
+ // calls at all.
195
+ telephonyMode: "none",
196
+ conversationStreamingMode: "realtime-ws",
197
+ // Flux emits turn lifecycle events on its transcript stream and numbers
198
+ // them, so a live-voice session may let them commit the turn.
199
+ turnDetection: "provider",
200
+ supportsDiarization: false,
201
+ // "no picker", not native detection: the Flux model is English-only and
202
+ // takes no language parameter, so audio in another language transcribes
203
+ // as English rather than being detected.
204
+ languageSelection: "auto",
205
+ credentialsGuide: DEEPGRAM_CREDENTIALS_GUIDE,
158
206
  },
159
207
  ],
160
208
  [
@@ -174,6 +222,7 @@ const CATALOG: ReadonlyMap<SttProviderId, SttProviderEntry> = new Map<
174
222
  ]),
175
223
  telephonyMode: "batch-only",
176
224
  conversationStreamingMode: "realtime-ws",
225
+ turnDetection: "none",
177
226
  supportsDiarization: false,
178
227
  languageSelection: "auto",
179
228
  credentialsGuide: {
@@ -200,6 +249,7 @@ const CATALOG: ReadonlyMap<SttProviderId, SttProviderEntry> = new Map<
200
249
  ]),
201
250
  telephonyMode: "batch-only",
202
251
  conversationStreamingMode: "incremental-batch",
252
+ turnDetection: "none",
203
253
  supportsDiarization: false,
204
254
  languageSelection: "auto",
205
255
  credentialsGuide: {
@@ -226,6 +276,7 @@ const CATALOG: ReadonlyMap<SttProviderId, SttProviderEntry> = new Map<
226
276
  ]),
227
277
  telephonyMode: "realtime-ws",
228
278
  conversationStreamingMode: "realtime-ws",
279
+ turnDetection: "none",
229
280
  supportsDiarization: false,
230
281
  // The relay dials Deepgram nova-3 server-side, which takes an explicit
231
282
  // language parameter.
@@ -248,6 +299,7 @@ const CATALOG: ReadonlyMap<SttProviderId, SttProviderEntry> = new Map<
248
299
  ]),
249
300
  telephonyMode: "batch-only",
250
301
  conversationStreamingMode: "realtime-ws",
302
+ turnDetection: "none",
251
303
  supportsDiarization: true,
252
304
  languageSelection: "manual",
253
305
  credentialsGuide: {
@@ -342,6 +394,32 @@ export function supportsBoundary(
342
394
  return CATALOG.get(id)?.supportedBoundaries.has(boundary) ?? false;
343
395
  }
344
396
 
397
+ /**
398
+ * Message explaining why a provider cannot serve the `daemon-batch` boundary.
399
+ *
400
+ * Streaming-only providers are selected through the same
401
+ * `services.stt.provider` key as batch-capable ones, so a batch caller that
402
+ * reports only "nothing is configured" points the operator at the wrong
403
+ * problem. This names the provider and, where one exists, the batch-capable
404
+ * provider on the same credential, so acting on it needs no catalog reading.
405
+ */
406
+ export function batchBoundaryGapReason(id: SttProviderId): string {
407
+ const entry = CATALOG.get(id);
408
+ const label = entry?.displayName ?? id;
409
+ const alternative = entry
410
+ ? [...CATALOG.values()].find(
411
+ (candidate) =>
412
+ candidate.id !== entry.id &&
413
+ candidate.credentialProvider === entry.credentialProvider &&
414
+ candidate.supportedBoundaries.has("daemon-batch"),
415
+ )
416
+ : undefined;
417
+ const remedy = alternative
418
+ ? `Batch transcription requires the ${alternative.id} provider: set services.stt.provider to "${alternative.id}".`
419
+ : "Set services.stt.provider to a provider that supports batch transcription.";
420
+ return `${label} is streaming-only. ${remedy}`;
421
+ }
422
+
345
423
  /**
346
424
  * Check whether a provider supports speaker diarization.
347
425
  *
@@ -353,6 +431,19 @@ export function supportsDiarization(id: SttProviderId): boolean {
353
431
  return CATALOG.get(id)?.supportsDiarization ?? false;
354
432
  }
355
433
 
434
+ /**
435
+ * Check whether a provider decides end-of-turn itself.
436
+ *
437
+ * Returns `false` for unknown provider IDs, which keeps an unrecognized
438
+ * provider on the local silence boundary rather than waiting for turn events
439
+ * that will never arrive. A live-voice session reads this instead of naming a
440
+ * provider, so a new turn-detecting provider is a catalog entry rather than a
441
+ * session change.
442
+ */
443
+ export function supportsProviderTurnDetection(id: SttProviderId): boolean {
444
+ return CATALOG.get(id)?.turnDetection === "provider";
445
+ }
446
+
356
447
  /**
357
448
  * Return all canonical provider IDs in deterministic (insertion) order.
358
449
  */
@@ -6,8 +6,10 @@ import type {
6
6
  StreamingTranscriber,
7
7
  SttProviderId,
8
8
  } from "../../stt/types.js";
9
+ import { SttError } from "../../stt/types.js";
9
10
  import { getLogger } from "../../util/logger.js";
10
11
  import {
12
+ batchBoundaryGapReason,
11
13
  getCredentialProvider,
12
14
  getProviderEntry,
13
15
  supportsBoundary,
@@ -88,8 +90,13 @@ export function effectiveSttLanguage(
88
90
  *
89
91
  * Returns `null` when:
90
92
  * - The configured provider is not in the catalog.
91
- * - The configured provider doesn't support the `daemon-batch` boundary.
92
93
  * - No credentials are configured for the resolved provider.
94
+ *
95
+ * Throws an {@link SttError} when the configured provider is in the catalog
96
+ * but has no `daemon-batch` boundary. That is a configuration mismatch the
97
+ * resolver can name, and returning `null` for it would reach the user as the
98
+ * "no speech-to-text provider is configured" copy every caller pairs with
99
+ * `null`, which is the opposite of what happened.
93
100
  */
94
101
  export async function resolveBatchTranscriber(): Promise<BatchTranscriber | null> {
95
102
  // Snapshot the stt config once, before any await, so a concurrent config
@@ -111,7 +118,9 @@ export async function resolveBatchTranscriber(): Promise<BatchTranscriber | null
111
118
 
112
119
  // Verify the provider supports the daemon-batch boundary.
113
120
  if (!supportsBoundary(provider as SttProviderId, "daemon-batch")) {
114
- return null;
121
+ const reason = batchBoundaryGapReason(provider as SttProviderId);
122
+ log.warn({ providerId: provider }, reason);
123
+ throw new SttError("provider-error", reason, { userFacing: true });
115
124
  }
116
125
 
117
126
  if (provider === "vellum") {
@@ -632,6 +641,20 @@ async function createStreamingTranscriber(
632
641
  : {}),
633
642
  });
634
643
  }
644
+ case "deepgram-flux": {
645
+ // Flux is streaming-only and dials Deepgram's /v2/listen conversational
646
+ // endpoint. Turn-detection tuning comes from `liveVoice.flux`, which
647
+ // the adapter reads itself, so only the transport-level sample rate is
648
+ // passed here. No language is forwarded: the spike pins the
649
+ // English-only `flux-general-en`, and `language_hint` means nothing to
650
+ // a monolingual model. Diarization is off in the catalog, so a
651
+ // `"required"` caller never reaches this case.
652
+ const { DeepgramFluxRealtimeTranscriber } =
653
+ await import("./deepgram-flux-realtime.js");
654
+ return new DeepgramFluxRealtimeTranscriber(apiKey, {
655
+ sampleRate: options.sampleRate,
656
+ });
657
+ }
635
658
  default: {
636
659
  const _exhaustive: never = providerId;
637
660
  return null;
@@ -25,6 +25,11 @@ import type { IpcEnvelope } from "@vellumai/ipc-server-utils";
25
25
  import { IpcFrameReader, writeMessage } from "@vellumai/ipc-server-utils";
26
26
 
27
27
  import { disableStreamSeqStamping } from "../runtime/assistant-stream-state.js";
28
+ import {
29
+ evictRouteSourceTree,
30
+ importRouteModule,
31
+ sourceRootForHandler,
32
+ } from "../runtime/routes/user-route-import.js";
28
33
  import { getLogger } from "../util/logger.js";
29
34
  import {
30
35
  ensureProcDir,
@@ -43,6 +48,9 @@ import {
43
48
 
44
49
  const log = getLogger("route-host");
45
50
 
51
+ /** Last entry-file mtime imported in this process, keyed by handler path. */
52
+ const lastHandlerMtime = new Map<string, number>();
53
+
46
54
  const socketPath = getProcSocketPath(ROUTE_HOST_PROC_NAME);
47
55
  const pidPath = getProcPidPath(ROUTE_HOST_PROC_NAME);
48
56
 
@@ -110,11 +118,15 @@ async function handleInvoke(
110
118
  params: RouteInvokeParams,
111
119
  body: Uint8Array | undefined,
112
120
  ): Promise<void> {
113
- // Each worker has its own module cache; cache-bust per mtime so an edited
114
- // handler is re-imported.
115
- const mod = (await import(
116
- `${params.filePath}?t=${params.mtimeMs}`
117
- )) as Record<string, unknown>;
121
+ // Each worker has its own module cache. When the entry file's mtime
122
+ // moves, evict the whole source tree so a new handler cannot bind to a
123
+ // stale helper, then re-import.
124
+ const lastMtime = lastHandlerMtime.get(params.filePath);
125
+ if (lastMtime !== params.mtimeMs) {
126
+ evictRouteSourceTree(sourceRootForHandler(params.filePath));
127
+ lastHandlerMtime.set(params.filePath, params.mtimeMs);
128
+ }
129
+ const mod = await importRouteModule(params.filePath);
118
130
 
119
131
  const handler = mod[params.method];
120
132
  if (typeof handler !== "function") {
@@ -29,6 +29,7 @@ import { buildVellumCardAffinity } from "../notifications/vellum-card-affinity.j
29
29
  import { IntegrityError } from "../util/errors.js";
30
30
  import { getLogger } from "../util/logger.js";
31
31
  import { resolveAnchoredGuardian } from "./anchored-guardian.js";
32
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
32
33
  import { CHALLENGE_TTL_MS } from "./channel-verification-service.js";
33
34
  import {
34
35
  type AccessRequestTrigger,
@@ -46,7 +47,6 @@ const log = getLogger("access-request-helper");
46
47
  export type { AccessRequestTrigger } from "./introduction-policy.js";
47
48
 
48
49
  export interface AccessRequestParams {
49
- canonicalAssistantId: string;
50
50
  sourceChannel: ChannelId;
51
51
  conversationExternalId: string;
52
52
  actorExternalId?: string;
@@ -93,15 +93,18 @@ export type AccessRequestResult =
93
93
  // ---------------------------------------------------------------------------
94
94
 
95
95
  /**
96
- * Assistant-scoped conversation id for an actor's access requests. Stable key
97
- * that dedupes pending prompts for the same (assistant, channel, actor).
96
+ * Conversation id for an actor's access requests. Stable key that dedupes
97
+ * pending prompts for the same (channel, actor).
98
+ *
99
+ * The `self` segment is part of a persisted id format: guardian request rows
100
+ * carry these ids, so the shape is fixed even though the daemon scopes all its
101
+ * storage to the one assistant (`DAEMON_INTERNAL_ASSISTANT_ID`).
98
102
  */
99
103
  export function accessRequestConversationId(
100
- canonicalAssistantId: string,
101
104
  sourceChannel: string,
102
105
  actorExternalId: string,
103
106
  ): string {
104
- return `access-req-${canonicalAssistantId}-${sourceChannel}-${actorExternalId}`;
107
+ return `access-req-${DAEMON_INTERNAL_ASSISTANT_ID}-${sourceChannel}-${actorExternalId}`;
105
108
  }
106
109
 
107
110
  /**
@@ -126,7 +129,6 @@ export function accessRequestConversationId(
126
129
  * fail-closed.
127
130
  */
128
131
  export async function isApprovalHandshakeInProgress(params: {
129
- canonicalAssistantId: string;
130
132
  sourceChannel: string;
131
133
  actorExternalId: string;
132
134
  }): Promise<boolean> {
@@ -134,7 +136,6 @@ export async function isApprovalHandshakeInProgress(params: {
134
136
  return false;
135
137
  }
136
138
  const conversationId = accessRequestConversationId(
137
- params.canonicalAssistantId,
138
139
  params.sourceChannel,
139
140
  params.actorExternalId,
140
141
  );
@@ -171,7 +172,6 @@ export async function notifyGuardianOfAccessRequest(
171
172
  params: AccessRequestParams,
172
173
  ): Promise<AccessRequestResult> {
173
174
  const {
174
- canonicalAssistantId,
175
175
  sourceChannel,
176
176
  conversationExternalId,
177
177
  actorExternalId,
@@ -219,7 +219,6 @@ export async function notifyGuardianOfAccessRequest(
219
219
  // from assistant A could be returned for assistant B, allowing the caller
220
220
  // to piggyback on A's guardian approval.
221
221
  const conversationId = accessRequestConversationId(
222
- canonicalAssistantId,
223
222
  sourceChannel,
224
223
  actorExternalId,
225
224
  );
@@ -270,7 +269,6 @@ export async function notifyGuardianOfAccessRequest(
270
269
  // re-prompting is allowed again.
271
270
  if (
272
271
  await isApprovalHandshakeInProgress({
273
- canonicalAssistantId,
274
272
  sourceChannel,
275
273
  actorExternalId,
276
274
  })
@@ -283,7 +281,7 @@ export async function notifyGuardianOfAccessRequest(
283
281
  }
284
282
 
285
283
  const senderIdentifier = actorDisplayName || actorUsername || actorExternalId;
286
- const requestId = `access-req-${canonicalAssistantId}-${sourceChannel}-${actorExternalId}-${Date.now()}`;
284
+ const requestId = `access-req-${DAEMON_INTERNAL_ASSISTANT_ID}-${sourceChannel}-${actorExternalId}-${Date.now()}`;
287
285
 
288
286
  // Access requests are decisionable: without a bound principal nobody could
289
287
  // ever decide them (mirrors the gateway create's integrity guard).
@@ -461,7 +459,6 @@ export async function maybeNotifyGuardianOfAdmittedContact(
461
459
  }
462
460
 
463
461
  const conversationId = accessRequestConversationId(
464
- params.canonicalAssistantId,
465
462
  params.sourceChannel,
466
463
  params.actorExternalId,
467
464
  );
@@ -718,9 +718,9 @@ export async function wakeAgentForOpportunity(
718
718
  return runWakeSingleFlight(conversationId, async () => {
719
719
  // Snapshot the conversation's resting trust before the resolver runs, so
720
720
  // it can be restored after. The resolver leaves the wake's trust on the
721
- // conversation, and a following no-trust wake would otherwise read it via
722
- // tool setup's `currentTurnTrustContext ?? trustContext` fallback. Null
723
- // when the conversation isn't resident yet (a fresh hydrate or a fork).
721
+ // conversation, and a following no-trust wake would otherwise pick it up
722
+ // from the resting trust its consumers fall back to. Null when the
723
+ // conversation isn't resident yet (a fresh hydrate or a fork).
724
724
  let priorPersistentTrust: TrustContext | null = null;
725
725
  if (opts.trustContext) {
726
726
  const { findConversation } =
@@ -55,9 +55,13 @@ export function hasReceivedUserMessage(): boolean {
55
55
  try {
56
56
  const row = rawGet<{ one: number }>(
57
57
  "preFirstMsg:hasReceivedUserMessage",
58
+ // Finalized user-role rows only: the grouped tool-result row is a
59
+ // user-role row reserved unfinalized mid-turn, and this gate must not
60
+ // read tool plumbing as "the user has messaged this assistant".
58
61
  `SELECT 1 AS one FROM messages m
59
62
  JOIN conversations c ON m.conversation_id = c.id
60
63
  WHERE m.role = 'user'
64
+ AND m.finalized = 1
61
65
  AND c.conversation_type = 'standard'
62
66
  LIMIT 1`,
63
67
  );
@@ -47,7 +47,7 @@ mock.module("../../../security/oauth2.js", () => ({
47
47
  }));
48
48
 
49
49
  const actualClaudeOauth = await import("../../../acp/acp-claude-oauth.js");
50
- const { CLAUDE_MANUAL_REDIRECT_URI } = actualClaudeOauth;
50
+ const { CLAUDE_MANUAL_REDIRECT_URI, CLAUDE_OAUTH_CONFIG } = actualClaudeOauth;
51
51
  const storeAcpClaudeTokenMock = mock(async (_token: string) => {});
52
52
  // The connect-status route reads token presence; mock it so the route test
53
53
  // doesn't reach real secure storage.
@@ -125,7 +125,7 @@ function deferredFlow(state: string): {
125
125
  reject = rej;
126
126
  });
127
127
  prepareOAuth2FlowMock.mockImplementationOnce(async () => ({
128
- authorizeUrl: `https://claude.ai/oauth/authorize?state=${state}`,
128
+ authorizeUrl: `${CLAUDE_OAUTH_CONFIG.authorizeUrl}?state=${state}`,
129
129
  state,
130
130
  completion,
131
131
  }));
@@ -153,7 +153,7 @@ describe("acp_claude_auth_start", () => {
153
153
 
154
154
  const url = new URL(result.authorize_url);
155
155
  expect(`${url.origin}${url.pathname}`).toBe(
156
- "https://claude.ai/oauth/authorize",
156
+ CLAUDE_OAUTH_CONFIG.authorizeUrl,
157
157
  );
158
158
  expect(url.searchParams.get("client_id")).toBe(CLAUDE_CLIENT_ID);
159
159
  expect(url.searchParams.get("code_challenge_method")).toBe("S256");
@@ -161,6 +161,11 @@ describe("acp_claude_auth_start", () => {
161
161
  const redirectUri = url.searchParams.get("redirect_uri") ?? "";
162
162
  expect(redirectUri).toMatch(/^http:\/\/localhost:\d+\/callback$/);
163
163
 
164
+ // The loopback flow performs a REAL redirect to the localhost callback;
165
+ // `code=true` (render the code on the page) belongs to the manual flow
166
+ // only and must not leak into this URL.
167
+ expect(url.searchParams.get("code")).toBeNull();
168
+
164
169
  expect(result.state).toBeTruthy();
165
170
  expect(url.searchParams.get("state")).toBe(result.state);
166
171
 
@@ -284,7 +289,7 @@ describe("acp_claude_auth_start (cloud/manual)", () => {
284
289
  expect(result.mode).toBe("manual");
285
290
  const url = new URL(result.authorize_url);
286
291
  expect(`${url.origin}${url.pathname}`).toBe(
287
- "https://claude.ai/oauth/authorize",
292
+ CLAUDE_OAUTH_CONFIG.authorizeUrl,
288
293
  );
289
294
  expect(url.searchParams.get("redirect_uri")).toBe(
290
295
  CLAUDE_MANUAL_REDIRECT_URI,
@@ -292,6 +297,9 @@ describe("acp_claude_auth_start (cloud/manual)", () => {
292
297
  expect(url.searchParams.get("client_id")).toBe(CLAUDE_CLIENT_ID);
293
298
  expect(url.searchParams.get("code_challenge_method")).toBe("S256");
294
299
  expect(url.searchParams.get("state")).toBe(result.state);
300
+ // Manual flow requires `code=true` so the callback page renders the
301
+ // `code#state` string to paste back.
302
+ expect(url.searchParams.get("code")).toBe("true");
295
303
 
296
304
  // The manual path must not bind a loopback.
297
305
  expect(prepareOAuth2FlowMock).not.toHaveBeenCalled();