@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
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
 
5
5
  // ---------------------------------------------------------------------------
@@ -574,4 +574,65 @@ describe("plugin routes", () => {
574
574
  );
575
575
  expect(other.status).toBe(404);
576
576
  });
577
+
578
+ test("a new route export is not bound to a stale helper after upgrade", async () => {
579
+ // Reproduces the browser plugin 500: `/frame` loaded `src/http.ts`
580
+ // first, then an upgrade added `requireNumber` to http.ts and a new
581
+ // `/input` that imports it. Cache-busting only the entry file rebound
582
+ // input.ts to the cached http.ts, which had no such export.
583
+ const plugin = "stale-helper";
584
+ const helperPath = join(getWorkspacePluginsDir(), plugin, "src", "http.ts");
585
+ mkdirSync(dirname(helperPath), { recursive: true });
586
+ writeFileSync(
587
+ helperPath,
588
+ `export function label() { return "v1"; }\n`,
589
+ );
590
+ writePluginHandler(
591
+ plugin,
592
+ "frame.ts",
593
+ `import { label } from "../src/http.js";
594
+ export function GET() { return Response.json({ label: label() }); }`,
595
+ );
596
+ writePluginHandler(
597
+ plugin,
598
+ "input.ts",
599
+ `import { label } from "../src/http.js";
600
+ export function POST() { return Response.json({ label: label() }); }`,
601
+ );
602
+
603
+ const dispatcher = makeDispatcher();
604
+ const frame1 = await dispatcher.dispatch(
605
+ `plugins/${plugin}/frame`,
606
+ makeRequest("GET"),
607
+ );
608
+ expect(frame1.status).toBe(200);
609
+ expect((await frame1.json()).label).toBe("v1");
610
+
611
+ await new Promise((resolve) => setTimeout(resolve, 50));
612
+ writeFileSync(
613
+ helperPath,
614
+ `export function label() { return "v2"; }
615
+ export function extra() { return true; }\n`,
616
+ );
617
+ writePluginHandler(
618
+ plugin,
619
+ "input.ts",
620
+ `import { extra } from "../src/http.js";
621
+ export function POST() { return Response.json({ extra: extra() }); }`,
622
+ );
623
+
624
+ const input = await dispatcher.dispatch(
625
+ `plugins/${plugin}/input`,
626
+ makeRequest("POST"),
627
+ );
628
+ expect(input.status).toBe(200);
629
+ expect((await input.json()).extra).toBe(true);
630
+
631
+ const frame2 = await dispatcher.dispatch(
632
+ `plugins/${plugin}/frame`,
633
+ makeRequest("GET"),
634
+ );
635
+ expect(frame2.status).toBe(200);
636
+ expect((await frame2.json()).label).toBe("v2");
637
+ });
577
638
  });
@@ -4,11 +4,11 @@
4
4
  * GET /v1/channels/available — return the channels this assistant can
5
5
  * surface to clients (Contacts / GuardianChannels views, etc.) along with
6
6
  * their display metadata (label, subtitle, icon, verification capability,
7
- * setup-message copy). Today this is a fixed base list plus `email` when
8
- * an inbox is registered; eventually the list will be driven by
9
- * plugins/skills the assistant has loaded. Clients should treat the
10
- * response as authoritative and stop carrying their own per-channel
11
- * switches.
7
+ * setup-message copy). A fixed base list, plus `email` when an inbox is
8
+ * registered, plus the channels installed plugins bring by declaring
9
+ * ingress. One list, because a plugin channel is a channel; each row's
10
+ * `source` says who contributes it. Clients should treat the response as
11
+ * authoritative and stop carrying their own per-channel switches.
12
12
  *
