@vellumai/assistant 0.11.6 → 0.11.7-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 (478) 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/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Typed user turns on the live-voice socket: text in, voice out.
3
+ *
4
+ * The seam these cover is narrow on purpose. A `text` frame is meant to join
5
+ * the pipeline at exactly the point a finished transcript would have, so what
6
+ * is worth pinning is not that a turn happens but that it is the *same* turn:
7
+ * same dispatch, same spoken reply, and no interference with the capture cycle
8
+ * a voice session is running alongside it.
9
+ */
10
+
11
+ import { describe, expect, mock, test } from "bun:test";
12
+
13
+ import type { VoiceTurnOptions } from "../../calls/voice-session-bridge.js";
14
+ import type {
15
+ StreamingTranscriber,
16
+ SttStreamServerEvent,
17
+ } from "../../stt/types.js";
18
+ import type { LiveVoiceCredentialReadiness } from "../live-voice-credential-preflight.js";
19
+ import {
20
+ LiveVoiceSession,
21
+ type LiveVoiceTurnStarter,
22
+ } from "../live-voice-session.js";
23
+ import type { LiveVoiceSessionFactoryContext } from "../live-voice-session-manager.js";
24
+ import {
25
+ createLiveVoiceServerFrameSequencer,
26
+ type LiveVoiceClientStartFrame,
27
+ type LiveVoiceServerFrame,
28
+ } from "../protocol.js";
29
+
30
+ const START_FRAME = {
31
+ type: "start",
32
+ conversationId: "conversation-123",
33
+ textInput: true,
34
+ audio: {
35
+ mimeType: "audio/pcm",
36
+ sampleRate: 24_000,
37
+ channels: 1,
38
+ },
39
+ } as const satisfies LiveVoiceClientStartFrame;
40
+
41
+ const STT_ONLY_GAP: LiveVoiceCredentialReadiness = {
42
+ status: "not-ready",
43
+ missing: [
44
+ {
45
+ kind: "stt",
46
+ providerId: "deepgram",
47
+ reason: 'STT provider "deepgram" is missing credentials',
48
+ },
49
+ ],
50
+ userMessage: "Live voice is unavailable because speech-to-text has no key.",
51
+ };
52
+
53
+ class MockStreamingTranscriber implements StreamingTranscriber {
54
+ readonly providerId = "deepgram" as const;
55
+ readonly boundaryId = "daemon-streaming" as const;
56
+ private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
57
+
58
+ async start(onEvent: (event: SttStreamServerEvent) => void): Promise<void> {
59
+ this.onEvent = onEvent;
60
+ }
61
+
62
+ sendAudio(): void {}
63
+
64
+ stop(): void {}
65
+
66
+ emit(event: SttStreamServerEvent): void {
67
+ this.onEvent?.(event);
68
+ }
69
+ }
70
+
71
+ function createHarness(
72
+ options: {
73
+ startFrame?: LiveVoiceClientStartFrame;
74
+ resolveCredentialReadiness?: () => Promise<LiveVoiceCredentialReadiness>;
75
+ startVoiceTurn?: LiveVoiceTurnStarter;
76
+ /**
77
+ * Complete each turn as soon as it starts. Needed by any test that runs a
78
+ * second turn: the floor gate refuses one while a turn is still in flight,
79
+ * so a turn that never finishes makes every later turn look refused for
80
+ * the wrong reason.
81
+ */
82
+ autoCompleteTurns?: boolean;
83
+ emitMetrics?: boolean;
84
+ } = {},
85
+ ) {
86
+ const sequencer = createLiveVoiceServerFrameSequencer();
87
+ const frames: LiveVoiceServerFrame[] = [];
88
+ const context: LiveVoiceSessionFactoryContext = {
89
+ sessionId: "session-123",
90
+ startFrame: options.startFrame ?? START_FRAME,
91
+ sendFrame: mock(async (payload) => {
92
+ const frame = sequencer.next(payload);
93
+ frames.push(frame);
94
+ return frame;
95
+ }),
96
+ };
97
+
98
+ const turnOptions: VoiceTurnOptions[] = [];
99
+ const startVoiceTurn =
100
+ options.startVoiceTurn ??
101
+ (mock(async (opts: VoiceTurnOptions) => {
102
+ turnOptions.push(opts);
103
+ if (options.autoCompleteTurns) {
104
+ opts.callbacks?.message_complete?.({
105
+ type: "message_complete",
106
+ conversationId: opts.conversationId,
107
+ messageId: `assistant-message-${turnOptions.length}`,
108
+ });
109
+ }
110
+ return { turnId: `bridge-turn-${turnOptions.length}`, abort: mock() };
111
+ }) as unknown as LiveVoiceTurnStarter);
112
+
113
+ const resolveTranscriber = mock(async () => new MockStreamingTranscriber());
114
+ const session = new LiveVoiceSession(context, {
115
+ resolveTranscriber,
116
+ startVoiceTurn,
117
+ createTurnId: () => "live-turn-1",
118
+ emitMetrics: options.emitMetrics ?? false,
119
+ ...(options.resolveCredentialReadiness
120
+ ? { resolveCredentialReadiness: options.resolveCredentialReadiness }
121
+ : {}),
122
+ });
123
+
124
+ return { frames, resolveTranscriber, session, startVoiceTurn, turnOptions };
125
+ }
126
+
127
+ async function waitFor(
128
+ predicate: () => boolean,
129
+ message = "Timed out waiting for a typed live-voice turn",
130
+ ): Promise<void> {
131
+ for (let attempt = 0; attempt < 40; attempt += 1) {
132
+ if (predicate()) {
133
+ return;
134
+ }
135
+ await new Promise((resolve) => setTimeout(resolve, 5));
136
+ }
137
+ throw new Error(message);
138
+ }
139
+
140
+ function errorFrames(
141
+ frames: LiveVoiceServerFrame[],
142
+ ): Extract<LiveVoiceServerFrame, { type: "error" }>[] {
143
+ return frames.filter(
144
+ (frame): frame is Extract<LiveVoiceServerFrame, { type: "error" }> =>
145
+ frame.type === "error",
146
+ );
147
+ }
148
+
149
+ describe("typed live-voice turns", () => {
150
+ test("a text frame dispatches a turn carrying the typed text", async () => {
151
+ const { session, startVoiceTurn, turnOptions } = createHarness();
152
+ await session.start();
153
+
154
+ await session.handleClientFrame({
155
+ type: "text",
156
+ text: "what is on my calendar",
157
+ });
158
+
159
+ await waitFor(() => turnOptions.length > 0);
160
+ expect(startVoiceTurn).toHaveBeenCalledTimes(1);
161
+ expect(turnOptions[0]?.content).toContain("what is on my calendar");
162
+
163
+ await session.close("websocket_close");
164
+ });
165
+
166
+ test("a hidden text frame runs the turn as an internal instruction", async () => {
167
+ // The greeting that opens a voice session is a machine signal, not
168
+ // something the user typed. It must still drive the turn and still reach
169
+ // the model, so it persists, but `hiddenSyntheticPrompt` is what keeps it
170
+ // out of the transcript (no echo, filtered from `/messages`).
171
+ const { session, turnOptions } = createHarness();
172
+ await session.start();
173
+
174
+ await session.handleClientFrame({
175
+ type: "text",
176
+ text: "this message is sent automatically",
177
+ hidden: true,
178
+ });
179
+
180
+ await waitFor(() => turnOptions.length > 0);
181
+ expect(turnOptions[0]?.hiddenSyntheticPrompt).toBe(true);
182
+ expect(turnOptions[0]?.content).toContain(
183
+ "this message is sent automatically",
184
+ );
185
+
186
+ await session.close("websocket_close");
187
+ });
188
+
189
+ test("an ordinary typed turn stays visible", async () => {
190
+ // A turn the user actually typed is a real message and must render like
191
+ // one, so the flag is never set by default.
192
+ const { session, turnOptions } = createHarness();
193
+ await session.start();
194
+
195
+ await session.handleClientFrame({ type: "text", text: "typed by hand" });
196
+
197
+ await waitFor(() => turnOptions.length > 0);
198
+ expect(turnOptions[0]?.hiddenSyntheticPrompt).toBeUndefined();
199
+
200
+ await session.close("websocket_close");
201
+ });
202
+
203
+ test("a typed turn emits the same thinking frame a spoken one does", async () => {
204
+ // The client draws its working state off `thinking`. A typed turn that
205
+ // skipped it would leave the room idle while the assistant worked.
206
+ const { frames, session } = createHarness();
207
+ await session.start();
208
+
209
+ await session.handleClientFrame({ type: "text", text: "hello" });
210
+
211
+ await waitFor(() => frames.some((frame) => frame.type === "thinking"));
212
+
213
+ await session.close("websocket_close");
214
+ });
215
+
216
+ test("a typed turn leaves the capture cycle alone", async () => {
217
+ // `currentUtterance` belongs to the microphone loop. A typed turn that
218
+ // installed its own carrier there would discard whatever the user was
219
+ // part-way through saying.
220
+ const { session } = createHarness();
221
+ await session.start();
222
+ await waitFor(() => session.finalTranscriptText === "");
223
+
224
+ await session.handleClientFrame({ type: "text", text: "typed" });
225
+
226
+ // The armed cycle still holds its own (empty) transcript rather than the
227
+ // typed text, which rode a carrier of its own.
228
+ expect(session.finalTranscriptText).toBe("");
229
+
230
+ await session.close("websocket_close");
231
+ });
232
+
233
+ test("a manual session takes a typed turn after the microphone has streamed", async () => {
234
+ // Manual capture forwards from the moment the microphone opens, not from a
235
+ // push-to-talk press, so the first chunk of silence latches
236
+ // `manualAudioCaptured` on the armed cycle. A manual cycle only completes
237
+ // on release, so treating that flag as "user is mid-utterance" would
238
+ // refuse every typed turn a few milliseconds into the session.
239
+ const { frames, session, turnOptions } = createHarness();
240
+ await session.start();
241
+
242
+ await session.handleClientFrame({
243
+ type: "audio",
244
+ dataBase64: Buffer.alloc(960).toString("base64"),
245
+ });
246
+
247
+ await session.handleClientFrame({ type: "text", text: "typed anyway" });
248
+
249
+ await waitFor(() => turnOptions.length === 1);
250
+ expect(turnOptions[0]?.content).toContain("typed anyway");
251
+ expect(errorFrames(frames)).toHaveLength(0);
252
+
253
+ await session.close("websocket_close");
254
+ });
255
+
256
+ test("a typed turn arriving mid-turn is refused recoverably", async () => {
257
+ const { frames, session, startVoiceTurn } = createHarness();
258
+ await session.start();
259
+
260
+ await session.handleClientFrame({ type: "text", text: "first" });
261
+ await waitFor(() => frames.some((frame) => frame.type === "thinking"));
262
+
263
+ await session.handleClientFrame({ type: "text", text: "second" });
264
+
265
+ const errors = errorFrames(frames);
266
+ expect(errors).toHaveLength(1);
267
+ expect(errors[0]).toMatchObject({
268
+ frameType: "text",
269
+ recoverable: true,
270
+ });
271
+ // Refused, not queued: the second turn must not run behind the first.
272
+ expect(startVoiceTurn).toHaveBeenCalledTimes(1);
273
+
274
+ await session.close("websocket_close");
275
+ });
276
+
277
+ test("a text-only session runs a typed turn without ever arming a transcriber", async () => {
278
+ const { frames, resolveTranscriber, session, turnOptions } = createHarness({
279
+ resolveCredentialReadiness: async () => STT_ONLY_GAP,
280
+ });
281
+
282
+ await session.start();
283
+
284
+ expect(frames[0]).toMatchObject({ type: "ready", audioInput: false });
285
+
286
+ await session.handleClientFrame({ type: "text", text: "still works" });
287
+
288
+ await waitFor(() => turnOptions.length > 0);
289
+ expect(turnOptions[0]?.content).toContain("still works");
290
+ expect(resolveTranscriber).not.toHaveBeenCalled();
291
+
292
+ await session.close("websocket_close");
293
+ });
294
+
295
+ test("a text-only server_vad session survives more than one typed turn", async () => {
296
+ // The post-turn re-arm is skipped when there is no turn detector, and a
297
+ // session that asked for server_vad has one even with the microphone leg
298
+ // dead. Without a guard on the arm itself, the re-arm after the first
299
+ // typed turn resolves a transcriber the preflight already rejected, fails
300
+ // the session, and closes it: the first turn works and the second never
301
+ // gets the chance.
302
+ const { frames, resolveTranscriber, session, turnOptions } = createHarness({
303
+ startFrame: { ...START_FRAME, turnDetection: "server_vad" },
304
+ resolveCredentialReadiness: async () => STT_ONLY_GAP,
305
+ autoCompleteTurns: true,
306
+ });
307
+ await session.start();
308
+
309
+ expect(frames[0]).toMatchObject({ type: "ready", audioInput: false });
310
+
311
+ await session.handleClientFrame({ type: "text", text: "first turn" });
312
+ await waitFor(() => turnOptions.length === 1);
313
+
314
+ // Let the post-turn re-arm run before asking for the second turn.
315
+ await new Promise((resolve) => setTimeout(resolve, 20));
316
+
317
+ expect(errorFrames(frames)).toHaveLength(0);
318
+ expect(resolveTranscriber).not.toHaveBeenCalled();
319
+
320
+ await session.handleClientFrame({ type: "text", text: "second turn" });
321
+ await waitFor(() => turnOptions.length === 2);
322
+ expect(turnOptions[1]?.content).toContain("second turn");
323
+
324
+ await session.close("websocket_close");
325
+ });
326
+
327
+ test("a typed turn stamps the end-of-input anchor a round trip needs", async () => {
328
+ // roundTripMs measures end-of-user-input to first audio, anchored on
329
+ // utteranceEnd ?? pttRelease. A typed turn travels neither, so without an
330
+ // anchor of its own every typed turn reports null. That does not merely
331
+ // lose rows: it biases the voice latency numbers toward spoken turns while
332
+ // still looking healthy.
333
+ //
334
+ // Asserted on the anchor rather than on roundTripMs itself, because the
335
+ // far end of that duration is the first TTS chunk actually reaching the
336
+ // client, which this harness does not drive.
337
+ const { frames, session, turnOptions } = createHarness({
338
+ autoCompleteTurns: true,
339
+ emitMetrics: true,
340
+ });
341
+ await session.start();
342
+
343
+ await session.handleClientFrame({ type: "text", text: "how long is this" });
344
+ await waitFor(() => turnOptions.length === 1);
345
+ await waitFor(() => frames.some((frame) => frame.type === "metrics"));
346
+
347
+ const metrics = frames.find(
348
+ (frame): frame is Extract<LiveVoiceServerFrame, { type: "metrics" }> =>
349
+ frame.type === "metrics",
350
+ );
351
+ const turn = (
352
+ metrics?.metrics as {
353
+ recentTurns?: {
354
+ timestamps: Record<string, number | null>;
355
+ }[];
356
+ }
357
+ )?.recentTurns?.[0];
358
+
359
+ expect(turn?.timestamps.utteranceEndAtMs).not.toBeNull();
360
+ // Nothing was transcribed, and stamping a transcript mark would invent a
361
+ // measurement rather than report one.
362
+ expect(turn?.timestamps.finalTranscriptAtMs).toBeNull();
363
+ expect(turn?.timestamps.speechStartAtMs).toBeNull();
364
+
365
+ await session.close("websocket_close");
366
+ });
367
+
368
+ test("a text-only session drops audio instead of failing on it", async () => {
369
+ // A client that has not caught up with `audioInput: false` may keep
370
+ // streaming. There is nothing to route it into, and ending the session
371
+ // over it would undo the point of opening one.
372
+ const { frames, resolveTranscriber, session } = createHarness({
373
+ resolveCredentialReadiness: async () => STT_ONLY_GAP,
374
+ });
375
+ await session.start();
376
+
377
+ await session.handleClientFrame({
378
+ type: "audio",
379
+ dataBase64: Buffer.alloc(960).toString("base64"),
380
+ });
381
+
382
+ expect(errorFrames(frames)).toHaveLength(0);
383
+ expect(resolveTranscriber).not.toHaveBeenCalled();
384
+
385
+ // Still able to take the turn it was opened for.
386
+ await session.handleClientFrame({ type: "text", text: "typed anyway" });
387
+ await waitFor(() => frames.some((frame) => frame.type === "thinking"));
388
+
389
+ await session.close("websocket_close");
390
+ });
391
+ });
@@ -4,6 +4,7 @@ import {
4
4
  createLiveVoiceServerFrameSequencer,
5
5
  type LiveVoiceClientFrame,
6
6
  type LiveVoiceServerFrame,
7
+ MAX_TEXT_TURN_CHARS,
7
8
  parseLiveVoiceBinaryAudioFrame,
8
9
  parseLiveVoiceClientTextFrame,
9
10
  validateLiveVoiceClientFrame,
@@ -462,6 +463,229 @@ describe("parseLiveVoiceClientTextFrame", () => {
462
463
  },
463
464
  );
464
465
 
466
+ test("parses a text turn frame", () => {
467
+ const result = parseLiveVoiceClientTextFrame(
468
+ JSON.stringify({ type: "text", text: "what is on my calendar" }),
469
+ );
470
+
471
+ expect(result.ok).toBe(true);
472
+ if (!result.ok) {
473
+ return;
474
+ }
475
+ expect(result.frame).toEqual({
476
+ type: "text",
477
+ text: "what is on my calendar",
478
+ });
479
+ });
480
+
481
+ test("trims a text turn so the validated text is the text that travels", () => {
482
+ const result = validateLiveVoiceClientFrame({
483
+ type: "text",
484
+ text: " what is on my calendar\n",
485
+ });
486
+
487
+ expect(result.ok).toBe(true);
488
+ if (!result.ok) {
489
+ return;
490
+ }
491
+ expect(result.frame).toEqual({
492
+ type: "text",
493
+ text: "what is on my calendar",
494
+ });
495
+ });
496
+
497
+ test("carries the hidden marker on a text turn that sets it", () => {
498
+ const result = validateLiveVoiceClientFrame({
499
+ type: "text",
500
+ text: "this message is sent automatically",
501
+ hidden: true,
502
+ });
503
+
504
+ expect(result.ok).toBe(true);
505
+ if (!result.ok) {
506
+ return;
507
+ }
508
+ expect(result.frame).toEqual({
509
+ type: "text",
510
+ text: "this message is sent automatically",
511
+ hidden: true,
512
+ });
513
+ });
514
+
515
+ test.each([[false], [undefined]])(
516
+ "omits the hidden marker when it is %s",
517
+ (hidden) => {
518
+ // Absent rather than `hidden: false`, so a turn the user typed is
519
+ // byte-identical to one sent by a client that predates the field.
520
+ const result = validateLiveVoiceClientFrame({
521
+ type: "text",
522
+ text: "typed by hand",
523
+ ...(hidden === undefined ? {} : { hidden }),
524
+ });
525
+
526
+ expect(result.ok).toBe(true);
527
+ if (!result.ok) {
528
+ return;
529
+ }
530
+ expect(result.frame).toEqual({ type: "text", text: "typed by hand" });
531
+ },
532
+ );
533
+
534
+ test("rejects a non-boolean hidden marker", () => {
535
+ const result = validateLiveVoiceClientFrame({
536
+ type: "text",
537
+ text: "hello",
538
+ hidden: "yes",
539
+ });
540
+
541
+ expect(result.ok).toBe(false);
542
+ if (result.ok) {
543
+ return;
544
+ }
545
+ expect(result.error).toMatchObject({
546
+ code: "invalid_field",
547
+ field: "hidden",
548
+ frameType: "text",
549
+ });
550
+ });
551
+
552
+ test.each([
553
+ ["empty", ""],
554
+ ["whitespace only", " \n\t "],
555
+ ])("rejects a %s text turn", (_label, text) => {
556
+ const result = validateLiveVoiceClientFrame({ type: "text", text });
557
+
558
+ expect(result.ok).toBe(false);
559
+ if (result.ok) {
560
+ return;
561
+ }
562
+ expect(result.error).toMatchObject({
563
+ code: "invalid_field",
564
+ field: "text",
565
+ frameType: "text",
566
+ });
567
+ });
568
+
569
+ test("rejects a text turn past the length cap", () => {
570
+ const result = validateLiveVoiceClientFrame({
571
+ type: "text",
572
+ text: "a".repeat(MAX_TEXT_TURN_CHARS + 1),
573
+ });
574
+
575
+ expect(result.ok).toBe(false);
576
+ if (result.ok) {
577
+ return;
578
+ }
579
+ expect(result.error).toMatchObject({
580
+ code: "invalid_field",
581
+ field: "text",
582
+ frameType: "text",
583
+ });
584
+ });
585
+
586
+ test("accepts a text turn exactly at the length cap", () => {
587
+ const result = validateLiveVoiceClientFrame({
588
+ type: "text",
589
+ text: "a".repeat(MAX_TEXT_TURN_CHARS),
590
+ });
591
+
592
+ expect(result.ok).toBe(true);
593
+ });
594
+
595
+ test("measures the length cap after trimming", () => {
596
+ // Padding is not content: a turn that fits once its surrounding
597
+ // whitespace is gone must not be rejected for the whitespace.
598
+ const result = validateLiveVoiceClientFrame({
599
+ type: "text",
600
+ text: ` ${"a".repeat(MAX_TEXT_TURN_CHARS)} `,
601
+ });
602
+
603
+ expect(result.ok).toBe(true);
604
+ });
605
+
606
+ test("rejects a text turn whose text is not a string", () => {
607
+ const result = validateLiveVoiceClientFrame({ type: "text", text: 42 });
608
+
609
+ expect(result.ok).toBe(false);
610
+ if (result.ok) {
611
+ return;
612
+ }
613
+ expect(result.error).toMatchObject({
614
+ code: "invalid_field",
615
+ field: "text",
616
+ frameType: "text",
617
+ });
618
+ });
619
+
620
+ test("rejects a text turn with no text field", () => {
621
+ const result = validateLiveVoiceClientFrame({ type: "text" });
622
+
623
+ expect(result.ok).toBe(false);
624
+ if (result.ok) {
625
+ return;
626
+ }
627
+ expect(result.error).toMatchObject({
628
+ code: "missing_required_field",
629
+ field: "text",
630
+ frameType: "text",
631
+ });
632
+ });
633
+
634
+ test("parses the textInput capability on the start frame", () => {
635
+ const result = validateLiveVoiceClientFrame({
636
+ type: "start",
637
+ textInput: true,
638
+ audio: { mimeType: "audio/pcm", sampleRate: 24000, channels: 1 },
639
+ });
640
+
641
+ expect(result.ok).toBe(true);
642
+ if (!result.ok) {
643
+ return;
644
+ }
645
+ expect(result.frame).toMatchObject({ type: "start", textInput: true });
646
+ });
647
+
648
+ test.each([
649
+ ["absent", {}],
650
+ ["false", { textInput: false }],
651
+ ])(
652
+ "omits textInput from the start frame when %s",
653
+ (_label, extra: Record<string, unknown>) => {
654
+ // Absent and false must be indistinguishable downstream: both mean the
655
+ // client cannot take a turn without the microphone, so neither may
656
+ // relax the credential preflight.
657
+ const result = validateLiveVoiceClientFrame({
658
+ type: "start",
659
+ ...extra,
660
+ audio: { mimeType: "audio/pcm", sampleRate: 24000, channels: 1 },
661
+ });
662
+
663
+ expect(result.ok).toBe(true);
664
+ if (!result.ok) {
665
+ return;
666
+ }
667
+ expect("textInput" in result.frame).toBe(false);
668
+ },
669
+ );
670
+
671
+ test("returns a typed protocol error for a non-boolean textInput", () => {
672
+ const result = validateLiveVoiceClientFrame({
673
+ type: "start",
674
+ textInput: "yes",
675
+ audio: { mimeType: "audio/pcm", sampleRate: 24000, channels: 1 },
676
+ });
677
+
678
+ expect(result.ok).toBe(false);
679
+ if (result.ok) {
680
+ return;
681
+ }
682
+ expect(result.error).toMatchObject({
683
+ code: "invalid_field",
684
+ field: "textInput",
685
+ frameType: "start",
686
+ });
687
+ });
688
+
465
689
  test("returns typed protocol errors for missing audio configuration fields", () => {
466
690
  const result = validateLiveVoiceClientFrame({
467
691
  type: "start",
@@ -76,7 +76,11 @@ export type LiveVoiceCredentialReadiness =
76
76
  * configured `referenceId` — live voice supplies no per-request voiceId).
77
77
  */
78
78
  export async function resolveLiveVoiceCredentialReadiness(): Promise<LiveVoiceCredentialReadiness> {
79
- const effective = await resolveEffectiveSpeechProviders();
79
+ // Readiness must ask about the same provider the session will dial, so it
80
+ // reads the live-voice role rather than the global setting.
81
+ const effective = await resolveEffectiveSpeechProviders(undefined, {
82
+ role: "liveVoice",
83
+ });
80
84
  const gaps = (
81
85
  await Promise.all([
82
86
  resolveSttGap(effective.stt),