@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -0,0 +1,1234 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ mock,
8
+ test,
9
+ } from "bun:test";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Module mocks — declared before imports of the module under test.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ // Mock the STT resolve module (used by MediaStreamSttSession)
16
+ mock.module("../providers/speech-to-text/resolve.js", () => ({
17
+ resolveTelephonySttCapability: jest.fn(),
18
+ resolveBatchTranscriber: jest.fn(),
19
+ }));
20
+
21
+ // Mock the logger to suppress output during tests
22
+ mock.module("../util/logger.js", () => ({
23
+ getLogger: () => ({
24
+ info: () => {},
25
+ warn: () => {},
26
+ error: () => {},
27
+ debug: () => {},
28
+ }),
29
+ }));
30
+
31
+ // Mock the call store — lightweight in-memory stubs
32
+ const mockSessions = new Map<string, Record<string, unknown>>();
33
+ const mockEvents: Array<{
34
+ callSessionId: string;
35
+ eventType: string;
36
+ data: unknown;
37
+ }> = [];
38
+
39
+ mock.module("../calls/call-store.js", () => ({
40
+ getCallSession: jest.fn((id: string) => mockSessions.get(id) ?? null),
41
+ updateCallSession: jest.fn((id: string, updates: Record<string, unknown>) => {
42
+ const session = mockSessions.get(id);
43
+ if (session) {
44
+ Object.assign(session, updates);
45
+ }
46
+ }),
47
+ recordCallEvent: jest.fn(
48
+ (callSessionId: string, eventType: string, data: unknown) => {
49
+ mockEvents.push({ callSessionId, eventType, data });
50
+ },
51
+ ),
52
+ createCallSession: jest.fn(),
53
+ getCallSessionByCallSid: jest.fn(),
54
+ getActiveCallSessionForConversation: jest.fn(),
55
+ createPendingQuestion: jest.fn(),
56
+ expirePendingQuestions: jest.fn(),
57
+ getPendingQuestion: jest.fn(),
58
+ answerPendingQuestion: jest.fn(),
59
+ }));
60
+
61
+ // Mock the call state machine
62
+ mock.module("../calls/call-state-machine.js", () => ({
63
+ isTerminalState: jest.fn(
64
+ (status: string) =>
65
+ status === "completed" || status === "failed" || status === "cancelled",
66
+ ),
67
+ }));
68
+
69
+ // Mock the call state (controller registry)
70
+ const mockControllers = new Map<string, unknown>();
71
+ mock.module("../calls/call-state.js", () => ({
72
+ registerCallController: jest.fn(
73
+ (callSessionId: string, controller: unknown) => {
74
+ mockControllers.set(callSessionId, controller);
75
+ },
76
+ ),
77
+ unregisterCallController: jest.fn((callSessionId: string) => {
78
+ mockControllers.delete(callSessionId);
79
+ }),
80
+ getCallController: jest.fn((callSessionId: string) =>
81
+ mockControllers.get(callSessionId),
82
+ ),
83
+ fireCallTranscriptNotifier: jest.fn(),
84
+ fireCallQuestionNotifier: jest.fn(),
85
+ fireCallCompletionNotifier: jest.fn(),
86
+ registerCallQuestionNotifier: jest.fn(),
87
+ unregisterCallQuestionNotifier: jest.fn(),
88
+ registerCallTranscriptNotifier: jest.fn(),
89
+ unregisterCallTranscriptNotifier: jest.fn(),
90
+ registerCallCompletionNotifier: jest.fn(),
91
+ unregisterCallCompletionNotifier: jest.fn(),
92
+ }));
93
+
94
+ // Mock the finalize-call module
95
+ mock.module("../calls/finalize-call.js", () => ({
96
+ finalizeCall: jest.fn(),
97
+ }));
98
+
99
+ // Mock the call pointer messages
100
+ mock.module("../calls/call-pointer-messages.js", () => ({
101
+ addPointerMessage: jest.fn(async () => {}),
102
+ formatDuration: jest.fn((ms: number) => `${Math.round(ms / 1000)}s`),
103
+ }));
104
+
105
+ // Mock the CallController to avoid pulling in the full conversation pipeline
106
+ const mockStartInitialGreeting = jest.fn(async () => {});
107
+ const mockHandleCallerUtterance = jest.fn(async () => {});
108
+ const mockHandleInterrupt = jest.fn();
109
+ const mockDestroy = jest.fn();
110
+
111
+ const mockHandleBargeIn = jest.fn(() => false);
112
+
113
+ mock.module("../calls/call-controller.js", () => ({
114
+ CallController: jest.fn().mockImplementation(() => ({
115
+ startInitialGreeting: mockStartInitialGreeting,
116
+ handleCallerUtterance: mockHandleCallerUtterance,
117
+ handleInterrupt: mockHandleInterrupt,
118
+ handleBargeIn: mockHandleBargeIn,
119
+ destroy: mockDestroy,
120
+ getState: jest.fn(() => "idle"),
121
+ setTrustContext: jest.fn(),
122
+ markNextCallerTurnAsOpeningAck: jest.fn(),
123
+ getPendingConsultationQuestionId: jest.fn(),
124
+ handleUserAnswer: jest.fn(),
125
+ handleUserInstruction: jest.fn(),
126
+ })),
127
+ }));
128
+
129
+ // Mock the assistant scope
130
+ mock.module("../runtime/assistant-scope.js", () => ({
131
+ DAEMON_INTERNAL_ASSISTANT_ID: "self",
132
+ }));
133
+
134
+ // Mock the relay setup router so handleStart() doesn't query the database.
135
+ // Default returns normal_call; individual tests can override via
136
+ // `mockRouteSetupResult` to exercise deny and unsupported-flow branches.
137
+ let mockRouteSetupResult: {
138
+ outcome: { action: string; [key: string]: unknown };
139
+ resolved: {
140
+ assistantId: string;
141
+ isInbound: boolean;
142
+ otherPartyNumber: string;
143
+ actorTrust: { trustClass: string; memberRecord: null };
144
+ };
145
+ } = {
146
+ outcome: { action: "normal_call" as const, isInbound: true },
147
+ resolved: {
148
+ assistantId: "self",
149
+ isInbound: true,
150
+ otherPartyNumber: "+15551234567",
151
+ actorTrust: {
152
+ trustClass: "guardian" as const,
153
+ memberRecord: null,
154
+ },
155
+ },
156
+ };
157
+
158
+ mock.module("../calls/relay-setup-router.js", () => ({
159
+ routeSetup: jest.fn(() => mockRouteSetupResult),
160
+ }));
161
+
162
+ // Mock the actor trust resolver (used by handleStart to derive trust context)
163
+ mock.module("../runtime/actor-trust-resolver.js", () => ({
164
+ toTrustContext: jest.fn(() => ({
165
+ sourceChannel: "phone",
166
+ trustClass: "guardian",
167
+ })),
168
+ resolveActorTrust: jest.fn(() => ({
169
+ trustClass: "guardian",
170
+ memberRecord: null,
171
+ })),
172
+ }));
173
+
174
+ // Mock the call speech output (speakSystemPrompt used in deny/unsupported paths)
175
+ mock.module("../calls/call-speech-output.js", () => ({
176
+ speakSystemPrompt: jest.fn(async () => {}),
177
+ }));
178
+
179
+ // Mock scoped approval grants (used in handleTransportClosed and early teardown)
180
+ mock.module("../memory/scoped-approval-grants.js", () => ({
181
+ revokeScopedApprovalGrantsForContext: jest.fn(),
182
+ }));
183
+
184
+ // Mock the TTS provider resolution so that the dynamic import inside
185
+ // MediaStreamOutput.processSynthesizeItem() doesn't pull in the real
186
+ // config/provider chain (which would hang or error in a test environment).
187
+ mock.module("../calls/resolve-call-tts-provider.js", () => ({
188
+ resolveCallTtsProvider: jest.fn(() => ({
189
+ provider: null,
190
+ useSynthesizedPath: false,
191
+ audioFormat: "mp3" as const,
192
+ })),
193
+ }));
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Now import the module under test.
197
+ // ---------------------------------------------------------------------------
198
+
199
+ import { speakSystemPrompt } from "../calls/call-speech-output.js";
200
+ import { registerCallController } from "../calls/call-state.js";
201
+ import { recordCallEvent, updateCallSession } from "../calls/call-store.js";
202
+ import { finalizeCall } from "../calls/finalize-call.js";
203
+ import {
204
+ activeMediaStreamSessions,
205
+ MediaStreamCallSession,
206
+ } from "../calls/media-stream-server.js";
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Mock WebSocket factory
210
+ // ---------------------------------------------------------------------------
211
+
212
+ function createMockWs() {
213
+ const sent: string[] = [];
214
+ let closed = false;
215
+ let closeCode: number | undefined;
216
+ let closeReason: string | undefined;
217
+
218
+ return {
219
+ ws: {
220
+ send(data: string) {
221
+ if (closed) throw new Error("WebSocket is closed");
222
+ sent.push(data);
223
+ },
224
+ close(code?: number, reason?: string) {
225
+ closed = true;
226
+ closeCode = code;
227
+ closeReason = reason;
228
+ },
229
+ } as unknown as import("bun").ServerWebSocket<unknown>,
230
+ get sent() {
231
+ return sent;
232
+ },
233
+ get closed() {
234
+ return closed;
235
+ },
236
+ get closeCode() {
237
+ return closeCode;
238
+ },
239
+ get closeReason() {
240
+ return closeReason;
241
+ },
242
+ };
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Fixture factories
247
+ // ---------------------------------------------------------------------------
248
+
249
+ function makeStartMessage(overrides?: {
250
+ callSid?: string;
251
+ streamSid?: string;
252
+ }): string {
253
+ return JSON.stringify({
254
+ event: "start",
255
+ sequenceNumber: "1",
256
+ streamSid: overrides?.streamSid ?? "MZ00000000000000000000000000000000",
257
+ start: {
258
+ accountSid: "AC00000000000000000000000000000000",
259
+ streamSid: overrides?.streamSid ?? "MZ00000000000000000000000000000000",
260
+ callSid: overrides?.callSid ?? "CA00000000000000000000000000000000",
261
+ tracks: ["inbound"],
262
+ customParameters: {},
263
+ mediaFormat: {
264
+ encoding: "audio/x-mulaw",
265
+ sampleRate: 8000,
266
+ channels: 1,
267
+ },
268
+ },
269
+ });
270
+ }
271
+
272
+ function makeMediaMessage(payload: string, chunk: string = "1"): string {
273
+ return JSON.stringify({
274
+ event: "media",
275
+ sequenceNumber: "2",
276
+ streamSid: "MZ00000000000000000000000000000000",
277
+ media: {
278
+ track: "inbound",
279
+ chunk,
280
+ timestamp: "100",
281
+ payload,
282
+ },
283
+ });
284
+ }
285
+
286
+ function makeStopMessage(): string {
287
+ return JSON.stringify({
288
+ event: "stop",
289
+ sequenceNumber: "99",
290
+ streamSid: "MZ00000000000000000000000000000000",
291
+ stop: {
292
+ accountSid: "AC00000000000000000000000000000000",
293
+ callSid: "CA00000000000000000000000000000000",
294
+ },
295
+ });
296
+ }
297
+
298
+ function makeMarkMessage(name: string): string {
299
+ return JSON.stringify({
300
+ event: "mark",
301
+ sequenceNumber: "50",
302
+ streamSid: "MZ00000000000000000000000000000000",
303
+ mark: { name },
304
+ });
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // Setup / teardown
309
+ // ---------------------------------------------------------------------------
310
+
311
+ beforeEach(() => {
312
+ jest.useFakeTimers();
313
+ mockSessions.clear();
314
+ mockEvents.length = 0;
315
+ mockControllers.clear();
316
+ activeMediaStreamSessions.clear();
317
+ mockStartInitialGreeting.mockClear();
318
+ mockHandleCallerUtterance.mockClear();
319
+ mockHandleInterrupt.mockClear();
320
+ mockHandleBargeIn.mockClear();
321
+ mockHandleBargeIn.mockReturnValue(false);
322
+ mockDestroy.mockClear();
323
+ (registerCallController as jest.Mock).mockClear();
324
+ (recordCallEvent as jest.Mock).mockClear();
325
+ (updateCallSession as jest.Mock).mockClear();
326
+ (finalizeCall as jest.Mock).mockClear();
327
+ (speakSystemPrompt as jest.Mock).mockClear();
328
+ // Reset routeSetup to default normal_call
329
+ mockRouteSetupResult = {
330
+ outcome: { action: "normal_call" as const, isInbound: true },
331
+ resolved: {
332
+ assistantId: "self",
333
+ isInbound: true,
334
+ otherPartyNumber: "+15551234567",
335
+ actorTrust: {
336
+ trustClass: "guardian" as const,
337
+ memberRecord: null,
338
+ },
339
+ },
340
+ };
341
+ });
342
+
343
+ afterEach(() => {
344
+ jest.useRealTimers();
345
+ activeMediaStreamSessions.clear();
346
+ });
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Tests
350
+ // ---------------------------------------------------------------------------
351
+
352
+ describe("MediaStreamCallSession", () => {
353
+ test("creates a session and exposes output adapter", () => {
354
+ const { ws } = createMockWs();
355
+ const session = new MediaStreamCallSession(ws, "call-1");
356
+ expect(session.callSessionId).toBe("call-1");
357
+ expect(session.getOutput()).toBeDefined();
358
+ expect(session.getOutput().getConnectionState()).toBe("connected");
359
+ });
360
+
361
+ describe("start event handling", () => {
362
+ test("start event registers a controller and records call_connected", () => {
363
+ const mock = createMockWs();
364
+ // Set up a call session in the mock store
365
+ mockSessions.set("call-1", {
366
+ id: "call-1",
367
+ conversationId: "conv-1",
368
+ status: "initiated",
369
+ task: "Test task",
370
+ startedAt: null,
371
+ toNumber: "+15551234567",
372
+ });
373
+
374
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
375
+ session.handleMessage(makeStartMessage());
376
+
377
+ // Controller should have been registered
378
+ expect(registerCallController).toHaveBeenCalledWith(
379
+ "call-1",
380
+ expect.anything(),
381
+ );
382
+
383
+ // call_connected event should have been recorded
384
+ expect(recordCallEvent).toHaveBeenCalledWith(
385
+ "call-1",
386
+ "call_connected",
387
+ expect.objectContaining({
388
+ callSid: "CA00000000000000000000000000000000",
389
+ transport: "media-stream",
390
+ }),
391
+ );
392
+
393
+ // Call session should have been updated
394
+ expect(updateCallSession).toHaveBeenCalledWith(
395
+ "call-1",
396
+ expect.objectContaining({
397
+ providerCallSid: "CA00000000000000000000000000000000",
398
+ status: "in_progress",
399
+ }),
400
+ );
401
+
402
+ // Initial greeting should have been fired
403
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
404
+ });
405
+
406
+ test("start event updates streamSid on the output adapter", () => {
407
+ const mock = createMockWs();
408
+ mockSessions.set("call-1", {
409
+ id: "call-1",
410
+ conversationId: "conv-1",
411
+ status: "initiated",
412
+ task: null,
413
+ startedAt: null,
414
+ toNumber: "+15551234567",
415
+ });
416
+
417
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
418
+ session.handleMessage(makeStartMessage({ streamSid: "MZ-custom-sid" }));
419
+
420
+ expect(session.getOutput().getStreamSid()).toBe("MZ-custom-sid");
421
+ });
422
+ });
423
+
424
+ describe("transport close handling", () => {
425
+ test("normal close (1000) marks session as completed", () => {
426
+ const mock = createMockWs();
427
+ mockSessions.set("call-1", {
428
+ id: "call-1",
429
+ conversationId: "conv-1",
430
+ status: "in_progress",
431
+ startedAt: Date.now() - 60000,
432
+ toNumber: "+15551234567",
433
+ });
434
+
435
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
436
+ session.handleTransportClosed(1000, "normal-close");
437
+
438
+ expect(updateCallSession).toHaveBeenCalledWith(
439
+ "call-1",
440
+ expect.objectContaining({ status: "completed" }),
441
+ );
442
+ expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
443
+ });
444
+
445
+ test("abnormal close marks session as failed", () => {
446
+ const mock = createMockWs();
447
+ mockSessions.set("call-1", {
448
+ id: "call-1",
449
+ conversationId: "conv-1",
450
+ status: "in_progress",
451
+ startedAt: Date.now() - 60000,
452
+ toNumber: "+15551234567",
453
+ });
454
+
455
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
456
+ session.handleTransportClosed(1006, "abnormal-close");
457
+
458
+ expect(updateCallSession).toHaveBeenCalledWith(
459
+ "call-1",
460
+ expect.objectContaining({
461
+ status: "failed",
462
+ lastError: expect.stringContaining("abnormal-close"),
463
+ }),
464
+ );
465
+ expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
466
+ });
467
+
468
+ test("close on already-terminal session is a no-op", () => {
469
+ const mock = createMockWs();
470
+ mockSessions.set("call-1", {
471
+ id: "call-1",
472
+ conversationId: "conv-1",
473
+ status: "completed",
474
+ toNumber: "+15551234567",
475
+ });
476
+
477
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
478
+ session.handleTransportClosed(1000);
479
+
480
+ // updateCallSession should NOT have been called because session
481
+ // was already terminal
482
+ expect(updateCallSession).not.toHaveBeenCalled();
483
+ });
484
+ });
485
+
486
+ describe("destroy", () => {
487
+ test("destroys the controller and marks output as closed", () => {
488
+ const mock = createMockWs();
489
+ mockSessions.set("call-1", {
490
+ id: "call-1",
491
+ conversationId: "conv-1",
492
+ status: "initiated",
493
+ task: null,
494
+ startedAt: null,
495
+ toNumber: "+15551234567",
496
+ });
497
+
498
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
499
+ // Trigger start to create a controller
500
+ session.handleMessage(makeStartMessage());
501
+
502
+ session.destroy();
503
+ expect(mockDestroy).toHaveBeenCalled();
504
+ expect(session.getOutput().getConnectionState()).toBe("closed");
505
+ });
506
+
507
+ test("destroy is idempotent", () => {
508
+ const mock = createMockWs();
509
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
510
+ session.destroy();
511
+ session.destroy(); // Should not throw
512
+ });
513
+
514
+ test("messages after destroy are dropped", () => {
515
+ const mock = createMockWs();
516
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
517
+ session.destroy();
518
+
519
+ // Should not throw or create side effects
520
+ session.handleMessage(makeStartMessage());
521
+ expect(registerCallController).not.toHaveBeenCalled();
522
+ });
523
+ });
524
+
525
+ describe("media event forwarding", () => {
526
+ test("media events are forwarded to the STT session without errors", () => {
527
+ const mock = createMockWs();
528
+ mockSessions.set("call-1", {
529
+ id: "call-1",
530
+ conversationId: "conv-1",
531
+ status: "initiated",
532
+ task: null,
533
+ startedAt: null,
534
+ toNumber: "+15551234567",
535
+ });
536
+
537
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
538
+ session.handleMessage(makeStartMessage());
539
+
540
+ // Send media frames — should not throw
541
+ const payload = Buffer.from("test-audio").toString("base64");
542
+ session.handleMessage(makeMediaMessage(payload, "1"));
543
+ session.handleMessage(makeMediaMessage(payload, "2"));
544
+ session.handleMessage(makeMediaMessage(payload, "3"));
545
+ });
546
+
547
+ test("mark events are forwarded without errors", () => {
548
+ const mock = createMockWs();
549
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
550
+
551
+ // Mark events should be silently handled
552
+ session.handleMessage(makeMarkMessage("end-of-turn"));
553
+ });
554
+
555
+ test("stop events are forwarded to the STT session", () => {
556
+ const mock = createMockWs();
557
+ mockSessions.set("call-1", {
558
+ id: "call-1",
559
+ conversationId: "conv-1",
560
+ status: "initiated",
561
+ task: null,
562
+ startedAt: null,
563
+ toNumber: "+15551234567",
564
+ });
565
+
566
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
567
+ session.handleMessage(makeStartMessage());
568
+ session.handleMessage(makeStopMessage());
569
+
570
+ // Stop is informational; the session continues until WebSocket closes
571
+ });
572
+ });
573
+
574
+ describe("malformed messages", () => {
575
+ test("invalid JSON is dropped silently", () => {
576
+ const mock = createMockWs();
577
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
578
+ // Should not throw
579
+ session.handleMessage("not json {{{");
580
+ });
581
+
582
+ test("unknown event types are dropped silently", () => {
583
+ const mock = createMockWs();
584
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
585
+ session.handleMessage(JSON.stringify({ event: "unknown_type" }));
586
+ });
587
+ });
588
+ });
589
+
590
+ describe("media-stream output egress", () => {
591
+ // These tests exercise the async playback queue which relies on real
592
+ // timers (setTimeout / Bun.sleep). Override the global fake-timers
593
+ // from the outer beforeEach for this block.
594
+ beforeEach(() => {
595
+ jest.useRealTimers();
596
+ });
597
+
598
+ test("sendTextToken with text produces outbound media frames", async () => {
599
+ const mockWs = createMockWs();
600
+ mockSessions.set("call-out-1", {
601
+ id: "call-out-1",
602
+ conversationId: "conv-out-1",
603
+ status: "initiated",
604
+ task: "Outbound test",
605
+ startedAt: null,
606
+ toNumber: "+15551234567",
607
+ });
608
+
609
+ const session = new MediaStreamCallSession(mockWs.ws, "call-out-1");
610
+ session.handleMessage(makeStartMessage());
611
+
612
+ // Simulate the controller sending text to the output adapter
613
+ const output = session.getOutput();
614
+ output.sendTextToken("Hello caller", true);
615
+
616
+ // Allow the async playback queue to drain
617
+ await Bun.sleep(50);
618
+
619
+ // The output should have sent at least an end-of-turn mark.
620
+ // Media frames depend on TTS provider availability (mocked away in
621
+ // this test suite), but the mark is always sent synchronously.
622
+ const markMessages = mockWs.sent.filter(
623
+ (s) => JSON.parse(s).event === "mark",
624
+ );
625
+ expect(markMessages.length).toBeGreaterThan(0);
626
+
627
+ const markParsed = JSON.parse(markMessages[0]);
628
+ expect(markParsed.mark.name).toBe("end-of-turn");
629
+ });
630
+
631
+ test("empty sendTextToken (end-of-turn signal) sends only a mark, no media", async () => {
632
+ const mockWs = createMockWs();
633
+ mockSessions.set("call-eot-1", {
634
+ id: "call-eot-1",
635
+ conversationId: "conv-eot-1",
636
+ status: "initiated",
637
+ task: null,
638
+ startedAt: null,
639
+ toNumber: "+15551234567",
640
+ });
641
+
642
+ const session = new MediaStreamCallSession(mockWs.ws, "call-eot-1");
643
+ session.handleMessage(makeStartMessage());
644
+
645
+ const output = session.getOutput();
646
+ output.sendTextToken("", true);
647
+
648
+ await Bun.sleep(50);
649
+
650
+ // Should send a mark but no media frames
651
+ const mediaMessages = mockWs.sent.filter(
652
+ (s) => JSON.parse(s).event === "media",
653
+ );
654
+ const markMessages = mockWs.sent.filter(
655
+ (s) => JSON.parse(s).event === "mark",
656
+ );
657
+
658
+ expect(mediaMessages).toHaveLength(0);
659
+ expect(markMessages.length).toBeGreaterThan(0);
660
+ });
661
+
662
+ test("sendAudioPayload sends media frames to Twilio", () => {
663
+ const mockWs = createMockWs();
664
+ mockSessions.set("call-audio-1", {
665
+ id: "call-audio-1",
666
+ conversationId: "conv-audio-1",
667
+ status: "initiated",
668
+ task: null,
669
+ startedAt: null,
670
+ toNumber: "+15551234567",
671
+ });
672
+
673
+ const session = new MediaStreamCallSession(mockWs.ws, "call-audio-1");
674
+ session.handleMessage(makeStartMessage());
675
+
676
+ const output = session.getOutput();
677
+ const payload = Buffer.from("test-audio-data").toString("base64");
678
+ output.sendAudioPayload(payload);
679
+
680
+ const mediaMessages = mockWs.sent.filter(
681
+ (s) => JSON.parse(s).event === "media",
682
+ );
683
+ expect(mediaMessages).toHaveLength(1);
684
+ expect(JSON.parse(mediaMessages[0]).media.payload).toBe(payload);
685
+ });
686
+
687
+ test("clearAudio sends clear command and flushes playback queue", async () => {
688
+ const mockWs = createMockWs();
689
+ mockSessions.set("call-barge-1", {
690
+ id: "call-barge-1",
691
+ conversationId: "conv-barge-1",
692
+ status: "initiated",
693
+ task: null,
694
+ startedAt: null,
695
+ toNumber: "+15551234567",
696
+ });
697
+
698
+ const session = new MediaStreamCallSession(mockWs.ws, "call-barge-1");
699
+ session.handleMessage(makeStartMessage());
700
+
701
+ const output = session.getOutput();
702
+
703
+ // Queue some output
704
+ output.sendTextToken("This will be interrupted", true);
705
+
706
+ // Immediately barge-in
707
+ output.clearAudio();
708
+
709
+ await Bun.sleep(50);
710
+
711
+ // Should have sent a clear command
712
+ const clearMessages = mockWs.sent.filter(
713
+ (s) => JSON.parse(s).event === "clear",
714
+ );
715
+ expect(clearMessages.length).toBeGreaterThanOrEqual(1);
716
+ });
717
+
718
+ test("barge-in via speech start clears audio and interrupts controller", () => {
719
+ const mockWs = createMockWs();
720
+ mockSessions.set("call-interrupt-1", {
721
+ id: "call-interrupt-1",
722
+ conversationId: "conv-interrupt-1",
723
+ status: "initiated",
724
+ task: "Test task",
725
+ startedAt: null,
726
+ toNumber: "+15551234567",
727
+ });
728
+
729
+ const session = new MediaStreamCallSession(mockWs.ws, "call-interrupt-1");
730
+ session.handleMessage(makeStartMessage());
731
+
732
+ // Verify the controller is created
733
+ expect(session.getController()).not.toBeNull();
734
+
735
+ // Simulate a caller starting to speak (barge-in) by sending media
736
+ // while the assistant would be speaking. The handleSpeechStart callback
737
+ // should clear audio and call handleInterrupt on the controller.
738
+ // Note: In the real flow, the STT session detects speech start from
739
+ // audio energy. Here we verify the wiring by checking that the
740
+ // controller's handleInterrupt was called (if speech start fires).
741
+ // The STT session is stubbed, so we verify the output adapter's
742
+ // clearAudio works independently.
743
+ const output = session.getOutput();
744
+ output.clearAudio();
745
+
746
+ const clearMessages = mockWs.sent.filter(
747
+ (s) => JSON.parse(s).event === "clear",
748
+ );
749
+ expect(clearMessages.length).toBeGreaterThanOrEqual(1);
750
+ });
751
+ });
752
+
753
+ describe("activeMediaStreamSessions registry", () => {
754
+ test("sessions can be added and retrieved", () => {
755
+ const mock = createMockWs();
756
+ const session = new MediaStreamCallSession(mock.ws, "call-1");
757
+ activeMediaStreamSessions.set("call-1", session);
758
+ expect(activeMediaStreamSessions.get("call-1")).toBe(session);
759
+ activeMediaStreamSessions.delete("call-1");
760
+ expect(activeMediaStreamSessions.get("call-1")).toBeUndefined();
761
+ });
762
+ });
763
+
764
+ // ---------------------------------------------------------------------------
765
+ // Scenario-driven setup outcome coverage
766
+ // ---------------------------------------------------------------------------
767
+ // These tests exercise the deny and unsupported-action branches in
768
+ // MediaStreamCallSession.handleStart by overriding mockRouteSetupResult
769
+ // before sending a start message.
770
+
771
+ describe("media-stream setup outcome scenarios", () => {
772
+ describe("deny outcome", () => {
773
+ test("deny outcome records inbound_acl_denied event and sets status to failed", () => {
774
+ mockRouteSetupResult = {
775
+ outcome: {
776
+ action: "deny",
777
+ message: "This number is not authorized.",
778
+ logReason: "Inbound voice ACL: blocked caller",
779
+ },
780
+ resolved: {
781
+ assistantId: "self",
782
+ isInbound: true,
783
+ otherPartyNumber: "+15559998888",
784
+ actorTrust: { trustClass: "unknown", memberRecord: null },
785
+ },
786
+ };
787
+
788
+ const mockWs = createMockWs();
789
+ mockSessions.set("call-deny-1", {
790
+ id: "call-deny-1",
791
+ conversationId: "conv-deny-1",
792
+ status: "initiated",
793
+ task: null,
794
+ startedAt: null,
795
+ fromNumber: "+15559998888",
796
+ toNumber: "+15550001111",
797
+ });
798
+
799
+ const session = new MediaStreamCallSession(mockWs.ws, "call-deny-1");
800
+ session.handleMessage(makeStartMessage());
801
+
802
+ // Should record an inbound_acl_denied event
803
+ expect(recordCallEvent).toHaveBeenCalledWith(
804
+ "call-deny-1",
805
+ "inbound_acl_denied",
806
+ expect.objectContaining({
807
+ from: "+15559998888",
808
+ }),
809
+ );
810
+
811
+ // Should update session to failed
812
+ expect(updateCallSession).toHaveBeenCalledWith(
813
+ "call-deny-1",
814
+ expect.objectContaining({
815
+ status: "failed",
816
+ lastError: "Inbound voice ACL: blocked caller",
817
+ }),
818
+ );
819
+
820
+ // Should NOT register a controller (deny path skips it)
821
+ expect(registerCallController).not.toHaveBeenCalled();
822
+ });
823
+
824
+ test("deny outcome speaks the denial message", () => {
825
+ mockRouteSetupResult = {
826
+ outcome: {
827
+ action: "deny",
828
+ message: "This number is not authorized to use this assistant.",
829
+ logReason: "Inbound voice ACL: member policy deny",
830
+ },
831
+ resolved: {
832
+ assistantId: "self",
833
+ isInbound: true,
834
+ otherPartyNumber: "+15559998888",
835
+ actorTrust: { trustClass: "unknown", memberRecord: null },
836
+ },
837
+ };
838
+
839
+ const mockWs = createMockWs();
840
+ mockSessions.set("call-deny-speak-1", {
841
+ id: "call-deny-speak-1",
842
+ conversationId: "conv-deny-speak-1",
843
+ status: "initiated",
844
+ task: null,
845
+ startedAt: null,
846
+ fromNumber: "+15559998888",
847
+ toNumber: "+15550001111",
848
+ });
849
+
850
+ const session = new MediaStreamCallSession(
851
+ mockWs.ws,
852
+ "call-deny-speak-1",
853
+ );
854
+ session.handleMessage(makeStartMessage());
855
+
856
+ // speakSystemPrompt should be called with the denial message
857
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
858
+ expect.anything(),
859
+ "This number is not authorized to use this assistant.",
860
+ );
861
+ });
862
+
863
+ test("deny outcome runs finalization", () => {
864
+ mockRouteSetupResult = {
865
+ outcome: {
866
+ action: "deny",
867
+ message: "Not authorized.",
868
+ logReason: "ACL deny",
869
+ },
870
+ resolved: {
871
+ assistantId: "self",
872
+ isInbound: true,
873
+ otherPartyNumber: "+15559998888",
874
+ actorTrust: { trustClass: "unknown", memberRecord: null },
875
+ },
876
+ };
877
+
878
+ const mockWs = createMockWs();
879
+ mockSessions.set("call-deny-finalize-1", {
880
+ id: "call-deny-finalize-1",
881
+ conversationId: "conv-deny-finalize-1",
882
+ status: "initiated",
883
+ task: null,
884
+ startedAt: null,
885
+ fromNumber: "+15559998888",
886
+ toNumber: "+15550001111",
887
+ });
888
+
889
+ const session = new MediaStreamCallSession(
890
+ mockWs.ws,
891
+ "call-deny-finalize-1",
892
+ );
893
+ session.handleMessage(makeStartMessage());
894
+
895
+ // finalizeCall should be called because early teardown runs it inline
896
+ expect(finalizeCall).toHaveBeenCalledWith(
897
+ "call-deny-finalize-1",
898
+ "conv-deny-finalize-1",
899
+ );
900
+ });
901
+ });
902
+
903
+ describe("unsupported interactive setup flow", () => {
904
+ test("verification outcome records call_failed with preflight-bypass reason", () => {
905
+ mockRouteSetupResult = {
906
+ outcome: {
907
+ action: "verification",
908
+ assistantId: "self",
909
+ fromNumber: "+14155551234",
910
+ },
911
+ resolved: {
912
+ assistantId: "self",
913
+ isInbound: true,
914
+ otherPartyNumber: "+14155551234",
915
+ actorTrust: { trustClass: "unknown", memberRecord: null },
916
+ },
917
+ };
918
+
919
+ const mockWs = createMockWs();
920
+ mockSessions.set("call-unsup-verify-1", {
921
+ id: "call-unsup-verify-1",
922
+ conversationId: "conv-unsup-verify-1",
923
+ status: "initiated",
924
+ task: null,
925
+ startedAt: null,
926
+ fromNumber: "+14155551234",
927
+ toNumber: "+15550001111",
928
+ });
929
+
930
+ const session = new MediaStreamCallSession(
931
+ mockWs.ws,
932
+ "call-unsup-verify-1",
933
+ );
934
+ session.handleMessage(makeStartMessage());
935
+
936
+ // Should record call_failed event with preflight-bypass note
937
+ expect(recordCallEvent).toHaveBeenCalledWith(
938
+ "call-unsup-verify-1",
939
+ "call_failed",
940
+ expect.objectContaining({
941
+ reason: expect.stringContaining("verification"),
942
+ transport: "media-stream",
943
+ }),
944
+ );
945
+
946
+ // Should set session status to failed
947
+ expect(updateCallSession).toHaveBeenCalledWith(
948
+ "call-unsup-verify-1",
949
+ expect.objectContaining({
950
+ status: "failed",
951
+ lastError: expect.stringContaining("preflight guard"),
952
+ }),
953
+ );
954
+
955
+ // Should NOT register a controller
956
+ expect(registerCallController).not.toHaveBeenCalled();
957
+ });
958
+
959
+ test("name_capture outcome speaks generic apology and tears down", () => {
960
+ mockRouteSetupResult = {
961
+ outcome: {
962
+ action: "name_capture",
963
+ assistantId: "self",
964
+ fromNumber: "+14155551234",
965
+ },
966
+ resolved: {
967
+ assistantId: "self",
968
+ isInbound: true,
969
+ otherPartyNumber: "+14155551234",
970
+ actorTrust: { trustClass: "unknown", memberRecord: null },
971
+ },
972
+ };
973
+
974
+ const mockWs = createMockWs();
975
+ mockSessions.set("call-unsup-name-1", {
976
+ id: "call-unsup-name-1",
977
+ conversationId: "conv-unsup-name-1",
978
+ status: "initiated",
979
+ task: null,
980
+ startedAt: null,
981
+ fromNumber: "+14155551234",
982
+ toNumber: "+15550001111",
983
+ });
984
+
985
+ const session = new MediaStreamCallSession(
986
+ mockWs.ws,
987
+ "call-unsup-name-1",
988
+ );
989
+ session.handleMessage(makeStartMessage());
990
+
991
+ // speakSystemPrompt should be called with the generic apology
992
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
993
+ expect.anything(),
994
+ expect.stringContaining("additional verification"),
995
+ );
996
+
997
+ // Should run finalization inline
998
+ expect(finalizeCall).toHaveBeenCalledWith(
999
+ "call-unsup-name-1",
1000
+ "conv-unsup-name-1",
1001
+ );
1002
+ });
1003
+
1004
+ test("callee_verification outcome fails with explicit reason", () => {
1005
+ mockRouteSetupResult = {
1006
+ outcome: {
1007
+ action: "callee_verification",
1008
+ verificationConfig: { maxAttempts: 3, codeLength: 6 },
1009
+ },
1010
+ resolved: {
1011
+ assistantId: "self",
1012
+ isInbound: false,
1013
+ otherPartyNumber: "+14155551234",
1014
+ actorTrust: { trustClass: "guardian", memberRecord: null },
1015
+ },
1016
+ };
1017
+
1018
+ const mockWs = createMockWs();
1019
+ mockSessions.set("call-unsup-callee-1", {
1020
+ id: "call-unsup-callee-1",
1021
+ conversationId: "conv-unsup-callee-1",
1022
+ status: "initiated",
1023
+ task: null,
1024
+ startedAt: null,
1025
+ fromNumber: "+15550001111",
1026
+ toNumber: "+14155551234",
1027
+ });
1028
+
1029
+ const session = new MediaStreamCallSession(
1030
+ mockWs.ws,
1031
+ "call-unsup-callee-1",
1032
+ );
1033
+ session.handleMessage(makeStartMessage());
1034
+
1035
+ // Should record the failure with the specific action
1036
+ expect(recordCallEvent).toHaveBeenCalledWith(
1037
+ "call-unsup-callee-1",
1038
+ "call_failed",
1039
+ expect.objectContaining({
1040
+ reason: expect.stringContaining("callee_verification"),
1041
+ }),
1042
+ );
1043
+
1044
+ // Session should be failed
1045
+ expect(updateCallSession).toHaveBeenCalledWith(
1046
+ "call-unsup-callee-1",
1047
+ expect.objectContaining({ status: "failed" }),
1048
+ );
1049
+ });
1050
+
1051
+ test("normal_call after deny scenario still creates controller", () => {
1052
+ // Verify that after a deny-scenario test, resetting to normal_call
1053
+ // properly creates a controller (no cross-test pollution).
1054
+ mockRouteSetupResult = {
1055
+ outcome: { action: "normal_call", isInbound: true },
1056
+ resolved: {
1057
+ assistantId: "self",
1058
+ isInbound: true,
1059
+ otherPartyNumber: "+15551234567",
1060
+ actorTrust: { trustClass: "guardian", memberRecord: null },
1061
+ },
1062
+ };
1063
+
1064
+ const mockWs = createMockWs();
1065
+ mockSessions.set("call-reset-1", {
1066
+ id: "call-reset-1",
1067
+ conversationId: "conv-reset-1",
1068
+ status: "initiated",
1069
+ task: "Test task",
1070
+ startedAt: null,
1071
+ toNumber: "+15551234567",
1072
+ });
1073
+
1074
+ const session = new MediaStreamCallSession(mockWs.ws, "call-reset-1");
1075
+ session.handleMessage(makeStartMessage());
1076
+
1077
+ // Controller should be registered for normal calls
1078
+ expect(registerCallController).toHaveBeenCalledWith(
1079
+ "call-reset-1",
1080
+ expect.anything(),
1081
+ );
1082
+
1083
+ // Initial greeting should fire
1084
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
1085
+ });
1086
+ });
1087
+
1088
+ // ── Barge-in regression ──────────────────────────────────────────
1089
+
1090
+ describe("barge-in gating", () => {
1091
+ test("immediate inbound audio after stream start does not trigger handleInterrupt", () => {
1092
+ const mockWs = createMockWs();
1093
+ mockSessions.set("call-bargein-1", {
1094
+ id: "call-bargein-1",
1095
+ conversationId: "conv-bargein-1",
1096
+ status: "initiated",
1097
+ task: null,
1098
+ startedAt: null,
1099
+ toNumber: "+15551234567",
1100
+ });
1101
+
1102
+ const session = new MediaStreamCallSession(mockWs.ws, "call-bargein-1");
1103
+
1104
+ // Stream start bootstraps the controller
1105
+ session.handleMessage(makeStartMessage());
1106
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
1107
+
1108
+ // Immediate inbound audio (speech-like payloads) — before the
1109
+ // assistant has spoken. The speech detector classifies these as
1110
+ // speech, so onSpeechStart fires and calls handleBargeIn. Since
1111
+ // the controller mock returns false (not speaking), handleInterrupt
1112
+ // should NOT be called.
1113
+ const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
1114
+ session.handleMessage(makeMediaMessage(speechPayload, "1"));
1115
+ session.handleMessage(makeMediaMessage(speechPayload, "2"));
1116
+ session.handleMessage(makeMediaMessage(speechPayload, "3"));
1117
+
1118
+ // handleBargeIn was called but returned false
1119
+ expect(mockHandleBargeIn).toHaveBeenCalled();
1120
+ expect(mockHandleInterrupt).not.toHaveBeenCalled();
1121
+
1122
+ // voice_session_aborted should NOT appear in recorded events
1123
+ const abortEvents = mockEvents.filter(
1124
+ (e) =>
1125
+ e.callSessionId === "call-bargein-1" &&
1126
+ e.eventType === "voice_session_aborted",
1127
+ );
1128
+ expect(abortEvents.length).toBe(0);
1129
+
1130
+ session.destroy();
1131
+ });
1132
+
1133
+ test("barge-in is accepted when controller is speaking", () => {
1134
+ // Configure mock to indicate the controller is speaking
1135
+ mockHandleBargeIn.mockReturnValue(true);
1136
+
1137
+ const mockWs = createMockWs();
1138
+ mockSessions.set("call-bargein-2", {
1139
+ id: "call-bargein-2",
1140
+ conversationId: "conv-bargein-2",
1141
+ status: "in_progress",
1142
+ task: null,
1143
+ startedAt: Date.now() - 5000,
1144
+ toNumber: "+15551234567",
1145
+ });
1146
+
1147
+ const session = new MediaStreamCallSession(mockWs.ws, "call-bargein-2");
1148
+ session.handleMessage(makeStartMessage());
1149
+
1150
+ // Simulate inbound speech audio while assistant is speaking.
1151
+ // Use a high-amplitude mu-law payload so speech detection triggers.
1152
+ const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
1153
+ session.handleMessage(makeMediaMessage(speechPayload, "1"));
1154
+
1155
+ // handleBargeIn should have been called (returning true)
1156
+ expect(mockHandleBargeIn).toHaveBeenCalled();
1157
+
1158
+ session.destroy();
1159
+ });
1160
+ });
1161
+
1162
+ // ── E2E regression scenario ──────────────────────────────────────
1163
+
1164
+ describe("end-to-end regression: connected call that stays active", () => {
1165
+ test("stream connects, inbound audio starts, call remains active for a turn, controller only destroyed at stop/hangup", () => {
1166
+ const mockWs = createMockWs();
1167
+ mockSessions.set("call-e2e-1", {
1168
+ id: "call-e2e-1",
1169
+ conversationId: "conv-e2e-1",
1170
+ status: "initiated",
1171
+ task: null,
1172
+ startedAt: null,
1173
+ toNumber: "+15551234567",
1174
+ });
1175
+
1176
+ const session = new MediaStreamCallSession(mockWs.ws, "call-e2e-1");
1177
+
1178
+ // 1. Stream connects — start event arrives
1179
+ session.handleMessage(makeStartMessage());
1180
+ expect(registerCallController).toHaveBeenCalledWith(
1181
+ "call-e2e-1",
1182
+ expect.anything(),
1183
+ );
1184
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
1185
+
1186
+ // Verify session was updated to in_progress
1187
+ expect(updateCallSession).toHaveBeenCalledWith(
1188
+ "call-e2e-1",
1189
+ expect.objectContaining({ status: "in_progress" }),
1190
+ );
1191
+
1192
+ // 2. Inbound audio starts immediately (controller idle — barge-in ignored)
1193
+ const payload = Buffer.from("test-audio").toString("base64");
1194
+ for (let i = 1; i <= 5; i++) {
1195
+ session.handleMessage(makeMediaMessage(payload, String(i)));
1196
+ }
1197
+
1198
+ // handleInterrupt should NOT have been called (gated barge-in)
1199
+ expect(mockHandleInterrupt).not.toHaveBeenCalled();
1200
+
1201
+ // 3. Controller is NOT destroyed yet — still active
1202
+ expect(mockDestroy).not.toHaveBeenCalled();
1203
+
1204
+ // 4. More media frames arrive (simulating ongoing call)
1205
+ for (let i = 6; i <= 10; i++) {
1206
+ session.handleMessage(makeMediaMessage(payload, String(i)));
1207
+ }
1208
+
1209
+ // Controller still not destroyed
1210
+ expect(mockDestroy).not.toHaveBeenCalled();
1211
+
1212
+ // 5. Stop event arrives — controller should be cleaned up
1213
+ // only when the session is fully destroyed
1214
+ session.handleMessage(makeStopMessage());
1215
+
1216
+ // WebSocket close triggers full teardown
1217
+ mockSessions.set("call-e2e-1", {
1218
+ ...mockSessions.get("call-e2e-1")!,
1219
+ status: "in_progress",
1220
+ startedAt: Date.now() - 30000,
1221
+ });
1222
+ session.handleTransportClosed(1000, "normal-close");
1223
+
1224
+ expect(updateCallSession).toHaveBeenCalledWith(
1225
+ "call-e2e-1",
1226
+ expect.objectContaining({ status: "completed" }),
1227
+ );
1228
+
1229
+ // Now destroy
1230
+ session.destroy();
1231
+ expect(mockDestroy).toHaveBeenCalled();
1232
+ });
1233
+ });
1234
+ });