@vellumai/assistant 0.11.3 → 0.11.4-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -4,6 +4,7 @@ import {
4
4
  isModelInCatalog,
5
5
  } from "../providers/model-catalog.js";
6
6
  import { resolveModelIntent } from "../providers/model-intents.js";
7
+ import { isCodexSubscriptionModel } from "../providers/openai/codex-models.js";
7
8
  import type { ModelIntent } from "../providers/types.js";
8
9
  import { getManagedUpstream } from "../providers/vellum-model-routing.js";
9
10
  import {
@@ -31,7 +32,8 @@ import {
31
32
  * structured as an intent × provider matrix: each default profile is an
32
33
  * intent, and each provider that can serve default profiles has a concrete
33
34
  * implementation of that intent (model, token budget, effort, thinking).
34
- * The `vellum` column is the platform-managed implementation; the other
35
+ * The `vellum` column is the platform-managed implementation and the
36
+ * `chatgpt` column is the ChatGPT-subscription implementation; the other
35
37
  * columns are the BYOK implementations resolved through `llm.defaultProvider`
36
38
  * on off-platform installs.
37
39
  *
@@ -96,7 +98,7 @@ const VELLUM_PROFILE_IMPLS: ProfileImpls = {
96
98
  },
97
99
  },
98
100
  "cost-optimized": {
99
- model: "accounts/fireworks/models/deepseek-v4-flash",
101
+ model: "accounts/fireworks/models/deepseek-v4-flash-0731",
100
102
  provider: "vellum",
101
103
  source: "managed",
102
104
  label: "Cost",
@@ -147,6 +149,69 @@ const VELLUM_PROFILE_IMPLS: ProfileImpls = {
147
149
  },
148
150
  };
149
151
 
152
+ /**
153
+ * The `chatgpt` column: ChatGPT-subscription implementations, stamped
154
+ * `provider: "chatgpt"` so dispatch routes through the canonical
155
+ * `chatgpt-subscription` row via `resolveRoutingIdentity` with no pinned
156
+ * connection. Models are pinned (never intents): the intent tables are
157
+ * keyed by concrete dispatch providers, and the Codex endpoint serves only
158
+ * `CODEX_SUBSCRIPTION_MODEL_IDS`. Cost and Speed are identical
159
+ * implementations here: the subscription serves no tier cheaper or faster
160
+ * than luna, and both profiles advertise reasoning off.
161
+ */
162
+ const CHATGPT_PROFILE_IMPLS: ProfileImpls = {
163
+ balanced: {
164
+ model: "gpt-5.6-luna",
165
+ provider: "chatgpt",
166
+ source: "managed",
167
+ label: "Balanced",
168
+ description: "Good balance of quality, cost, and speed",
169
+ // Matches the vellum column's Balanced (same model): the Codex path
170
+ // sends no max_output_tokens, so this only sizes internal budgeting.
171
+ maxTokens: 32000,
172
+ effort: "high",
173
+ thinking: { enabled: true, streamThinking: true },
174
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
175
+ },
176
+ "quality-optimized": {
177
+ model: "gpt-5.6-sol",
178
+ provider: "chatgpt",
179
+ source: "managed",
180
+ label: "Quality",
181
+ description: "Best results with the most capable model",
182
+ maxTokens: 32000,
183
+ effort: "high",
184
+ thinking: { enabled: true, streamThinking: true },
185
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
186
+ },
187
+ "cost-optimized": {
188
+ model: "gpt-5.6-luna",
189
+ provider: "chatgpt",
190
+ source: "managed",
191
+ label: "Cost",
192
+ description: "Cheapest responses, for high-volume work",
193
+ maxTokens: 8192,
194
+ effort: "none",
195
+ thinking: { enabled: false, streamThinking: false },
196
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
197
+ },
198
+ "latency-optimized": {
199
+ model: "gpt-5.6-luna",
200
+ provider: "chatgpt",
201
+ source: "managed",
202
+ label: "Speed",
203
+ description: "Fastest responses, with reasoning turned off",
204
+ maxTokens: 8192,
205
+ // Explicit reasoning opt-out, matching the other columns: this profile
206
+ // advertises reasoning as off, and OpenAI-compat APIs default reasoning
207
+ // to "medium" when the field is omitted, so the opt-out has to be stated
208
+ // rather than implied.
209
+ effort: "none",
210
+ thinking: { enabled: false, streamThinking: false },
211
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
212
+ },
213
+ };
214
+
150
215
  /**
151
216
  * The BYOK implementation of each default profile intent, shared by every
152
217
  * non-vellum provider. The concrete model resolves per provider from the
@@ -219,7 +284,9 @@ export const PROFILE_IMPLS: Record<
219
284
  provider,
220
285
  provider === "vellum"
221
286
  ? VELLUM_PROFILE_IMPLS[key]
222
- : { ...BYOK_PROFILE_IMPLS[key], provider },
287
+ : provider === "chatgpt"
288
+ ? CHATGPT_PROFILE_IMPLS[key]
289
+ : { ...BYOK_PROFILE_IMPLS[key], provider },
223
290
  ]),
224
291
  ) as Record<DefaultProfileProvider, DefaultProfileTemplate>,
225
292
  ]),
@@ -302,9 +369,9 @@ export const OS_BETA_PROFILE_TEMPLATE: DefaultProfileTemplate = {
302
369
  /**
303
370
  * Profiles whose body is code-owned outright: no workspace overlay, and no
304
371
  * user-owned shadow. The shadow rule below lets a user replace a default they
305
- * can select, but `latency-optimized` fronts every live-voice turn through
306
- * `voiceFrontDecision`/`voiceFrontDoor`, where a model outside the latency
307
- * envelope is audible dead air rather than a slow reply. A same-named
372
+ * can select, but `latency-optimized` serves `voiceFrontDoor` and
373
+ * `voiceProgressNarration`, where a model outside the latency envelope is
374
+ * audible dead air rather than a slow reply. A same-named
308
375
  * workspace entry stays on disk and stays listed; it just never governs what
309
376
  * this name resolves to.
310
377
  */
@@ -372,21 +439,23 @@ for (const key of DEFAULT_PROFILE_KEYS) {
372
439
  `PROFILE_IMPLS[${key}][${provider}] must set exactly one of \`intent\` or \`model\`.`,
373
440
  );
374
441
  }
