@vellumai/assistant 0.11.3 → 0.11.4-staging.1

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 (324) 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 +140 -38
  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 +2 -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 +55 -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__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -25,7 +25,7 @@ import { createConversation } from "../../../persistence/conversation-crud.js";
25
25
  import { getDb } from "../../../persistence/db-connection.js";
26
26
  import { initializeDb } from "../../../persistence/db-init.js";
27
27
  import { createGroup } from "../../../persistence/group-crud.js";
28
- import { rawRun } from "../../../persistence/raw-query.js";
28
+ import { rawExec, rawRun } from "../../../persistence/raw-query.js";
29
29
  import {
30
30
  conversationAssistantAttentionState,
31
31
  conversationAttentionEvents,
@@ -782,3 +782,221 @@ describe("GET /v1/conversations/unread-count", () => {
782
782
  expect(invokeUnreadCount()).toEqual({ count: 1 });
783
783
  });
784
784
  });
785
+
786
+ describe("GET /v1/conversations/sections", () => {
787
+ const sectionsHandler = findHandler(
788
+ CONVERSATION_LIST_ROUTES,
789
+ "getConversationSections",
790
+ );
791
+
792
+ interface SectionRow {
793
+ kind: "pinned" | "group" | "channel" | "chats";
794
+ groupId?: string;
795
+ name?: string;
796
+ icon?: string | null;
797
+ sortPosition?: number;
798
+ channelId?: string;
799
+ total: number;
800
+ unread: number;
801
+ }
802
+
803
+ function invokeSections(): SectionRow[] {
804
+ return (sectionsHandler({}) as { sections: SectionRow[] }).sections;
805
+ }
806
+
807
+ function fileIntoGroup(conversationId: string, groupId: string): void {
808
+ rawRun(
809
+ "test:fileIntoGroup",
810
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
811
+ groupId,
812
+ conversationId,
813
+ );
814
+ }
815
+
816
+ function stampChannel(conversationId: string, channel: string): void {
817
+ rawRun(
818
+ "test:stampChannel",
819
+ "UPDATE conversations SET origin_channel = ? WHERE id = ?",
820
+ channel,
821
+ conversationId,
822
+ );
823
+ }
824
+
825
+ function pin(conversationId: string): void {
826
+ rawRun(
827
+ "test:pinConversation",
828
+ "UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
829
+ conversationId,
830
+ );
831
+ }
832
+
833
+ function seedUnseen(conversationId: string): void {
834
+ projectAssistantMessage({
835
+ conversationId,
836
+ messageId: `msg-${conversationId}`,
837
+ messageAt: Date.now(),
838
+ });
839
+ }
840
+
841
+ beforeEach(() => {
842
+ clearConversations();
843
+ });
844
+
845
+ test("an empty conversation table yields Chats alone, at zero", () => {
846
+ // Chats is the leftover bucket and renders regardless, so its counts
847
+ // are part of the contract even when nothing exists.
848
+ expect(invokeSections()).toEqual([{ kind: "chats", total: 0, unread: 0 }]);
849
+ });
850
+
851
+ test("every section kind appears with its own totals", () => {
852
+ const pinned = createConversation({ title: "pinned-1" });
853
+ pin(pinned.id);
854
+ const group = createGroup("Sections Spread Group");
855
+ fileIntoGroup(createConversation({ title: "g-1" }).id, group.id);
856
+ fileIntoGroup(createConversation({ title: "g-2" }).id, group.id);
857
+ stampChannel(createConversation({ title: "slack-1" }).id, "slack");
858
+ createConversation({ title: "native-unstamped" });
859
+ stampChannel(createConversation({ title: "native-stamped" }).id, "vellum");
860
+
861
+ const sections = invokeSections();
862
+
863
+ expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
864
+ expect(sections).toContainEqual({
865
+ kind: "group",
866
+ groupId: group.id,
867
+ name: "Sections Spread Group",
868
+ icon: null,
869
+ sortPosition: group.sortPosition,
870
+ total: 2,
871
+ unread: 0,
872
+ });
873
+ expect(sections).toContainEqual({
874
+ kind: "channel",
875
+ channelId: "slack",
876
+ total: 1,
877
+ unread: 0,
878
+ });
879
+ // NULL origin_channel reads as native, exactly as the list filter reads
880
+ // it, so the stamped and unstamped native rows share the Chats bucket.
881
+ expect(sections).toContainEqual({ kind: "chats", total: 2, unread: 0 });
882
+ });
883
+
884
+ test("unread counts follow the seen state per section", () => {
885
+ const pinned = createConversation({ title: "pinned-seen" });
886
+ pin(pinned.id);
887
+ seedUnseen(pinned.id);
888
+ recordConversationSeenSignal({
889
+ conversationId: pinned.id,
890
+ sourceChannel: "vellum",
891
+ signalType: "macos_conversation_opened",
892
+ confidence: "explicit",
893
+ source: "test",
894
+ });
895
+
896
+ const group = createGroup("Sections Unread Group");
897
+ const unreadInGroup = createConversation({ title: "g-unread" });
898
+ fileIntoGroup(unreadInGroup.id, group.id);
899
+ seedUnseen(unreadInGroup.id);
900
+ // A member with no attention projection at all reads as seen.
901
+ fileIntoGroup(createConversation({ title: "g-quiet" }).id, group.id);
902
+
903
+ const chatsUnread = createConversation({ title: "chat-unread" });
904
+ seedUnseen(chatsUnread.id);
905
+
906
+ const sections = invokeSections();
907
+
908
+ expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
909
+ expect(sections).toContainEqual({
910
+ kind: "group",
911
+ groupId: group.id,
912
+ name: "Sections Unread Group",
913
+ icon: null,
914
+ sortPosition: group.sortPosition,
915
+ total: 2,
916
+ unread: 1,
917
+ });
918
+ expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 1 });
919
+ });
920
+
921
+ test("a background row filed into a group counts toward total but never unread", () => {
922
+ // Mirrors the unread-count contract: the custom-group visibility arm
923
+ // admits the row into the section, and the not-background unread rule
924
+ // keeps it out of the badge.
925
+ const group = createGroup("Sections Background Group");
926
+ const bg = createConversation({
927
+ title: "bg-in-group",
928
+ conversationType: "background",
929
+ });
930
+ fileIntoGroup(bg.id, group.id);
931
+ seedUnseen(bg.id);
932
+
933
+ const sections = invokeSections();
934
+
935
+ expect(sections).toContainEqual({
936
+ kind: "group",
937
+ groupId: group.id,
938
+ name: "Sections Background Group",
939
+ icon: null,
940
+ sortPosition: group.sortPosition,
941
+ total: 1,
942
+ unread: 0,
943
+ });
944
+ });
945
+
946
+ test("an empty custom group gets no section", () => {
947
+ const group = createGroup("Sections Empty Group");
948
+
949
+ const sections = invokeSections();
950
+
951
+ expect(sections.some((s) => s.groupId === group.id)).toBe(false);
952
+ });
953
+
954
+ test("archived rows count toward no section", () => {
955
+ const group = createGroup("Sections Archived Group");
956
+ fileIntoGroup(seedArchived("archived-in-group"), group.id);
957
+
958
+ const sections = invokeSections();
959
+
960
+ expect(sections.some((s) => s.groupId === group.id)).toBe(false);
961
+ });
962
+
963
+ test("a dangling group id is skipped, not surfaced", () => {
964
+ /* Live write paths cannot create this state: group_id carries a foreign
965
+ key, and the placement write sanitizes unknown groups precisely to
966
+ avoid violating it. A restored snapshot can, though: in-place restore
967
+ swaps the SQLite file without running migrations in-process (the same
968
+ window that lets legacy private rows exist transiently), so the
969
+ fixture creates the state the way reality does, with enforcement off. */
970
+ rawExec("PRAGMA foreign_keys = OFF");
971
+ try {
972
+ fileIntoGroup(
973
+ createConversation({ title: "dangling" }).id,
974
+ "00000000-0000-4000-8000-00000000dead",
975
+ );
976
+ } finally {
977
+ rawExec("PRAGMA foreign_keys = ON");
978
+ }
979
+
980
+ const sections = invokeSections();
981
+
982
+ expect(sections.some((s) => s.kind === "group")).toBe(false);
983
+ // Grouped, so it does not leak into the ungrouped Chats bucket either.
984
+ expect(sections).toContainEqual({ kind: "chats", total: 0, unread: 0 });
985
+ });
986
+
987
+ test("an unsurfaced background row pinned by raw column writes stays invisible", () => {
988
+ // Standard-listing visibility admits a pinned background row only when
989
+ // it is surfaced (the pinned group fails the custom-group arm on its
990
+ // system: prefix), so it appears in neither the Pinned list nor the
991
+ // Pinned count.
992
+ const bg = createConversation({
993
+ title: "bg-pinned",
994
+ conversationType: "background",
995
+ });
996
+ pin(bg.id);
997
+
998
+ const sections = invokeSections();
999
+
1000
+ expect(sections.some((s) => s.kind === "pinned")).toBe(false);
1001
+ });
1002
+ });
@@ -1712,3 +1712,55 @@ describe("config invariant flag enrichment", () => {
1712
1712
  }
1713
1713
  });
