@vellumai/assistant 0.11.6 → 0.11.7-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -6,6 +6,7 @@ import {
6
6
  type LiveVoiceClientStartFrame,
7
7
  type LiveVoiceServerFrame,
8
8
  type LiveVoiceServerFramePayload,
9
+ type LiveVoiceSessionHolder,
9
10
  } from "./protocol.js";
10
11
 
11
12
  type MaybePromise<T> = T | Promise<T>;
@@ -157,6 +158,23 @@ function waitForSlotRelease(
157
158
  });
158
159
  }
159
160
 
161
+ /** {@link LiveVoiceSessionHolder}, as the manager keeps it while filling it in. */
162
+ interface MutableLiveVoiceSessionHolder {
163
+ client?: LiveVoiceSessionHolder["client"];
164
+ conversationId?: string;
165
+ }
166
+
167
+ /** Drop an all-empty holder, so `busy` carries a field or says nothing. */
168
+ function describeHolder(
169
+ holder: MutableLiveVoiceSessionHolder,
170
+ ): LiveVoiceSessionHolder | null {
171
+ const described: MutableLiveVoiceSessionHolder = {
172
+ ...(holder.client ? { client: holder.client } : {}),
173
+ ...(holder.conversationId ? { conversationId: holder.conversationId } : {}),
174
+ };
175
+ return Object.keys(described).length > 0 ? described : null;
176
+ }
177
+
160
178
  export class LiveVoiceSessionStartupError extends Error {
161
179
  constructor(message: string) {
162
180
  super(message);
@@ -208,6 +226,8 @@ interface ActiveLiveVoiceSession {
208
226
  sessionId: string;
209
227
  session: LiveVoiceSession;
210
228
  sink: LiveVoiceServerFrameSink;
229
+ /** What a refused client is told about who holds the slot. */
230
+ holder: MutableLiveVoiceSessionHolder;
211
231
  closing: boolean;
212
232
  silenceTimer: ReturnType<typeof setTimeout> | null;
213
233
  /** Resolves once this session has let go of the slot, however it went. */
@@ -272,12 +292,15 @@ export class LiveVoiceSessionManager {
272
292
  }
273
293
  }
274
294
 
275
- const existingSessionId = this.activeSessionId;
276
- if (existingSessionId !== null) {
295
+ const existing = this.activeSession;
296
+ const existingSessionId = existing?.sessionId ?? null;
297
+ if (existing !== null && existingSessionId !== null) {
277
298
  const busySequencer = createLiveVoiceServerFrameSequencer();
299
+ const holder = describeHolder(existing.holder);
278
300
  const frame = busySequencer.next({
279
301
  type: "busy",
280
302
  activeSessionId: existingSessionId,
303
+ ...(holder ? { holder } : {}),
281
304
  });
282
305
  await sink.sendFrame(frame);
283
306
  return {
@@ -289,10 +312,23 @@ export class LiveVoiceSessionManager {
289
312
 
290
313
  const sessionId = this.createSessionId();
291
314
  const sequencer = createLiveVoiceServerFrameSequencer();
315
+ const holder: MutableLiveVoiceSessionHolder = {
316
+ ...(startFrame.client ? { client: startFrame.client } : {}),
317
+ ...(startFrame.conversationId
318
+ ? { conversationId: startFrame.conversationId }
319
+ : {}),
320
+ };
292
321
  const context: LiveVoiceSessionFactoryContext = {
293
322
  sessionId,
294
323
  startFrame,
295
324
  sendFrame: async (payload) => {
325
+ // `ready` carries the conversation the session actually landed in,
326
+ // which is the only source for one it minted itself. Read in passing
327
+ // rather than through a new session method: every frame already goes
328
+ // through here.
329
+ if (payload.type === "ready") {
330
+ holder.conversationId = payload.conversationId;
331
+ }
296
332
  const frame = sequencer.next(payload);
297
333
  await sink.sendFrame(frame);
298
334
  return frame;
@@ -310,6 +346,7 @@ export class LiveVoiceSessionManager {
310
346
  sessionId,
311
347
  session,
312
348
  sink,
349
+ holder,
313
350
  closing: false,
314
351
  silenceTimer: null,
315
352
  released,
@@ -59,6 +59,7 @@ import {
59
59
  dominantLanguageTag,
60
60
  voteDominantLanguage,
61
61
  } from "../stt/language-metadata.js";
62
+ import { sttCatalogKeyForRole } from "../stt/roles.js";
62
63
  import {
63
64
  DEFAULT_SPEECH_ENERGY_THRESHOLD,
64
65
  pcm16MaxNormalizedCorrelation,
@@ -131,6 +132,7 @@ import {
131
132
  import {
132
133
  type LiveVoiceClientAttachImageFrame,
133
134
  type LiveVoiceClientFrame,
135
+ type LiveVoiceClientTextTurnFrame,
134
136
  type LiveVoiceClientUpdateConfigFrame,
135
137
  LiveVoiceProtocolErrorCode,
136
138
  type LiveVoiceServerFramePayload,
@@ -360,7 +362,8 @@ export interface LiveVoiceSessionOptions {
360
362
  /**
361
363
  * Deepgram Flux turn-detection tuning. The production factory seeds this
362
364
  * from `liveVoice.flux` config when unset; absent fields fall back to the
363
- * schema defaults, which leave `turnEnd.enabled` false.
365
+ * schema defaults, which leave `turnEnd.enabled` on. A test that needs the
366
+ * local silence boundary has to say so.
364
367
  */
365
368
  fluxConfig?: Partial<LiveVoiceFluxConfig>;
366
369
  /**
@@ -705,6 +708,10 @@ interface ActiveAssistantTurn {
705
708
  // no user utterance behind it — `content` is CONTINUATION_DELIVERY_CONTENT and
706
709
  // the answer rides the control prompt (buildLiveDeliveryNote).
707
710
  continuationDelivery: ContinuationDelivery | null;
711
+ // The turn's content is an internal instruction rather than user speech (the
712
+ // greeting that opens a session, say). The row still persists and the model
713
+ // still sees it; `hiddenSyntheticPrompt` keeps it out of the transcript.
714
+ hiddenPrompt: boolean;
708
715
  // Set when a barge-in handed the interrupted work to a background subagent:
709
716
  // that request's transcript, so the model can tell the user the work is
710
717
  // still running instead of appearing to have dropped it.
@@ -993,6 +1000,19 @@ function createSyntheticUtterance(): UtteranceCycle {
993
1000
  };
994
1001
  }
995
1002
 
1003
+ // Carrier cycle for a turn the user typed rather than spoke. Identical to the
1004
+ // announcement carrier except that it arrives with its transcript already
1005
+ // complete: the text the client sent is the finalized transcript, so the cycle
1006
+ // joins the pipeline exactly where STT would have handed one over. Like the
1007
+ // announcement carrier it is never installed as `currentUtterance`, so a typed
1008
+ // turn taken during a voice session cannot collide with the armed capture cycle.
1009
+ function createTypedUtterance(text: string): UtteranceCycle {
1010
+ return {
1011
+ ...createSyntheticUtterance(),
1012
+ finalTranscriptSegments: [text],
1013
+ };
1014
+ }
1015
+
996
1016
  // Objective handed to the background subagent that continues a barged-in turn.
997
1017
  // The subagent forks the live conversation, so it already sees the interrupted
998
1018
  // turn's completed tool calls in history and resumes from there. The request
@@ -1143,6 +1163,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1143
1163
  private receivedAudio = false;
1144
1164
  private detectedSpeech = false;
1145
1165
  private dispatchedTurn = false;
1166
+ // The client declared a text input affordance on the start frame, so it can
1167
+ // take a turn without the microphone. Governs one thing only: whether a
1168
+ // missing speech-to-text leg is fatal to startup (see start()).
1169
+ private readonly textInput: boolean;
1170
+ // Whether this session's speech-to-text leg came up. False only when the
1171
+ // preflight found it missing and `textInput` let the session open anyway, in
1172
+ // which case nothing arms a transcriber and typed turns are the only input.
1173
+ private audioInput = true;
1146
1174
  // Non-null iff the start frame requested turnDetection "server_vad".
1147
1175
  private readonly turnDetector: MediaTurnDetector | null;
1148
1176
  // Base energy gate for server-VAD speech classification. During estimated
@@ -1398,6 +1426,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1398
1426
  context.startFrame.audio.sampleRate *
1399
1427
  2 *
1400
1428
  SERVER_VAD_PENDING_AUDIO_MAX_SECONDS;
1429
+ this.textInput = context.startFrame.textInput === true;
1401
1430
  }
1402
1431
 
1403
1432
  get finalTranscriptText(): string {
@@ -1418,10 +1447,30 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1418
1447
  if (this.resolveCredentialReadiness) {
1419
1448
  const readiness = await this.resolveCredentialReadiness();
1420
1449
  if (readiness.status === "not-ready") {
1421
- return await this.failStartup(
1422
- readiness.userMessage,
1423
- LiveVoiceProtocolErrorCode.CredentialsUnavailable,
1424
- );
1450
+ // A client that can type survives a dead speech-to-text leg: it opens
1451
+ // text-only rather than not at all. Requires the gap to be entirely on
1452
+ // the STT side, because the text-in path still speaks its reply, so a
1453
+ // session missing the TTS leg has nothing left to degrade to.
1454
+ //
1455
+ // A not-ready verdict naming no gap is not a downgrade candidate:
1456
+ // `every` is vacuously true over an empty list, and a resolver bug must
1457
+ // not read as "only STT is missing" and quietly open a half-working
1458
+ // session on the strength of it.
1459
+ const sttOnlyGap =
1460
+ readiness.missing.length > 0 &&
1461
+ readiness.missing.every((gap) => gap.kind === "stt");
1462
+ if (this.textInput && sttOnlyGap) {
1463
+ this.audioInput = false;
1464
+ log.warn(
1465
+ { sessionId: this.context.sessionId, missing: readiness.missing },
1466
+ "Live voice starting text-only: speech-to-text unavailable",
1467
+ );
1468
+ } else {
1469
+ return await this.failStartup(
1470
+ readiness.userMessage,
1471
+ LiveVoiceProtocolErrorCode.CredentialsUnavailable,
1472
+ );
1473
+ }
1425
1474
  }
1426
1475
  }
1427
1476
 
@@ -1444,6 +1493,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1444
1493
  sessionId: this.context.sessionId,
1445
1494
  conversationId: this.conversationId,
1446
1495
  turnDetection: this.turnDetector ? "server_vad" : "manual",
1496
+ // Unconditional: it answers "does this daemon take text frames", which
1497
+ // is a property of the daemon and not of what this client asked for.
1498
+ // A client reading it back absent is talking to one that predates it.
1499
+ textInput: true,
1500
+ ...(this.audioInput ? {} : { audioInput: false }),
1447
1501
  });
1448
1502
  }
1449
1503
 
@@ -1472,9 +1526,66 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1472
1526
  case "attach_image":
1473
1527
  this.persistPhoto(frame);
1474
1528
  return;
1529
+ case "text":
1530
+ await this.handleTextTurn(frame);
1531
+ return;
1475
1532
  }
1476
1533
  }
1477
1534
 
1535
+ /**
1536
+ * Run a turn the user typed instead of spoke.
1537
+ *
1538
+ * Joins the pipeline at the point a finished transcript would have: the same
1539
+ * turn runner, the same streaming TTS, the same activity and metrics. The
1540
+ * reply is spoken, which is the whole point: this is the input half being
1541
+ * text, not the session becoming a chat.
1542
+ *
1543
+ * Gated on {@link sessionTurnFloorBlocker}, the same check the continuation
1544
+ * announcement uses: no turn in flight, no assistant audio still draining,
1545
+ * no speech onset, and no utterance mid-capture. A typed turn asks the
1546
+ * identical question, so it asks it in the identical way rather than growing
1547
+ * a second set of conditions that could drift from the first.
1548
+ *
1549
+ * A blocked turn is refused rather than queued, and refused recoverably: the
1550
+ * session is fine and the user can send again. Typing over a reply in
1551
+ * progress reads as an interrupt, but making it one means choosing barge-in
1552
+ * semantics for a modality that has no speech onset to measure, which is its
1553
+ * own decision and not this seam's.
1554
+ */
1555
+ private async handleTextTurn(
1556
+ frame: LiveVoiceClientTextTurnFrame,
1557
+ ): Promise<void> {
1558
+ if (!this.startVoiceTurn) {
1559
+ return;
1560
+ }
1561
+
1562
+ const blocker = this.sessionTurnFloorBlocker({
1563
+ ignoreManualCapture: true,
1564
+ });
1565
+ if (blocker !== null) {
1566
+ await this.sendFrame({
1567
+ type: "error",
1568
+ code: LiveVoiceProtocolErrorCode.InvalidFrame,
1569
+ message: "The assistant is busy with the current turn. Send again.",
1570
+ frameType: "text",
1571
+ recoverable: true,
1572
+ });
1573
+ log.debug(
1574
+ { sessionId: this.context.sessionId, blocker },
1575
+ "Typed live-voice turn refused",
1576
+ );
1577
+ return;
1578
+ }
1579
+
1580
+ const utterance = createTypedUtterance(frame.text);
1581
+ // Before the launch, so the anchor is the moment the text arrived rather
1582
+ // than whatever the dispatch costs.
1583
+ this.markTypedTurnSubmitted(utterance);
1584
+ await this.launchAssistantTurn(utterance, frame.text, {
1585
+ ...(frame.hidden === true ? { hiddenPrompt: true } : {}),
1586
+ });
1587
+ }
1588
+
1478
1589
  /**
1479
1590
  * Persist a photo taken mid-call into the conversation, running no turn.
1480
1591
  *
@@ -1546,6 +1657,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1546
1657
  ? null
1547
1658
  : liveVoiceSilenceReason({
1548
1659
  reachedActive: this.reachedActive,
1660
+ audioInput: this.audioInput,
1549
1661
  receivedAudio: this.receivedAudio,
1550
1662
  detectedSpeech: this.detectedSpeech,
1551
1663
  });
@@ -1637,9 +1749,17 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1637
1749
  // outside: the session looks like a turn-detecting session while its
1638
1750
  // opening turn is not one. The resolved provider reconciles this guess
1639
1751
  // below.
1752
+ //
1753
+ // The guess reads the live-voice role, which is what the dial below
1754
+ // resolves. Reading the global provider instead makes the guess wrong
1755
+ // in exactly the configuration roles exist for (live voice on flux,
1756
+ // global on base deepgram), which reinstates the race this seed
1757
+ // prevents.
1640
1758
  this.setProviderTurnEndActive(
1641
1759
  this.fluxConfig.turnEnd.enabled &&
1642
- supportsProviderTurnDetection(stt.provider as SttProviderId) &&
1760
+ supportsProviderTurnDetection(
1761
+ sttCatalogKeyForRole(stt, "liveVoice"),
1762
+ ) &&
1643
1763
  this.turnDetector !== null,
1644
1764
  );
1645
1765
  const transcriber = await this.resolveTranscriber({
@@ -1838,6 +1958,17 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1838
1958
  }
1839
1959
 
1840
1960
  private async armUtterance(): Promise<void> {
1961
+ // Nothing to arm without a speech-to-text leg. Guarded here rather than at
1962
+ // the call sites because there are several, and the post-turn re-arm
1963
+ // reaches this through `scheduleRearmAfterTurn` from five of them: a
1964
+ // text-only session that requested `server_vad` still holds a turn
1965
+ // detector, so the re-arm is not skipped for want of one. Arming anyway
1966
+ // would resolve a transcriber the preflight already said has no
1967
+ // credential, fail the session, and close it after its first typed turn.
1968
+ if (!this.audioInput) {
1969
+ return;
1970
+ }
1971
+
1841
1972
  const result = await this.beginUtterance();
1842
1973
  if (result.status === "started" || result.status === "stale") {
1843
1974
  return;
@@ -1867,6 +1998,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
1867
1998
  // open", which a chunk arriving at all settles regardless of what the
1868
1999
  // session then does with it.
1869
2000
  this.receivedAudio = true;
2001
+ // A text-only session armed no transcriber, so there is nothing downstream
2002
+ // to route this into. Dropped rather than errored: the client was told on
2003
+ // `ready` that the microphone leg is dead, and a client still streaming is
2004
+ // one that has not caught up, not one to end the session over. The flag
2005
+ // above is still set, so telemetry keeps seeing that the mic did open.
2006
+ if (!this.audioInput) {
2007
+ return;
2008
+ }
1870
2009
  if (this.turnDetector) {
1871
2010
  await this.handleServerVadAudio(this.turnDetector, chunk);
1872
2011
  return;
@@ -3118,6 +3257,41 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
3118
3257
  if (this.pendingAnnouncement === null) {
3119
3258
  return "nothing_pending";
3120
3259
  }
3260
+ return this.sessionTurnFloorBlocker();
3261
+ }
3262
+
3263
+ /**
3264
+ * Why the session may not start a turn of its own right now, or null when it
3265
+ * may.
3266
+ *
3267
+ * "Of its own" means a turn with no released utterance behind it: a queued
3268
+ * continuation announcement, or a turn the user typed. Both need the same
3269
+ * assurance, that starting one would not talk over the assistant, cut off the
3270
+ * user, or race a capture cycle already in flight, so both ask here rather
3271
+ * than growing separate condition sets that could drift apart.
3272
+ */
3273
+ private sessionTurnFloorBlocker(opts?: {
3274
+ /**
3275
+ * Ignore `manualAudioCaptured` when judging whether an utterance is in
3276
+ * flight. Set only for a typed turn.
3277
+ *
3278
+ * That flag means "manual capture routed at least one chunk into this
3279
+ * cycle", and manual capture forwards from the moment the microphone
3280
+ * opens rather than from a push-to-talk press. So in a manual session it
3281
+ * latches on the first chunk of silence and, because a manual cycle only
3282
+ * completes on release, never clears. Honouring it would refuse every
3283
+ * typed turn a few milliseconds into the session.
3284
+ *
3285
+ * Safe to ignore here and nowhere else. The flag exists because
3286
+ * transcript signals trail the provider, which would leave an
3287
+ * announcement talking over someone who is mid-sentence with no text yet.
3288
+ * A typed turn has evidence an announcement does not: the user is at the
3289
+ * keyboard, and the act of sending is itself the signal that they are not
3290
+ * currently speaking. The transcript-derived conditions still apply, so a
3291
+ * cycle that has actually produced words still blocks.
3292
+ */
3293
+ readonly ignoreManualCapture?: boolean;
3294
+ }): string | null {
3121
3295
  if (this.isClosed || this.state === "failed" || !this.startVoiceTurn) {
3122
3296
  return "session_unavailable";
3123
3297
  }
@@ -3146,7 +3320,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
3146
3320
  !utterance.completed &&
3147
3321
  (utterance.released ||
3148
3322
  utterance.assistantTurnStarted ||
3149
- utterance.manualAudioCaptured ||
3323
+ (utterance.manualAudioCaptured && opts?.ignoreManualCapture !== true) ||
3150
3324
  utterance.finalTranscriptSegments.length > 0 ||
3151
3325
  utterance.latestPartialText !== null)
3152
3326
  ) {
@@ -4544,6 +4718,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
4544
4718
  // leading verdict commits the turn (see commitSpeculativeTurn); a hold
4545
4719
  // verdict rolls everything back instead.
4546
4720
  speculative?: boolean;
4721
+ // Marks `content` as an internal instruction rather than user speech, so
4722
+ // the row persists and drives the turn but never renders: no echo, and
4723
+ // `/messages` filters it after a reload.
4724
+ hiddenPrompt?: boolean;
4547
4725
  },
4548
4726
  ): Promise<boolean> {
4549
4727
  utterance.assistantTurnStarted = true;
@@ -4604,6 +4782,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
4604
4782
  consumedAnnouncement: pending?.announcement ?? null,
4605
4783
  pendingContextStopGeneration: this.detachStopGeneration,
4606
4784
  continuationDelivery: opts?.continuationDelivery ?? null,
4785
+ hiddenPrompt: opts?.hiddenPrompt === true,
4607
4786
  deltaEpoch: 0,
4608
4787
  escalationHandedOff: false,
4609
4788
  ttsBuffer: "",
@@ -4778,6 +4957,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
4778
4957
  const handle = await this.startVoiceTurn({
4779
4958
  conversationId: this.conversationId,
4780
4959
  voiceSessionId: this.context.sessionId,
4960
+ ...(activeTurn.hiddenPrompt ? { hiddenSyntheticPrompt: true } : {}),
4781
4961
  userMessageChannel: "vellum",
4782
4962
  assistantMessageChannel: "vellum",
4783
4963
  // Fixed, and NOT the originating client: this pair resolves the turn's
@@ -5950,6 +6130,29 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
5950
6130
  );
5951
6131
  }
5952
6132
 
6133
+ /**
6134
+ * Stamp the moment a typed turn's text arrived as the end of the user's
6135
+ * input.
6136
+ *
6137
+ * `roundTripMs` measures from the user finishing their turn to the first
6138
+ * audio they hear, and it anchors on `utteranceEndAtMs ?? pttReleaseAtMs`.
6139
+ * A typed turn produces neither, so without this every typed turn reports a
6140
+ * null round trip and disappears from the latency numbers. That would bias
6141
+ * them toward spoken turns rather than simply losing rows, which is the
6142
+ * worse failure: the metric would still look healthy while describing less
6143
+ * and less of the traffic.
6144
+ *
6145
+ * The text frame landing is the honest equivalent of an utterance closing:
6146
+ * it is the instant the user stopped providing input and started waiting.
6147
+ * `sttMs` and the partial-transcript durations stay null, because nothing
6148
+ * was transcribed and claiming otherwise would invent a measurement.
6149
+ */
6150
+ private markTypedTurnSubmitted(utterance: UtteranceCycle): void {
6151
+ this.markUtteranceMetric(utterance, "utteranceEndAtMs", (turnId) =>
6152
+ this.metrics.markUtteranceEnd(turnId),
6153
+ );
6154
+ }
6155
+
5953
6156
  // Manual mode stamps the PTT release; server_vad stamps the utterance-end
5954
6157
  // boundary instead (utteranceEndToFinalTranscript plays the sttMs role).
5955
6158
  private markUtteranceReleased(utterance: UtteranceCycle): void {
@@ -6609,7 +6812,9 @@ async function defaultResolveStreamingTranscriber(
6609
6812
  await import("../config/managed-speech-defaults.js");
6610
6813
  const { resolveStreamingTranscriber } =
6611
6814
  await import("../providers/speech-to-text/resolve.js");
6612
- const { stt } = await resolveEffectiveSpeechProviders();
6815
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
6816
+ role: "liveVoice",
6817
+ });
6613
6818
  return resolveStreamingTranscriber({ ...options, providerId: stt });
6614
6819
  }
6615
6820