@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
@@ -15,7 +15,15 @@ import { getDb } from "../persistence/db-connection.js";
15
15
  import { acpSessionHistory } from "../persistence/schema/index.js";
16
16
  import * as pendingInteractions from "../runtime/pending-interactions.js";
17
17
  import { getLogger } from "../util/logger.js";
18
+ import { markAcpConnectCardRaised } from "./acp-connect-card-state.js";
18
19
  import { AcpAgentProcess } from "./agent-process.js";
20
+ import {
21
+ ACP_AUTH_RECOVERY_GUIDANCE,
22
+ ACP_CLAUDE_AUTH_REQUIRED_CODE,
23
+ CLAUDE_ACP_COMMAND,
24
+ isAcpAuthRequired,
25
+ isClaudeAuthFailureMessage,
26
+ } from "./auth-required.js";
19
27
  import { resolveAgentWithAutoInstall } from "./auto-install.js";
20
28
  import { VellumAcpClientHandler } from "./client-handler.js";
21
29
  import { deriveFailureError } from "./failure-error.js";
@@ -26,6 +34,30 @@ import type { AcpAgentConfig, AcpSessionState } from "./types.js";
26
34
 
27
35
  const log = getLogger("acp:session-manager");
28
36
 
37
+ /**
38
+ * The `authCode` for a run that failed on its Claude credential, or undefined
39
+ * for any other failure. Adapter-gated first: the signals are Claude-specific,
40
+ * and the marker promises a repair only the Connect Claude flow can perform.
41
+ * Checks both auth-failure shapes (see `auth-required.ts`) against both the
42
+ * raw rejection and the derived failure message, since `deriveFailureError`
43
+ * may replace one with the other.
44
+ */
45
+ function claudeAuthRequiredCode(
46
+ err: unknown,
47
+ failureMessage: string,
48
+ entry: { command: string },
49
+ ): string | undefined {
50
+ if (entry.command !== CLAUDE_ACP_COMMAND) {
51
+ return undefined;
52
+ }
53
+ const rawMessage = err instanceof Error ? err.message : String(err);
54
+ return isAcpAuthRequired(err) ||
55
+ isClaudeAuthFailureMessage(rawMessage) ||
56
+ isClaudeAuthFailureMessage(failureMessage)
57
+ ? ACP_CLAUDE_AUTH_REQUIRED_CODE
58
+ : undefined;
59
+ }
60
+
29
61
  /**
30
62
  * The manager's "unknown session id" error. Thrown whenever an operation
31
63
  * references an acpSessionId with no in-memory entry (and, for resume, no
@@ -1093,6 +1125,11 @@ export class AcpSessionManager {
1093
1125
  err.message,
1094
1126
  current.process.stderrSince(stderrMark),
1095
1127
  );
1128
+ const errorCode = claudeAuthRequiredCode(
1129
+ err,
1130
+ failureMessage,
1131
+ current,
1132
+ );
1096
1133
  if (current.state.status !== "cancelled") {
1097
1134
  current.state.status = "failed";
1098
1135
  current.state.completedAt = Date.now();
@@ -1100,14 +1137,40 @@ export class AcpSessionManager {
1100
1137
  }
1101
1138
  current.currentPrompt = null;
1102
1139
  log.error(
1103
- { acpSessionId, error: err.message, failureMessage },
1140
+ { acpSessionId, error: err.message, failureMessage, errorCode },
1104
1141
  "ACP prompt failed",
1105
1142
  );
1143
+ // `acp_session_error` keeps its pre-existing shape so older packaged
1144
+ // clients still parse it; the recovery signal rides its own event
1145
+ // below (see `api/events/acp-auth-required.ts`).
1106
1146
  current.sendToVellum({
1107
1147
  type: "acp_session_error",
1108
1148
  acpSessionId,
1109
1149
  error: failureMessage,
1110
1150
  });
1151
+ // The recovery surface (event, model guidance, prompt-dedup marker)
1152
+ // is raised as a unit, and only when a spawning tool call exists to
1153
+ // anchor the card AND the user did not already stop the run. A
1154
+ // cancelled run renders no card (the client guards it), so marking
1155
+ // the never-cleared prompt-dedup registry here would suppress the
1156
+ // secure token prompt against a card that never appears.
1157
+ const recoveryAnchor =
1158
+ errorCode !== undefined && current.state.status !== "cancelled"
1159
+ ? current.parentToolUseId
1160
+ : undefined;
1161
+ if (errorCode !== undefined && recoveryAnchor !== undefined) {
1162
+ current.sendToVellum({
1163
+ type: "acp_auth_required",
1164
+ acpSessionId,
1165
+ authCode: errorCode,
1166
+ agent: current.state.agentId,
1167
+ parentToolUseId: recoveryAnchor,
1168
+ });
1169
+ // Same registry as the missing-token spawn path, so the
1170
+ // credential-prompt route redirects a redundant secure prompt at
1171
+ // the card instead of stacking a second one.
1172
+ markAcpConnectCardRaised(current.parentConversationId);
1173
+ }
1111
1174
 
1112
1175
  // Persist the terminal row before teardown clears the buffer.
1113
1176
  this.persistTerminal(acpSessionId, current);
@@ -1123,7 +1186,10 @@ export class AcpSessionManager {
1123
1186
  if (current.state.status !== "cancelled") {
1124
1187
  this.notifyParent(
1125
1188
  current,
1126
- `[ACP agent "${current.state.agentId}" failed]\n\n${failureMessage}`,
1189
+ `[ACP agent "${current.state.agentId}" failed]\n\n${failureMessage}` +
1190
+ (recoveryAnchor !== undefined
1191
+ ? `\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`
1192
+ : ""),
1127
1193
  );
1128
1194
  }
1129
1195
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `acp_auth_required` SSE event.
3
+ *
4
+ * Server to client notification that an ACP run failed because the agent
5
+ * rejected its stored credential and the client can offer a re-authentication
6
+ * affordance. Always emitted immediately AFTER the `acp_session_error` that
7
+ * carries the human-readable failure.
8
+ *
9
+ * It is a separate event rather than a field on `acp_session_error` because
10
+ * every event schema here is `.strict()` and clients fall back to an inert
11
+ * `unknown` event when parsing fails: a new FIELD makes an older packaged
12
+ * client (iOS/macOS bundle the web app) reject the whole error event and
13
+ * render the run as still active, while an unknown event TYPE is simply
14
+ * ignored. Additive signals on existing events need their own event type for
15
+ * the same reason.
16
+ *
17
+ * Canonical wire-contract source. Re-exported to external consumers via
18
+ * `@vellumai/assistant-api` (the `api/index.ts` barrel).
19
+ */
20
+
21
+ import { z } from "zod";
22
+
23
+ /**
24
+ * The code for a `claude-agent-acp` run whose Claude credential needs to be
25
+ * reconnected: the `authCode` on this event for post-spawn failures, and the
26
+ * `errorCode` on the failed `acp_spawn` tool result for pre-spawn ones.
27
+ * Single source for both sides of the wire: the daemon re-exports it via
28
+ * `acp/auth-required.ts`, the web client via
29
+ * `domains/chat/utils/acp-connect.ts`.
30
+ */
31
+ export const ACP_CLAUDE_AUTH_REQUIRED_CODE = "acp_claude_auth_required";
32
+
33
+ export const AcpAuthRequiredEventSchema = z
34
+ .object({
35
+ type: z.literal("acp_auth_required"),
36
+ acpSessionId: z.string(),
37
+ /**
38
+ * Machine-readable classification of what kind of re-authentication is
39
+ * needed, so clients branch on a stable value rather than on the failure
40
+ * text (which varies by adapter and failure mode). Currently only
41
+ * {@link ACP_CLAUDE_AUTH_REQUIRED_CODE}.
42
+ */
43
+ authCode: z.string(),
44
+ /** Agent id the run was spawned with, for display. */
45
+ agent: z.string(),
46
+ /**
47
+ * Tool-use id of the `acp_spawn` call that started this run, so the
48
+ * client anchors the inline affordance to the right transcript row.
49
+ * Absent when the run was not started by a tool call.
50
+ */
51
+ parentToolUseId: z.string().optional(),
52
+ })
53
+ .strict();
54
+
55
+ export type AcpAuthRequiredEvent = z.infer<typeof AcpAuthRequiredEventSchema>;
package/src/api/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { AcpAuthRequiredEventSchema } from "./events/acp-auth-required.js";
3
4
  import { AcpSessionCompletedEventSchema } from "./events/acp-session-completed.js";