1714
1714
  });
1715
+
1716
+ describe("provider membership at the write choke point", () => {
1717
+ const configPatchRoute = ROUTES.find(
1718
+ (r) => r.operationId === "config_patch",
1719
+ )!;
1720
+
1721
+ test("a pre-existing unknown provider does not block unrelated writes", async () => {
1722
+ // Read tolerance means a stored entry-name provider is legal on disk;
1723
+ // re-validating it on every write would make all later settings saves
1724
+ // fail with no in-product repair path.
1725
+ rawConfigFixture = {
1726
+ llm: {
1727
+ profiles: {
1728
+ work: {
1729
+ source: "user",
1730
+ provider: "anthropic-work",
1731
+ model: "claude-opus-4-8",
1732
+ },
1733
+ },
1734
+ },
1735
+ };
1736
+ seedRawConfig();
1737
+
1738
+ await configPatchRoute.handler({
1739
+ body: { llm: { callSites: { mainAgent: { maxTokens: 2048 } } } },
1740
+ });
1741
+
1742
+ const llm = loadRawConfig().llm as {
1743
+ callSites?: Record<string, { maxTokens?: number }>;
1744
+ profiles?: Record<string, { provider?: string }>;
1745
+ };
1746
+ expect(llm.callSites?.mainAgent?.maxTokens).toBe(2048);
1747
+ expect(llm.profiles?.work?.provider).toBe("anthropic-work");
1748
+ });
1749
+
1750
+ test("introducing an unknown provider is rejected", async () => {
1751
+ rawConfigFixture = { llm: {} };
1752
+ seedRawConfig();
1753
+
1754
+ await expect(
1755
+ configPatchRoute.handler({
1756
+ body: {
1757
+ llm: {
1758
+ profiles: {
1759
+ rogue: { source: "user", provider: "not-a-provider", model: "m" },
1760
+ },
1761
+ },
1762
+ },
1763
+ }),
1764
+ ).rejects.toThrow(/Invalid provider/);
1765
+ });
1766
+ });
@@ -432,6 +432,31 @@ describe("PUT config/llm/default-provider", () => {
432
432
  expect(availability(result)).toEqual({ status: "ok" });
433
433
  });
