@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
@@ -0,0 +1,325 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { collapseProfileBindingsToEntriesMigration } from "../workspace/migrations/145-collapse-profile-bindings-to-entries.js";
14
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
15
+
16
+ let workspaceDir: string;
17
+
18
+ function writeConfig(data: Record<string, unknown>): void {
19
+ writeFileSync(
20
+ join(workspaceDir, "config.json"),
21
+ JSON.stringify(data, null, 2) + "\n",
22
+ );
23
+ }
24
+
25
+ function readProfiles(): Record<string, Record<string, unknown>> {
26
+ const raw = JSON.parse(
27
+ readFileSync(join(workspaceDir, "config.json"), "utf-8"),
28
+ ) as { llm?: { profiles?: Record<string, Record<string, unknown>> } };
29
+ return raw.llm?.profiles ?? {};
30
+ }
31
+
32
+ function seedRows(rows: Array<{ name: string; provider: string }>): void {
33
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
34
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
35
+ db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
36
+ name TEXT PRIMARY KEY,
37
+ provider TEXT NOT NULL,
38
+ auth TEXT NOT NULL,
39
+ created_at INTEGER NOT NULL,
40
+ updated_at INTEGER NOT NULL
41
+ )`);
42
+ for (const row of rows) {
43
+ db.query(
44
+ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
45
+ ).run(row.name, row.provider);
46
+ }
47
+ db.close();
48
+ }
49
+
50
+ function run(): void {
51
+ collapseProfileBindingsToEntriesMigration.run(workspaceDir);
52
+ }
53
+
54
+ beforeEach(() => {
55
+ workspaceDir = join(
56
+ tmpdir(),
57
+ `vellum-migration-145-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
58
+ );
59
+ mkdirSync(workspaceDir, { recursive: true });
60
+ });
61
+
62
+ afterEach(() => {
63
+ if (existsSync(workspaceDir)) {
64
+ assertNotLiveDb(workspaceDir);
65
+ rmSync(workspaceDir, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ describe("145-collapse-profile-bindings-to-entries", () => {
70
+ test("folds a kind-agreeing binding into provider as the entry name", () => {
71
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
72
+ writeConfig({
73
+ llm: {
74
+ profiles: {
75
+ work: {
76
+ provider: "anthropic",
77
+ provider_connection: "anthropic-work",
78
+ model: "claude-opus-4-8",
79
+ },
80
+ },
81
+ },
82
+ });
83
+
84
+ run();
85
+
86
+ const work = readProfiles().work;
87
+ expect(work.provider).toBe("anthropic-work");
88
+ expect(work).not.toHaveProperty("provider_connection");
89
+ expect(work.model).toBe("claude-opus-4-8");
90
+ });
91
+
92
+ test("maps identity kinds when judging agreement", () => {
93
+ seedRows([
94
+ { name: "team-managed", provider: "vellum" },
95
+ { name: "chatgpt-subscription", provider: "chatgpt" },
96
+ ]);
97
+ writeConfig({
98
+ llm: {
99
+ profiles: {
100
+ managed: {
101
+ provider: "anthropic",
102
+ provider_connection: "team-managed",
103
+ model: "claude-opus-4-8",
104
+ },
105
+ codex: {
106
+ provider: "openai",
107
+ provider_connection: "chatgpt-subscription",
108
+ model: "gpt-5.5",
109
+ },
110
+ },
111
+ },
112
+ });
113
+
114
+ run();
115
+
116
+ expect(readProfiles().managed.provider).toBe("team-managed");
117
+ expect(readProfiles().codex.provider).toBe("chatgpt-subscription");
118
+ });
119
+
120
+ test("keeps the vendor and drops a dangling binding with a warn", () => {
121
+ seedRows([]);
122
+ writeConfig({
123
+ llm: {
124
+ profiles: {
125
+ stale: {
126
+ provider: "anthropic",
127
+ provider_connection: "deleted-row",
128
+ model: "claude-opus-4-8",
129
+ },
130
+ },
131
+ },
132
+ });
133
+
134
+ run();
135
+
136
+ const stale = readProfiles().stale;
137
+ expect(stale.provider).toBe("anthropic");
138
+ expect(stale).not.toHaveProperty("provider_connection");
139
+ });
140
+
141
+ test("keeps the vendor and drops a kind-disagreeing binding", () => {
142
+ seedRows([{ name: "ollama-local", provider: "ollama" }]);
143
+ writeConfig({
144
+ llm: {
145
+ profiles: {
146
+ conflicted: {
147
+ provider: "anthropic",
148
+ provider_connection: "ollama-local",
149
+ model: "claude-opus-4-8",
150
+ },
151
+ },
152
+ },
153
+ });
154
+
155
+ run();
156
+
157
+ const conflicted = readProfiles().conflicted;
158
+ expect(conflicted.provider).toBe("anthropic");
159
+ expect(conflicted).not.toHaveProperty("provider_connection");
160
+ });
161
+
162
+ test("strips a stray binding from a routing-identity profile", () => {
163
+ seedRows([{ name: "vellum", provider: "vellum" }]);
164
+ writeConfig({
165
+ llm: {
166
+ profiles: {
167
+ managed: {
168
+ provider: "vellum",
169
+ provider_connection: "vellum",
170
+ model: "claude-opus-4-8",
171
+ },
172
+ },
173
+ },
174
+ });
175
+
176
+ run();
177
+
178
+ const managed = readProfiles().managed;
179
+ expect(managed.provider).toBe("vellum");
180
+ expect(managed).not.toHaveProperty("provider_connection");
181
+ });
182
+
183
+ test("deletes a self-referential binding when no row claims that name", () => {
184
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
185
+ writeConfig({
186
+ llm: {
187
+ profiles: {
188
+ plain: {
189
+ provider: "anthropic",
190
+ provider_connection: "anthropic",
191
+ model: "claude-opus-4-8",
192
+ },
193
+ },
194
+ },
195
+ });
196
+
197
+ run();
198
+
199
+ expect(readProfiles().plain.provider).toBe("anthropic");
200
+ expect(readProfiles().plain).not.toHaveProperty("provider_connection");
201
+ });
202
+
203
+ test("an absent DB file means genuinely dangling bindings (restored config)", () => {
204
+ writeConfig({
205
+ llm: {
206
+ profiles: {
207
+ bound: {
208
+ provider: "anthropic",
209
+ provider_connection: "anthropic-work",
210
+ model: "claude-opus-4-8",
211
+ },
212
+ },
213
+ },
214
+ });
215
+ // No DB at all: a config restored into a fresh workspace has no rows.
216
+
217
+ run();
218
+
219
+ const bound = readProfiles().bound;
220
+ expect(bound.provider).toBe("anthropic");
221
+ expect(bound).not.toHaveProperty("provider_connection");
222
+ });
223
+
224
+ test("throws (for retry) when bindings exist but the DB is unqueryable", () => {
225
+ writeConfig({
226
+ llm: {
227
+ profiles: {
228
+ bound: {
229
+ provider: "anthropic",
230
+ provider_connection: "anthropic-work",
231
+ model: "claude-opus-4-8",
232
+ },
233
+ },
234
+ },
235
+ });
236
+ // DB file exists but carries no provider_connections table.
237
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
238
+ new Database(join(workspaceDir, "data", "db", "assistant.db")).close();
239
+
240
+ expect(() => run()).toThrow(/not readable/);
241
+ // Config untouched: no destructive guess was made.
242
+ expect(readProfiles().bound.provider_connection).toBe("anthropic-work");
243
+ expect(
244
+ collapseProfileBindingsToEntriesMigration.retryFailedCheckpoint,
245
+ ).toBe(true);
246
+ });
247
+
248
+ test("folds a canonical vellum binding only for identity-servable models", () => {
249
+ seedRows([{ name: "vellum", provider: "vellum" }]);
250
+ writeConfig({
251
+ llm: {
252
+ profiles: {
253
+ routable: {
254
+ provider: "anthropic",
255
+ provider_connection: "vellum",
256
+ model: "claude-opus-4-8",
257
+ },
258
+ stale: {
259
+ provider: "anthropic",
260
+ provider_connection: "vellum",
261
+ model: "claude-ancient-1",
262
+ },
263
+ },
264
+ },
265
+ });
266
+
267
+ run();
268
+
269
+ const routable = readProfiles().routable;
270
+ expect(routable.provider).toBe("vellum");
271
+ expect(routable).not.toHaveProperty("provider_connection");
272
+ // An unservable model would be stripped by the read-path schema if
273
+ // folded to the identity; the profile stays untouched instead.
274
+ const stale = readProfiles().stale;
275
+ expect(stale.provider).toBe("anthropic");
276
+ expect(stale.provider_connection).toBe("vellum");
277
+ });
278
+
279
+ test("keeps a self-named binding when a row by that name exists", () => {
280
+ seedRows([
281
+ { name: "anthropic", provider: "anthropic" },
282
+ { name: "anthropic-2", provider: "anthropic" },
283
+ ]);
284
+ writeConfig({
285
+ llm: {
286
+ profiles: {
287
+ pinned: {
288
+ provider: "anthropic",
289
+ provider_connection: "anthropic",
290
+ model: "claude-opus-4-8",
291
+ },
292
+ },
293
+ },
294
+ });
295
+
296
+ run();
297
+
298
+ // The explicit pin survives: with sibling rows, the bare vendor would
299
+ // auto-resolve differently.
300
+ expect(readProfiles().pinned.provider_connection).toBe("anthropic");
301
+ });
302
+
303
+ test("no-ops without bindings and is idempotent", () => {
304
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
305
+ writeConfig({
306
+ llm: {
307
+ profiles: {
308
+ work: {
309
+ provider: "anthropic",
310
+ provider_connection: "anthropic-work",
311
+ model: "claude-opus-4-8",
312
+ },
313
+ unbound: { provider: "vellum", model: "claude-opus-4-8" },
314
+ },
315
+ },
316
+ });
317
+
318
+ run();
319
+ const afterFirst = readProfiles();
320
+ run();
321
+
322
+ expect(readProfiles()).toEqual(afterFirst);
323
+ expect(afterFirst.unbound.provider).toBe("vellum");
324
+ });
325
+ });
@@ -0,0 +1,235 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { repairRetiredFireworksDeepseekFlashModelIdMigration } from "../workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.js";
14
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
15
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
16
+
17
+ const STALE = "accounts/fireworks/models/deepseek-v4-flash";
18
+ const REPLACEMENT = "accounts/fireworks/models/deepseek-v4-flash-0731";
19
+
20
+ let workspaceDir: string;
21
+
22
+ function freshWorkspace(): void {
23
+ workspaceDir = join(
24
+ tmpdir(),
25
+ `vellum-migration-146-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
26
+ );
27
+ mkdirSync(workspaceDir, { recursive: true });
28
+ }
29
+
30
+ function writeConfig(data: Record<string, unknown>): void {
31
+ writeFileSync(
32
+ join(workspaceDir, "config.json"),
33
+ JSON.stringify(data, null, 2) + "\n",
34
+ );
35
+ }
36
+
37
+ function readConfig(): Record<string, unknown> {
38
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
39
+ }
40
+
41
+ function seedRows(rows: Array<{ name: string; provider: string }>): void {
42
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
43
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
44
+ db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
45
+ name TEXT PRIMARY KEY,
46
+ provider TEXT NOT NULL,
47
+ auth TEXT NOT NULL,
48
+ created_at INTEGER NOT NULL,
49
+ updated_at INTEGER NOT NULL
50
+ )`);
51
+ for (const row of rows) {
52
+ db.query(
53
+ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
54
+ ).run(row.name, row.provider);
55
+ }
56
+ db.close();
57
+ }
58
+
59
+ beforeEach(() => {
60
+ freshWorkspace();
61
+ });
62
+
63
+ afterEach(() => {
64
+ if (existsSync(workspaceDir)) {
65
+ assertNotLiveDb(workspaceDir);
66
+ rmSync(workspaceDir, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ describe("146-repair-retired-fireworks-deepseek-flash-model-id migration", () => {
71
+ test("has correct migration id and is registered", () => {
72
+ expect(repairRetiredFireworksDeepseekFlashModelIdMigration.id).toBe(
73
+ "146-repair-retired-fireworks-deepseek-flash-model-id",
74
+ );
75
+ expect(WORKSPACE_MIGRATIONS.map((m) => m.id)).toContain(
76
+ "146-repair-retired-fireworks-deepseek-flash-model-id",
77
+ );
78
+ });
79
+
80
+ test("repairs the stale ID in default, call sites, and profiles", () => {
81
+ writeConfig({
82
+ llm: {
83
+ default: { provider: "fireworks", model: STALE },
84
+ callSites: {
85
+ recall: { model: STALE, maxTokens: 4096 },
86
+ heartbeat: { model: `${STALE}-tuned` },
87
+ malformed: STALE,
88
+ },
89
+ profiles: {
90
+ "cost-optimized": { provider: "vellum", model: STALE },
91
+ legacy: { model: STALE },
92
+ },
93
+ },
94
+ });
95
+
96
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
97
+
98
+ const llm = readConfig().llm as Record<string, any>;
99
+ expect(llm.default.model).toBe(REPLACEMENT);
100
+ expect(llm.callSites.recall.model).toBe(REPLACEMENT);
101
+ expect(llm.callSites.recall.maxTokens).toBe(4096);
102
+ // Non-exact matches and malformed leaves are untouched. The dated
103
+ // replacement itself extends the stale ID, so the exact match must not
104
+ // fire on already-repaired leaves.
105
+ expect(llm.callSites.heartbeat.model).toBe(`${STALE}-tuned`);
106
+ expect(llm.callSites.malformed).toBe(STALE);
107
+ // Managed profiles stamped provider "vellum" carry Fireworks model IDs.
108
+ expect(llm.profiles["cost-optimized"].model).toBe(REPLACEMENT);
109
+ expect(llm.profiles.legacy.model).toBe(REPLACEMENT);
110
+ });
111
+
112
+ test("repairs entry-bound fragments whose row kind is fireworks or vellum", () => {
113
+ seedRows([
114
+ { name: "my-fireworks", provider: "fireworks" },
115
+ { name: "managed-alt", provider: "vellum" },
116
+ ]);
117
+ writeConfig({
118
+ llm: {
119
+ profiles: {
120
+ bound: { provider: "my-fireworks", model: STALE },
121
+ managed: { provider: "managed-alt", model: STALE },
122
+ },
123
+ },
124
+ });
125
+
126
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
127
+
128
+ const llm = readConfig().llm as Record<string, any>;
129
+ expect(llm.profiles.bound.model).toBe(REPLACEMENT);
130
+ expect(llm.profiles.managed.model).toBe(REPLACEMENT);
131
+ });
132
+
133
+ test("leaves fragments with an explicit other provider untouched", () => {
134
+ seedRows([{ name: "byo-compat", provider: "openai-compatible" }]);
135
+ writeConfig({
136
+ llm: {
137
+ default: { provider: "openai-compatible", model: STALE },
138
+ profiles: {
139
+ byo: { provider: "openai-compatible", model: STALE },
140
+ // Entry-bound profile whose row kind is not repairable.
141
+ compatBound: { provider: "byo-compat", model: STALE },
142
+ // Entry name with no row: dangling, so nothing proves it is a
143
+ // fireworks-kind route.
144
+ dangling: { provider: "ghost", model: STALE },
145
+ },
146
+ },
147
+ });
148
+
149
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
150
+
151
+ const llm = readConfig().llm as Record<string, any>;
152
+ expect(llm.default.model).toBe(STALE);
153
+ expect(llm.profiles.byo.model).toBe(STALE);
154
+ expect(llm.profiles.compatBound.model).toBe(STALE);
155
+ expect(llm.profiles.dangling.model).toBe(STALE);
156
+ });
157
+
158
+ test("leaves entry-name providers untouched when no DB file exists", () => {
159
+ writeConfig({
160
+ llm: {
161
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
162
+ },
163
+ });
164
+
165
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
166
+
167
+ const llm = readConfig().llm as Record<string, any>;
168
+ expect(llm.profiles.bound.model).toBe(STALE);
169
+ });
170
+
171
+ test("throws when an entry-name provider needs rows and the DB is unreadable", () => {
172
+ // A DB file without the provider_connections table is unqueryable, so
173
+ // the run must fail (and retry later) instead of skipping the profile.
174
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
175
+ writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), "");
176
+ writeConfig({
177
+ llm: {
178
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
179
+ },
180
+ });
181
+
182
+ expect(() =>
183
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir),
184
+ ).toThrow();
185
+ const llm = readConfig().llm as Record<string, any>;
186
+ expect(llm.profiles.bound.model).toBe(STALE);
187
+
188
+ // Vendor and absent providers never need the rows, so the same broken
189
+ // DB does not block their repair.
190
+ writeConfig({
191
+ llm: {
192
+ default: { provider: "fireworks", model: STALE },
193
+ profiles: { legacy: { model: STALE } },
194
+ },
195
+ });
196
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
197
+ const repaired = readConfig().llm as Record<string, any>;
198
+ expect(repaired.default.model).toBe(REPLACEMENT);
199
+ expect(repaired.profiles.legacy.model).toBe(REPLACEMENT);
200
+ });
201
+
202
+ test("is idempotent and a no-op without the stale ID", () => {
203
+ writeConfig({
204
+ llm: {
205
+ default: { provider: "fireworks", model: STALE },
206
+ profiles: { fine: { provider: "fireworks", model: REPLACEMENT } },
207
+ },
208
+ });
209
+
210
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
211
+ const first = readConfig();
212
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
213
+ expect(readConfig()).toEqual(first);
214
+
215
+ const llm = first.llm as Record<string, any>;
216
+ expect(llm.default.model).toBe(REPLACEMENT);
217
+ expect(llm.profiles.fine.model).toBe(REPLACEMENT);
218
+ });
219
+
220
+ test("handles missing config, missing llm block, and invalid JSON", () => {
221
+ // No config.json at all.
222
+ expect(() =>
223
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir),
224
+ ).not.toThrow();
225
+
226
+ writeConfig({ theme: "dark" });
227
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir);
228
+ expect(readConfig()).toEqual({ theme: "dark" });
229
+
230
+ writeFileSync(join(workspaceDir, "config.json"), "{not json");
231
+ expect(() =>
232
+ repairRetiredFireworksDeepseekFlashModelIdMigration.run(workspaceDir),
233
+ ).not.toThrow();
234
+ });
235
+ });
@@ -59,8 +59,11 @@ beforeEach(() => {
59
59
 
60
60
  describe("CLAUDE_OAUTH_CONFIG", () => {
61
61
  test("matches the verified endpoints, client id, and scope", () => {
62
+ // The single literal pin for the endpoint. Every other test asserts
63
+ // against CLAUDE_OAUTH_CONFIG.authorizeUrl, so an endpoint change edits
64
+ // exactly this one value.
62
65
  expect(CLAUDE_OAUTH_CONFIG.authorizeUrl).toBe(
63
- "https://claude.ai/oauth/authorize",
66
+ "https://claude.com/cai/oauth/authorize",
64
67
  );
65
68
  expect(CLAUDE_OAUTH_CONFIG.tokenExchangeUrl).toBe(
66
69
  "https://platform.claude.com/v1/oauth/token",
@@ -92,8 +95,10 @@ describe("buildClaudeAuthorizeUrl", () => {
92
95
  });
93
96
 
94
97
  const parsed = new URL(url);
98
+ // The property under test is that the builder uses the configured
99
+ // endpoint; the value itself is guarded by the literal pin above.
95
100
  expect(`${parsed.origin}${parsed.pathname}`).toBe(
96
- "https://claude.ai/oauth/authorize",
101
+ CLAUDE_OAUTH_CONFIG.authorizeUrl,
97
102
  );
98
103
 
99
104
  const params = parsed.searchParams;
@@ -106,6 +111,9 @@ describe("buildClaudeAuthorizeUrl", () => {
106
111
  expect(params.get("state")).toBe("state-abc");
107
112
  expect(params.get("code_challenge")).toBe("challenge-123");
108
113
  expect(params.get("code_challenge_method")).toBe("S256");
114
+ // The manual flow's defining param: tells Claude to render `code#state`
115
+ // on the callback page. Claude rejects a manual-redirect grant without it.
116
+ expect(params.get("code")).toBe("true");
109
117
  });
110
118
 
111
119
  test("works with the manual redirect URI too", () => {