13
13
  * Distinct from `/v1/channels/readiness` (which answers "is this channel
14
14
  * configured and working?"). Availability answers "could this channel be
@@ -18,8 +18,9 @@
18
18
  import { z } from "zod";
19
19
 
20
20
  import { isA2AEnabled } from "../../a2a/feature-gate.js";
21
+ import { discoverPluginChannels } from "../../channels/plugin-channel-declarations.js";
21
22
  import {
22
- CHANNEL_IDS,
23
+ type AvailableChannel,
23
24
  CHANNEL_METADATA,
24
25
  type ChannelId,
25
26
  type ChannelInfo,
@@ -73,7 +74,7 @@ async function hasRegisteredInbox(): Promise<boolean> {
73
74
 
74
75
  async function handleGetChannelAvailability(
75
76
  _args: RouteHandlerArgs,
76
- ): Promise<{ channels: ChannelInfo[] }> {
77
+ ): Promise<{ channels: AvailableChannel[] }> {
77
78
  const ids: ChannelId[] = [...BASE_AVAILABLE_CHANNELS];
78
79
  if (await hasRegisteredInbox()) {
79
80
  ids.push("email");
@@ -86,14 +87,26 @@ async function handleGetChannelAvailability(
86
87
  // contains channels that BASE_AVAILABLE_CHANNELS / the email branch
87
88
  // explicitly chose, so the lookup is always defined — filter to satisfy
88
89
  // the type system without a non-null assertion.
89
- const channels = ids
90
+ const builtIn: AvailableChannel[] = ids
90
91
  .map((id) => CHANNEL_METADATA[id])
91
- .filter((info): info is ChannelInfo => info !== undefined);
92
- return { channels };
92
+ .filter((info): info is ChannelInfo => info !== undefined)
93
+ .map((info) => ({ ...info, source: "default" as const }));
94
+ // One list, because a plugin channel is a channel: what differs is who
95
+ // contributes it, which is what `source` says. Built-ins lead, so a client
96
+ // rendering in order gets a stable list that plugin installs append to.
97
+ return { channels: [...builtIn, ...(await discoverPluginChannels())] };
93
98
  }
94
99
 
95
100
  const channelInfoSchema = z.object({
96
- id: z.enum(CHANNEL_IDS),
101
+ // A string rather than the channel enum: a plugin channel's id is its
102
+ // plugin name, and `source` is what says which kind a row is.
103
+ id: z.string(),
104
+ source: z
105
+ .string()
106
+ .describe(
107
+ "`default` for a channel the assistant ships, `plugin:<name>` for one " +
108
+ "an installed plugin brings",
109
+ ),
97
110
  label: z.string(),
98
111
  subtitle: z.string(),
99
112
  icon: z.string(),
@@ -117,14 +130,19 @@ export const ROUTES: RouteDefinition[] = [
117
130
  description:
118
131
  "Return the channels this assistant can surface to clients, with " +
119
132
  "display metadata (label, icon, verification capability, setup " +
120
- "copy). Today this is a fixed base list plus `email` when an inbox " +
121
- "is registered; will become plugin/skill-driven in future.",
133
+ "copy). A fixed base list plus `email` when an inbox is registered, " +
134
+ "plus the channels installed plugins bring by declaring ingress " +
135
+ "routes. Each carries a `source` saying which of those it is.",
122
136
  tags: ["channels"],
123
137
  handler: handleGetChannelAvailability,
124
138
  responseBody: z.object({
125
139
  channels: z
126
140
  .array(channelInfoSchema)
127
- .describe("Available channels in display order"),
141
+ .describe(
142
+ "Available channels in display order: the ones the assistant " +
143
+ "ships, then the ones installed plugins bring by declaring " +
144
+ "ingress routes. `source` distinguishes them.",
145
+ ),
128
146
  }),
129
147
  },
130
148
  ];
@@ -2,7 +2,6 @@
2
2
  * Shared types, constants, and utilities used across channel route modules.
3
3
  */
4
4
  import type { ChannelId } from "../../channels/types.js";
5
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
6
5
  import {
7
6
  type ApprovalAction,
8
7
  type ApprovalDecisionResult,
@@ -10,11 +9,6 @@ import {
10
9
  isApprovalAction,
11
10
  } from "../channel-approval-types.js";
12
11
 
13
- /** Canonicalize assistantId for channel ingress paths. */
14
- export function canonicalChannelAssistantId(_assistantId: string): string {
15
- return DAEMON_INTERNAL_ASSISTANT_ID;
16
- }
17
-
18
12
  // ---------------------------------------------------------------------------
19
13
  // Actor role
20
14
  // ---------------------------------------------------------------------------
@@ -159,13 +159,13 @@ async function handleExchange(args: RouteHandlerArgs) {
159
159
 
160
160
  const existing = getConnection(db, CONNECTION_NAME);
161
161
  if (existing) {
162
- // Stamp the provider with the auth: this row is the subscription
163
- // connection by name, and dispatch derives auth from the provider for
164
- // managed rows, so a claiming row with e.g. provider "vellum" would
165
- // otherwise strand the fresh token behind derived platform auth.
162
+ // Stamp the provider with the auth: this row IS the subscription route
163
+ // (auth modality = provider identity), and dispatch derives the openai
164
+ // upstream from the identity per-request. Stamping also heals a
165
+ // claiming row whose provider would otherwise misroute the fresh token.
166
166
  const updateResult = updateConnection(db, CONNECTION_NAME, {
167
167
  auth: authInput,
168
- provider: "openai",
168
+ provider: "chatgpt",
169
169
  });
170
170
  if (!updateResult.ok) {
171
171
  log.error(
@@ -177,7 +177,7 @@ async function handleExchange(args: RouteHandlerArgs) {
177
177
  } else {
178
178
  const createResult = createConnection(db, {
179
179
  name: CONNECTION_NAME,
180
- provider: "openai",
180
+ provider: "chatgpt",
181
181
  auth: authInput,
182
182
  });
183
183
  if (!createResult.ok) {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * GET /v1/conversations — paginated conversation list
5
5
  * GET /v1/conversations/unread-count - count of unseen foreground conversations
6
+ * GET /v1/conversations/sections - per-section totals for the sidebar index
6
7
  * POST /v1/conversations/seen — record a seen signal (single)
7
8
  * POST /v1/conversations/seen/bulk — record seen signals (batch)
8
9
  * POST /v1/conversations/unread — mark a conversation unread
@@ -29,6 +30,7 @@ import { resolveConversationId } from "../../persistence/conversation-key-store.
29
30
  import {
30
31
  type ConversationListFilter,
31
32
  countConversations,
33
+ countConversationSections,
32
34
  countUnreadConversations,
33
35
  listConversations,
34
36
  listPinnedConversations,
@@ -155,6 +157,42 @@ const unreadConversationCountResponseSchema = z.object({
155
157
  count: z.number(),
156
158
  });
157
159
 
160
+ const sectionCountFields = {
161
+ total: z.number(),
162
+ unread: z.number(),
163
+ };
164
+
165
+ /**
166
+ * One renderable sidebar section. Discriminated by `kind` so clients get a
167
+ * typed union: group sections carry their metadata inline (one consistent
168
+ * snapshot, no join against `GET /v1/groups` that could disagree with it),
169
+ * channel sections carry only the id (labels are client-side i18n), and
170
+ * `chats` is the ungrouped-native bucket. In a view without channel sections
171
+ * the Chats card holds every ungrouped row: the buckets are disjoint, so
172
+ * that reading is `chats` plus the sum of `channel` entries.
173
+ */
174
+ const conversationSectionSchema = z.discriminatedUnion("kind", [
175
+ z.object({ kind: z.literal("pinned"), ...sectionCountFields }),
176
+ z.object({
177
+ kind: z.literal("group"),
178
+ groupId: z.string(),
179
+ name: z.string(),
180
+ icon: z.string().nullable(),
181
+ sortPosition: z.number(),
182
+ ...sectionCountFields,
183
+ }),
184
+ z.object({
185
+ kind: z.literal("channel"),
186
+ channelId: z.string(),
187
+ ...sectionCountFields,
188
+ }),
189
+ z.object({ kind: z.literal("chats"), ...sectionCountFields }),
190
+ ]);
191
+
192
+ const conversationSectionsResponseSchema = z.object({
193
+ sections: z.array(conversationSectionSchema),
194
+ });
195
+
158
196
  // ---------------------------------------------------------------------------
159
197
  // Helpers
160
198
  // ---------------------------------------------------------------------------
@@ -292,6 +330,64 @@ function handleGetUnreadConversationCount() {
292
330
  return { count: countUnreadConversations() };
293
331
  }
294
332
 
333
+ /**
334
+ * One row per non-empty sidebar section, no conversation rows. What this
335
+ * exists to answer without a list drain: which sections render at all,
336
+ * and what their rail badges say.
337
+ *
338
+ * A count row whose `group_id` names no existing non-system group is
339
+ * skipped rather than surfaced: those rows are unreachable as a section
340
+ * (the sidebar builds group sections from the groups table), and a
341
+ * dangling id is a transient state around group deletion, not a section.
342
+ */
343
+ function handleGetConversationSections() {
344
+ const counts = countConversationSections();
345
+ const groupsById = new Map(listGroups().map((g) => [g.id, g]));
346
+ const sections: Array<z.infer<typeof conversationSectionSchema>> = [];
347
+
348
+ for (const row of counts.groups) {
349
+ if (row.groupId === "system:pinned") {
350
+ sections.push({ kind: "pinned", total: row.total, unread: row.unread });
351
+ continue;
352
+ }
353
+ const meta = groupsById.get(row.groupId);
354
+ if (!meta || meta.isSystemGroup) {
355
+ continue;
356
+ }
357
+ sections.push({
358
+ kind: "group",
359
+ groupId: row.groupId,
360
+ name: meta.name,
361
+ icon: meta.icon,
362
+ sortPosition: meta.sortPosition,
363
+ total: row.total,
364
+ unread: row.unread,
365
+ });
366
+ }
367
+
368
+ for (const row of counts.channels) {
369
+ if (row.channel === "vellum") {
370
+ sections.push({ kind: "chats", total: row.total, unread: row.unread });
371
+ continue;
372
+ }
373
+ sections.push({
374
+ kind: "channel",
375
+ channelId: row.channel,
376
+ total: row.total,
377
+ unread: row.unread,
378
+ });
379
+ }
380
+
381
+ /* Every other section exists only when it has rows; Chats is the leftover
382
+ bucket and renders regardless, so its counts are part of the contract
383
+ even at zero. */
384
+ if (!sections.some((s) => s.kind === "chats")) {
385
+ sections.push({ kind: "chats", total: 0, unread: 0 });
386
+ }
387
+
388
+ return { sections };
389
+ }
390
+
295
391
  function handleRecordSeen({ body = {}, headers }: RouteHandlerArgs) {
296
392
  const rawConversationId = body.conversationId as string | undefined;
297
393
  if (!rawConversationId) {
@@ -482,7 +578,7 @@ export const ROUTES: RouteDefinition[] = [
482
578
  type: "string",
483
579
  required: false,
484
580
  description:
485
- 'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is ordered by the user\'s arrangement (display order, then recency) and never has pinned rows appended to it. Omit to span every group.',
581
+ 'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is recency-ordered like every list read (COALESCE(last_message_at, updated_at) descending) and never has pinned rows appended to it. Omit to span every group.',
486
582
  },
487
583
  ],
488
584
  responseBody: listConversationsResponseSchema,
@@ -503,6 +599,21 @@ export const ROUTES: RouteDefinition[] = [
503
599
  responseBody: unreadConversationCountResponseSchema,
504
600
  handler: handleGetUnreadConversationCount,
505
601
  },
602
+ {
603
+ operationId: "getConversationSections",
604
+ endpoint: "conversations/sections",
605
+ method: "GET",
606
+ policy: {
607
+ requiredScopes: ["chat.read"],
608
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
609
+ },
610
+ summary: "Sidebar section index",
611
+ description:
612
+ "One row per renderable sidebar section (Pinned, each non-empty custom group, each origin channel with unassigned conversations, Chats) with total and unread counts and no conversation rows. Lets a client know which sections exist, and what their badges say, without fetching any conversation list. Totals follow the standard listing visibility; unread applies the same rules as GET /v1/conversations/unread-count scoped to the section. Chats is always present, even at zero: it is the leftover bucket and renders regardless.",
613
+ tags: ["conversations"],
614
+ responseBody: conversationSectionsResponseSchema,
615
+ handler: handleGetConversationSections,
616
+ },
506
617
  {
507
618
  operationId: "recordConversationSeen",
508
619
  endpoint: "conversations/seen",
@@ -41,7 +41,10 @@ import {
41
41
  withSuppressedConfigDiskWrites,
42
42
  withSuppressedConfigDiskWritesSync,
43
43
  } from "../../config/loader.js";
44
- import { completeCustomProfile } from "../../config/profile-materialization.js";
44
+ import {
45
+ completeCustomProfile,
46
+ mergePreservingUnknownKeys,
47
+ } from "../../config/profile-materialization.js";
45
48
  import { AssistantConfigSchema } from "../../config/schema.js";
46
49
  import { getSchemaAtPath } from "../../config/schema-utils.js";
47
50
  import {
@@ -50,6 +53,7 @@ import {
50
53
  LLMConfigFragment,
51
54
  ProfileEntry,
52
55
  routingIdentityModelIssue,
56
+ unknownLlmProviderIssue,
53
57
  } from "../../config/schemas/llm.js";
54
58
  import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
55
59
  import { ServiceModeSchema } from "../../config/schemas/services.js";
@@ -96,6 +100,7 @@ import { type LogRow } from "../../persistence/llm-request-log-store.js";
96
100
  import { getMemoryRecallLogByMessageIds } from "../../plugins/defaults/memory/memory-recall-log-store.js";
97
101
  import { getMemoryV2ActivationLogByMessageIds } from "../../plugins/defaults/memory/v2/activation-log-store.js";
98
102
  import { getMemoryV3SelectionForInspectorByMessageIds } from "../../plugins/defaults/memory/v3/selection-log-store.js";
103
+ import { writableProfileProviderIssue } from "../../providers/connection-resolution.js";
99
104
  import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
100
105
  import { PROVIDERS_REQUIRING_BASE_URL_AND_MODELS } from "../../providers/inference/auth.js";
101
106
  import {
@@ -1252,30 +1257,6 @@ function assertInvariantProfilesPreserved(
1252
1257
  }
1253
1258
  }
1254
1259
 
1255
- /**
1256
- * `{...raw, ...completed}` recursively: completed (schema-known) values win,
1257
- * raw keys the schema stripped survive at every depth.
1258
- */
1259
- function mergePreservingUnknownKeys(
1260
- raw: Record<string, unknown>,
1261
- completed: Record<string, unknown>,
1262
- ): Record<string, unknown> {
1263
- const out: Record<string, unknown> = { ...raw, ...completed };
1264
- for (const [key, value] of Object.entries(completed)) {
1265
- const rawValue = raw[key];
1266
- if (
1267
- readPlainObject(value) !== undefined &&
1268
- readPlainObject(rawValue) !== undefined
1269
- ) {
1270
- out[key] = mergePreservingUnknownKeys(
1271
- rawValue as Record<string, unknown>,
1272
- value as Record<string, unknown>,
1273
- );
1274
- }
1275
- }
1276
- return out;
1277
- }
1278
-
1279
1260
  /**
1280
1261
  * Custom profiles persist as complete overrides: any user-source, non-mix
1281
1262
  * profile entry this write creates or changes is completed against the
@@ -1343,7 +1324,9 @@ function completeChangedCustomProfiles(
1343
1324
  * would let the pair reach disk and be stripped on the next read — a silent
1344
1325
  * no-op where the user expects a saved override.
1345
1326
  */
1346
- function assertRoutableIdentityEntries(raw: Record<string, unknown>): void {
1327
+ function collectProviderEntries(
1328
+ raw: Record<string, unknown>,
1329
+ ): [string, { provider?: unknown; model?: unknown }][] {
1347
1330
  const llm = raw.llm as
1348
1331
  | {
1349
1332
  default?: { provider?: unknown; model?: unknown } | null;
@@ -1371,10 +1354,40 @@ function assertRoutableIdentityEntries(raw: Record<string, unknown>): void {
1371
1354
  }
1372
1355
  }
1373
1356
  }
1357
+ return entries;
1358
+ }
1359
+
1360
+ function assertRoutableIdentityEntries(
1361
+ preWrite: Record<string, unknown>,
1362
+ raw: Record<string, unknown>,
1363
+ ): void {
1364
+ const entries = collectProviderEntries(raw);
1365
+ const priorProviders = new Map(
1366
+ collectProviderEntries(preWrite).flatMap(([label, entry]) =>
1367
+ typeof entry.provider === "string" ? [[label, entry.provider]] : [],
1368
+ ),
1369
+ );
1374
1370
  for (const [label, entry] of entries) {
1375
1371
  if (typeof entry.provider !== "string") {
1376
1372
  continue;
1377
1373
  }
1374
+ // The provider schema is an open string (a stored value outside the
1375
+ // known set parses instead of stripping its profile), so write-time
1376
+ // membership is enforced here and at the profiles route. Scoped to
1377
+ // providers this write introduces or changes: a stored value outside
1378
+ // the known set is readable and dispatchable by design, and
1379
+ // re-validating it on every write would make all later settings saves
1380
+ // fail with no in-product repair path. Profiles accept entry names;
1381
+ // call-site fragments and the legacy default blob stay vendor-only
1382
+ // (overrides become model-only with the entries demolition).
1383
+ if (entry.provider !== priorProviders.get(label)) {
1384
+ const providerIssue = label.startsWith("llm.profiles.")
1385
+ ? writableProfileProviderIssue(entry.provider)
1386
+ : unknownLlmProviderIssue(entry.provider);
1387
+ if (providerIssue) {
1388
+ throw new BadRequestError(`${providerIssue} (${label})`);
1389
+ }
1390
+ }
1378
1391
  const issue = routingIdentityModelIssue(
1379
1392
  entry.provider,
1380
1393
  typeof entry.model === "string" ? entry.model : undefined,
@@ -1396,7 +1409,7 @@ export async function commitConfigWrite(
1396
1409
  const preWrite = loadRawConfig();
1397
1410
  completeChangedCustomProfiles(preWrite, raw);
1398
1411
  assertInvariantProfilesPreserved(preWrite, raw);
1399
- assertRoutableIdentityEntries(raw);
1412
+ assertRoutableIdentityEntries(preWrite, raw);
1400
1413
 
1401
1414
  // Suppress the file-watcher callback for the duration of the debounce
1402
1415
  // window. Without this, the ConfigWatcher detects the config.json write
@@ -55,14 +55,11 @@ const CredentialPromptParams = z.object({
55
55
  *
56
56
  * The prompt blocks until the user answers, so by the time a caller reads the
57
57
  * result the secure input is already gone from the UI. A bare `ok: true` reads
58
- * as "the prompt opened successfully", which leads a model to follow up with
59
- * "the prompt is open, paste your value there" — pointing the user at a surface
60
- * that has already closed. The user, having just submitted, re-answers a prompt
61
- * that never reappears, and the flow loops. State the close explicitly so the
62
- * only available reading is the correct one.
58
+ * as "the prompt opened successfully", which invites a follow-up telling the
59
+ * user to paste into a surface that has already closed. Stating the close
60
+ * leaves only the correct reading.
63
61
  */
64
- const PROMPT_CLOSED_NOTICE =
65
- "The secure prompt has closed — do not ask the user to enter this value again.";
62
+ const PROMPT_CLOSED_NOTICE = "The secure prompt has closed.";
66
63
 
67
64
  export type CredentialPromptResult = {
68
65
  ok: boolean;
@@ -25,6 +25,7 @@ import {
25
25
  DEFAULT_PROVIDER_CHOICES,
26
26
  DefaultProviderSchema,
27
27
  } from "../../config/schemas/llm.js";
28
+ import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
28
29
  import {
29
30
  computeConnectionAvailability,
30
31
  CONNECTION_AVAILABILITY_STATUSES,
@@ -93,6 +94,20 @@ async function handlePutDefaultProvider({
93
94
  )}; "connectionName" is optional and must be a non-empty string.`,
94
95
  );
95
96
  }
97
+ // Routing identities dispatch through their canonical row regardless of
98
+ // any stored pin (`resolveRoutingIdentity`), so a noncanonical
99
+ // connectionName would be judged by availability and the deletion guards
100
+ // while inference uses a different row. Reject it here rather than
101
+ // persisting a pin that status and dispatch disagree about.
102
+ const { provider, connectionName } = result.data;
103
+ if (connectionName != null && ROUTING_IDENTITY_PROVIDERS.has(provider)) {
104
+ const canonical = resolveDefaultConnectionName({ provider });
105
+ if (connectionName !== canonical) {
106
+ throw new BadRequestError(
107
+ `Provider "${provider}" always dispatches through its canonical connection "${canonical}". Omit "connectionName" or pass "${canonical}".`,
108
+ );
109
+ }
110
+ }
96
111
  setDefaultProvider(result.data);
97
112
  return handleGetDefaultProvider();
98
113
  }