@vellumai/assistant 0.6.2 → 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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -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
@@ -81,7 +93,7 @@ describe("AssistantConfigSchema", () => {
81
93
  "inference-provider-native",
82
94
  );
83
95
  expect(result.services["web-search"].mode).toBe("your-own");
84
- expect(result.maxTokens).toBe(16000);
96
+ expect(result.maxTokens).toBe(64000);
85
97
  expect(result.thinking).toEqual({
86
98
  enabled: true,
87
99
  streamThinking: true,
@@ -107,9 +119,6 @@ describe("AssistantConfigSchema", () => {
107
119
  toolExecutionTimeoutSec: 120,
108
120
  providerStreamTimeoutSec: 1800,
109
121
  });
110
- expect(result.sandbox).toEqual({
111
- enabled: false,
112
- });
113
122
  expect(result.rateLimit).toEqual({
114
123
  maxRequestsPerMinute: 0,
115
124
  });
@@ -169,7 +178,7 @@ describe("AssistantConfigSchema", () => {
169
178
  enqueueIntervalMs: 6 * 60 * 60 * 1000,
170
179
  supersededItemRetentionMs: 30 * 24 * 60 * 60 * 1000,
171
180
  conversationRetentionDays: 0,
172
- llmRequestLogRetentionMs: 7 * 24 * 60 * 60 * 1000,
181
+ llmRequestLogRetentionMs: 1 * 60 * 60 * 1000,
173
182
  });
174
183
  });
175
184
 
@@ -180,6 +189,63 @@ describe("AssistantConfigSchema", () => {
180
189
  expect(result.success).toBe(false);
181
190
  });
182
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
+
183
249
  test("rejects invalid provider", () => {
184
250
  const result = AssistantConfigSchema.safeParse({
185
251
  services: { inference: { provider: "invalid" } },
@@ -400,29 +466,10 @@ describe("AssistantConfigSchema", () => {
400
466
  }
401
467
  });
402
468
 
403
- test("sandbox with only enabled still parses", () => {
404
- const result = AssistantConfigSchema.parse({ sandbox: { enabled: false } });
405
- expect(result.sandbox.enabled).toBe(false);
406
- });
407
-
408
- test("rejects unknown sandbox fields", () => {
409
- const result = AssistantConfigSchema.safeParse({
410
- sandbox: { backend: "docker" },
411
- });
412
- // Unknown keys are stripped by Zod passthrough/strip, so parse should still succeed
413
- // but the unknown field should not appear in the output
414
- if (result.success) {
415
- expect(
416
- (result.data.sandbox as Record<string, unknown>)["backend"],
417
- ).toBeUndefined();
418
- }
419
- });
420
-
421
469
  test("defaults permissions.mode to workspace", () => {
422
470
  const result = AssistantConfigSchema.parse({});
423
471
  expect(result.permissions).toEqual({
424
472
  mode: "workspace",
425
- askBeforeActing: true,
426
473
  hostAccess: false,
427
474
  });
428
475
  });
@@ -624,8 +671,6 @@ describe("AssistantConfigSchema", () => {
624
671
  },
625
672
  voice: {
626
673
  language: "en-US",
627
- transcriptionProvider: "Deepgram",
628
- ttsProvider: "elevenlabs",
629
674
  hints: [],
630
675
  interruptSensitivity: "low",
631
676
  },
@@ -733,29 +778,8 @@ describe("AssistantConfigSchema", () => {
733
778
  test("config without calls.voice parses correctly and produces defaults", () => {
734
779
  const result = AssistantConfigSchema.parse({});
735
780
  expect(result.calls.voice.language).toBe("en-US");
736
- expect(result.calls.voice.transcriptionProvider).toBe("Deepgram");
737
- });
738
-
739
- test("elevenlabs tuning params have correct defaults", () => {
740
- const result = AssistantConfigSchema.parse({});
741
- expect(result.elevenlabs.voiceModelId).toBe("");
742
- expect(result.elevenlabs.speed).toBe(1.0);
743
- expect(result.elevenlabs.stability).toBe(0.5);
744
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
745
- });
746
-
747
- test("rejects elevenlabs.speed below 0.7", () => {
748
- const result = AssistantConfigSchema.safeParse({
749
- elevenlabs: { speed: 0.5 },
750
- });
751
- expect(result.success).toBe(false);
752
- });
753
-
754
- test("rejects elevenlabs.speed above 1.2", () => {
755
- const result = AssistantConfigSchema.safeParse({
756
- elevenlabs: { speed: 1.5 },
757
- });
758
- expect(result.success).toBe(false);
781
+ expect(result.calls.voice.hints).toEqual([]);
782
+ expect(result.calls.voice.interruptSensitivity).toBe("low");
759
783
  });
760
784
 
761
785
  test("accepts valid calls.voice overrides", () => {
@@ -763,39 +787,20 @@ describe("AssistantConfigSchema", () => {
763
787
  calls: {
764
788
  voice: {
765
789
  language: "es-ES",
766
- transcriptionProvider: "Google",
767
790
  },
768
791
  },
769
- elevenlabs: {
770
- stability: 0.8,
771
- },
772
792
  });
773
793
  expect(result.calls.voice.language).toBe("es-ES");
774
- expect(result.calls.voice.transcriptionProvider).toBe("Google");
775
- expect(result.elevenlabs.stability).toBe(0.8);
776
- // Defaults preserved for unset fields
777
- expect(result.elevenlabs.voiceModelId).toBe("");
778
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
779
- });
780
-
781
- test("rejects invalid calls.voice.transcriptionProvider", () => {
782
- const result = AssistantConfigSchema.safeParse({
783
- calls: { voice: { transcriptionProvider: "AWS" } },
784
- });
785
- expect(result.success).toBe(false);
786
- if (!result.success) {
787
- const msgs = result.error.issues.map((i) => i.message);
788
- expect(
789
- msgs.some((m) => m.includes("calls.voice.transcriptionProvider")),
790
- ).toBe(true);
791
- }
792
794
  });
793
795
 
794
- test("rejects elevenlabs.stability out of range", () => {
795
- const result = AssistantConfigSchema.safeParse({
796
- 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" } },
797
800
  });
798
- expect(result.success).toBe(false);
801
+ expect(
802
+ (result.calls.voice as Record<string, unknown>).transcriptionProvider,
803
+ ).toBeUndefined();
799
804
  });
800
805
 
801
806
  test("accepts optional calls.model", () => {
@@ -861,169 +866,1116 @@ describe("AssistantConfigSchema", () => {
861
866
  });
862
867
  expect(result.calls.callerIdentity.allowPerCallOverride).toBe(true);
863
868
  });
864
- });
865
869
 
866
- // ---------------------------------------------------------------------------
867
- // Tests: Voice quality profile resolver
868
- // ---------------------------------------------------------------------------
870
+ // ── hostBrowser.cdpInspect config ─────────────────────────────────
869
871
 
870
- describe("resolveVoiceQualityProfile", () => {
871
- test("always returns ElevenLabs ttsProvider", () => {
872
- const config = AssistantConfigSchema.parse({});
873
- const profile = resolveVoiceQualityProfile(config);
874
- expect(profile.ttsProvider).toBe("ElevenLabs");
875
- expect(profile.transcriptionProvider).toBe("Deepgram");
872
+ test("applies hostBrowser.cdpInspect defaults", () => {
873
+ const result = AssistantConfigSchema.parse({});
874
+ expect(result.hostBrowser).toEqual({
875
+ cdpInspect: {
876
+ enabled: false,
877
+ host: "localhost",
878
+ port: 9222,
879
+ probeTimeoutMs: 500,
880
+ desktopAuto: {
881
+ enabled: true,
882
+ cooldownMs: 30_000,
883
+ },
884
+ },
885
+ });
876
886
  });
877
887
 
878
- test("uses shared elevenlabs.voiceId for voice", () => {
879
- const config = AssistantConfigSchema.parse({
880
- elevenlabs: { voiceId: "test-voice-id" },
888
+ test("accepts hostBrowser.cdpInspect enabled with custom host/port", () => {
889
+ const result = AssistantConfigSchema.parse({
890
+ hostBrowser: {
891
+ cdpInspect: {
892
+ enabled: true,
893
+ host: "127.0.0.1",
894
+ port: 9333,
895
+ },
896
+ },
881
897
  });
882
- const profile = resolveVoiceQualityProfile(config);
883
- expect(profile.ttsProvider).toBe("ElevenLabs");
884
- expect(profile.voice).toBe("test-voice-id");
898
+ expect(result.hostBrowser.cdpInspect.enabled).toBe(true);
899
+ expect(result.hostBrowser.cdpInspect.host).toBe("127.0.0.1");
900
+ expect(result.hostBrowser.cdpInspect.port).toBe(9333);
901
+ // Unset field should still receive its default.
902
+ expect(result.hostBrowser.cdpInspect.probeTimeoutMs).toBe(500);
885
903
  });
886
904
 
887
- test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
888
- const config = AssistantConfigSchema.parse({});
889
- const profile = resolveVoiceQualityProfile(config);
890
- expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
905
+ test("accepts hostBrowser.cdpInspect custom probeTimeoutMs", () => {
906
+ const result = AssistantConfigSchema.parse({
907
+ hostBrowser: { cdpInspect: { probeTimeoutMs: 1000 } },
908
+ });
909
+ expect(result.hostBrowser.cdpInspect.probeTimeoutMs).toBe(1000);
891
910
  });
892
911
 
893
- test("applies voice tuning params from elevenlabs config", () => {
894
- const config = AssistantConfigSchema.parse({
895
- elevenlabs: {
896
- voiceId: "abc123",
897
- voiceModelId: "turbo_v2_5",
898
- speed: 0.9,
899
- stability: 0.8,
900
- similarityBoost: 0.9,
901
- },
912
+ test("rejects hostBrowser.cdpInspect.port below 1", () => {
913
+ const result = AssistantConfigSchema.safeParse({
914
+ hostBrowser: { cdpInspect: { port: 0 } },
902
915
  });
903
- const profile = resolveVoiceQualityProfile(config);
904
- expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
916
+ expect(result.success).toBe(false);
917
+ if (!result.success) {
918
+ expect(
919
+ result.error.issues.some((issue) =>
920
+ issue.path.join(".").includes("hostBrowser.cdpInspect.port"),
921
+ ),
922
+ ).toBe(true);
923
+ }
905
924
  });
906
- });
907
925
 
908
- // ---------------------------------------------------------------------------
909
- // Tests: buildElevenLabsVoiceSpec
910
- // ---------------------------------------------------------------------------
926
+ test("rejects hostBrowser.cdpInspect.port above 65535", () => {
927
+ const result = AssistantConfigSchema.safeParse({
928
+ hostBrowser: { cdpInspect: { port: 70000 } },
929
+ });
930
+ expect(result.success).toBe(false);
931
+ if (!result.success) {
932
+ expect(
933
+ result.error.issues.some((issue) =>
934
+ issue.path.join(".").includes("hostBrowser.cdpInspect.port"),
935
+ ),
936
+ ).toBe(true);
937
+ }
938
+ });
911
939
 
912
- describe("buildElevenLabsVoiceSpec", () => {
913
- test("produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity", () => {
914
- const spec = buildElevenLabsVoiceSpec({
915
- voiceId: "abc123",
916
- voiceModelId: "turbo_v2_5",
917
- speed: 1.0,
918
- stability: 0.5,
919
- similarityBoost: 0.75,
940
+ test("rejects non-integer hostBrowser.cdpInspect.port", () => {
941
+ const result = AssistantConfigSchema.safeParse({
942
+ hostBrowser: { cdpInspect: { port: 9222.5 } },
920
943
  });
921
- expect(spec).toBe("abc123-turbo_v2_5-1_0.5_0.75");
944
+ expect(result.success).toBe(false);
922
945
  });
923
946
 
924
- test("returns empty string when voiceId is empty", () => {
925
- const spec = buildElevenLabsVoiceSpec({
926
- voiceId: "",
927
- voiceModelId: "turbo_v2_5",
928
- speed: 1.0,
929
- stability: 0.5,
930
- similarityBoost: 0.75,
947
+ test("rejects hostBrowser.cdpInspect.probeTimeoutMs below 50", () => {
948
+ const result = AssistantConfigSchema.safeParse({
949
+ hostBrowser: { cdpInspect: { probeTimeoutMs: 10 } },
931
950
  });
932
- expect(spec).toBe("");
951
+ expect(result.success).toBe(false);
952
+ if (!result.success) {
953
+ expect(
954
+ result.error.issues.some((issue) =>
955
+ issue.path
956
+ .join(".")
957
+ .includes("hostBrowser.cdpInspect.probeTimeoutMs"),
958
+ ),
959
+ ).toBe(true);
960
+ }
933
961
  });
934
962
 
935
- test("formats custom parameters correctly", () => {
936
- const spec = buildElevenLabsVoiceSpec({
937
- voiceId: "myVoice",
938
- voiceModelId: "eleven_multilingual_v2",
939
- speed: 0.9,
940
- stability: 0.8,
941
- similarityBoost: 0.9,
963
+ test("rejects hostBrowser.cdpInspect.probeTimeoutMs above 5000", () => {
964
+ const result = AssistantConfigSchema.safeParse({
965
+ hostBrowser: { cdpInspect: { probeTimeoutMs: 10000 } },
942
966
  });
943
- expect(spec).toBe("myVoice-eleven_multilingual_v2-0.9_0.8_0.9");
967
+ expect(result.success).toBe(false);
968
+ if (!result.success) {
969
+ expect(
970
+ result.error.issues.some((issue) =>
971
+ issue.path
972
+ .join(".")
973
+ .includes("hostBrowser.cdpInspect.probeTimeoutMs"),
974
+ ),
975
+ ).toBe(true);
976
+ }
944
977
  });
945
978
 
946
- test("default config uses a bare voiceId when no model override is set", () => {
947
- const config = AssistantConfigSchema.parse({
948
- elevenlabs: { voiceId: "test" },
979
+ test("rejects non-integer hostBrowser.cdpInspect.probeTimeoutMs", () => {
980
+ const result = AssistantConfigSchema.safeParse({
981
+ hostBrowser: { cdpInspect: { probeTimeoutMs: 500.5 } },
949
982
  });
950
- const spec = buildElevenLabsVoiceSpec(config.elevenlabs);
951
- expect(spec).toBe("test");
983
+ expect(result.success).toBe(false);
952
984
  });
953
- });
954
985
 
955
- // ---------------------------------------------------------------------------
956
- // Tests: loader integration (config file -> loadConfig with fallback)
957
- // ---------------------------------------------------------------------------
986
+ test("rejects non-boolean hostBrowser.cdpInspect.enabled", () => {
987
+ const result = AssistantConfigSchema.safeParse({
988
+ hostBrowser: { cdpInspect: { enabled: "yes" } },
989
+ });
990
+ expect(result.success).toBe(false);
991
+ });
958
992
 
959
- describe("loadConfig with schema validation", () => {
960
- beforeEach(() => {
961
- // Keep WORKSPACE_DIR and logs in place to avoid racing async logger stream init.
962
- ensureTestDir();
963
- const resetPaths = [
964
- CONFIG_PATH,
965
- join(WORKSPACE_DIR, "keys.enc"),
966
- join(WORKSPACE_DIR, "data"),
967
- join(WORKSPACE_DIR, "data", "memory"),
968
- ];
969
- for (const path of resetPaths) {
970
- if (existsSync(path)) {
971
- rmSync(path, { recursive: true, force: true });
972
- }
973
- }
974
- ensureTestDir();
975
- _setStorePath(join(WORKSPACE_DIR, "keys.enc"));
976
- invalidateConfigCache();
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");
977
1016
  });
978
1017
 
979
- afterEach(() => {
980
- _setStorePath(null);
981
- invalidateConfigCache();
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");
982
1024
  });
983
1025
 
984
- // Intentionally do not remove WORKSPACE_DIR in afterAll.
985
- // A late async logger flush may still target logs under this path and can
986
- // intermittently trigger unhandled ENOENT in CI if the directory is removed.
987
- test("loads valid config", () => {
988
- writeConfig({
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({
989
1036
  services: {
990
- inference: { provider: "openai", model: "gpt-4" },
1037
+ tts: {
1038
+ providers: {
1039
+ elevenlabs: { voiceId: "custom-voice", speed: 0.8 },
1040
+ },
1041
+ },
991
1042
  },
992
- maxTokens: 4096,
993
1043
  });
994
- const config = loadConfig();
995
- expect(config.services.inference.provider).toBe("openai");
996
- expect(config.services.inference.model).toBe("gpt-4");
997
- expect(config.maxTokens).toBe(4096);
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);
998
1050
  });
999
1051
 
1000
- test("applies defaults for missing fields", () => {
1001
- writeConfig({});
1002
- const config = loadConfig();
1003
- expect(config.services.inference.provider).toBe("anthropic");
1004
- expect(config.services.inference.model).toBe("claude-opus-4-6");
1005
- expect(config.maxTokens).toBe(16000);
1006
- expect(config.thinking).toEqual({
1007
- enabled: true,
1008
- streamThinking: true,
1009
- });
1010
- expect(config.contextWindow).toEqual({
1011
- enabled: true,
1012
- maxInputTokens: 200000,
1013
- targetBudgetRatio: 0.3,
1014
- compactThreshold: 0.8,
1015
- summaryBudgetRatio: 0.05,
1016
- overflowRecovery: {
1017
- enabled: true,
1018
- safetyMarginRatio: 0.05,
1019
- maxAttempts: 3,
1020
- interactiveLatestTurnCompression: "summarize",
1021
- nonInteractiveLatestTurnCompression: "truncate",
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
+ },
1022
1060
  },
1023
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);
1024
1068
  });
1025
1069
 
1026
- test("falls back to default for invalid provider", () => {
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
+ });
1396
+ });
1397
+
1398
+ // ---------------------------------------------------------------------------
1399
+ // Tests: Voice quality profile resolver
1400
+ // ---------------------------------------------------------------------------
1401
+
1402
+ describe("resolveVoiceQualityProfile", () => {
1403
+ test("always returns ElevenLabs ttsProvider", () => {
1404
+ const config = AssistantConfigSchema.parse({});
1405
+ const profile = resolveVoiceQualityProfile(config);
1406
+ expect(profile.ttsProvider).toBe("ElevenLabs");
1407
+ });
1408
+
1409
+ test("uses services.tts.providers.elevenlabs.voiceId for voice", () => {
1410
+ const config = AssistantConfigSchema.parse({
1411
+ services: {
1412
+ tts: {
1413
+ providers: { elevenlabs: { voiceId: "test-voice-id" } },
1414
+ },
1415
+ },
1416
+ });
1417
+ const profile = resolveVoiceQualityProfile(config);
1418
+ expect(profile.ttsProvider).toBe("ElevenLabs");
1419
+ expect(profile.voice).toBe("test-voice-id");
1420
+ });
1421
+
1422
+ test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
1423
+ const config = AssistantConfigSchema.parse({});
1424
+ const profile = resolveVoiceQualityProfile(config);
1425
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
1426
+ });
1427
+
1428
+ test("applies voice tuning params from services.tts.providers.elevenlabs config", () => {
1429
+ const config = AssistantConfigSchema.parse({
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
+ },
1442
+ },
1443
+ });
1444
+ const profile = resolveVoiceQualityProfile(config);
1445
+ expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
1446
+ });
1447
+ });
1448
+
1449
+ // ---------------------------------------------------------------------------
1450
+ // Tests: buildElevenLabsVoiceSpec
1451
+ // ---------------------------------------------------------------------------
1452
+
1453
+ describe("buildElevenLabsVoiceSpec", () => {
1454
+ test("produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity", () => {
1455
+ const spec = buildElevenLabsVoiceSpec({
1456
+ voiceId: "abc123",
1457
+ voiceModelId: "turbo_v2_5",
1458
+ speed: 1.0,
1459
+ stability: 0.5,
1460
+ similarityBoost: 0.75,
1461
+ });
1462
+ expect(spec).toBe("abc123-turbo_v2_5-1_0.5_0.75");
1463
+ });
1464
+
1465
+ test("returns empty string when voiceId is empty", () => {
1466
+ const spec = buildElevenLabsVoiceSpec({
1467
+ voiceId: "",
1468
+ voiceModelId: "turbo_v2_5",
1469
+ speed: 1.0,
1470
+ stability: 0.5,
1471
+ similarityBoost: 0.75,
1472
+ });
1473
+ expect(spec).toBe("");
1474
+ });
1475
+
1476
+ test("formats custom parameters correctly", () => {
1477
+ const spec = buildElevenLabsVoiceSpec({
1478
+ voiceId: "myVoice",
1479
+ voiceModelId: "eleven_multilingual_v2",
1480
+ speed: 0.9,
1481
+ stability: 0.8,
1482
+ similarityBoost: 0.9,
1483
+ });
1484
+ expect(spec).toBe("myVoice-eleven_multilingual_v2-0.9_0.8_0.9");
1485
+ });
1486
+
1487
+ test("default config uses a bare voiceId when no model override is set", () => {
1488
+ const config = AssistantConfigSchema.parse({
1489
+ services: {
1490
+ tts: {
1491
+ providers: { elevenlabs: { voiceId: "test" } },
1492
+ },
1493
+ },
1494
+ });
1495
+ const spec = buildElevenLabsVoiceSpec(
1496
+ config.services.tts.providers.elevenlabs,
1497
+ );
1498
+ expect(spec).toBe("test");
1499
+ });
1500
+ });
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
+
1907
+ // ---------------------------------------------------------------------------
1908
+ // Tests: loader integration (config file -> loadConfig with fallback)
1909
+ // ---------------------------------------------------------------------------
1910
+
1911
+ describe("loadConfig with schema validation", () => {
1912
+ beforeEach(() => {
1913
+ // Keep WORKSPACE_DIR and logs in place to avoid racing async logger stream init.
1914
+ ensureTestDir();
1915
+ const resetPaths = [
1916
+ CONFIG_PATH,
1917
+ join(WORKSPACE_DIR, "keys.enc"),
1918
+ join(WORKSPACE_DIR, "data"),
1919
+ join(WORKSPACE_DIR, "data", "memory"),
1920
+ ];
1921
+ for (const path of resetPaths) {
1922
+ if (existsSync(path)) {
1923
+ rmSync(path, { recursive: true, force: true });
1924
+ }
1925
+ }
1926
+ ensureTestDir();
1927
+ _setStorePath(join(WORKSPACE_DIR, "keys.enc"));
1928
+ invalidateConfigCache();
1929
+ });
1930
+
1931
+ afterEach(() => {
1932
+ _setStorePath(null);
1933
+ invalidateConfigCache();
1934
+ });
1935
+
1936
+ // Intentionally do not remove WORKSPACE_DIR in afterAll.
1937
+ // A late async logger flush may still target logs under this path and can
1938
+ // intermittently trigger unhandled ENOENT in CI if the directory is removed.
1939
+ test("loads valid config", () => {
1940
+ writeConfig({
1941
+ services: {
1942
+ inference: { provider: "openai", model: "gpt-4" },
1943
+ },
1944
+ maxTokens: 4096,
1945
+ });
1946
+ const config = loadConfig();
1947
+ expect(config.services.inference.provider).toBe("openai");
1948
+ expect(config.services.inference.model).toBe("gpt-4");
1949
+ expect(config.maxTokens).toBe(4096);
1950
+ });
1951
+
1952
+ test("applies defaults for missing fields", () => {
1953
+ writeConfig({});
1954
+ const config = loadConfig();
1955
+ expect(config.services.inference.provider).toBe("anthropic");
1956
+ expect(config.services.inference.model).toBe("claude-opus-4-6");
1957
+ expect(config.maxTokens).toBe(64000);
1958
+ expect(config.thinking).toEqual({
1959
+ enabled: true,
1960
+ streamThinking: true,
1961
+ });
1962
+ expect(config.contextWindow).toEqual({
1963
+ enabled: true,
1964
+ maxInputTokens: 200000,
1965
+ targetBudgetRatio: 0.3,
1966
+ compactThreshold: 0.8,
1967
+ summaryBudgetRatio: 0.05,
1968
+ overflowRecovery: {
1969
+ enabled: true,
1970
+ safetyMarginRatio: 0.05,
1971
+ maxAttempts: 3,
1972
+ interactiveLatestTurnCompression: "summarize",
1973
+ nonInteractiveLatestTurnCompression: "truncate",
1974
+ },
1975
+ });
1976
+ });
1977
+
1978
+ test("falls back to default for invalid provider", () => {
1027
1979
  writeConfig({
1028
1980
  services: { inference: { provider: "invalid-provider" } },
1029
1981
  });
@@ -1034,7 +1986,7 @@ describe("loadConfig with schema validation", () => {
1034
1986
  test("falls back to default for invalid maxTokens", () => {
1035
1987
  writeConfig({ maxTokens: -100 });
1036
1988
  const config = loadConfig();
1037
- expect(config.maxTokens).toBe(16000);
1989
+ expect(config.maxTokens).toBe(64000);
1038
1990
  });
1039
1991
 
1040
1992
  test("falls back to defaults for invalid nested values", () => {
@@ -1059,13 +2011,13 @@ describe("loadConfig with schema validation", () => {
1059
2011
  expect(config.services.inference.provider).toBe("openai");
1060
2012
  expect(config.services.inference.model).toBe("gpt-4");
1061
2013
  expect(config.thinking.enabled).toBe(true);
1062
- expect(config.maxTokens).toBe(16000);
2014
+ expect(config.maxTokens).toBe(64000);
1063
2015
  });
1064
2016
 
1065
2017
  test("handles no config file", () => {
1066
2018
  const config = loadConfig();
1067
2019
  expect(config.services.inference.provider).toBe("anthropic");
1068
- expect(config.maxTokens).toBe(16000);
2020
+ expect(config.maxTokens).toBe(64000);
1069
2021
  });
1070
2022
 
1071
2023
  test("partial nested objects get defaults for missing fields", () => {
@@ -1084,25 +2036,6 @@ describe("loadConfig with schema validation", () => {
1084
2036
  expect(config.secretDetection.action).toBe("redact");
1085
2037
  });
1086
2038
 
1087
- test("falls back for invalid sandbox.enabled", () => {
1088
- writeConfig({ sandbox: { enabled: "yes" } });
1089
- const config = loadConfig();
1090
- expect(config.sandbox.enabled).toBe(false);
1091
- });
1092
-
1093
- test("loads sandbox with only enabled field", () => {
1094
- writeConfig({ sandbox: { enabled: false } });
1095
- const config = loadConfig();
1096
- expect(config.sandbox.enabled).toBe(false);
1097
- });
1098
-
1099
- test("strips unknown sandbox fields", () => {
1100
- writeConfig({ sandbox: { enabled: true, backend: "docker" } });
1101
- const config = loadConfig();
1102
- expect(config.sandbox.enabled).toBe(true);
1103
- expect("backend" in config.sandbox).toBe(false);
1104
- });
1105
-
1106
2039
  test("falls back for invalid contextWindow relationship", () => {
1107
2040
  writeConfig({
1108
2041
  contextWindow: { targetBudgetRatio: 0.8, compactThreshold: 0.8 },
@@ -1131,7 +2064,6 @@ describe("loadConfig with schema validation", () => {
1131
2064
  const config = loadConfig();
1132
2065
  expect(config.permissions).toEqual({
1133
2066
  mode: "workspace",
1134
- askBeforeActing: true,
1135
2067
  hostAccess: false,
1136
2068
  });
1137
2069
  });
@@ -1167,6 +2099,93 @@ describe("loadConfig with schema validation", () => {
1167
2099
  expect(config.calls.provider).toBe("twilio");
1168
2100
  });
1169
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
+
1170
2189
  test("applies calls defaults when not specified", () => {
1171
2190
  writeConfig({});
1172
2191
  const config = loadConfig();
@@ -1176,7 +2195,12 @@ describe("loadConfig with schema validation", () => {
1176
2195
  expect(config.calls.disclosure.enabled).toBe(true);
1177
2196
  expect(config.calls.safety.denyCategories).toEqual([]);
1178
2197
  expect(config.calls.voice.language).toBe("en-US");
1179
- 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();
1180
2204
  expect(config.calls.model).toBeUndefined();
1181
2205
  expect(config.calls.callerIdentity).toEqual({
1182
2206
  allowPerCallOverride: true,