4
5
  import { AcpSessionErrorEventSchema } from "./events/acp-session-error.js";
5
6
  import { AcpSessionSpawnedEventSchema } from "./events/acp-session-spawned.js";
@@ -149,6 +150,11 @@ export {
149
150
  SSE_REPLAY_RING_COUNT_LIMIT,
150
151
  } from "./constants/sse-replay.js";
151
152
  export { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "./constants/tool-execution.js";
153
+ export {
154
+ ACP_CLAUDE_AUTH_REQUIRED_CODE,
155
+ type AcpAuthRequiredEvent,
156
+ AcpAuthRequiredEventSchema,
157
+ } from "./events/acp-auth-required.js";
152
158
  export {
153
159
  type AcpSessionCompletedEvent,
154
160
  AcpSessionCompletedEventSchema,
@@ -895,6 +901,7 @@ export {
895
901
  * migration recipe.
896
902
  */
897
903
  export const AssistantEventSchema = z.discriminatedUnion("type", [
904
+ AcpAuthRequiredEventSchema,
898
905
  AcpSessionCompletedEventSchema,
899
906
  AcpSessionErrorEventSchema,
900
907
  AcpSessionSpawnedEventSchema,
@@ -591,6 +591,7 @@ export const SURFACE_TYPES = [
591
591
  "skill_card",
592
592
  "call_summary",
593
593
  "visual",
594
+ "voice_picker",
594
595
  ] as const;
595
596
 
596
597
  export const SurfaceTypeSchema = z.enum(SURFACE_TYPES);
@@ -662,9 +663,10 @@ export type SurfaceData =
662
663
  * and serves verbatim but whose shape it does not model, which is why they
663
664
  * are absent from the renderable `SurfaceData` union: `channel_setup` (a
664
665
  * side-effect command forwarded to the setup panel), `task_preferences` (a
665
- * fixed grid that reads no data), and `skill_card` / `call_summary` (cards
666
- * the daemon appends to history directly — the memory retrospective and a
667
- * call summary — and whose data shape is owned by their client renderers).
666
+ * fixed grid that reads no data), `voice_picker` (a settings card that reads
667
+ * its own config), and `skill_card` / `call_summary` (cards the daemon appends
668
+ * to history directly, the memory retrospective and a call summary, whose
669
+ * data shape is owned by their client renderers).
668
670
  */
669
671
  export interface SurfaceDataByType {
670
672
  card: CardSurfaceData;
@@ -684,6 +686,7 @@ export interface SurfaceDataByType {
684
686
  skill_card: Record<string, unknown>;
685
687
  call_summary: Record<string, unknown>;
686
688
  visual: VisualSurfaceData;
689
+ voice_picker: Record<string, unknown>;
687
690
  }
688
691
 
689
692
  /** Any surface `data` payload, including the opaque (non-renderable) types. */
@@ -717,6 +720,7 @@ export const SURFACE_DATA_SCHEMAS: {
717
720
  skill_card: z.record(z.string(), z.unknown()),
718
721
  call_summary: z.record(z.string(), z.unknown()),
719
722
  visual: VisualSurfaceDataSchema,
723
+ voice_picker: z.record(z.string(), z.unknown()),
720
724
  };
721
725
 
722
726
  /**
@@ -1422,6 +1422,9 @@ export function backfillAppConversationIds(): void {
1422
1422
  }
1423
1423
 
1424
1424
  try {
1425
+ // Unfinalized rows cannot match: their content is a { ref } pointer,
1426
+ // never inline surface JSON, so this scan sees finalized content only by
1427
+ // shape. No completeness predicate needed.
1425
1428
  const rows = rawAll<{ conversation_id: string; content: string }>(
1426
1429
  "apps:backfillConversationIds",
1427
1430
  `SELECT conversation_id, content FROM messages WHERE content LIKE '%"type":"ui_surface"%'`,
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { existsSync } from "node:fs";
9
- import { mkdir, stat } from "node:fs/promises";
9
+ import { mkdir } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
 
12
12
  import { ensureBun } from "../util/bun-runtime.js";
@@ -24,8 +24,8 @@ export const ALLOWED_PACKAGES: readonly string[] = [
24
24
  "clsx",
25
25
  ] as const;
26
26
 
27
+ /** Bounds a runaway install; the allowlist above bounds what may be installed. */
27
28
  const INSTALL_TIMEOUT_MS = 10_000;
28
- const MAX_PACKAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
29
29
 
30
30
  /** In-flight install promises keyed by package name, to deduplicate concurrent requests. */
31
31
  const inflight = new Map<string, Promise<string | null>>();
@@ -148,37 +148,9 @@ async function installPackage(
148
148
  return null;
149
149
  }
150
150
 
151
- // Enforce max size
152
- if (existsSync(pkgDir)) {
153
- const size = await dirSize(pkgDir);
154
- if (size > MAX_PACKAGE_SIZE_BYTES) {
155
- log.warn({ pkg, size }, "Package exceeds size limit, removing");
156
- const { rm } = await import("node:fs/promises");
157
- await rm(pkgDir, { recursive: true, force: true });
158
- return null;
159
- }
160
- }
161
-
162
151
  return existsSync(pkgDir) ? nodeModulesDir : null;
163
152
  } catch (err) {
164
153
  log.warn({ pkg, err }, "Package resolution failed");
165
154
  return null;
166
155
  }
167
156
  }
168
-
169
- /** Recursively sum file sizes under a directory. */
170
- async function dirSize(dir: string): Promise<number> {
171
- const { readdir } = await import("node:fs/promises");
172
- const entries = await readdir(dir, { withFileTypes: true });
173
- let total = 0;
174
- for (const entry of entries) {
175
- const full = join(dir, entry.name);
176
- if (entry.isDirectory()) {
177
- total += await dirSize(full);
178
- } else {
179
- const s = await stat(full);
180
- total += s.size;
181
- }
182
- }
183
- return total;
184
- }
@@ -73,7 +73,11 @@ mock.module("../../persistence/conversation-crud.js", () => ({
73
73
  import { setConfig } from "../../__tests__/helpers/set-config.js";
74
74
  import { ABORT_WATCHDOG_MS } from "../../daemon/abort-watchdog.js";
75
75
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
76
- import { CALL_OPENING_MARKER } from "../voice-control-protocol.js";
76
+ import {
77
+ CALL_OPENING_MARKER,
78
+ ESCALATE_VERDICT_TOKEN,
79
+ HOLD_VERDICT_TOKEN,
80
+ } from "../voice-control-protocol.js";
77
81
  import {
78
82
  cutFrontDoorContentAtVerdict,
79
83
  preSpeechLanguageRuleFragment,
@@ -1701,6 +1705,174 @@ describe("cutFrontDoorContentAtVerdict", () => {
1701
1705
  });
1702
1706
  });
1703
1707
 
1708
+ /**
1709
+ * The front-door leg's raw stream is a control plane: its leading tokens are
1710
+ * the routing verdict, not speech. The hub broadcast (web / passive devices)
1711
+ * must never carry those tokens, and must still carry every word of real
1712
+ * assistant content: an over-broad filter would leave the shared transcript
1713
+ * silent or truncated, which is worse than the leak it fixes.
1714
+ */
1715
+ describe("front-door hub stream gate", () => {
1716
+ beforeEach(resetCrudLog);
1717
+
1718
+ /**
1719
+ * Conversation whose scripted agent loop announces a reserved row, streams
1720
+ * `deltas` in order, then ends the leg with `finalEvent`.
1721
+ */
1722
+ function makeStreamingConversation(
1723
+ deltas: string[],
1724
+ finalEvent:
1725
+ | "message_complete"
1726
+ | "generation_cancelled" = "message_complete",
1727
+ ): void {
1728
+ const fake = makeFakeConversation({ processing: false });
1729
+ fake.conversation.runAgentLoop = async (...args: unknown[]) => {
1730
+ const { onEvent } = args[2] as { onEvent: (msg: unknown) => void };
1731
+ onEvent({
1732
+ type: "assistant_turn_start",
1733
+ messageId: "assistant-row-1",
1734
+ conversationId: "conv-voice-bridge-test",
1735
+ });
1736
+ for (const text of deltas) {
1737
+ onEvent({
1738
+ type: "assistant_text_delta",
1739
+ text,
1740
+ messageId: "assistant-row-1",
1741
+ conversationId: "conv-voice-bridge-test",
1742
+ });
1743
+ }
1744
+ onEvent({
1745
+ type: finalEvent,
1746
+ ...(finalEvent === "message_complete"
1747
+ ? { messageId: "assistant-row-1" }
1748
+ : {}),
1749
+ conversationId: "conv-voice-bridge-test",
1750
+ });
1751
+ };
1752
+ fakeConversation = fake.conversation;
1753
+ }
1754
+
1755
+ /** The text of every `assistant_text_delta` `turn` publishes to the hub. */
1756
+ async function collectBroadcastText(
1757
+ turn: () => Promise<unknown>,
1758
+ ): Promise<string[]> {
1759
+ const texts: string[] = [];
1760
+ const subscription = assistantEventHub.subscribe({
1761
+ type: "process",
1762
+ filter: { conversationId: "conv-voice-bridge-test" },
1763
+ callback: (event) => {
1764
+ const msg = event.message as { type: string; text?: string };
1765
+ if (msg.type === "assistant_text_delta") {
1766
+ texts.push(msg.text ?? "");
1767
+ }
1768
+ },
1769
+ });
1770
+ try {
1771
+ await turn();
1772
+ // The hub publishes off a promise chain, so every broadcast costs a
1773
+ // few microtask hops before it reaches a subscriber.
1774
+ await new Promise((resolve) => setTimeout(resolve, 0));
1775
+ await flushMicrotasks();
1776
+ } finally {
1777
+ subscription.dispose();
1778
+ }
1779
+ return texts;
1780
+ }
1781
+
1782
+ test("an escalating leg broadcasts only the capped bridge, never the verdict token", async () => {
1783
+ makeStreamingConversation([
1784
+ "[1]",
1785
+ " Let me check your calendar.",
1786
+ " weak answer past the cap",
1787
+ ]);
1788
+
1789
+ const texts = await collectBroadcastText(() =>
1790
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
1791
+ );
1792
+
1793
+ expect(texts).toEqual(["Let me check your calendar."]);
1794
+ expect(texts.join("")).not.toContain(ESCALATE_VERDICT_TOKEN);
1795
+ });
1796
+
1797
+ test("a front-door answer reaches the hub in full", async () => {
1798
+ makeStreamingConversation(["It is ", "Tuesday", ", and it is sunny."]);
1799
+
1800
+ const texts = await collectBroadcastText(() =>
1801
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
1802
+ );
1803
+
1804
+ expect(texts.join("")).toBe("It is Tuesday, and it is sunny.");
1805
+ });
1806
+
1807
+ test("an answer that merely opens with a bracket is released in full", async () => {
1808
+ // "[" alone could still become the escalate token, so the gate holds it;
1809
+ // the next delta disproves the token and the whole prefix must come out.
1810
+ makeStreamingConversation(["[", "A] is the option to pick."]);
1811
+
1812
+ const texts = await collectBroadcastText(() =>
1813
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
1814
+ );
1815
+
1816
+ expect(texts.join("")).toBe("[A] is the option to pick.");
1817
+ });
1818
+
1819
+ test("a hold verdict broadcasts nothing", async () => {
1820
+ makeStreamingConversation([HOLD_VERDICT_TOKEN]);
1821
+
1822
+ const texts = await collectBroadcastText(() =>
1823
+ startVoiceTurn({
1824
+ ...makeTurnOptions(),
1825
+ routingLeg: "front-door",
1826
+ unifiedVerdict: true,
1827
+ }),
1828
+ );
1829
+
1830
+ expect(texts).toEqual([]);
1831
+ });
1832
+
1833
+ test("a leg that completes mid-bridge broadcasts what it handed off with", async () => {
1834
+ makeStreamingConversation(["[1] Let me check"]);
1835
+
1836
+ const texts = await collectBroadcastText(() =>
1837
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
1838
+ );
1839
+
1840
+ expect(texts).toEqual(["Let me check"]);
1841
+ });
1842
+
1843
+ test("a leg cancelled mid-bridge never hands off, so it broadcasts nothing", async () => {
1844
+ makeStreamingConversation(["[1] Let me check"], "generation_cancelled");
1845
+
1846
+ const texts = await collectBroadcastText(() =>
1847
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
1848
+ );
1849
+
1850
+ expect(texts).toEqual([]);
1851
+ });
1852
+
1853
+ test("the escalated continuation streams to the hub untouched", async () => {
1854
+ // The leg that produces the real answer is never gated: its deltas are
1855
+ // assistant speech from the first token.
1856
+ makeStreamingConversation(["Your next meeting ", "is at four."]);
1857
+
1858
+ const texts = await collectBroadcastText(() =>
1859
+ startVoiceTurn({ ...makeTurnOptions(), routingLeg: "escalated" }),
1860
+ );
1861
+
1862
+ expect(texts).toEqual(["Your next meeting ", "is at four."]);
1863
+ });
1864
+
1865
+ test("an un-routed voice leg streams to the hub untouched", async () => {
1866
+ makeStreamingConversation(["[1] not a verdict here"]);
1867
+
1868
+ const texts = await collectBroadcastText(() =>
1869
+ startVoiceTurn(makeTurnOptions()),
1870
+ );
1871
+
1872
+ expect(texts).toEqual(["[1] not a verdict here"]);
1873
+ });
1874
+ });
1875
+
1704
1876
  describe("transcript hygiene (teardown pass)", () => {
1705
1877
  beforeEach(resetCrudLog);
1706
1878
 
@@ -4,6 +4,7 @@ import { DEEPGRAM_MULTI_LANGUAGE_CODES } from "../../providers/speech-to-text/de
4
4
  import {
5
5
  capEscalationBridge,
6
6
  classifyFrontDoorLeading,
7
+ createFrontDoorStreamGate,
7
8
  ESCALATE_VERDICT_TOKEN,
8
9
  escalatedContinuationRule,
9
10
  ESCALATION_CONTINUATION_CONTENT,
@@ -439,3 +440,96 @@ describe("live-voice escalation orchestration (end-to-end — TODO)", () => {
439
440
  () => {},
440
441
  );
441
442
  });
443
+
444
+ describe("createFrontDoorStreamGate", () => {
445
+ /** Feed `deltas` through a gate and return what it released, in order. */
446
+ function release(
447
+ deltas: string[],
448
+ opts?: { holdEnabled?: boolean; finish?: boolean },
449
+ ): string[] {
450
+ const gate = createFrontDoorStreamGate(opts?.holdEnabled === true);
451
+ const out = deltas
452
+ .map((delta) => gate.push(delta))
453
+ .filter((chunk) => chunk.length > 0);
454
+ if (opts?.finish !== false) {
455
+ const trailing = gate.finish();
456
+ if (trailing.length > 0) {
457
+ out.push(trailing);
458
+ }
459
+ }
460
+ return out;
461
+ }
462
+
463
+ test("an answer passes through delta by delta", () => {
464
+ expect(release(["It is ", "Tuesday."])).toEqual(["It is ", "Tuesday."]);
465
+ });
466
+
467
+ test("leading text held while the verdict was pending is not lost", () => {
468
+ expect(release(["[", "A] is the option."])).toEqual(["[A] is the option."]);
469
+ });
470
+
471
+ test("an escalation releases the capped bridge and nothing else", () => {
472
+ expect(
473
+ release([
474
+ ESCALATE_VERDICT_TOKEN,
475
+ " Let me check your calendar.",
476
+ " rambling past the cap",
477
+ ]),
478
+ ).toEqual(["Let me check your calendar."]);
479
+ });
480
+
481
+ test("a verdict token split across deltas still never escapes", () => {
482
+ expect(release(["[", "1", "] One moment.", " more"])).toEqual([
483
+ "One moment.",
484
+ ]);
485
+ });
486
+
487
+ test("a too-short bridge releases nothing, matching the audio-only fallback", () => {
488
+ // Under MIN_SPOKEN_BRIDGE_CHARS the session discards the model's bridge
489
+ // and plays a canned fallback with no caption and no persisted row, so
490
+ // releasing the capped text would show words the caller never heard.
491
+ expect(release([ESCALATE_VERDICT_TOKEN, " ok"])).toEqual([]);
492
+ expect(release([ESCALATE_VERDICT_TOKEN, " ."])).toEqual([]);
493
+ });
494
+
495
+ test("a too-short bridge releases nothing via finish either", () => {
496
+ const gate = createFrontDoorStreamGate(false);
497
+ expect(gate.push(`${ESCALATE_VERDICT_TOKEN} ok`)).toBe("");
498
+ expect(gate.finish()).toBe("");
499
+ });
500
+
501
+ test("a bridge at the spoken threshold is still released", () => {
502
+ // The boundary belongs to the spoken side: the session speaks anything
503
+ // that is not BELOW the threshold, so the gate must release it too.
504
+ expect(release([ESCALATE_VERDICT_TOKEN, " Sure."])).toEqual(["Sure."]);
505
+ });
506
+
507
+ test("a bridge with no terminator is released by finish", () => {
508
+ const gate = createFrontDoorStreamGate(false);
509
+ expect(gate.push("[1] Let me check")).toBe("");
510
+ expect(gate.finish()).toBe("Let me check");
511
+ });
512
+
513
+ test("a bridge is dropped when the leg never finishes (cancellation)", () => {
514
+ expect(release(["[1] Let me check"], { finish: false })).toEqual([]);
515
+ });
516
+
517
+ test("a hold verdict releases nothing", () => {
518
+ expect(release([HOLD_VERDICT_TOKEN], { holdEnabled: true })).toEqual([]);
519
+ });
520
+
521
+ test("the hold token is ordinary text on a leg that was never taught it", () => {
522
+ // A non-speculative leg's prompt has no hold branch, so its output must
523
+ // not be swallowed by a token it could only have parroted.
524
+ expect(release([`${HOLD_VERDICT_TOKEN} is the index.`])).toEqual([
525
+ `${HOLD_VERDICT_TOKEN} is the index.`,
526
+ ]);
527
+ });
528
+
529
+ test("nothing more escapes after the bridge is released", () => {
530
+ const gate = createFrontDoorStreamGate(false);
531
+ expect(gate.push("[1] One moment.")).toBe("One moment.");
532
+ expect(gate.push(" and a leaked continuation")).toBe("");
533
+ expect(gate.finish()).toBe("");
534
+ });
535
+ });
@@ -21,6 +21,7 @@ import type { TrustContext } from "../daemon/trust-context-types.js";
21
21
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
22
22
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
23
23
  import { getCatalogProvider } from "../tts/provider-catalog.js";
24
+ import { createReasoningTagFilter } from "../tts/reasoning-tag-filter.js";
24
25
  import { extractSpeakableSegments } from "../tts/speakable-segments.js";
25
26
  import {
26
27
  type AudioStoreSink,
@@ -728,6 +729,12 @@ export class CallController {
728
729
  // could be the start of a control marker.
729
730
  let ttsBuffer = "";
730
731
  let fullResponseText = "";
732
+ // Reasoning models can inline <think> spans in the content stream when a
733
+ // profile has not opted into parseThinkTags. Neither the spoken path nor
734
+ // the post-turn consumers of fullResponseText (transcripts,
735
+ // assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
736
+ // are fed only filtered text.
737
+ const reasoningFilter = createReasoningTagFilter();
731
738
 
732
739
  // Synthesized path: text is split at speakable boundaries as it streams
733
740
  // and each segment is synthesized while the LLM keeps generating. The
@@ -930,8 +937,13 @@ export class CallController {
930
937
  if (!this.isCurrentRun(runVersion)) {
931
938
  return;
932
939
  }
933
- fullResponseText += text;
934
- ttsBuffer += text;
940
+ // One filter feeds both consumers: the spoken stream and the text
941
+ // used for transcripts, assistant_spoke, and END_CALL/ASK_GUARDIAN
942
+ // marker detection. A control marker inside a reasoning span must
943
+ // never trigger a real action the caller did not hear.
944
+ const speakable = reasoningFilter.push(text);
945
+ fullResponseText += speakable;
946
+ ttsBuffer += speakable;
935
947
  ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
936
948
  flushSafeText();
937
949
  };
@@ -1010,7 +1022,11 @@ export class CallController {
1010
1022
  return fullResponseText;
1011
1023
  }
1012
1024
 
1013
- // Final sweep: strip any remaining control markers from the buffer
1025
+ // Final sweep: release any held-back partial tag to both consumers,
1026
+ // then strip any remaining control markers from the buffer.
1027
+ const filterTail = reasoningFilter.flush();
1028
+ fullResponseText += filterTail;
1029
+ ttsBuffer += filterTail;
1014
1030
  ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
1015
1031
  if (ttsBuffer.length > 0) {
1016
1032
  emitSafeChunk(ttsBuffer);
@@ -1393,7 +1393,6 @@ export class CallSetupFlow {
1393
1393
 
1394
1394
  try {
1395
1395
  const accessResult = await ndeps.notifyGuardianOfAccessRequest({
1396
- canonicalAssistantId: accessRequest.assistantId,
1397
1396
  sourceChannel: "phone",
1398
1397
  conversationExternalId: accessRequest.fromNumber,
1399
1398
  actorExternalId: accessRequest.fromNumber,
@@ -538,6 +538,17 @@ export class MediaStreamSttSession {
538
538
  switch (event.type) {
539
539
  case "partial":
540
540
  return;
541
+ case "finalized":
542
+ return;
543
+ case "turn-start":
544
+ case "eager-turn-end":
545
+ case "turn-resumed":
546
+ case "turn-end":
547
+ // The local VAD and the provider's boundary finals own turn
548
+ // taking. Listed case-by-case rather than under `default` so the
549
+ // exhaustiveness check flags this site when turn detection is
550
+ // wired in.
551
+ return;
541
552
  case "final": {
542
553
  const durationMs = Math.round(this.utteranceAudioMs);
543
554
  this.utteranceAudioMs = 0;
@@ -570,6 +581,10 @@ export class MediaStreamSttSession {
570
581
  this.enterBatchMode();
571
582
  }
572
583
  return;
584
+ default: {
585
+ const _exhaustive: never = event;
586
+ return;
587
+ }
573
588
  }
574
589
  }
575
590