@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
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
10
 
@@ -51,8 +57,14 @@ import { invalidateConfigCache, loadConfig } from "../config/loader.js";
51
57
  import {
52
58
  AssistantConfigSchema,
53
59
  DEFAULT_ELEVENLABS_VOICE_ID,
60
+ SttServiceSchema,
61
+ TtsServiceSchema,
62
+ VALID_TTS_SERVICE_PROVIDERS,
54
63
  } from "../config/schema.js";
64
+ import type { AssistantConfig } from "../config/types.js";
55
65
  import { _setStorePath } from "../security/encrypted-store.js";
66
+ import { listCatalogProviderIds } from "../tts/provider-catalog.js";
67
+ import { resolveTtsConfig } from "../tts/tts-config-resolver.js";
56
68
 
57
69
  // ---------------------------------------------------------------------------
58
70
  // Helpers
@@ -166,7 +178,7 @@ describe("AssistantConfigSchema", () => {
166
178
  enqueueIntervalMs: 6 * 60 * 60 * 1000,
167
179
  supersededItemRetentionMs: 30 * 24 * 60 * 60 * 1000,
168
180
  conversationRetentionDays: 0,
169
- llmRequestLogRetentionMs: 1 * 24 * 60 * 60 * 1000,
181
+ llmRequestLogRetentionMs: 1 * 60 * 60 * 1000,
170
182
  });
171
183
  });
172
184
 
@@ -177,6 +189,63 @@ describe("AssistantConfigSchema", () => {
177
189
  expect(result.success).toBe(false);
178
190
  });
179
191
 