375
- if (impl.provider === "vellum" && impl.model == null) {
442
+ if (ROUTING_IDENTITY_PROVIDERS.has(impl.provider) && impl.model == null) {
376
443
  throw new Error(
377
- `PROFILE_IMPLS[${key}][${provider}] must pin a \`model\`: the vellum ` +
378
- `column has no intent table, and the model selects the upstream.`,
444
+ `PROFILE_IMPLS[${key}][${provider}] must pin a \`model\`: routing ` +
445
+ `identities have no intent table, and the model selects the route.`,
379
446
  );
380
447
  }
381
448
  if (impl.model != null) {
382
449
  const routable =
383
450
  impl.provider === "vellum"
384
451
  ? getManagedUpstream(impl.model) !== null
385
- : isModelInCatalog(impl.provider, impl.model);
452
+ : impl.provider === "chatgpt"
453
+ ? isCodexSubscriptionModel(impl.model)
454
+ : isModelInCatalog(impl.provider, impl.model);
386
455
  if (!routable) {
387
456
  throw new Error(
388
457
  `PROFILE_IMPLS[${key}][${provider}] references model "${impl.model}" ` +
389
- `which is not ${impl.provider === "vellum" ? "served by any managed upstream" : `in PROVIDER_CATALOG for provider "${impl.provider}"`}. ` +
458
+ `which is not ${impl.provider === "vellum" ? "served by any managed upstream" : impl.provider === "chatgpt" ? "in CODEX_SUBSCRIPTION_MODEL_IDS" : `in PROVIDER_CATALOG for provider "${impl.provider}"`}. ` +
390
459
  `Update model-catalog.ts or default-profile-catalog.ts.`,
391
460
  );
392
461
  }
@@ -517,7 +586,9 @@ function resolveAgainstBody(
517
586
  * Non-obvious rules:
518
587
  *
519
588
  * - The `vellum` column stamps `provider: "vellum"` with no connection —
520
- * dispatch derives the upstream from the model per-request.
589
+ * dispatch derives the upstream from the model per-request. The `chatgpt`
590
+ * column likewise stamps its routing identity with no connection;
591
+ * dispatch resolves the canonical subscription row per-request.
521
592
  * - A default provider without a named matrix column materializes from the
522
593
  * shared `BYOK_PROFILE_IMPLS` templates, with `resolveModelIntent`
523
594
  * falling back to the provider's catalog `defaultModel`.
@@ -38,7 +38,9 @@ export const OS_BETA_PROFILE_KEY = "os-beta";
38
38
  /**
39
39
  * The named columns of the intent × provider matrix. `vellum` is the
40
40
  * platform-managed column (routed through the single `vellum` connection to
41
- * an underlying provider per profile); the rest are BYOK columns whose
41
+ * an underlying provider per profile) and `chatgpt` is the
42
+ * ChatGPT-subscription column (routed through the `chatgpt-subscription`
43
+ * connection to the Codex endpoint); the rest are BYOK columns whose
42
44
  * models resolve per provider via `resolveModelIntent`. The full set of
43
45
  * providers that can back `llm.defaultProvider` is wider, see
44
46
  * `DEFAULT_PROVIDER_CHOICES` in `schemas/llm.ts`.
@@ -53,6 +55,7 @@ export const DEFAULT_PROFILE_PROVIDERS = [
53
55
  "gemini",
54
56
  "fireworks",
55
57
  "openrouter",
58
+ "chatgpt",
56
59
  "vellum",
57
60
  ] as const;
58
61
  export type DefaultProfileProvider = (typeof DEFAULT_PROFILE_PROVIDERS)[number];
@@ -6,6 +6,7 @@
6
6
  * conveniences (`getDefaultProvider()` without an argument,
7
7
  * `setDefaultProvider`) live in `default-provider.ts`.
8
8
  */
9
+ import { CHATGPT_SUBSCRIPTION_CONNECTION_NAME } from "../providers/inference/auth.js";
9
10
  import { VELLUM_MANAGED_CONNECTION_NAME } from "../providers/vellum-model-routing.js";
10
11
  import type { DefaultProviderConfig } from "./schemas/llm.js";
11
12
  import type { AssistantConfig } from "./types.js";
@@ -29,5 +30,8 @@ export function resolveDefaultConnectionName(
29
30
  if (dp.provider === "vellum") {
30
31
  return VELLUM_MANAGED_CONNECTION_NAME;
31
32
  }
33
+ if (dp.provider === "chatgpt") {
34
+ return CHATGPT_SUBSCRIPTION_CONNECTION_NAME;
35
+ }
32
36
  return `${dp.provider}-personal`;
33
37
  }
@@ -1,3 +1,4 @@
1
+ import { resolveEntryProviderKind } from "../providers/connection-resolution.js";
1
2
  import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
2
3
  import {
3
4
  getCatalogProviderForModel,
@@ -54,11 +55,18 @@ export function resolveEffectiveContextWindow({
54
55
  forceOverrideProfile,
55
56
  selectionSeed,
56
57
  });
57
- // Routing identities are not catalog providers; the model's catalog owner
58
- // carries its context-window limits.
58
+ // Routing identities dispatch built-in catalog models, so the model's
59
+ // catalog owner carries their limits. An entry-name provider resolves
60
+ // through its row's kind instead: a custom-endpoint kind has no catalog
61
+ // models, so its models keep the conservative default even when a model
62
+ // id collides with a built-in one (the custom endpoint's "gpt-5.5" is not
63
+ // OpenAI's). Labels with no row fall back to the model's catalog owner.
59
64
  const catalogProviderId = ROUTING_IDENTITY_PROVIDERS.has(resolved.provider)
60
65
  ? getCatalogProviderForModel(resolved.model)
61
- : resolved.provider;
66
+ : PROVIDER_CATALOG.some((p) => p.id === resolved.provider)
67
+ ? resolved.provider
68
+ : (resolveEntryProviderKind(resolved.provider, resolved.model) ??
69
+ getCatalogProviderForModel(resolved.model));
62
70
  const catalogModel = PROVIDER_CATALOG.find(
63
71
  (provider) => provider.id === catalogProviderId,
64
72
  )?.models.find((model) => model.id === resolved.model);
@@ -76,6 +76,15 @@ import {
76
76
  */
77
77
  export interface ResolveCallSiteOpts {
78
78
  overrideProfile?: string;
79
+ /**
80
+ * Whether a profile's provider value can actually dispatch: a known
81
+ * vendor, or a connection entry row. Selection is pure and DB-blind, so
82
+ * dispatch-side callers supply this (see `dispatchProviderResolvable` in
83
+ * `connection-resolution.ts`); a profile failing it is unusable and
84
+ * selection falls through to the next rung, keeping model and transport
85
+ * coherent. When absent, every provider is assumed resolvable.
86
+ */
87
+ isResolvableProvider?: (provider: string) => boolean;
79
88
  /**
80
89
  * Float `overrideProfile` above the call-site layers for non-main-agent call
81
90
  * sites. Retained for API compatibility; under single-winner selection the
@@ -112,7 +121,11 @@ export interface ResolveCallSiteOpts {
112
121
  }) => void;
113
122
  }
114
123
 
115
- export type ResolutionFallbackReason = "missing" | "disabled" | "incomplete";
124
+ export type ResolutionFallbackReason =
125
+ | "missing"
126
+ | "disabled"
127
+ | "incomplete"
128
+ | "unresolvable";
116
129
 
117
130
  export interface ResolvedCallSiteConfig {
118
131
  config: z.infer<typeof LLMConfigBase>;
@@ -313,6 +326,20 @@ function usableEntry(
313
326
  report(name, "incomplete");
314
327
  return undefined;
315
328
  }
329
+ // A provider the caller cannot resolve (not a known vendor, not an entry
330
+ // row) makes the profile unusable, and selection falls through to the
331
+ // next rung the same way an incomplete profile does: the fallback keeps
332
+ // model and transport coherent, where a dispatch-time fallback would pair
333
+ // this profile's model with the default transport. Selection is DB-blind,
334
+ // so the predicate is supplied by dispatch-side callers; when absent,
335
+ // every provider is assumed resolvable.
336
+ if (
337
+ opts.isResolvableProvider != null &&
338
+ !opts.isResolvableProvider(entry.provider)
339
+ ) {
340
+ report(name, "unresolvable");
341
+ return undefined;
342
+ }
316
343
  return { name, entry };
317
344
  }
318
345
 
@@ -1,9 +1,12 @@
1
+ import { getDb } from "../persistence/db-connection.js";
1
2
  import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
3
+ import { getConnection } from "../providers/inference/connections.js";
2
4
  import {
3
5
  getCatalogProviderForModel,
4
6
  isModelInCatalog,
5
7
  } from "../providers/model-catalog.js";
6
8
  import {
9
+ getManagedUpstream,
7
10
  MANAGED_ROUTABLE_PROVIDERS,
8
11
  VELLUM_MANAGED_CONNECTION_NAME,
9
12
  } from "../providers/vellum-model-routing.js";
@@ -97,35 +100,80 @@ export function completeCustomProfile(
97
100
  }
98
101
  }
99
102
 
100
- // A provider-specific connection baked onto a different provider would pin
101
- // a mismatch (dispatch auto-resolves an absent connection by provider
102
- // instead). The Vellum managed connection must survive a provider change —
103
- // dispatch routes it via `expectedProvider` — but only onto providers it
104
- // can actually route; a non-managed-routable provider (openrouter, ollama)
105
- // would hit the mismatch path instead of auto-resolution.
106
- // Routing-identity providers resolve their connection per-request from
107
- // the provider value; a stamped provider_connection would be dead weight
108
- // at best and a misroute at worst.
103
+ // Completion never stamps a `provider_connection`; an inherited binding
104
+ // would only re-introduce the collapsed field on disk. The default's
105
+ // explicit binding is instead inherited IN the provider value, the
106
+ // entries-model representation: the vellum binding becomes the routing
107
+ // identity (only when it can serve the model, or the read-path schema
108
+ // would strip the profile), and a same-vendor binding becomes the entry
109
+ // name, so a completed profile keeps signing with the credential the
110
+ // workspace default names rather than whatever auto-resolution finds
111
+ // first.
109
112
  if (
110
113
  completed.provider !== undefined &&
111
- ROUTING_IDENTITY_PROVIDERS.has(completed.provider)
114
+ !ROUTING_IDENTITY_PROVIDERS.has(completed.provider) &&
115
+ profile.provider_connection === undefined &&
116
+ dflt.provider_connection !== undefined
112
117
  ) {
113
- return structuredClone(completed);
118
+ if (dflt.provider_connection === VELLUM_MANAGED_CONNECTION_NAME) {
119
+ // Only managed-servable pairs inherit the managed binding; anything
120
+ // else inherits nothing and lets dispatch auto-resolve by vendor,
121
+ // matching the pre-entries inheritance contract.
122
+ if (
123
+ MANAGED_ROUTABLE_PROVIDERS.has(completed.provider) &&
124
+ completed.model !== undefined &&
125
+ getManagedUpstream(completed.model) !== null
126
+ ) {
127
+ completed.provider = "vellum";
128
+ }
129
+ } else if (completed.provider === dflt.provider) {
130
+ // Folding to the entry name is only safe against a verified row; a
131
+ // dangling or kind-disagreeing binding stays in the legacy field,
132
+ // where the collapse migration's recovery can judge it.
133
+ if (bindingRowKind(dflt.provider_connection) === completed.provider) {
134
+ completed.provider = dflt.provider_connection;
135
+ } else {
136
+ completed.provider_connection = dflt.provider_connection;
137
+ }
138
+ }
114
139
  }
140
+ return structuredClone(completed);
141
+ }
115
142
 
116
- const vellumRoutable =
117
- dflt.provider_connection === VELLUM_MANAGED_CONNECTION_NAME &&
118
- completed.provider !== undefined &&
119
- MANAGED_ROUTABLE_PROVIDERS.has(completed.provider);
120
- if (
121
- profile.provider_connection === undefined &&
122
- dflt.provider_connection !== undefined &&
123
- (completed.provider === dflt.provider || vellumRoutable)
124
- ) {
125
- completed.provider_connection = dflt.provider_connection;
143
+ /**
144
+ * `{...raw, ...completed}` recursively: completed (schema-known) values win,
145
+ * raw keys the schema stripped survive at every depth. Used by boot
146
+ * materialization and the config write path so both preserve unknown keys
147
+ * the same way after `safeParse`.
148
+ */
149
+ export function mergePreservingUnknownKeys(
150
+ raw: Record<string, unknown>,
151
+ completed: Record<string, unknown>,
152
+ ): Record<string, unknown> {
153
+ const out: Record<string, unknown> = { ...raw, ...completed };
154
+ for (const [key, value] of Object.entries(completed)) {
155
+ const rawValue = raw[key];
156
+ if (isRecord(value) && isRecord(rawValue)) {
157
+ out[key] = mergePreservingUnknownKeys(rawValue, value);
158
+ }
126
159
  }
160
+ return out;
161
+ }
127
162
 
128
- return structuredClone(completed);
163
+ function isRecord(value: unknown): value is Record<string, unknown> {
164
+ return value !== null && typeof value === "object" && !Array.isArray(value);
165
+ }
166
+
167
+ /**
168
+ * The provider kind stored on a connection row, or null when the row is
169
+ * missing or the DB is unavailable (both mean "unverifiable" to callers).
170
+ */
171
+ function bindingRowKind(name: string): string | null {
172
+ try {
173
+ return getConnection(getDb(), name)?.provider ?? null;
174
+ } catch {
175
+ return null;
176
+ }
129
177
  }
130
178
 
131
179
  type PlainObject = Record<string, unknown>;
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  LiveVoiceConfigSchema,
5
+ LiveVoiceFluxConfigSchema,
5
6
  LiveVoiceFrontModelConfigSchema,
6
7
  LiveVoiceVadConfigSchema,
7
8
  VALID_LIVE_VOICE_MODES,
@@ -21,11 +22,18 @@ const FRONT_MODEL_DEFAULTS = {
21
22
  endpointDecisionTimeoutMs: 1200,
22
23
  endpointExtensionMs: 1500,
23
24
  endpointMaxExtensions: 2,
24
- ackFirstDeltaTimeoutMs: 2500,
25
- ackGenerationTimeoutMs: 600,
26
25
  progress: PROGRESS_DEFAULTS,
27
26
  };
28
27
 
28
+ // `eagerEotThreshold` is deliberately absent: it has no default, and leaving it
29
+ // unset is what keeps Deepgram from emitting speculative turn events.
30
+ const FLUX_DEFAULTS = {
31
+ turnEnd: { enabled: false },
32
+ model: "flux-general-en",
33
+ eotThreshold: 0.7,
34
+ eotTimeoutMs: 5_000,
35
+ };
36
+
29
37
  describe("LiveVoiceVadConfigSchema", () => {
30
38
  test("empty object parses to defaults", () => {
31
39
  const parsed = LiveVoiceVadConfigSchema.parse({});
@@ -124,8 +132,15 @@ describe("LiveVoiceFrontModelConfigSchema", () => {
124
132
  expect(parsed.endpointMaxExtensions).toBe(0);
125
133
  // Unspecified fields still get defaults
126
134
  expect(parsed.endpointExtensionMs).toBe(1500);
127
- expect(parsed.ackFirstDeltaTimeoutMs).toBe(2500);
128
- expect(parsed.ackGenerationTimeoutMs).toBe(600);
135
+ });
136
+
137
+ test("strips retired generated-ack settings", () => {
138
+ const parsed = LiveVoiceFrontModelConfigSchema.parse({
139
+ ackFirstDeltaTimeoutMs: 2500,
140
+ ackGenerationTimeoutMs: 600,
141
+ });
142
+ expect(parsed).not.toHaveProperty("ackFirstDeltaTimeoutMs");
143
+ expect(parsed).not.toHaveProperty("ackGenerationTimeoutMs");
129
144
  });
130
145
 
131
146
  test("rejects non-positive endpointDecisionTimeoutMs", () => {
@@ -213,6 +228,87 @@ describe("LiveVoiceFrontModelConfigSchema", () => {
213
228
  });
214
229
  });
215
230
 
231
+ describe("LiveVoiceFluxConfigSchema", () => {
232
+ test("empty object parses to defaults, with turn-end off", () => {
233
+ expect(LiveVoiceFluxConfigSchema.parse({})).toEqual(FLUX_DEFAULTS);
234
+ });
235
+
236
+ test("an unset eagerEotThreshold is absent, not zero", () => {
237
+ expect("eagerEotThreshold" in LiveVoiceFluxConfigSchema.parse({})).toBe(
238
+ false,
239
+ );
240
+ });
241
+
242
+ test("accepts overrides", () => {
243
+ const parsed = LiveVoiceFluxConfigSchema.parse({
244
+ turnEnd: { enabled: true },
245
+ model: "flux-general-multi",
246
+ eotThreshold: 0.85,
247
+ eagerEotThreshold: 0.45,
248
+ eotTimeoutMs: 12_000,
249
+ });
250
+ expect(parsed.turnEnd.enabled).toBe(true);
251
+ expect(parsed.model).toBe("flux-general-multi");
252
+ expect(parsed.eotThreshold).toBe(0.85);
253
+ expect(parsed.eagerEotThreshold).toBe(0.45);
254
+ expect(parsed.eotTimeoutMs).toBe(12_000);
255
+ });
256
+
257
+ test("partial overrides merge with defaults", () => {
258
+ const parsed = LiveVoiceFluxConfigSchema.parse({ eotThreshold: 0.6 });
259
+ expect(parsed.eotThreshold).toBe(0.6);
260
+ expect(parsed.turnEnd.enabled).toBe(false);
261
+ expect(parsed.model).toBe("flux-general-en");
262
+ expect(parsed.eotTimeoutMs).toBe(5_000);
263
+ });
264
+
265
+ test("rejects an eotThreshold outside 0.5..0.9", () => {
266
+ const above = LiveVoiceFluxConfigSchema.safeParse({ eotThreshold: 0.95 });
267
+ expect(above.success).toBe(false);
268
+ expect(above.error?.issues.map((i) => i.message)).toEqual([
269
+ "liveVoice.flux.eotThreshold must be <= 0.9",
270
+ ]);
271
+
272
+ const below = LiveVoiceFluxConfigSchema.safeParse({ eotThreshold: 0.4 });
273
+ expect(below.success).toBe(false);
274
+ expect(below.error?.issues.map((i) => i.message)).toEqual([
275
+ "liveVoice.flux.eotThreshold must be >= 0.5",
276
+ ]);
277
+ });
278
+
279
+ test("rejects an eagerEotThreshold outside 0.3..0.9", () => {
280
+ expect(
281
+ LiveVoiceFluxConfigSchema.safeParse({ eagerEotThreshold: 0.2 }).success,
282
+ ).toBe(false);
283
+ expect(
284
+ LiveVoiceFluxConfigSchema.safeParse({ eagerEotThreshold: 0.95 }).success,
285
+ ).toBe(false);
286
+ });
287
+
288
+ test("rejects an eotTimeoutMs outside 500..60000, and non-integers", () => {
289
+ expect(
290
+ LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 499 }).success,
291
+ ).toBe(false);
292
+ expect(
293
+ LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 60_001 }).success,
294
+ ).toBe(false);
295
+ expect(
296
+ LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 1_500.5 }).success,
297
+ ).toBe(false);
298
+ });
299
+
300
+ test("rejects a non-boolean turnEnd.enabled", () => {
301
+ const result = LiveVoiceFluxConfigSchema.safeParse({
302
+ turnEnd: { enabled: "yes" },
303
+ });
304
+ expect(result.success).toBe(false);
305
+ const msgs = result.error?.issues.map((i) => i.message) ?? [];
306
+ expect(msgs.some((m) => m.includes("liveVoice.flux.turnEnd.enabled"))).toBe(
307
+ true,
308
+ );
309
+ });
310
+ });
311
+
216
312
  describe("LiveVoiceConfigSchema", () => {
217
313
  test("empty object parses to defaults", () => {
218
314
  const parsed = LiveVoiceConfigSchema.parse({});
@@ -228,6 +324,9 @@ describe("LiveVoiceConfigSchema", () => {
228
324
  echoDrainSlackMs: 300,
229
325
  },
230
326
  frontModel: FRONT_MODEL_DEFAULTS,
327
+ // Off by default: Flux turn detection is opt-in, so the front-door hold
328
+ // verdict keeps committing turns until it is enabled.
329
+ flux: FLUX_DEFAULTS,
231
330
  maxSessionDurationSeconds: 1800,
232
331
  // Off by default: voice turns carry only their transcript, no audio
233
332
  // artifacts on the conversation messages (JARVIS-1283).
@@ -255,6 +354,7 @@ describe("LiveVoiceConfigSchema", () => {
255
354
  mode: "ptt",
256
355
  vad: { silenceThresholdMs: 900 },
257
356
  frontModel: { endpointDecisionTimeoutMs: 300 },
357
+ flux: { turnEnd: { enabled: true } },
258
358
  maxSessionDurationSeconds: 600,
259
359
  });
260
360
  expect(parsed.mode).toBe("ptt");
@@ -265,6 +365,9 @@ describe("LiveVoiceConfigSchema", () => {
265
365
  // Partial frontModel overrides merge with defaults
266
366
  expect(parsed.frontModel.endpointDecisionTimeoutMs).toBe(300);
267
367
  expect(parsed.frontModel.endpointExtensionMs).toBe(1500);
368
+ // Partial flux overrides merge with defaults
369
+ expect(parsed.flux.turnEnd.enabled).toBe(true);
370
+ expect(parsed.flux.eotThreshold).toBe(0.7);
268
371
  expect(parsed.maxSessionDurationSeconds).toBe(600);
269
372
  });
270
373
 
@@ -289,11 +289,11 @@ const CATALOG_RECORD: CatalogRecord = {
289
289
  "Captions images via a vision-capable profile for text-only model fallback.",
290
290
  domain: "skills",
291
291
  },
292
- voiceFrontDecision: {
293
- id: "voiceFrontDecision",
294
- displayName: "Voice Front Decision",
292
+ voiceProgressNarration: {
293
+ id: "voiceProgressNarration",
294
+ displayName: "Voice Progress Narration",
295
295
  description:
296
- "Fast turn-taking and presence decisions during live voice (semantic endpointing, ack generation).",
296
+ "Phrases short spoken progress updates during long-running live voice turns.",
297
297
  domain: "agentLoop",
298
298
  },
299
299
  voiceFrontDoor: {
@@ -207,34 +207,64 @@ export const LiveVoiceFrontModelConfigSchema = z
207
207
  )
208
208
  .default(2)
209
209
  .describe("Cap on consecutive 'hold' extensions per utterance"),
210
- ackFirstDeltaTimeoutMs: z
211
- .number({
212
- error: "liveVoice.frontModel.ackFirstDeltaTimeoutMs must be a number",
213
- })
214
- .int("liveVoice.frontModel.ackFirstDeltaTimeoutMs must be an integer")
215
- .positive(
216
- "liveVoice.frontModel.ackFirstDeltaTimeoutMs must be a positive integer",
217
- )
218
- .default(2500)
219
- .describe(
220
- "Keyword-delay budget (ms): a spoken ack fires if no first assistant delta has arrived by then",
221
- ),
222
- ackGenerationTimeoutMs: z
223
- .number({
224
- error: "liveVoice.frontModel.ackGenerationTimeoutMs must be a number",
225
- })
226
- .int("liveVoice.frontModel.ackGenerationTimeoutMs must be an integer")
227
- .positive(
228
- "liveVoice.frontModel.ackGenerationTimeoutMs must be a positive integer",
229
- )
230
- .default(600)
231
- .describe("Budget (ms) for LLM-generated ack text"),
232
210
  progress: LiveVoiceProgressConfigSchema.default(
233
211
  LiveVoiceProgressConfigSchema.parse({}),
234
212
  ),
235
213
  })
236
214
  .describe(
237
- "Front-model presence layer tuning for live voice sessions (semantic endpointing + spoken acks + progress narration)",
215
+ "Voice front-door endpointing and long-turn progress narration tuning",
216
+ );
217
+
218
+ const LiveVoiceFluxTurnEndConfigSchema = z
219
+ .object({
220
+ enabled: z
221
+ .boolean({ error: "liveVoice.flux.turnEnd.enabled must be a boolean" })
222
+ .default(false)
223
+ .describe(
224
+ "Commit the live-voice turn on Flux's EndOfTurn instead of the front-door [0] hold verdict. Requires services.stt.provider to be deepgram-flux; ignored otherwise.",
225
+ ),
226
+ })
227
+ .describe(
228
+ "Which signal commits a live voice turn when Deepgram Flux is the STT provider",
229
+ );
230
+
231
+ export const LiveVoiceFluxConfigSchema = z
232
+ .object({
233
+ turnEnd: LiveVoiceFluxTurnEndConfigSchema.default(
234
+ LiveVoiceFluxTurnEndConfigSchema.parse({}),
235
+ ),
236
+ model: z
237
+ .string({ error: "liveVoice.flux.model must be a string" })
238
+ .default("flux-general-en")
239
+ .describe("Deepgram Flux model requested when opening the STT stream"),
240
+ eotThreshold: z
241
+ .number({ error: "liveVoice.flux.eotThreshold must be a number" })
242
+ .min(0.5, "liveVoice.flux.eotThreshold must be >= 0.5")
243
+ .max(0.9, "liveVoice.flux.eotThreshold must be <= 0.9")
244
+ .default(0.7)
245
+ .describe(
246
+ "End-of-turn confidence Flux must reach before it emits EndOfTurn. Lower values commit sooner and cut speakers off more often; higher values wait longer and add end-of-turn latency.",
247
+ ),
248
+ eagerEotThreshold: z
249
+ .number({ error: "liveVoice.flux.eagerEotThreshold must be a number" })
250
+ .min(0.3, "liveVoice.flux.eagerEotThreshold must be >= 0.3")
251
+ .max(0.9, "liveVoice.flux.eagerEotThreshold must be <= 0.9")
252
+ .optional()
253
+ .describe(
254
+ "Confidence at which Flux starts speculating that the turn has ended. Leaving it unset disables Deepgram's EagerEndOfTurn / TurnResumed events; enabling it raises LLM calls 50-70% because speculative turns that resume are thrown away.",
255
+ ),
256
+ eotTimeoutMs: z
257
+ .number({ error: "liveVoice.flux.eotTimeoutMs must be a number" })
258
+ .int("liveVoice.flux.eotTimeoutMs must be an integer")
259
+ .min(500, "liveVoice.flux.eotTimeoutMs must be >= 500")
260
+ .max(60_000, "liveVoice.flux.eotTimeoutMs must be <= 60000")
261
+ .default(5_000)
262
+ .describe(
263
+ "Silence (ms) after which Flux force-ends the turn even though its end-of-turn confidence never reached eotThreshold",
264
+ ),
265
+ })
266
+ .describe(
267
+ "Deepgram Flux turn-detection tuning for live voice sessions (model-integrated end-of-turn)",
238
268
  );
239
269
 
240
270
  export const LiveVoiceConfigSchema = z
@@ -251,6 +281,9 @@ export const LiveVoiceConfigSchema = z
251
281
  frontModel: LiveVoiceFrontModelConfigSchema.default(
252
282
  LiveVoiceFrontModelConfigSchema.parse({}),
253
283
  ),
284
+ flux: LiveVoiceFluxConfigSchema.default(
285
+ LiveVoiceFluxConfigSchema.parse({}),
286
+ ),
254
287
  maxSessionDurationSeconds: z
255
288
  .number({
256
289
  error: "liveVoice.maxSessionDurationSeconds must be a number",
@@ -280,3 +313,4 @@ export type LiveVoiceFrontModelConfig = z.infer<
280
313
  export type LiveVoiceProgressConfig = z.infer<
281
314
  typeof LiveVoiceProgressConfigSchema
282
315
  >;
316
+ export type LiveVoiceFluxConfig = z.infer<typeof LiveVoiceFluxConfigSchema>;