434
434
 
435
+ test("chatgpt persists and resolves the canonical subscription row", async () => {
436
+ seedConnection({
437
+ name: "chatgpt-subscription",
438
+ provider: "chatgpt",
439
+ auth: {
440
+ type: "oauth_subscription",
441
+ credential: "credential/chatgpt/access_token",
442
+ },
443
+ });
444
+ secureKeyResults["credential/chatgpt/access_token"] = {
445
+ value: "token",
446
+ unreachable: false,
447
+ };
448
+
449
+ const result = (await put({ provider: "chatgpt" })) as Record<
450
+ string,
451
+ unknown
452
+ >;
453
+
454
+ expect(persistedDefaultProvider()).toEqual({ provider: "chatgpt" });
455
+ expect(result.provider).toBe("chatgpt");
456
+ expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
457
+ expect(availability(result)).toEqual({ status: "ok" });
458
+ });
459
+
435
460
  test("persists an explicit connectionName", async () => {
436
461
  await put({ provider: "openai", connectionName: "work-openai" });
437
462
  expect(persistedDefaultProvider()).toEqual({
@@ -449,6 +474,42 @@ describe("PUT config/llm/default-provider", () => {
449
474
  expect(availability(result).status).toBe("missing_connection");
450
475
  });
451
476
 
477
+ test("a noncanonical pin on a routing identity → 400 naming the canonical row", async () => {
478
+ for (const [provider, canonical] of [
479
+ ["chatgpt", "chatgpt-subscription"],
480
+ ["vellum", "vellum"],
481
+ ] as const) {
482
+ const err = await put({
483
+ provider,
484
+ connectionName: "some-other-row",
485
+ }).catch((e: unknown) => e);
486
+ expect(String(err)).toContain(canonical);
487
+ }
488
+ });
489
+
490
+ test("the canonical pin on a routing identity is accepted", async () => {
491
+ seedConnection({
492
+ name: "chatgpt-subscription",
493
+ provider: "chatgpt",
494
+ auth: {
495
+ type: "oauth_subscription",
496
+ credential: "credential/chatgpt/access_token",
497
+ },
498
+ });
499
+ secureKeyResults["credential/chatgpt/access_token"] = {
500
+ value: "token",
501
+ unreachable: false,
502
+ };
503
+
504
+ const result = (await put({
505
+ provider: "chatgpt",
506
+ connectionName: "chatgpt-subscription",
507
+ })) as Record<string, unknown>;
508
+
509
+ expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
510
+ expect(availability(result)).toEqual({ status: "ok" });
511
+ });
512
+
452
513
  test("invalid provider → 400 naming the allowed providers", async () => {
453
514
  const err = await put({ provider: "not-a-provider" }).catch(
454
515
  (e: unknown) => e,
@@ -150,6 +150,50 @@ describe("POST inference/profiles (create) validation", () => {
150
150
  ).rejects.toBeInstanceOf(BadRequestError);
151
151
  });
152
152
 
153
+ test("creates an entry-backed profile, validating the model by the row's kind", async () => {
154
+ seedConnection("anthropic-work", "anthropic", {
155
+ type: "api_key",
156
+ credential: "credential/anthropic-work/api_key",
157
+ });
158
+ const result = (await call("inference_profiles_create", {
159
+ body: {
160
+ name: "work-profile",
161
+ provider: "anthropic-work",
162
+ model: "claude-opus-4-8",
163
+ },
164
+ })) as { entry: Record<string, unknown> };
165
+ expect(result.entry.provider).toBe("anthropic-work");
166
+ expect(result.entry.model).toBe("claude-opus-4-8");
167
+ });
168
+
169
+ test("rejects an entry-backed profile whose model the row's kind cannot serve", async () => {
170
+ seedConnection("anthropic-work", "anthropic", {
171
+ type: "api_key",
172
+ credential: "credential/anthropic-work/api_key",
173
+ });
174
+ await expect(
175
+ call("inference_profiles_create", {
176
+ body: {
177
+ name: "work-profile",
178
+ provider: "anthropic-work",
179
+ model: "gpt-5.5",
180
+ },
181
+ }),
182
+ ).rejects.toBeInstanceOf(BadRequestError);
183
+ });
184
+
185
+ test("rejects a provider naming no known vendor or entry", async () => {
186
+ await expect(
187
+ call("inference_profiles_create", {
188
+ body: {
189
+ name: "ghost-profile",
190
+ provider: "no-such-entry",
191
+ model: "claude-opus-4-8",
192
+ },
193
+ }),
194
+ ).rejects.toBeInstanceOf(BadRequestError);
195
+ });
196
+
153
197
  test("creates a vellum profile for a managed-routable model", async () => {
154
198
  seedVellumConnection();
155
199
  const result = (await call("inference_profiles_create", {
@@ -232,6 +232,61 @@ describe("POST inference/provider-connections (create)", () => {
232
232
  expect((err as BadRequestError).message).toContain("vellum");
233
233
  });
234
234
 
235
+ test("rejects a chatgpt identity row under a non-canonical name", async () => {
236
+ const err = await call(
237
+ findHandler("inference_provider_connections_create"),
238
+ {
239
+ body: {
240
+ name: "my-chatgpt",
241
+ provider: "chatgpt",
242
+ auth: { type: "oauth_subscription", credential: "credential/x" },
243
+ },
244
+ },
245
+ ).catch((e: unknown) => e);
246
+
247
+ expect(err).toBeInstanceOf(BadRequestError);
248
+ expect((err as BadRequestError).message).toContain("chatgpt-subscription");
249
+ });
250
+
251
+ test("rejects key auth on the chatgpt provider, derived or explicit", async () => {
252
+ for (const body of [
253
+ {
254
+ name: "chatgpt-subscription",
255
+ provider: "chatgpt",
256
+ auth: { type: "api_key", credential: "vault/x" },
257
+ },
258
+ // No explicit auth: derivation would fall through to api_key.
259
+ {
260
+ name: "chatgpt-subscription",
261
+ provider: "chatgpt",
262
+ credential: "vault/x",
263
+ },
264
+ ]) {
265
+ const err = await call(
266
+ findHandler("inference_provider_connections_create"),
267
+ { body },
268
+ ).catch((e: unknown) => e);
269
+ expect(err).toBeInstanceOf(BadRequestError);
270
+ expect((err as BadRequestError).message).toContain("sign-in");
271
+ }
272
+ });
273
+
274
+ test("rejects subscription auth on a non-chatgpt provider", async () => {
275
+ const err = await call(
276
+ findHandler("inference_provider_connections_create"),
277
+ {
278
+ body: {
279
+ name: "keyed-oauth",
280
+ provider: "openai",
281
+ auth: { type: "oauth_subscription", credential: "credential/x" },
282
+ },
283
+ },
284
+ ).catch((e: unknown) => e);
285
+
286
+ expect(err).toBeInstanceOf(BadRequestError);
287
+ expect((err as BadRequestError).message).toContain("chatgpt");
288
+ });
289
+
235
290
  test("rejects key auth on the vellum provider", async () => {
236
291
  const err = await call(
237
292
  findHandler("inference_provider_connections_create"),
@@ -396,7 +451,7 @@ describe("POST inference/provider-connections (create)", () => {
396
451
  models: [{ id: "my-model" }],
397
452
  },
398
453
  }),
399
- ).rejects.toThrow(/belongs to a built-in provider/);
454
+ ).rejects.toThrow(/reserved as a provider id/);
400
455
 
401
456
  await call(findHandler("inference_provider_connections_create"), {
402
457
  body: {
@@ -677,6 +732,52 @@ describe("PATCH inference/provider-connections/:name (update)", () => {
677
732
 
678
733
  // The guard must not trap a legacy row in its mismatched state: an auth
679
734
  // change that repairs the pairing is exactly what should be allowed.
735
+ // The canonical subscription row owns the "chatgpt" identity: writing
736
+ // subscription auth to it stamps the provider (the CLI login-chatgpt path
737
+ // PATCHes auth through this route on a row the identity migration
738
+ // deliberately skipped).
739
+ test("stamps the chatgpt identity when subscription auth lands on the canonical row", async () => {
740
+ seedConnection({
741
+ name: "chatgpt-subscription",
742
+ provider: "openai",
743
+ auth: { type: "api_key", credential: "vault/openai/key" },
744
+ });
745
+
746
+ const result = (await call(
747
+ findHandler("inference_provider_connections_update"),
748
+ {
749
+ pathParams: { name: "chatgpt-subscription" },
750
+ body: {
751
+ auth: {
752
+ type: "oauth_subscription",
753
+ credential: "credential/chatgpt/access_token",
754
+ },
755
+ },
756
+ },
757
+ )) as { provider: string; auth: { type: string } };
758
+ expect(result.provider).toBe("chatgpt");
759
+ expect(result.auth.type).toBe("oauth_subscription");
760
+ });
761
+
762
+ test("does not stamp the identity for non-subscription auth on that name", async () => {
763
+ seedConnection({
764
+ name: "chatgpt-subscription",
765
+ provider: "openai",
766
+ auth: { type: "api_key", credential: "vault/openai/key" },
767
+ });
768
+
769
+ const result = (await call(
770
+ findHandler("inference_provider_connections_update"),
771
+ {
772
+ pathParams: { name: "chatgpt-subscription" },
773
+ body: {
774
+ auth: { type: "api_key", credential: "vault/openai/other-key" },
775
+ },
776
+ },
777
+ )) as { provider: string };
778
+ expect(result.provider).toBe("openai");
779
+ });
780
+
680
781
  test("allows repairing a legacy mismatched row with a valid auth change", async () => {
681
782
  seedConnection({
682
783
  name: "legacy-managed-anthropic",
@@ -109,6 +109,7 @@ import {
109
109
  } from "../../../cli/lib/uninstall-plugin.js";
110
110
  import {
111
111
  PluginMergeBaselineError,
112
+ PluginNotCuratedError,
112
113
  PluginNotUpgradableError,
113
114
  type PluginUpgradeResult,
114
115
  type UpgradePluginDeps,
@@ -252,6 +253,7 @@ const upgradeSpy = mock(
252
253
 
253
254
  mock.module("../../../cli/lib/upgrade-plugin.js", () => ({
254
255
  PluginMergeBaselineError,
256
+ PluginNotCuratedError,
255
257
  PluginNotUpgradableError,
256
258
  upgradePlugin: upgradeSpy,
257
259
  }));
@@ -2127,6 +2129,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
2127
2129
  name: "level-up",
2128
2130
  dryRun: false,
2129
2131
  strategy: undefined,
2132
+ marketplaceOnly: false,
2130
2133
  });
2131
2134
  });
2132
2135
 
@@ -2148,6 +2151,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
2148
2151
  name: "level-up",
2149
2152
  dryRun: undefined,
2150
2153
  strategy: "ours",
2154
+ marketplaceOnly: false,
2151
2155
  });
2152
2156
  });
2153
2157
 
@@ -2175,6 +2179,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
2175
2179
  name: "level-up",
2176
2180
  dryRun: undefined,
2177
2181
  strategy: "assistant",
2182
+ marketplaceOnly: false,
2178
2183
  });
2179
2184
  });
2180
2185
 
@@ -2300,6 +2305,10 @@ describe("POST /v1/plugins/:name/upgrade", () => {
2300
2305
  expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
2301
2306
  name: "level-up",
2302
2307
  dryRun: undefined,
2308
+ strategy: undefined,
2309
+ // Unlike dryRun, an absent flag here is a definite "no": the untrusted
2310
+ // direct path stays available unless a caller opts out of it.
2311
+ marketplaceOnly: false,
2303
2312
  });
2304
2313
  });
2305
2314
 
@@ -2343,6 +2352,41 @@ describe("POST /v1/plugins/:name/upgrade", () => {
2343
2352
  ).rejects.toBeInstanceOf(ConflictError);
2344
2353
  });
2345
2354
 
2355
+ test("marketplaceOnly is forwarded so the lib enforces it, not the caller", async () => {
2356
+ // The lib re-inspects the catalog inside the call, so the flag has to
2357
+ // travel with the request: a caller's own pre-check cannot bind a decision
2358
+ // the lib makes afterwards.
2359
+ upgradeSpy.mockImplementation(async () => upgradeResult());
2360
+
2361
+ await invokeUpgrade({
2362
+ pathParams: { name: "level-up" },
2363
+ body: { marketplaceOnly: true },
2364
+ });
2365
+
2366
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
2367
+ name: "level-up",
2368
+ dryRun: undefined,
2369
+ strategy: undefined,
2370
+ marketplaceOnly: true,
2371
+ });
2372
+ });
2373
+
2374
+ test("PluginNotCuratedError → ConflictError (409)", async () => {
2375
+ // A marketplaceOnly caller asked about an install the catalog does not
2376
+ // claim. Retrying changes nothing: only a human upgrading it without the
2377
+ // flag can move it.
2378
+ upgradeSpy.mockImplementation(async () => {
2379
+ throw new PluginNotCuratedError("level-up");
2380
+ });
2381
+
2382
+ await expect(
2383
+ invokeUpgrade({
2384
+ pathParams: { name: "level-up" },
2385
+ body: { marketplaceOnly: true },
2386
+ }),
2387
+ ).rejects.toBeInstanceOf(ConflictError);
2388
+ });
2389
+
2346
2390
  test("PluginNotFoundError → NotFoundError (404)", async () => {
2347
2391
  upgradeSpy.mockImplementation(async () => {
2348
2392
  throw new PluginNotFoundError("level-up", "main", "vellum-ai/level-up");
@@ -288,6 +288,31 @@ describe("stt-routes", () => {
288
288
  expect(err.message).toContain("not available");
289
289
  });
290
290
 
291
+ test("surfaces a typed resolver error verbatim instead of the generic copy", async () => {
292
+ // A streaming-only provider IS configured, so the "nothing configured"
293
+ // and "not available" strings would both send the caller looking for a
294
+ // problem that does not exist.
295
+ const reason =
296
+ 'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".';
297
+ mockResolveError = new SttError("provider-error", reason, {
298
+ userFacing: true,
299
+ });
300
+
301
+ const { handler } = getRoute("stt/transcribe");
302
+ const err = await expectRouteError(
303
+ () =>
304
+ handler(
305
+ makeArgs({
306
+ audioBase64: toBase64("audio-data"),
307
+ mimeType: "audio/wav",
308
+ }),
309
+ ),
310
+ 503,
311
+ "SERVICE_UNAVAILABLE",
312
+ );
313
+ expect(err.message).toBe(reason);
314
+ });
315
+
291
316
  // -- Timeout --------------------------------------------------------------
292
317
 
293
318
  test("throws 504 when transcription times out", async () => {