192
+ test("accepts memory.cleanup.llmRequestLogRetentionMs at the 365-day boundary", () => {
193
+ const max = 365 * 24 * 60 * 60 * 1000;
194
+ const result = AssistantConfigSchema.safeParse({
195
+ memory: { cleanup: { llmRequestLogRetentionMs: max } },
196
+ });
197
+ expect(result.success).toBe(true);
198
+ if (result.success) {
199
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(max);
200
+ }
201
+ });
202
+
203
+ test("rejects memory.cleanup.llmRequestLogRetentionMs above 365 days", () => {
204
+ // This must match the gateway's MAX_LLM_REQUEST_LOG_RETENTION_MS. Without
205
+ // the Zod .max(), a manually edited config.json with a large value would
206
+ // be silently accepted by the daemon and then truncated by the macOS
207
+ // picker on the next PATCH — a quiet data-loss bug.
208
+ const overMax = 365 * 24 * 60 * 60 * 1000 + 1;
209
+ const result = AssistantConfigSchema.safeParse({
210
+ memory: { cleanup: { llmRequestLogRetentionMs: overMax } },
211
+ });
212
+ expect(result.success).toBe(false);
213
+ if (!result.success) {
214
+ expect(
215
+ result.error.issues.some((i) =>
216
+ i.path.includes("llmRequestLogRetentionMs"),
217
+ ),
218
+ ).toBe(true);
219
+ }
220
+ });
221
+
222
+ test("rejects negative memory.cleanup.llmRequestLogRetentionMs", () => {
223
+ const result = AssistantConfigSchema.safeParse({
224
+ memory: { cleanup: { llmRequestLogRetentionMs: -1 } },
225
+ });
226
+ expect(result.success).toBe(false);
227
+ });
228
+
229
+ test("accepts null memory.cleanup.llmRequestLogRetentionMs (keep forever)", () => {
230
+ const result = AssistantConfigSchema.safeParse({
231
+ memory: { cleanup: { llmRequestLogRetentionMs: null } },
232
+ });
233
+ expect(result.success).toBe(true);
234
+ if (result.success) {
235
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBeNull();
236
+ }
237
+ });
238
+
239
+ test("accepts memory.cleanup.llmRequestLogRetentionMs: 0 (prune immediately)", () => {
240
+ const result = AssistantConfigSchema.safeParse({
241
+ memory: { cleanup: { llmRequestLogRetentionMs: 0 } },
242
+ });
243
+ expect(result.success).toBe(true);
244
+ if (result.success) {
245
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(0);
246
+ }
247
+ });
248
+
180
249
  test("rejects invalid provider", () => {
181
250
  const result = AssistantConfigSchema.safeParse({
182
251
  services: { inference: { provider: "invalid" } },
@@ -602,8 +671,6 @@ describe("AssistantConfigSchema", () => {
602
671
  },
603
672
  voice: {
604
673
  language: "en-US",
605
- transcriptionProvider: "Deepgram",
606
- ttsProvider: "elevenlabs",
607
674
  hints: [],
608
675
  interruptSensitivity: "low",
609
676
  },
@@ -711,29 +778,8 @@ describe("AssistantConfigSchema", () => {
711
778
  test("config without calls.voice parses correctly and produces defaults", () => {
712
779
  const result = AssistantConfigSchema.parse({});
713
780
  expect(result.calls.voice.language).toBe("en-US");
714
- expect(result.calls.voice.transcriptionProvider).toBe("Deepgram");
715
- });
716
-
717
- test("elevenlabs tuning params have correct defaults", () => {
718
- const result = AssistantConfigSchema.parse({});
719
- expect(result.elevenlabs.voiceModelId).toBe("");
720
- expect(result.elevenlabs.speed).toBe(1.0);
721
- expect(result.elevenlabs.stability).toBe(0.5);
722
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
723
- });
724
-
725
- test("rejects elevenlabs.speed below 0.7", () => {
726
- const result = AssistantConfigSchema.safeParse({
727
- elevenlabs: { speed: 0.5 },
728
- });
729
- expect(result.success).toBe(false);
730
- });
731
-
732
- test("rejects elevenlabs.speed above 1.2", () => {
733
- const result = AssistantConfigSchema.safeParse({
734
- elevenlabs: { speed: 1.5 },
735
- });
736
- expect(result.success).toBe(false);
781
+ expect(result.calls.voice.hints).toEqual([]);
782
+ expect(result.calls.voice.interruptSensitivity).toBe("low");
737
783
  });
738
784
 
739
785
  test("accepts valid calls.voice overrides", () => {
@@ -741,39 +787,20 @@ describe("AssistantConfigSchema", () => {
741
787
  calls: {
742
788
  voice: {
743
789
  language: "es-ES",
744
- transcriptionProvider: "Google",
745
790
  },
746
791
  },
747
- elevenlabs: {
748
- stability: 0.8,
749
- },
750
792
  });
751
793
  expect(result.calls.voice.language).toBe("es-ES");
752
- expect(result.calls.voice.transcriptionProvider).toBe("Google");
753
- expect(result.elevenlabs.stability).toBe(0.8);
754
- // Defaults preserved for unset fields
755
- expect(result.elevenlabs.voiceModelId).toBe("");
756
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
757
- });
758
-
759
- test("rejects invalid calls.voice.transcriptionProvider", () => {
760
- const result = AssistantConfigSchema.safeParse({
761
- calls: { voice: { transcriptionProvider: "AWS" } },
762
- });
763
- expect(result.success).toBe(false);
764
- if (!result.success) {
765
- const msgs = result.error.issues.map((i) => i.message);
766
- expect(
767
- msgs.some((m) => m.includes("calls.voice.transcriptionProvider")),
768
- ).toBe(true);
769
- }
770
794
  });
771
795
 
772
- test("rejects elevenlabs.stability out of range", () => {
773
- const result = AssistantConfigSchema.safeParse({
774
- elevenlabs: { stability: 1.5 },
796
+ test("transcriptionProvider is no longer part of the voice config schema", () => {
797
+ // Zod strips unrecognized keys by default — the legacy field is silently ignored.
798
+ const result = AssistantConfigSchema.parse({
799
+ calls: { voice: { transcriptionProvider: "Google" } },
775
800
  });
776
- expect(result.success).toBe(false);
801
+ expect(
802
+ (result.calls.voice as Record<string, unknown>).transcriptionProvider,
803
+ ).toBeUndefined();
777
804
  });
778
805
 
779
806
  test("accepts optional calls.model", () => {
@@ -850,6 +877,10 @@ describe("AssistantConfigSchema", () => {
850
877
  host: "localhost",
851
878
  port: 9222,
852
879
  probeTimeoutMs: 500,
880
+ desktopAuto: {
881
+ enabled: true,
882
+ cooldownMs: 30_000,
883
+ },
853
884
  },
854
885
  });
855
886
  });
@@ -958,6 +989,410 @@ describe("AssistantConfigSchema", () => {
958
989
  });
959
990
  expect(result.success).toBe(false);
960
991
  });
992
+
993
+ // ── services.tts config ──────────────────────────────────────────────
994
+
995
+ test("applies services.tts defaults when not specified", () => {
996
+ const result = AssistantConfigSchema.parse({});
997
+ expect(result.services.tts.mode).toBe("your-own");
998
+ expect(result.services.tts.provider).toBe("elevenlabs");
999
+ expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
1000
+ DEFAULT_ELEVENLABS_VOICE_ID,
1001
+ );
1002
+ expect(result.services.tts.providers.elevenlabs.speed).toBe(1.0);
1003
+ expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
1004
+ expect(result.services.tts.providers.elevenlabs.similarityBoost).toBe(0.75);
1005
+ expect(
1006
+ result.services.tts.providers.elevenlabs.conversationTimeoutSeconds,
1007
+ ).toBe(30);
1008
+ expect(result.services.tts.providers["fish-audio"].referenceId).toBe("");
1009
+ expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
1010
+ expect(result.services.tts.providers["fish-audio"].format).toBe("mp3");
1011
+ expect(result.services.tts.providers["fish-audio"].speed).toBe(1.0);
1012
+ expect(result.services.tts.providers.deepgram.model).toBe(
1013
+ "aura-asteria-en",
1014
+ );
1015
+ expect(result.services.tts.providers.deepgram.format).toBe("mp3");
1016
+ });
1017
+
1018
+ test("accepts valid services.tts provider override", () => {
1019
+ const result = AssistantConfigSchema.parse({
1020
+ services: { tts: { provider: "fish-audio" } },
1021
+ });
1022
+ expect(result.services.tts.provider).toBe("fish-audio");
1023
+ expect(result.services.tts.mode).toBe("your-own");
1024
+ });
1025
+
1026
+ test("accepts deepgram as services.tts.provider", () => {
1027
+ const result = AssistantConfigSchema.parse({
1028
+ services: { tts: { provider: "deepgram" } },
1029
+ });
1030
+ expect(result.services.tts.provider).toBe("deepgram");
1031
+ expect(result.services.tts.mode).toBe("your-own");
1032
+ });
1033
+
1034
+ test("accepts valid services.tts.providers.elevenlabs overrides", () => {
1035
+ const result = AssistantConfigSchema.parse({
1036
+ services: {
1037
+ tts: {
1038
+ providers: {
1039
+ elevenlabs: { voiceId: "custom-voice", speed: 0.8 },
1040
+ },
1041
+ },
1042
+ },
1043
+ });
1044
+ expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
1045
+ "custom-voice",
1046
+ );
1047
+ expect(result.services.tts.providers.elevenlabs.speed).toBe(0.8);
1048
+ // Unset fields preserve defaults
1049
+ expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
1050
+ });
1051
+
1052
+ test("accepts valid services.tts.providers.fish-audio overrides", () => {
1053
+ const result = AssistantConfigSchema.parse({
1054
+ services: {
1055
+ tts: {
1056
+ providers: {
1057
+ "fish-audio": { referenceId: "my-voice", format: "wav" },
1058
+ },
1059
+ },
1060
+ },
1061
+ });
1062
+ expect(result.services.tts.providers["fish-audio"].referenceId).toBe(
1063
+ "my-voice",
1064
+ );
1065
+ expect(result.services.tts.providers["fish-audio"].format).toBe("wav");
1066
+ // Defaults preserved
1067
+ expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
1068
+ });
1069
+
1070
+ test("accepts valid services.tts.providers.deepgram overrides", () => {
1071
+ const result = AssistantConfigSchema.parse({
1072
+ services: {
1073
+ tts: {
1074
+ providers: {
1075
+ deepgram: { model: "aura-luna-en", format: "opus" },
1076
+ },
1077
+ },
1078
+ },
1079
+ });
1080
+ expect(result.services.tts.providers.deepgram.model).toBe("aura-luna-en");
1081
+ expect(result.services.tts.providers.deepgram.format).toBe("opus");
1082
+ });
1083
+
1084
+ test("rejects services.tts.mode = managed", () => {
1085
+ const result = AssistantConfigSchema.safeParse({
1086
+ services: { tts: { mode: "managed" } },
1087
+ });
1088
+ expect(result.success).toBe(false);
1089
+ if (!result.success) {
1090
+ const msgs = result.error.issues.map((i) => i.message);
1091
+ expect(
1092
+ msgs.some((m) => m.includes("your-own") || m.includes("managed")),
1093
+ ).toBe(true);
1094
+ }
1095
+ });
1096
+
1097
+ // ── hostBrowser.cdpInspect.desktopAuto config ───────────────────────
1098
+
1099
+ test("applies hostBrowser.cdpInspect.desktopAuto defaults", () => {
1100
+ const result = AssistantConfigSchema.parse({});
1101
+ expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
1102
+ enabled: true,
1103
+ cooldownMs: 30_000,
1104
+ });
1105
+ });
1106
+
1107
+ test("accepts hostBrowser.cdpInspect.desktopAuto overrides", () => {
1108
+ const result = AssistantConfigSchema.parse({
1109
+ hostBrowser: {
1110
+ cdpInspect: {
1111
+ desktopAuto: { enabled: false, cooldownMs: 10_000 },
1112
+ },
1113
+ },
1114
+ });
1115
+ expect(result.hostBrowser.cdpInspect.desktopAuto.enabled).toBe(false);
1116
+ expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(10_000);
1117
+ });
1118
+
1119
+ test("accepts hostBrowser.cdpInspect.desktopAuto.cooldownMs of 0 (disable cooldown)", () => {
1120
+ const result = AssistantConfigSchema.parse({
1121
+ hostBrowser: {
1122
+ cdpInspect: { desktopAuto: { cooldownMs: 0 } },
1123
+ },
1124
+ });
1125
+ expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(0);
1126
+ });
1127
+
1128
+ test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs below 0", () => {
1129
+ const result = AssistantConfigSchema.safeParse({
1130
+ hostBrowser: {
1131
+ cdpInspect: { desktopAuto: { cooldownMs: -1 } },
1132
+ },
1133
+ });
1134
+ expect(result.success).toBe(false);
1135
+ if (!result.success) {
1136
+ expect(
1137
+ result.error.issues.some((issue) =>
1138
+ issue.path.join(".").includes("cooldownMs"),
1139
+ ),
1140
+ ).toBe(true);
1141
+ }
1142
+ });
1143
+
1144
+ test("rejects invalid services.tts.provider", () => {
1145
+ const result = AssistantConfigSchema.safeParse({
1146
+ services: { tts: { provider: "aws-polly" } },
1147
+ });
1148
+ expect(result.success).toBe(false);
1149
+ if (!result.success) {
1150
+ const msgs = result.error.issues.map((i) => i.message);
1151
+ expect(msgs.some((m) => m.includes("services.tts.provider"))).toBe(true);
1152
+ }
1153
+ });
1154
+
1155
+ test("services.tts.mode only accepts your-own as literal", () => {
1156
+ // Explicit your-own should work
1157
+ const valid = TtsServiceSchema.safeParse({ mode: "your-own" });
1158
+ expect(valid.success).toBe(true);
1159
+
1160
+ // managed should be rejected
1161
+ const invalid = TtsServiceSchema.safeParse({ mode: "managed" });
1162
+ expect(invalid.success).toBe(false);
1163
+
1164
+ // Any other string should be rejected
1165
+ const invalid2 = TtsServiceSchema.safeParse({ mode: "self-hosted" });
1166
+ expect(invalid2.success).toBe(false);
1167
+ });
1168
+
1169
+ // ── services.stt config ──────────────────────────────────────────────
1170
+
1171
+ test("rejects services.stt without explicit provider", () => {
1172
+ const result = AssistantConfigSchema.safeParse({
1173
+ services: { stt: { mode: "your-own" } },
1174
+ });
1175
+ expect(result.success).toBe(false);
1176
+ if (!result.success) {
1177
+ expect(
1178
+ result.error.issues.some((i) => i.path.join(".").includes("provider")),
1179
+ ).toBe(true);
1180
+ }
1181
+ });
1182
+
1183
+ test("applies services.stt structural defaults when provider is explicit", () => {
1184
+ const result = AssistantConfigSchema.parse({
1185
+ services: { stt: { provider: "openai-whisper" } },
1186
+ });
1187
+ expect(result.services.stt.mode).toBe("your-own");
1188
+ expect(result.services.stt.provider).toBe("openai-whisper");
1189
+ // providers defaults to empty sparse map
1190
+ expect(result.services.stt.providers).toEqual({});
1191
+ });
1192
+
1193
+ test("accepts valid services.stt provider override", () => {
1194
+ const result = AssistantConfigSchema.parse({
1195
+ services: { stt: { provider: "openai-whisper" } },
1196
+ });
1197
+ expect(result.services.stt.provider).toBe("openai-whisper");
1198
+ expect(result.services.stt.mode).toBe("your-own");
1199
+ });
1200
+
1201
+ test("accepts valid services.stt.providers.openai-whisper overrides", () => {
1202
+ const result = AssistantConfigSchema.parse({
1203
+ services: {
1204
+ stt: {
1205
+ provider: "openai-whisper",
1206
+ providers: {
1207
+ "openai-whisper": {},
1208
+ },
1209
+ },
1210
+ },
1211
+ });
1212
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1213
+ });
1214
+
1215
+ test("parses when providers map is empty (sparse default)", () => {
1216
+ const result = AssistantConfigSchema.parse({
1217
+ services: { stt: { provider: "deepgram", providers: {} } },
1218
+ });
1219
+ expect(result.services.stt.providers).toEqual({});
1220
+ expect(result.services.stt.provider).toBe("deepgram");
1221
+ });
1222
+
1223
+ test("parses when unknown future provider blobs exist under providers", () => {
1224
+ const result = AssistantConfigSchema.parse({
1225
+ services: {
1226
+ stt: {
1227
+ provider: "openai-whisper",
1228
+ providers: {
1229
+ "openai-whisper": {},
1230
+ "future-provider": { model: "next-gen", lang: "en" },
1231
+ },
1232
+ },
1233
+ },
1234
+ });
1235
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1236
+ expect(result.services.stt.providers["future-provider"]).toEqual({
1237
+ model: "next-gen",
1238
+ lang: "en",
1239
+ });
1240
+ });
1241
+
1242
+ test("rejects services.stt.mode = managed", () => {
1243
+ const result = AssistantConfigSchema.safeParse({
1244
+ services: { stt: { mode: "managed" } },
1245
+ });
1246
+ expect(result.success).toBe(false);
1247
+ if (!result.success) {
1248
+ const msgs = result.error.issues.map((i) => i.message);
1249
+ expect(
1250
+ msgs.some((m) => m.includes("your-own") || m.includes("managed")),
1251
+ ).toBe(true);
1252
+ }
1253
+ });
1254
+
1255
+ test("rejects invalid services.stt.provider", () => {
1256
+ const result = AssistantConfigSchema.safeParse({
1257
+ services: { stt: { provider: "azure-speech" } },
1258
+ });
1259
+ expect(result.success).toBe(false);
1260
+ if (!result.success) {
1261
+ const msgs = result.error.issues.map((i) => i.message);
1262
+ expect(msgs.some((m) => m.includes("services.stt.provider"))).toBe(true);
1263
+ }
1264
+ });
1265
+
1266
+ test("accepts deepgram as services.stt.provider", () => {
1267
+ const result = AssistantConfigSchema.parse({
1268
+ services: { stt: { provider: "deepgram" } },
1269
+ });
1270
+ expect(result.services.stt.provider).toBe("deepgram");
1271
+ expect(result.services.stt.mode).toBe("your-own");
1272
+ });
1273
+
1274
+ test("accepts google-gemini as services.stt.provider", () => {
1275
+ const result = AssistantConfigSchema.parse({
1276
+ services: { stt: { provider: "google-gemini" } },
1277
+ });
1278
+ expect(result.services.stt.provider).toBe("google-gemini");
1279
+ expect(result.services.stt.mode).toBe("your-own");
1280
+ });
1281
+
1282
+ test("applies services.stt structural defaults when google-gemini provider is explicit", () => {
1283
+ const result = AssistantConfigSchema.parse({
1284
+ services: { stt: { provider: "google-gemini" } },
1285
+ });
1286
+ expect(result.services.stt.mode).toBe("your-own");
1287
+ expect(result.services.stt.provider).toBe("google-gemini");
1288
+ expect(result.services.stt.providers).toEqual({});
1289
+ });
1290
+
1291
+ test("accepts valid services.stt.providers.deepgram overrides", () => {
1292
+ const result = AssistantConfigSchema.parse({
1293
+ services: {
1294
+ stt: {
1295
+ provider: "deepgram",
1296
+ providers: {
1297
+ deepgram: {},
1298
+ },
1299
+ },
1300
+ },
1301
+ });
1302
+ expect(result.services.stt.providers.deepgram).toEqual({});
1303
+ });
1304
+
1305
+ test("existing configs with explicit per-provider objects continue to parse", () => {
1306
+ // Configs with explicit per-provider objects must continue to
1307
+ // parse and round-trip successfully.
1308
+ const result = AssistantConfigSchema.parse({
1309
+ services: {
1310
+ stt: {
1311
+ provider: "openai-whisper",
1312
+ providers: {
1313
+ "openai-whisper": {},
1314
+ deepgram: {},
1315
+ },
1316
+ },
1317
+ },
1318
+ });
1319
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1320
+ expect(result.services.stt.providers.deepgram).toEqual({});
1321
+ });
1322
+
1323
+ test("services.stt.provider is required (no implicit default)", () => {
1324
+ const result = AssistantConfigSchema.safeParse({
1325
+ services: { stt: {} },
1326
+ });
1327
+ expect(result.success).toBe(false);
1328
+ });
1329
+
1330
+ test("services.stt.mode only accepts your-own as literal", () => {
1331
+ // Explicit your-own should work
1332
+ const valid = SttServiceSchema.safeParse({
1333
+ mode: "your-own",
1334
+ provider: "openai-whisper",
1335
+ });
1336
+ expect(valid.success).toBe(true);
1337
+
1338
+ // managed should be rejected
1339
+ const invalid = SttServiceSchema.safeParse({
1340
+ mode: "managed",
1341
+ provider: "openai-whisper",
1342
+ });
1343
+ expect(invalid.success).toBe(false);
1344
+
1345
+ // Any other string should be rejected
1346
+ const invalid2 = SttServiceSchema.safeParse({
1347
+ mode: "self-hosted",
1348
+ provider: "openai-whisper",
1349
+ });
1350
+ expect(invalid2.success).toBe(false);
1351
+ });
1352
+
1353
+ test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs above 300000", () => {
1354
+ const result = AssistantConfigSchema.safeParse({
1355
+ hostBrowser: {
1356
+ cdpInspect: { desktopAuto: { cooldownMs: 500_000 } },
1357
+ },
1358
+ });
1359
+ expect(result.success).toBe(false);
1360
+ if (!result.success) {
1361
+ expect(
1362
+ result.error.issues.some((issue) =>
1363
+ issue.path.join(".").includes("cooldownMs"),
1364
+ ),
1365
+ ).toBe(true);
1366
+ }
1367
+ });
1368
+
1369
+ test("rejects non-integer hostBrowser.cdpInspect.desktopAuto.cooldownMs", () => {
1370
+ const result = AssistantConfigSchema.safeParse({
1371
+ hostBrowser: {
1372
+ cdpInspect: { desktopAuto: { cooldownMs: 5000.5 } },
1373
+ },
1374
+ });
1375
+ expect(result.success).toBe(false);
1376
+ });
1377
+
1378
+ test("rejects non-boolean hostBrowser.cdpInspect.desktopAuto.enabled", () => {
1379
+ const result = AssistantConfigSchema.safeParse({
1380
+ hostBrowser: {
1381
+ cdpInspect: { desktopAuto: { enabled: "yes" } },
1382
+ },
1383
+ });
1384
+ expect(result.success).toBe(false);
1385
+ });
1386
+
1387
+ test("desktopAuto defaults preserved when only cdpInspect.enabled is set", () => {
1388
+ const result = AssistantConfigSchema.parse({
1389
+ hostBrowser: { cdpInspect: { enabled: true } },
1390
+ });
1391
+ expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
1392
+ enabled: true,
1393
+ cooldownMs: 30_000,
1394
+ });
1395
+ });
961
1396
  });
