@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
@@ -21,6 +21,7 @@ import { getDb } from "../../persistence/db-connection.js";
21
21
  import {
22
22
  type Auth,
23
23
  AuthSchema,
24
+ CHATGPT_SUBSCRIPTION_CONNECTION_NAME,
24
25
  type ConnectionModel,
25
26
  ConnectionModelSchema,
26
27
  ConnectionProviderSchema,
@@ -205,14 +206,26 @@ function deriveConnectionAuth(provider: string, credential: unknown): Auth {
205
206
  function assertAuthMatchesProvider(provider: string, auth: Auth): void {
206
207
  const managedAuth = auth.type === "platform";
207
208
  const managedProvider = provider === VELLUM_MANAGED_PROVIDER;
208
- if (managedAuth === managedProvider) {
209
- return;
209
+ if (managedAuth !== managedProvider) {
210
+ throw new BadRequestError(
211
+ managedAuth
212
+ ? `Auth type "platform" is only valid for provider "${VELLUM_MANAGED_PROVIDER}", not "${provider}". Vellum-managed routing is selected by the provider, so omit "auth" to derive it.`
213
+ : `Provider "${VELLUM_MANAGED_PROVIDER}" is always platform-authenticated; "${auth.type}" auth is not valid for it. Omit "auth" to derive it, or name a real provider for key-based auth.`,
214
+ );
215
+ }
216
+ // Same rule for the subscription identity: provider "chatgpt" and
217
+ // oauth_subscription auth record one fact and must pair, or a key-auth
218
+ // row under the identity would dispatch against the API while the user
219
+ // believes they are on subscription billing.
220
+ const subscriptionAuth = auth.type === "oauth_subscription";
221
+ const subscriptionProvider = provider === "chatgpt";
222
+ if (subscriptionAuth !== subscriptionProvider) {
223
+ throw new BadRequestError(
224
+ subscriptionAuth
225
+ ? `Auth type "oauth_subscription" is only valid for provider "chatgpt", not "${provider}". Run the ChatGPT sign-in flow to connect a subscription.`
226
+ : `Provider "chatgpt" is always subscription-authenticated; "${auth.type}" auth is not valid for it. Run the ChatGPT sign-in flow, or name a real provider for key-based auth.`,
227
+ );
210
228
  }
211
- throw new BadRequestError(
212
- managedAuth
213
- ? `Auth type "platform" is only valid for provider "${VELLUM_MANAGED_PROVIDER}", not "${provider}". Vellum-managed routing is selected by the provider, so omit "auth" to derive it.`
214
- : `Provider "${VELLUM_MANAGED_PROVIDER}" is always platform-authenticated; "${auth.type}" auth is not valid for it. Omit "auth" to derive it, or name a real provider for key-based auth.`,
215
- );
216
229
  }
217
230
 
218
231
  /**
@@ -323,6 +336,15 @@ async function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
323
336
  `Connection name "${name}" is reserved for the Vellum-managed connection. Pick another name.`,
324
337
  );
325
338
  }
339
+ // Provider ids and routing identities are labels in profile config, so an
340
+ // entry under one of those names could never be referenced by name (the
341
+ // label reads as the vendor), and a future catalog addition must never
342
+ // capture an existing user name. Reserve the whole vocabulary.
343
+ if (VALID_CONNECTION_PROVIDERS.includes(name)) {
344
+ throw new BadRequestError(
345
+ `Connection name "${name}" is reserved as a provider id. Pick another name.`,
346
+ );
347
+ }
326
348
 
327
349
  const providerResult = ConnectionProviderSchema.safeParse(provider);
328
350
  if (!providerResult.success) {
@@ -330,15 +352,27 @@ async function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
330
352
  `Invalid provider "${String(provider)}". Valid: ${VALID_CONNECTION_PROVIDERS.join(", ")}`,
331
353
  );
332
354
  }
355
+ // The chatgpt identity lives on its canonical row: routing resolves the
356
+ // subscription by that name, so an identity row under any other name can
357
+ // never dispatch.
358
+ if (
359
+ providerResult.data === "chatgpt" &&
360
+ name !== CHATGPT_SUBSCRIPTION_CONNECTION_NAME
361
+ ) {
362
+ throw new BadRequestError(
363
+ `Provider "chatgpt" is reserved for the "${CHATGPT_SUBSCRIPTION_CONNECTION_NAME}" connection. Run the ChatGPT sign-in flow to connect a subscription.`,
364
+ );
365
+ }
333
366
  const authResult = AuthSchema.safeParse(
334
367
  auth ?? deriveConnectionAuth(providerResult.data, body.credential),
335
368
  );
336
369
  if (!authResult.success) {
337
370
  throw new BadRequestError(`Invalid auth: ${authResult.error.message}`);
338
371
  }
339
- if (auth !== undefined) {
340
- assertAuthMatchesProvider(providerResult.data, authResult.data);
341
- }
372
+ // Asserted on derived auth as well: derivation pairs every provider
373
+ // correctly except the chatgpt identity, whose fallthrough would mint
374
+ // api_key auth from a bare credential.
375
+ assertAuthMatchesProvider(providerResult.data, authResult.data);
342
376
 
343
377
  const labelRaw = body.label;
344
378
  if (
@@ -434,15 +468,31 @@ async function handleUpdateConnection({
434
468
  if (!authResult.success) {
435
469
  throw new BadRequestError(`Invalid auth: ${authResult.error.message}`);
436
470
  }
471
+ // The canonical subscription row owns the "chatgpt" identity: writing
472
+ // subscription auth to it stamps the provider with the auth, mirroring
473
+ // the daemon's own sign-in exchange route. The CLI's login-chatgpt PATCHes
474
+ // auth through this route, and without the stamp a row the identity
475
+ // migration deliberately skipped (a claiming row with key auth) would end
476
+ // up as provider "openai" with subscription auth. Provider stays
477
+ // immutable for every other row.
478
+ const chatgptIdentityStamp =
479
+ name === CHATGPT_SUBSCRIPTION_CONNECTION_NAME &&
480
+ authResult.data.type === "oauth_subscription" &&
481
+ existing.provider !== "chatgpt";
482
+
437
483
  // The pairing is enforced on an actual auth change, not on the field being
438
484
  // present: the web editor and the CLI both resend the stored auth on every
439
485
  // edit, so a row whose columns already disagree stays relabelable and
440
- // re-pointable.
486
+ // re-pointable. Judged against the stamped provider when the stamp
487
+ // applies, since that is the pair being written.
441
488
  if (
442
489
  body.auth !== undefined &&
443
490
  authFingerprint(authResult.data) !== authFingerprint(existing.auth)
444
491
  ) {
445
- assertAuthMatchesProvider(existing.provider, authResult.data);
492
+ assertAuthMatchesProvider(
493
+ chatgptIdentityStamp ? "chatgpt" : existing.provider,
494
+ authResult.data,
495
+ );
446
496
  }
447
497
 
448
498
  const labelRaw = body.label;
@@ -473,6 +523,7 @@ async function handleUpdateConnection({
473
523
 
474
524
  const result = updateConnection(getDb(), name, {
475
525
  auth: authResult.data,
526
+ ...(chatgptIdentityStamp ? { provider: "chatgpt" } : {}),
476
527
  ...(labelRaw !== undefined ? { label: labelRaw as string | null } : {}),
477
528
  ...customFields,
478
529
  });
@@ -574,10 +625,21 @@ async function handleDeleteConnection({ pathParams = {} }: RouteHandlerArgs) {
574
625
  // above; this keeps the scan a faithful backstop rather than one that
575
626
  // silently skips the defaults.
576
627
  const profiles = getEffectiveProfilesForProvider(config.llm?.profiles, dp);
628
+ // A binding lives in `provider` (the entry name) under the entries model,
629
+ // or in the legacy `provider_connection` field; both count, or deleting a
630
+ // bound entry would dangle the profile behind selection's silent healing.
631
+ // Provider values count only for non-vendor names: a profile saying
632
+ // "vellum" references the routing identity, never a row that happens to
633
+ // claim that name, and the claiming-row delete is the recovery path.
634
+ const nameIsEntryName = !VALID_CONNECTION_PROVIDERS.includes(name);
577
635
  const referencingProfiles = Object.entries(profiles)
578
- .filter(
579
- ([, p]) => (p as Record<string, unknown>).provider_connection === name,
580
- )
636
+ .filter(([, p]) => {
637
+ const entry = p as Record<string, unknown>;
638
+ return (
639
+ entry.provider_connection === name ||
640
+ (nameIsEntryName && entry.provider === name)
641
+ );
642
+ })
581
643
  .map(([profileName]) => profileName);
582
644
 
583
645
  const result = deleteConnection(getDb(), name, {
@@ -143,6 +143,9 @@ async function handleExport({
143
143
  ? [eq(messages.conversationId, conversationId)]
144
144
  : [];
145
145
 
146
+ // Any-state export, deliberately: this is a diagnostic surface, and an
147
+ // in-flight row (finalized = 0, content a { ref } into the turn's delta
148
+ // file) is often exactly what a bug report needs to show.
146
149
  const messageRows = capRows(
147
150
  db
148
151
  .select()
@@ -19,6 +19,7 @@ import type { McpConfig, McpServerConfig } from "../../config/schemas/mcp.js";
19
19
  import { estimateToolDefinitionTokens } from "../../context/token-estimator.js";
20
20
  import { reloadMcpServers } from "../../daemon/mcp-reload-service.js";
21
21
  import { McpClient } from "../../mcp/client.js";
22
+ import { getMcpServerManager } from "../../mcp/manager.js";
22
23
  import { orchestrateMcpOAuthConnect } from "../../mcp/mcp-auth-orchestrator.js";
23
24
  import { getMcpAuthState } from "../../mcp/mcp-auth-state.js";
24
25
  import {
@@ -30,6 +31,7 @@ import {
30
31
  deleteMcpOAuthCredentials,
31
32
  hasMcpOAuthTokens,
32
33
  } from "../../mcp/mcp-oauth-provider.js";
34
+ import { readPluginMcpServers } from "../../plugins/mcp-servers.js";
33
35
  import { getMcpToolsByServer } from "../../tools/registry.js";
34
36
  import { getLogger } from "../../util/logger.js";
35
37
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
@@ -224,8 +226,33 @@ interface McpServerEntry {
224
226
  authHeaderName?: string;
225
227
  allowedTools?: string[];
226
228
  blockedTools?: string[];
229
+ /**
230
+ * Where the definition came from: the workspace `config.json` or a
231
+ * plugin's `mcp.json`. A plugin server is read-only from the CLI's
232
+ * perspective, since `mcp remove` and `mcp update` act on the workspace
233
+ * config, so the caller needs to be able to tell the two apart.
234
+ */
235
+ source: "workspace" | "plugin";
236
+ /** Plugin that declared this server. Present only when `source` is `plugin`. */
237
+ pluginName?: string;
227
238
  }
228
239
 
240
+ /**
241
+ * Status reported for a plugin-declared server the MCP manager holds no
242
+ * live client for — it has not been started yet, or its connection failed.
243
+ *
244
+ * A plugin server is never health-checked the way a workspace server is.
245
+ * `checkMachineReadableHealth` constructs its own `McpClient`, which
246
+ * resolves `mcp:<serverId>:headers` and `mcp:<serverId>:tokens` from the
247
+ * credential store, and a plugin controls both its server key and its URL,
248
+ * so probing one would send workspace-owned credentials to an endpoint the
249
+ * plugin chose whenever an id happens to match a stored key. Skipping the
250
+ * probe also avoids spawning a plugin's declared stdio command as a side
251
+ * effect of listing. The manager's own client — started with credentials
252
+ * isolated — is the only thing consulted instead.
253
+ */
254
+ const PLUGIN_SERVER_STATUS = "declared";
255
+
229
256
  function detectAuthType(headers: Record<string, string>): "bearer" | "api-key" {
230
257
  const authValue = headers["Authorization"] ?? headers["authorization"];
231
258
  if (authValue?.startsWith("Bearer ")) {
@@ -240,65 +267,129 @@ async function handleMcpList(_args: {
240
267
  const raw = loadRawConfig();
241
268
  const mcpConfig = raw.mcp as Partial<McpConfig> | undefined;
242
269
  const servers = mcpConfig?.servers ?? {};
243
- const entries = Object.entries(servers) as [string, McpServerConfig][];
244
-
245
- const results: McpServerEntry[] = await Promise.all(
246
- entries
247
- .filter(([, config]) => config && typeof config === "object")
248
- .map(async ([id, config]) => {
249
- const enabled = config.enabled !== false;
250
- let status: string;
251
- if (!enabled) {
252
- status = "disabled";
253
- } else {
254
- status = await checkMachineReadableHealth(id, config);
255
- }
256
- const hasOAuth =
257
- config.transport.type !== "stdio"
258
- ? await hasMcpOAuthTokens(id)
259
- : false;
260
-
261
- // Check credential store for stored static auth headers
262
- const storedHeaders = await getMcpHeaders(id);
263
- // Also check legacy config-level headers
264
- const configHeaders =
265
- config.transport.type !== "stdio"
266
- ? config.transport.headers
267
- : undefined;
268
- const effectiveHeaders = storedHeaders ?? configHeaders;
269
- const hasStaticAuth =
270
- !!effectiveHeaders && Object.keys(effectiveHeaders).length > 0;
271
- const authType: "none" | "bearer" | "api-key" = hasStaticAuth
272
- ? detectAuthType(effectiveHeaders!)
273
- : "none";
274
- const authHeaderName =
275
- authType === "api-key" && effectiveHeaders
276
- ? Object.keys(effectiveHeaders).find(
277
- (k) => k.toLowerCase() !== "authorization",
278
- )
279
- : undefined;
280
-
281
- // Strip headers from transport — never return secrets
282
- const { headers: _stripped, ...safeTransport } =
283
- config.transport as Record<string, unknown>;
284
-
285
- return {
286
- id,
287
- status,
288
- transport: safeTransport as McpServerEntry["transport"],
289
- enabled,
290
- defaultRiskLevel: config.defaultRiskLevel ?? "high",
291
- hasOAuth,
292
- hasStaticAuth,
293
- authType,
294
- ...(authHeaderName && { authHeaderName }),
295
- ...(config.allowedTools && { allowedTools: config.allowedTools }),
296
- ...(config.blockedTools && { blockedTools: config.blockedTools }),
297
- };
298
- }),
270
+ const configEntries = (
271
+ Object.entries(servers) as [string, McpServerConfig][]
272
+ ).filter(([, config]) => config && typeof config === "object");
273
+
274
+ const workspaceEntries: McpServerEntry[] = await Promise.all(
275
+ configEntries.map(async ([id, config]) => {
276
+ const enabled = config.enabled !== false;
277
+ let status: string;
278
+ if (!enabled) {
279
+ status = "disabled";
280
+ } else {
281
+ status = await checkMachineReadableHealth(id, config);
282
+ }
283
+ const hasOAuth =
284
+ config.transport.type !== "stdio" ? await hasMcpOAuthTokens(id) : false;
285
+
286
+ // Check credential store for stored static auth headers
287
+ const storedHeaders = await getMcpHeaders(id);
288
+ // Also check legacy config-level headers
289
+ const configHeaders =
290
+ config.transport.type !== "stdio"
291
+ ? config.transport.headers
292
+ : undefined;
293
+ const effectiveHeaders = storedHeaders ?? configHeaders;
294
+ const hasStaticAuth =
295
+ !!effectiveHeaders && Object.keys(effectiveHeaders).length > 0;
296
+ const authType: "none" | "bearer" | "api-key" = hasStaticAuth
297
+ ? detectAuthType(effectiveHeaders!)
298
+ : "none";
299
+ const authHeaderName =
300
+ authType === "api-key" && effectiveHeaders
301
+ ? Object.keys(effectiveHeaders).find(
302
+ (k) => k.toLowerCase() !== "authorization",
303
+ )
304
+ : undefined;
305
+
306
+ // Strip headers from transport, never return secrets
307
+ const { headers: _stripped, ...safeTransport } =
308
+ config.transport as Record<string, unknown>;
309
+
310
+ return {
311
+ id,
312
+ status,
313
+ transport: safeTransport as McpServerEntry["transport"],
314
+ enabled,
315
+ defaultRiskLevel: config.defaultRiskLevel ?? "high",
316
+ hasOAuth,
317
+ hasStaticAuth,
318
+ authType,
319
+ ...(authHeaderName && { authHeaderName }),
320
+ ...(config.allowedTools && { allowedTools: config.allowedTools }),
321
+ ...(config.blockedTools && { blockedTools: config.blockedTools }),
322
+ source: "workspace" as const,
323
+ };
324
+ }),
299
325
  );
300
326
 
301
- return { servers: results };
327
+ return {
328
+ servers: [...workspaceEntries, ...listPluginServerEntries(configEntries)],
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Project plugin-declared servers onto listing entries.
334
+ *
335
+ * Nothing here touches the credential store or the network: a plugin
336
+ * server carries no auth state the assistant owns, and its status comes
337
+ * from the manager's in-memory client rather than a probe (see
338
+ * {@link PLUGIN_SERVER_STATUS}).
339
+ */
340
+ function listPluginServerEntries(
341
+ configEntries: [string, McpServerConfig][],
342
+ ): McpServerEntry[] {
343
+ const { servers, issues } = readPluginMcpServers();
344
+ for (const issue of issues) {
345
+ log.warn(
346
+ {
347
+ plugin: issue.pluginName,
348
+ ...(issue.serverKey && { serverKey: issue.serverKey }),
349
+ },
350
+ `Plugin MCP declaration problem: ${issue.message}`,
351
+ );
352
+ }
353
+
354
+ // A workspace entry outranks a plugin's declaration of the same id: the
355
+ // user's own configuration is the more specific statement, and it is the
356
+ // one they can edit. `buildEffectiveMcpConfig` applies the same
357
+ // precedence, so this listing describes the set the daemon actually runs.
358
+ const workspaceIds = new Set(configEntries.map(([id]) => id));
359
+ const manager = getMcpServerManager();
360
+
361
+ return servers
362
+ .filter((server) => {
363
+ if (!workspaceIds.has(server.id)) {
364
+ return true;
365
+ }
366
+ log.warn(
367
+ { plugin: server.pluginName, serverId: server.id },
368
+ "Plugin MCP server shadowed by a workspace server of the same id; skipping",
369
+ );
370
+ return false;
371
+ })
372
+ .map((server) => {
373
+ const { headers: _stripped, ...safeTransport } = server.config
374
+ .transport as Record<string, unknown>;
375
+ return {
376
+ id: server.id,
377
+ status: manager.getClient(server.id)?.isConnected
378
+ ? "connected"
379
+ : PLUGIN_SERVER_STATUS,
380
+ transport: safeTransport as McpServerEntry["transport"],
381
+ enabled: true,
382
+ defaultRiskLevel: server.config.defaultRiskLevel,
383
+ // A plugin server has no assistant-owned credentials. Resolving
384
+ // these against `mcp:<id>:*` would report, and could disclose, a
385
+ // workspace credential that happens to share the id.
386
+ hasOAuth: false,
387
+ hasStaticAuth: false,
388
+ authType: "none" as const,
389
+ source: "plugin" as const,
390
+ pluginName: server.pluginName,
391
+ };
392
+ });
302
393
  }
303
394
 
304
395
  // ---------------------------------------------------------------------------
@@ -85,6 +85,7 @@ import {
85
85
  } from "../../cli/lib/uninstall-plugin.js";
86
86
  import {
87
87
  PluginMergeBaselineError,
88
+ PluginNotCuratedError,
88
89
  PluginNotUpgradableError,
89
90
  upgradePlugin,
90
91
  } from "../../cli/lib/upgrade-plugin.js";
@@ -606,6 +607,12 @@ const pluginUpgradeRequestSchema = z.object({
606
607
  .describe(
607
608
  "How to reconcile local edits with the pin. `overwrite` (default) discards local edits and re-installs the pin; `ours`/`theirs` three-way merge, resolving conflicting hunks toward the local edit or the pin respectively; `assistant` is not yet supported.",
608
609
  ),
610
+ marketplaceOnly: z
611
+ .boolean()
612
+ .optional()
613
+ .describe(
614
+ "Refuse the upgrade (409) when the marketplace does not claim this plugin, instead of advancing it to whatever its recorded GitHub ref now resolves to. Set by unattended callers such as the automatic update sweep, for which running code no curator reviewed is never an acceptable outcome. Defaults to false.",
615
+ ),
609
616
  });
610
617
 
611
618
  const pluginUpgradeResponseSchema = z.object({
@@ -1465,6 +1472,11 @@ async function handleUpgradePlugin({
1465
1472
  typeof body.strategy === "string"
1466
1473
  ? (body.strategy as PluginUpgradeStrategy)
1467
1474
  : undefined;
1475
+ // Enforced here, not just by whoever decided to call: `upgradePlugin` fetches
1476
+ // the catalog itself, so an entry that vanishes between a caller's own check
1477
+ // and this handler would otherwise reroute a curated upgrade onto the
1478
+ // install's mutable recorded ref.
1479
+ const marketplaceOnly = body.marketplaceOnly === true;
1468
1480
 
1469
1481
  // Like install, the upgrade target ref is the curated marketplace pin
1470
1482
  // (resolved inside `upgradePlugin` via `inspectPlugin`), never a
@@ -1498,7 +1510,7 @@ async function handleUpgradePlugin({
1498
1510
  // upgraded revision adds is surfaced to the user by the schedule
1499
1511
  // reconciler's `schedule.declared` notification when it arms.
1500
1512
  const result = await upgradePlugin(
1501
- { name, dryRun, strategy },
1513
+ { name, dryRun, strategy, marketplaceOnly },
1502
1514
  {
1503
1515
  fetch: globalThis.fetch.bind(globalThis),
1504
1516
  // Tear the old version down BEFORE the staged tree replaces its
@@ -1568,6 +1580,12 @@ async function handleUpgradePlugin({
1568
1580
  if (err instanceof PluginNotUpgradableError) {
1569
1581
  throw new ConflictError(err.message);
1570
1582
  }
1583
+ // A `marketplaceOnly` caller asked about an install the catalog does not
1584
+ // claim. Retrying changes nothing; only a human upgrading it deliberately
1585
+ // (without the flag) can move it, so 409 rather than a retryable 503.
1586
+ if (err instanceof PluginNotCuratedError) {
1587
+ throw new ConflictError(err.message);
1588
+ }
1571
1589
  // A rate-limited or temporarily-down source (the plugin repo or the
1572
1590
  // marketplace catalog) is a retryable outage, not a conflict — 503.
1573
1591
  if (err instanceof PluginSourceUnavailableError) {
@@ -2039,7 +2057,7 @@ export const ROUTES: RouteDefinition[] = [
2039
2057
  },
2040
2058
  summary: "Upgrade a plugin to its source's current revision",
2041
2059
  description:
2042
- 'Move an installed plugin to its source\'s current revision, re-materializing it under `<workspaceDir>/plugins/<name>/`. A marketplace plugin advances to the curated pin; a plugin installed directly from a GitHub URL (untrusted, not in the marketplace) advances to whatever its recorded ref now resolves to — a pinned SHA is immutable (a no-op), a branch/tag/HEAD moves as upstream does — re-materialized verbatim with no curated adapter overlay, exactly as the original untrusted install was. The target ref is never taken from the request (no caller-supplied ref), mirroring `plugins install`\'s curation boundary. A no-op (`outcome: "already-up-to-date"`) when the installed commit already equals the target; pass `dryRun` to preview the move (`outcome: "would-upgrade"`) without touching the install. Installs lacking provenance are re-pinned to the current SHA. The upgraded code is picked up live on the next read (no restart required). `strategy` controls how local edits are reconciled: `overwrite` (default) discards them and re-installs the target wholesale; `ours`/`theirs`/`assistant` perform a three-way merge against the re-materialized install commit, carrying non-conflicting edits from both sides forward and resolving conflicting hunks toward the local edit (`ours`) or the target (`theirs`), or writing git conflict markers into the file and reporting them in `conflicts`/`binaryConflicts` for the assistant to resolve (`assistant`). A merge strategy whose install-time baseline cannot be reconstructed returns 409. Mirrors the CLI\'s `assistant plugins upgrade <name> [--strategy <s>]`.',
2060
+ 'Move an installed plugin to its source\'s current revision, re-materializing it under `<workspaceDir>/plugins/<name>/`. A marketplace plugin advances to the curated pin; a plugin installed directly from a GitHub URL (untrusted, not in the marketplace) advances to whatever its recorded ref now resolves to — a pinned SHA is immutable (a no-op), a branch/tag/HEAD moves as upstream does — re-materialized verbatim with no curated adapter overlay, exactly as the original untrusted install was. The target ref is never taken from the request (no caller-supplied ref), mirroring `plugins install`\'s curation boundary. A no-op (`outcome: "already-up-to-date"`) when the installed commit already equals the target; pass `dryRun` to preview the move (`outcome: "would-upgrade"`) without touching the install. Installs lacking provenance are re-pinned to the current SHA. The upgraded code is picked up live on the next read (no restart required). `strategy` controls how local edits are reconciled: `overwrite` (default) discards them and re-installs the target wholesale; `ours`/`theirs`/`assistant` perform a three-way merge against the re-materialized install commit, carrying non-conflicting edits from both sides forward and resolving conflicting hunks toward the local edit (`ours`) or the target (`theirs`), or writing git conflict markers into the file and reporting them in `conflicts`/`binaryConflicts` for the assistant to resolve (`assistant`). A merge strategy whose install-time baseline cannot be reconstructed returns 409. Pass `marketplaceOnly` to refuse the untrusted direct path outright (409), which is what the unattended auto-update sweep does. Mirrors the CLI\'s `assistant plugins upgrade <name> [--strategy <s>]`.',
2043
2061
  tags: ["plugins"],
2044
2062
  pathParams: [
2045
2063
  {
@@ -2062,7 +2080,7 @@ export const ROUTES: RouteDefinition[] = [
2062
2080
  },
2063
2081
  "409": {
2064
2082
  description:
2065
- "The install has neither a marketplace entry nor a recorded GitHub source to advance to, or a merge strategy was requested whose install-time baseline cannot be reconstructed.",
2083
+ "The install has neither a marketplace entry nor a recorded GitHub source to advance to, a `marketplaceOnly` upgrade was requested for an install the marketplace does not claim, or a merge strategy was requested whose install-time baseline cannot be reconstructed.",
2066
2084
  },
2067
2085
  "503": {
2068
2086
  description:
@@ -223,6 +223,35 @@ const STT_ERROR_MAP: Record<SttErrorCategory, () => RouteError> = {
223
223
  "provider-error": () => new BadGatewayError("STT provider error"),
224
224
  };
225
225
 
226
+ /**
227
+ * Resolve the configured batch transcriber, or fail the request with a 503
228
+ * naming which kind of unavailability this is.
229
+ *
230
+ * A typed {@link SttError} already names the situation and its fix (a
231
+ * configured provider that cannot batch transcribe), so its message is
232
+ * surfaced verbatim. `null` means nothing is configured at all, and any other
233
+ * throw is an unexpected resolver failure.
234
+ */
235
+ async function resolveBatchTranscriberOrUnavailable(): Promise<BatchTranscriber> {
236
+ let transcriber: BatchTranscriber | null;
237
+ try {
238
+ transcriber = await resolveBatchTranscriber();
239
+ } catch (err) {
240
+ if (err instanceof SttError) {
241
+ throw new ServiceUnavailableError(err.message);
242
+ }
243
+ log.error({ err }, "Failed to resolve STT transcriber");
244
+ throw new ServiceUnavailableError("STT provider is not available");
245
+ }
246
+
247
+ if (!transcriber) {
248
+ throw new ServiceUnavailableError(
249
+ "No speech-to-text provider is configured",
250
+ );
251
+ }
252
+ return transcriber;
253
+ }
254
+
226
255
  // ---------------------------------------------------------------------------
227
256
  // Handlers
228
257
  // ---------------------------------------------------------------------------
@@ -288,19 +317,7 @@ async function handleTranscribe({ body }: RouteHandlerArgs) {
288
317
  }
289
318
 
290
319
  // -- Resolve transcriber --------------------------------------------------
291
- let transcriber;
292
- try {
293
- transcriber = await resolveBatchTranscriber();
294
- } catch (err) {
295
- log.error({ err }, "Failed to resolve STT transcriber");
296
- throw new ServiceUnavailableError("STT provider is not available");
297
- }
298
-
299
- if (!transcriber) {
300
- throw new ServiceUnavailableError(
301
- "No speech-to-text provider is configured",
302
- );
303
- }
320
+ const transcriber = await resolveBatchTranscriberOrUnavailable();
304
321
 
305
322
  // -- Transcribe with timeout ----------------------------------------------
306
323
  const abortController = new AbortController();
@@ -358,18 +375,7 @@ async function handleTranscribeFile({ body }: RouteHandlerArgs) {
358
375
  );
359
376
  }
360
377
 
361
- let transcriber;
362
- try {
363
- transcriber = await resolveBatchTranscriber();
364
- } catch (err) {
365
- log.error({ err }, "Failed to resolve STT transcriber");
366
- throw new ServiceUnavailableError("STT provider is not available");
367
- }
368
- if (!transcriber) {
369
- throw new ServiceUnavailableError(
370
- "No speech-to-text provider is configured",
371
- );
372
- }
378
+ const transcriber = await resolveBatchTranscriberOrUnavailable();
373
379
 
374
380
  const startTime = Date.now();
375
381
  let wavPath: string | null = null;
@@ -64,6 +64,9 @@ export async function resolveSurfaceConversation(
64
64
  const escaped = surfaceId.replace(/[\\%_]/g, "\\$&");
65
65
  const row = rawGet<{ conversation_id: string }>(
66
66
  "surfaceResolver:resolveConversation",
67
+ // Unfinalized rows cannot match: their content is a { ref } pointer,
68
+ // never inline surface JSON, so the LIKE probes below see finalized
69
+ // content only by shape. No completeness predicate needed.
67
70
  `SELECT conversation_id FROM messages
68
71
  WHERE content LIKE ? ESCAPE '\\'
69
72
  ORDER BY created_at DESC