962
1397
 
963
1398
  // ---------------------------------------------------------------------------
@@ -969,12 +1404,15 @@ describe("resolveVoiceQualityProfile", () => {
969
1404
  const config = AssistantConfigSchema.parse({});
970
1405
  const profile = resolveVoiceQualityProfile(config);
971
1406
  expect(profile.ttsProvider).toBe("ElevenLabs");
972
- expect(profile.transcriptionProvider).toBe("Deepgram");
973
1407
  });
974
1408
 
975
- test("uses shared elevenlabs.voiceId for voice", () => {
1409
+ test("uses services.tts.providers.elevenlabs.voiceId for voice", () => {
976
1410
  const config = AssistantConfigSchema.parse({
977
- elevenlabs: { voiceId: "test-voice-id" },
1411
+ services: {
1412
+ tts: {
1413
+ providers: { elevenlabs: { voiceId: "test-voice-id" } },
1414
+ },
1415
+ },
978
1416
  });
979
1417
  const profile = resolveVoiceQualityProfile(config);
980
1418
  expect(profile.ttsProvider).toBe("ElevenLabs");
@@ -987,14 +1425,20 @@ describe("resolveVoiceQualityProfile", () => {
987
1425
  expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
988
1426
  });
989
1427
 
990
- test("applies voice tuning params from elevenlabs config", () => {
1428
+ test("applies voice tuning params from services.tts.providers.elevenlabs config", () => {
991
1429
  const config = AssistantConfigSchema.parse({
992
- elevenlabs: {
993
- voiceId: "abc123",
994
- voiceModelId: "turbo_v2_5",
995
- speed: 0.9,
996
- stability: 0.8,
997
- similarityBoost: 0.9,
1430
+ services: {
1431
+ tts: {
1432
+ providers: {
1433
+ elevenlabs: {
1434
+ voiceId: "abc123",
1435
+ voiceModelId: "turbo_v2_5",
1436
+ speed: 0.9,
1437
+ stability: 0.8,
1438
+ similarityBoost: 0.9,
1439
+ },
1440
+ },
1441
+ },
998
1442
  },
999
1443
  });
1000
1444
  const profile = resolveVoiceQualityProfile(config);
@@ -1042,13 +1486,424 @@ describe("buildElevenLabsVoiceSpec", () => {
1042
1486
 
1043
1487
  test("default config uses a bare voiceId when no model override is set", () => {
1044
1488
  const config = AssistantConfigSchema.parse({
1045
- elevenlabs: { voiceId: "test" },
1489
+ services: {
1490
+ tts: {
1491
+ providers: { elevenlabs: { voiceId: "test" } },
1492
+ },
1493
+ },
1046
1494
  });
1047
- const spec = buildElevenLabsVoiceSpec(config.elevenlabs);
1495
+ const spec = buildElevenLabsVoiceSpec(
1496
+ config.services.tts.providers.elevenlabs,
1497
+ );
1048
1498
  expect(spec).toBe("test");
1049
1499
  });
1050
1500
  });
1051
1501
 
1502
+ // ---------------------------------------------------------------------------
1503
+ // Tests: TTS config resolver
1504
+ // ---------------------------------------------------------------------------
1505
+
1506
+ describe("resolveTtsConfig", () => {
1507
+ test("returns default provider and config from empty config", () => {
1508
+ const config = AssistantConfigSchema.parse({});
1509
+ const resolved = resolveTtsConfig(config);
1510
+ expect(resolved.provider).toBe("elevenlabs");
1511
+ expect(resolved.providerConfig).toMatchObject({
1512
+ voiceId: DEFAULT_ELEVENLABS_VOICE_ID,
1513
+ speed: 1.0,
1514
+ stability: 0.5,
1515
+ similarityBoost: 0.75,
1516
+ });
1517
+ });
1518
+
1519
+ test("uses canonical services.tts.provider when set", () => {
1520
+ const config = AssistantConfigSchema.parse({
1521
+ services: { tts: { provider: "fish-audio" } },
1522
+ });
1523
+ const resolved = resolveTtsConfig(config);
1524
+ expect(resolved.provider).toBe("fish-audio");
1525
+ expect(resolved.providerConfig).toMatchObject({
1526
+ referenceId: "",
1527
+ chunkLength: 200,
1528
+ format: "mp3",
1529
+ speed: 1.0,
1530
+ });
1531
+ });
1532
+
1533
+ test("returns canonical elevenlabs config from services.tts.providers", () => {
1534
+ const config = AssistantConfigSchema.parse({
1535
+ services: {
1536
+ tts: {
1537
+ provider: "elevenlabs",
1538
+ providers: {
1539
+ elevenlabs: { voiceId: "canonical-voice", stability: 0.9 },
1540
+ },
1541
+ },
1542
+ },
1543
+ });
1544
+ const resolved = resolveTtsConfig(config);
1545
+ expect(resolved.provider).toBe("elevenlabs");
1546
+ expect(resolved.providerConfig).toMatchObject({
1547
+ voiceId: "canonical-voice",
1548
+ stability: 0.9,
1549
+ });
1550
+ });
1551
+
1552
+ test("uses canonical elevenlabs config exclusively (no legacy fallback)", () => {
1553
+ const config = AssistantConfigSchema.parse({
1554
+ services: {
1555
+ tts: {
1556
+ providers: {
1557
+ elevenlabs: { voiceId: "canonical-voice", speed: 0.9 },
1558
+ },
1559
+ },
1560
+ },
1561
+ });
1562
+ const resolved = resolveTtsConfig(config);
1563
+ expect(resolved.provider).toBe("elevenlabs");
1564
+ expect(resolved.providerConfig).toMatchObject({
1565
+ voiceId: "canonical-voice",
1566
+ speed: 0.9,
1567
+ });
1568
+ });
1569
+
1570
+ test("uses canonical fish-audio config exclusively (no legacy fallback)", () => {
1571
+ const config = AssistantConfigSchema.parse({
1572
+ services: {
1573
+ tts: {
1574
+ provider: "fish-audio",
1575
+ providers: {
1576
+ "fish-audio": { referenceId: "canonical-ref", format: "wav" },
1577
+ },
1578
+ },
1579
+ },
1580
+ });
1581
+ const resolved = resolveTtsConfig(config);
1582
+ expect(resolved.provider).toBe("fish-audio");
1583
+ expect(resolved.providerConfig).toMatchObject({
1584
+ referenceId: "canonical-ref",
1585
+ format: "wav",
1586
+ });
1587
+ });
1588
+
1589
+ test("returns empty config for unknown provider", () => {
1590
+ // Force an unknown provider via type assertion for coverage.
1591
+ // structuredClone prevents mutation from leaking into Zod's shared
1592
+ // default objects (Zod 4 stores defaults by reference).
1593
+ const config = structuredClone(
1594
+ AssistantConfigSchema.parse({}),
1595
+ ) as AssistantConfig;
1596
+ (config.services.tts as { provider: string }).provider = "aws-polly";
1597
+ const resolved = resolveTtsConfig(config);
1598
+ expect(resolved.provider).toBe("aws-polly");
1599
+ expect(resolved.providerConfig).toEqual({});
1600
+ });
1601
+
1602
+ test("unknown provider resolution is deterministic across repeated calls", () => {
1603
+ const config = structuredClone(
1604
+ AssistantConfigSchema.parse({}),
1605
+ ) as AssistantConfig;
1606
+ (config.services.tts as { provider: string }).provider = "nonexistent";
1607
+ const first = resolveTtsConfig(config);
1608
+ const second = resolveTtsConfig(config);
1609
+ expect(first).toEqual(second);
1610
+ expect(first.providerConfig).toEqual({});
1611
+ });
1612
+ });
1613
+
1614
+ // ---------------------------------------------------------------------------
1615
+ // Tests: TTS provider catalog integration
1616
+ // ---------------------------------------------------------------------------
1617
+
1618
+ describe("TTS provider catalog integration", () => {
1619
+ test("VALID_TTS_SERVICE_PROVIDERS matches catalog provider IDs", () => {
1620
+ const catalogIds = listCatalogProviderIds();
1621
+ expect([...VALID_TTS_SERVICE_PROVIDERS]).toEqual(catalogIds);
1622
+ });
1623
+
1624
+ test("schema accepts all catalog provider IDs as services.tts.provider", () => {
1625
+ for (const providerId of listCatalogProviderIds()) {
1626
+ const result = AssistantConfigSchema.safeParse({
1627
+ services: { tts: { provider: providerId } },
1628
+ });
1629
+ expect(result.success).toBe(true);
1630
+ if (result.success) {
1631
+ expect(result.data.services.tts.provider).toBe(providerId);
1632
+ }
1633
+ }
1634
+ });
1635
+
1636
+ test("TtsProvidersSchema has a key for every catalog provider", () => {
1637
+ const parsed = AssistantConfigSchema.parse({});
1638
+ const providerKeys = Object.keys(parsed.services.tts.providers);
1639
+ for (const providerId of listCatalogProviderIds()) {
1640
+ expect(providerKeys).toContain(providerId);
1641
+ }
1642
+ });
1643
+
1644
+ test("resolveTtsConfig returns correct defaults for each catalog provider", () => {
1645
+ for (const providerId of listCatalogProviderIds()) {
1646
+ const config = AssistantConfigSchema.parse({
1647
+ services: { tts: { provider: providerId } },
1648
+ });
1649
+ const resolved = resolveTtsConfig(config);
1650
+ expect(resolved.provider).toBe(providerId);
1651
+ // Every catalog provider should resolve to a non-empty config object
1652
+ expect(Object.keys(resolved.providerConfig).length).toBeGreaterThan(0);
1653
+ }
1654
+ });
1655
+
1656
+ test("resolveTtsConfig returns overridden values for elevenlabs", () => {
1657
+ const config = AssistantConfigSchema.parse({
1658
+ services: {
1659
+ tts: {
1660
+ provider: "elevenlabs",
1661
+ providers: {
1662
+ elevenlabs: { voiceId: "override-voice", speed: 0.7 },
1663
+ },
1664
+ },
1665
+ },
1666
+ });
1667
+ const resolved = resolveTtsConfig(config);
1668
+ expect(resolved.provider).toBe("elevenlabs");
1669
+ expect(resolved.providerConfig).toMatchObject({
1670
+ voiceId: "override-voice",
1671
+ speed: 0.7,
1672
+ // Defaults still present for unset fields
1673
+ stability: 0.5,
1674
+ similarityBoost: 0.75,
1675
+ });
1676
+ });
1677
+
1678
+ test("resolveTtsConfig returns overridden values for fish-audio", () => {
1679
+ const config = AssistantConfigSchema.parse({
1680
+ services: {
1681
+ tts: {
1682
+ provider: "fish-audio",
1683
+ providers: {
1684
+ "fish-audio": {
1685
+ referenceId: "override-ref",
1686
+ format: "opus",
1687
+ speed: 1.5,
1688
+ },
1689
+ },
1690
+ },
1691
+ },
1692
+ });
1693
+ const resolved = resolveTtsConfig(config);
1694
+ expect(resolved.provider).toBe("fish-audio");
1695
+ expect(resolved.providerConfig).toMatchObject({
1696
+ referenceId: "override-ref",
1697
+ format: "opus",
1698
+ speed: 1.5,
1699
+ // Defaults for unset fields
1700
+ chunkLength: 200,
1701
+ });
1702
+ });
1703
+ });
1704
+
1705
+ // ---------------------------------------------------------------------------
1706
+ // Tests: TTS migration 032
1707
+ // ---------------------------------------------------------------------------
1708
+
1709
+ describe("032-tts-provider-unification migration", () => {
1710
+ const migrationDir = join(WORKSPACE_DIR, "_mig032");
1711
+
1712
+ beforeEach(() => {
1713
+ if (existsSync(migrationDir)) {
1714
+ rmSync(migrationDir, { recursive: true, force: true });
1715
+ }
1716
+ mkdirSync(migrationDir, { recursive: true });
1717
+ });
1718
+
1719
+ afterEach(() => {
1720
+ if (existsSync(migrationDir)) {
1721
+ rmSync(migrationDir, { recursive: true, force: true });
1722
+ }
1723
+ });
1724
+
1725
+ function writeMigConfig(obj: unknown): void {
1726
+ writeFileSync(
1727
+ join(migrationDir, "config.json"),
1728
+ JSON.stringify(obj, null, 2),
1729
+ );
1730
+ }
1731
+
1732
+ function readMigConfig(): Record<string, unknown> {
1733
+ return JSON.parse(
1734
+ readFileSync(join(migrationDir, "config.json"), "utf-8"),
1735
+ ) as Record<string, unknown>;
1736
+ }
1737
+
1738
+ test("backfills provider from calls.voice.ttsProvider", async () => {
1739
+ writeMigConfig({
1740
+ calls: { voice: { ttsProvider: "fish-audio" } },
1741
+ });
1742
+ const { ttsProviderUnificationMigration } =
1743
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1744
+ await ttsProviderUnificationMigration.run(migrationDir);
1745
+ const result = readMigConfig();
1746
+ const tts = (result.services as Record<string, unknown>).tts as Record<
1747
+ string,
1748
+ unknown
1749
+ >;
1750
+ expect(tts.provider).toBe("fish-audio");
1751
+ expect(tts.mode).toBe("your-own");
1752
+ });
1753
+
1754
+ test("backfills elevenlabs provider config from legacy keys", async () => {
1755
+ writeMigConfig({
1756
+ calls: { voice: { ttsProvider: "elevenlabs" } },
1757
+ elevenlabs: { voiceId: "my-voice", speed: 0.8 },
1758
+ });
1759
+ const { ttsProviderUnificationMigration } =
1760
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1761
+ await ttsProviderUnificationMigration.run(migrationDir);
1762
+ const result = readMigConfig();
1763
+ const tts = (result.services as Record<string, unknown>).tts as Record<
1764
+ string,
1765
+ unknown
1766
+ >;
1767
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
1768
+ expect(providers.elevenlabs.voiceId).toBe("my-voice");
1769
+ expect(providers.elevenlabs.speed).toBe(0.8);
1770
+ });
1771
+
1772
+ test("backfills fish-audio provider config from legacy keys", async () => {
1773
+ writeMigConfig({
1774
+ calls: { voice: { ttsProvider: "fish-audio" } },
1775
+ fishAudio: { referenceId: "my-ref", format: "wav" },
1776
+ });
1777
+ const { ttsProviderUnificationMigration } =
1778
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1779
+ await ttsProviderUnificationMigration.run(migrationDir);
1780
+ const result = readMigConfig();
1781
+ const tts = (result.services as Record<string, unknown>).tts as Record<
1782
+ string,
1783
+ unknown
1784
+ >;
1785
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
1786
+ expect(providers["fish-audio"].referenceId).toBe("my-ref");
1787
+ expect(providers["fish-audio"].format).toBe("wav");
1788
+ });
1789
+
1790
+ test("removes legacy fields after migration", async () => {
1791
+ writeMigConfig({
1792
+ calls: { voice: { ttsProvider: "elevenlabs", language: "en-US" } },
1793
+ elevenlabs: { voiceId: "my-voice" },
1794
+ });
1795
+ const { ttsProviderUnificationMigration } =
1796
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1797
+ await ttsProviderUnificationMigration.run(migrationDir);
1798
+ const result = readMigConfig();
1799
+ // Legacy keys removed
1800
+ expect(
1801
+ (
1802
+ (result.calls as Record<string, unknown>).voice as Record<
1803
+ string,
1804
+ unknown
1805
+ >
1806
+ ).ttsProvider,
1807
+ ).toBeUndefined();
1808
+ expect(result.elevenlabs).toBeUndefined();
1809
+ // Other voice fields preserved
1810
+ expect(
1811
+ (
1812
+ (result.calls as Record<string, unknown>).voice as Record<
1813
+ string,
1814
+ unknown
1815
+ >
1816
+ ).language,
1817
+ ).toBe("en-US");
1818
+ });
1819
+
1820
+ test("is idempotent — repeated runs produce no changes", async () => {
1821
+ writeMigConfig({
1822
+ calls: { voice: { ttsProvider: "fish-audio" } },
1823
+ fishAudio: { referenceId: "my-ref" },
1824
+ });
1825
+ const { ttsProviderUnificationMigration } =
1826
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1827
+ await ttsProviderUnificationMigration.run(migrationDir);
1828
+ const afterFirst = readMigConfig();
1829
+ await ttsProviderUnificationMigration.run(migrationDir);
1830
+ const afterSecond = readMigConfig();
1831
+ expect(afterSecond).toEqual(afterFirst);
1832
+ });
1833
+
1834
+ test("does not overwrite existing services.tts.provider", async () => {
1835
+ writeMigConfig({
1836
+ services: { tts: { provider: "elevenlabs" } },
1837
+ calls: { voice: { ttsProvider: "fish-audio" } },
1838
+ });
1839
+ const { ttsProviderUnificationMigration } =
1840
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1841
+ await ttsProviderUnificationMigration.run(migrationDir);
1842
+ const result = readMigConfig();
1843
+ const tts = (result.services as Record<string, unknown>).tts as Record<
1844
+ string,
1845
+ unknown
1846
+ >;
1847
+ // Should keep the existing canonical value, not the legacy one
1848
+ expect(tts.provider).toBe("elevenlabs");
1849
+ });
1850
+
1851
+ test("does not overwrite existing canonical provider config keys", async () => {
1852
+ writeMigConfig({
1853
+ services: {
1854
+ tts: {
1855
+ providers: {
1856
+ elevenlabs: { voiceId: "canonical-voice" },
1857
+ },
1858
+ },
1859
+ },
1860
+ elevenlabs: { voiceId: "legacy-voice", speed: 0.8 },
1861
+ });
1862
+ const { ttsProviderUnificationMigration } =
1863
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1864
+ await ttsProviderUnificationMigration.run(migrationDir);
1865
+ const result = readMigConfig();
1866
+ const tts = (result.services as Record<string, unknown>).tts as Record<
1867
+ string,
1868
+ unknown
1869
+ >;
1870
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
1871
+ // Canonical voiceId preserved, legacy speed backfilled
1872
+ expect(providers.elevenlabs.voiceId).toBe("canonical-voice");
1873
+ expect(providers.elevenlabs.speed).toBe(0.8);
1874
+ // Legacy top-level key removed
1875
+ expect(result.elevenlabs).toBeUndefined();
1876
+ });
1877
+
1878
+ test("skips config without any legacy TTS fields", async () => {
1879
+ writeMigConfig({ maxTokens: 4096 });
1880
+ const { ttsProviderUnificationMigration } =
1881
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1882
+ const before = readMigConfig();
1883
+ await ttsProviderUnificationMigration.run(migrationDir);
1884
+ const after = readMigConfig();
1885
+ // Should remain unchanged (no services.tts added)
1886
+ expect(after).toEqual(before);
1887
+ });
1888
+
1889
+ test("down removes services.tts from config", async () => {
1890
+ writeMigConfig({
1891
+ services: {
1892
+ inference: { provider: "anthropic" },
1893
+ tts: { provider: "elevenlabs", mode: "your-own" },
1894
+ },
1895
+ });
1896
+ const { ttsProviderUnificationMigration } =
1897
+ await import("../workspace/migrations/032-tts-provider-unification.js");
1898
+ await ttsProviderUnificationMigration.down(migrationDir);
1899
+ const result = readMigConfig();
1900
+ const services = result.services as Record<string, unknown>;
1901
+ expect(services.tts).toBeUndefined();
1902
+ // Other services keys preserved
1903
+ expect(services.inference).toBeDefined();
1904
+ });
1905
+ });
1906
+
1052
1907
  // ---------------------------------------------------------------------------
1053
1908
  // Tests: loader integration (config file -> loadConfig with fallback)
1054
1909
  // ---------------------------------------------------------------------------
@@ -1244,6 +2099,93 @@ describe("loadConfig with schema validation", () => {
1244
2099
  expect(config.calls.provider).toBe("twilio");
1245
2100
  });
1246
2101
 
2102
+ test("recovers from partial filing.activeHours without wiping unrelated fields", () => {
2103
+ // Only activeHoursStart is set. The superRefine must emit the issue so
2104
+ // the loader's delete-and-retry can strip the set field; otherwise the
2105
+ // mismatch persists and the config falls back to full defaults (which
2106
+ // would reset maxTokens below to 64000).
2107
+ writeConfig({
2108
+ maxTokens: 4096,
2109
+ filing: { activeHoursStart: 8 },
2110
+ });
2111
+ const config = loadConfig();
2112
+ expect(config.maxTokens).toBe(4096);
2113
+ expect(config.filing.activeHoursStart).toBeNull();
2114
+ expect(config.filing.activeHoursEnd).toBeNull();
2115
+ });
2116
+
2117
+ test("recovers from partial heartbeat.activeHours without wiping unrelated fields", () => {
2118
+ // activeHoursStart is explicitly nulled while activeHoursEnd defaults to
2119
+ // 22 — a mismatch. Dual-emit strips both sides; both defaults restore
2120
+ // (8, 22). maxTokens is unaffected.
2121
+ writeConfig({
2122
+ maxTokens: 4096,
2123
+ heartbeat: { activeHoursStart: null },
2124
+ });
2125
+ const config = loadConfig();
2126
+ expect(config.maxTokens).toBe(4096);
2127
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2128
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2129
+ });
2130
+
2131
+ test("recovers from heartbeat.activeHours null-mismatch where explicit value equals opposite default", () => {
2132
+ // { start: null, end: 8 } — single-emit on the null side would strip
2133
+ // start, the default 8 would restore it, and the equal-hours check would
2134
+ // fire, cascading to a full defaults reset that wipes maxTokens.
2135
+ // Dual-emit strips both sides in one pass.
2136
+ writeConfig({
2137
+ maxTokens: 4096,
2138
+ heartbeat: { activeHoursStart: null, activeHoursEnd: 8 },
2139
+ });
2140
+ const config = loadConfig();
2141
+ expect(config.maxTokens).toBe(4096);
2142
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2143
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2144
+ });
2145
+
2146
+ test("recovers from heartbeat.activeHours null-mismatch on the end side", () => {
2147
+ // { start: 22, end: null } — same cascade class as above, mirrored.
2148
+ writeConfig({
2149
+ maxTokens: 4096,
2150
+ heartbeat: { activeHoursStart: 22, activeHoursEnd: null },
2151
+ });
2152
+ const config = loadConfig();
2153
+ expect(config.maxTokens).toBe(4096);
2154
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2155
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2156
+ });
2157
+
2158
+ test("recovers from equal heartbeat.activeHours without wiping unrelated fields", () => {
2159
+ // { start: 22, end: 22 } — both equal to the default for end. Single-emit
2160
+ // on one path would strip one side, the default would recreate the
2161
+ // equal-hours mismatch, and the loader would fall back to full defaults,
2162
+ // wiping maxTokens. Dual-emit strips both sides at once.
2163
+ writeConfig({
2164
+ maxTokens: 4096,
2165
+ heartbeat: { activeHoursStart: 22, activeHoursEnd: 22 },
2166
+ });
2167
+ const config = loadConfig();
2168
+ expect(config.maxTokens).toBe(4096);
2169
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2170
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2171
+ });
2172
+
2173
+ test("recovers from equal filing.activeHours without wiping unrelated fields", () => {
2174
+ // activeHoursStart === activeHoursEnd is invalid (empty window). Filing's
2175
+ // defaults are null/null, so single-emit on one path would strip one side
2176
+ // and the null default would recreate a mismatch — cascading to a full
2177
+ // defaults reset that wipes maxTokens. Dual-emit strips both sides so
2178
+ // both defaults restore to null.
2179
+ writeConfig({
2180
+ maxTokens: 1234,
2181
+ filing: { activeHoursStart: 5, activeHoursEnd: 5 },
2182
+ });
2183
+ const config = loadConfig();
2184
+ expect(config.maxTokens).toBe(1234);
2185
+ expect(config.filing.activeHoursStart).toBeNull();
2186
+ expect(config.filing.activeHoursEnd).toBeNull();
2187
+ });
2188
+
1247
2189
  test("applies calls defaults when not specified", () => {
1248
2190
  writeConfig({});
1249
2191
  const config = loadConfig();
@@ -1253,7 +2195,12 @@ describe("loadConfig with schema validation", () => {
1253
2195
  expect(config.calls.disclosure.enabled).toBe(true);
1254
2196
  expect(config.calls.safety.denyCategories).toEqual([]);
1255
2197
  expect(config.calls.voice.language).toBe("en-US");
1256
- expect(config.calls.voice.transcriptionProvider).toBe("Deepgram");
2198
+ expect(
2199
+ (config.calls.voice as Record<string, unknown>).transcriptionProvider,
2200
+ ).toBeUndefined();
2201
+ expect(
2202
+ (config.calls.voice as Record<string, unknown>).ttsProvider,
2203
+ ).toBeUndefined();
1257
2204
  expect(config.calls.model).toBeUndefined();
1258
2205
  expect(config.calls.callerIdentity).toEqual({
1259
2206
  allowPerCallOverride: true,