@vellumai/assistant 0.6.3 → 0.6.5

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 (1114) hide show
  1. package/.prettierignore +5 -0
  2. package/ARCHITECTURE.md +298 -39
  3. package/Dockerfile +14 -3
  4. package/README.md +3 -4
  5. package/bun.lock +13 -16
  6. package/docs/architecture/integrations.md +1 -20
  7. package/docs/architecture/security.md +16 -16
  8. package/docs/backup-troubleshooting.md +52 -0
  9. package/docs/browser-use-architecture-phase2.md +174 -0
  10. package/docs/error-handling.md +111 -0
  11. package/docs/skills.md +10 -10
  12. package/docs/stt-provider-onboarding.md +121 -0
  13. package/knip.json +20 -3
  14. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  15. package/node_modules/@vellumai/ces-contracts/package.json +5 -4
  16. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  17. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  18. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  19. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  20. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  21. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  22. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  23. package/openapi.yaml +1094 -72
  24. package/package.json +9 -8
  25. package/scripts/generate-openapi.ts +50 -12
  26. package/scripts/test.sh +73 -18
  27. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  28. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  29. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  30. package/src/__tests__/agent-loop.test.ts +235 -1
  31. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  32. package/src/__tests__/anthropic-provider.test.ts +434 -12
  33. package/src/__tests__/approval-cascade.test.ts +31 -10
  34. package/src/__tests__/approval-routes-http.test.ts +134 -10
  35. package/src/__tests__/assistant-attachments.test.ts +44 -0
  36. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  37. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  38. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  39. package/src/__tests__/browser-fill-credential.test.ts +12 -1
  40. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  41. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  42. package/src/__tests__/browser-skill-endstate.test.ts +52 -159
  43. package/src/__tests__/btw-routes.test.ts +54 -1
  44. package/src/__tests__/call-controller.test.ts +582 -22
  45. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  46. package/src/__tests__/catalog-cache.test.ts +27 -4
  47. package/src/__tests__/catalog-files.test.ts +138 -0
  48. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  49. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  50. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  51. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  52. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  53. package/src/__tests__/checker.test.ts +576 -502
  54. package/src/__tests__/clawhub-files.test.ts +347 -0
  55. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  56. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  57. package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
  58. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  59. package/src/__tests__/config-analysis.test.ts +83 -0
  60. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  61. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  62. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  63. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  64. package/src/__tests__/config-schema.test.ts +1458 -198
  65. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  66. package/src/__tests__/config-watcher.test.ts +45 -10
  67. package/src/__tests__/contact-store-user-file.test.ts +511 -0
  68. package/src/__tests__/contacts-write.test.ts +197 -0
  69. package/src/__tests__/context-token-estimator.test.ts +191 -1
  70. package/src/__tests__/context-window-manager.test.ts +618 -2
  71. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -16
  72. package/src/__tests__/conversation-agent-loop-overflow.test.ts +62 -17
  73. package/src/__tests__/conversation-agent-loop.test.ts +510 -84
  74. package/src/__tests__/conversation-attachments.test.ts +1 -1
  75. package/src/__tests__/conversation-confirmation-signals.test.ts +165 -9
  76. package/src/__tests__/conversation-error.test.ts +102 -1
  77. package/src/__tests__/conversation-history-web-search.test.ts +17 -4
  78. package/src/__tests__/conversation-init.benchmark.test.ts +42 -1
  79. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  80. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  81. package/src/__tests__/conversation-list-source.test.ts +145 -0
  82. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  83. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -16
  84. package/src/__tests__/conversation-process-callsite.test.ts +306 -0
  85. package/src/__tests__/conversation-provider-retry-repair.test.ts +32 -16
  86. package/src/__tests__/conversation-queue.test.ts +932 -76
  87. package/src/__tests__/conversation-routes-disk-view.test.ts +299 -1
  88. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  89. package/src/__tests__/conversation-runtime-assembly.test.ts +2790 -55
  90. package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
  91. package/src/__tests__/conversation-skill-tools.test.ts +12 -143
  92. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  93. package/src/__tests__/conversation-slash-queue.test.ts +120 -34
  94. package/src/__tests__/conversation-slash-unknown.test.ts +32 -16
  95. package/src/__tests__/conversation-speed-override.test.ts +30 -11
  96. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  97. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  98. package/src/__tests__/conversation-title-service.test.ts +2 -2
  99. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  100. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  101. package/src/__tests__/conversation-usage.test.ts +3 -1
  102. package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
  103. package/src/__tests__/conversation-workspace-injection.test.ts +45 -15
  104. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -16
  105. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  106. package/src/__tests__/credential-health-service.test.ts +352 -0
  107. package/src/__tests__/credential-security-invariants.test.ts +8 -3
  108. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  109. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  110. package/src/__tests__/credential-vault-unit.test.ts +495 -3
  111. package/src/__tests__/credentials-cli.test.ts +32 -16
  112. package/src/__tests__/cross-provider-web-search.test.ts +230 -35
  113. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  114. package/src/__tests__/delete-propagation.test.ts +437 -0
  115. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  116. package/src/__tests__/device-id.test.ts +112 -0
  117. package/src/__tests__/dm-backfill.test.ts +417 -0
  118. package/src/__tests__/dm-persistence.test.ts +227 -0
  119. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  120. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  121. package/src/__tests__/edit-propagation.test.ts +280 -0
  122. package/src/__tests__/email-html-renderer.test.ts +71 -0
  123. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  124. package/src/__tests__/emit-event-signal.test.ts +71 -0
  125. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  126. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  127. package/src/__tests__/estimator-calibration.test.ts +213 -0
  128. package/src/__tests__/extension-id-sync-guard.test.ts +101 -15
  129. package/src/__tests__/file-write-tool.test.ts +151 -1
  130. package/src/__tests__/filing-service.test.ts +255 -0
  131. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  132. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  133. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  134. package/src/__tests__/gemini-provider.test.ts +64 -3
  135. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  136. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  137. package/src/__tests__/headless-browser-interactions.test.ts +44 -1
  138. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  139. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  140. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  141. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  142. package/src/__tests__/heartbeat-service.test.ts +166 -32
  143. package/src/__tests__/home-state-routes.test.ts +162 -0
  144. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  145. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  146. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  147. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  148. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  149. package/src/__tests__/host-shell-tool.test.ts +124 -18
  150. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  151. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  152. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  153. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  154. package/src/__tests__/intent-routing.test.ts +1 -40
  155. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  156. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  157. package/src/__tests__/llm-context-normalization.test.ts +609 -0
  158. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  159. package/src/__tests__/llm-resolver.test.ts +214 -0
  160. package/src/__tests__/llm-schema.test.ts +223 -0
  161. package/src/__tests__/llm-usage-store.test.ts +363 -0
  162. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  163. package/src/__tests__/media-stream-output.test.ts +555 -0
  164. package/src/__tests__/media-stream-parser.test.ts +374 -0
  165. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  166. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  167. package/src/__tests__/media-turn-detector.test.ts +440 -0
  168. package/src/__tests__/message-queue.test.ts +125 -0
  169. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  170. package/src/__tests__/migration-export-http.test.ts +6 -6
  171. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  172. package/src/__tests__/migration-import-from-url.test.ts +684 -0
  173. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  174. package/src/__tests__/migration-validate-http.test.ts +3 -3
  175. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  176. package/src/__tests__/model-intents.test.ts +10 -84
  177. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  178. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  179. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  180. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  181. package/src/__tests__/oauth-cli.test.ts +2 -0
  182. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  183. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  184. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  185. package/src/__tests__/oauth-store.test.ts +95 -7
  186. package/src/__tests__/oauth2-gateway-transport.test.ts +257 -9
  187. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  188. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  189. package/src/__tests__/openai-provider.test.ts +183 -0
  190. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  191. package/src/__tests__/openai-responses-provider.test.ts +1501 -0
  192. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  193. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  194. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  195. package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
  196. package/src/__tests__/permission-mode.test.ts +16 -0
  197. package/src/__tests__/permission-types.test.ts +0 -1
  198. package/src/__tests__/persona-resolver.test.ts +251 -0
  199. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  200. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -1
  201. package/src/__tests__/platform.test.ts +92 -1
  202. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  203. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  204. package/src/__tests__/pricing.test.ts +224 -3
  205. package/src/__tests__/profiler-routes.test.ts +1 -1
  206. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  207. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  208. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  209. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  210. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  211. package/src/__tests__/proxy-approval-callback.test.ts +0 -1
  212. package/src/__tests__/qdrant-manager.test.ts +29 -8
  213. package/src/__tests__/reaction-persistence.test.ts +560 -0
  214. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  215. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  216. package/src/__tests__/relay-server.test.ts +424 -6
  217. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  218. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  219. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  220. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  221. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  222. package/src/__tests__/search-skills-unified.test.ts +118 -0
  223. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  224. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  225. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  226. package/src/__tests__/secret-scanner-executor.test.ts +5 -1
  227. package/src/__tests__/secure-keys.test.ts +107 -0
  228. package/src/__tests__/send-endpoint-busy.test.ts +34 -2
  229. package/src/__tests__/sequence-store.test.ts +1 -1
  230. package/src/__tests__/server-history-render.test.ts +80 -0
  231. package/src/__tests__/settings-routes.test.ts +201 -0
  232. package/src/__tests__/shell-parser-property.test.ts +13 -13
  233. package/src/__tests__/skill-cache-store.test.ts +182 -0
  234. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  235. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  236. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  237. package/src/__tests__/skills.test.ts +19 -30
  238. package/src/__tests__/skillssh-files.test.ts +446 -0
  239. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  240. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  241. package/src/__tests__/slack-channel-config.test.ts +564 -1
  242. package/src/__tests__/slack-skill.test.ts +3 -8
  243. package/src/__tests__/starter-bundle.test.ts +35 -0
  244. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  245. package/src/__tests__/stt-stream-session.test.ts +535 -0
  246. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  247. package/src/__tests__/suggestion-routes.test.ts +160 -3
  248. package/src/__tests__/system-prompt.test.ts +126 -53
  249. package/src/__tests__/task-runner.test.ts +3 -1
  250. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  251. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  252. package/src/__tests__/terminal-tools.test.ts +26 -7
  253. package/src/__tests__/test-preload.ts +18 -0
  254. package/src/__tests__/test-support/browser-skill-harness.ts +2 -49
  255. package/src/__tests__/thread-backfill.test.ts +941 -0
  256. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
  257. package/src/__tests__/tool-executor-lifecycle-events.test.ts +10 -6
  258. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  259. package/src/__tests__/tool-executor.test.ts +88 -113
  260. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  261. package/src/__tests__/trust-store.test.ts +442 -103
  262. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  263. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  264. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  265. package/src/__tests__/twilio-routes.test.ts +376 -0
  266. package/src/__tests__/unicode.test.ts +293 -0
  267. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  268. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  269. package/src/__tests__/usage-routes.test.ts +25 -4
  270. package/src/__tests__/user-reference.test.ts +46 -61
  271. package/src/__tests__/verification-control-plane-policy.test.ts +5 -22
  272. package/src/__tests__/voice-config-update.test.ts +403 -0
  273. package/src/__tests__/voice-quality.test.ts +434 -19
  274. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  275. package/src/__tests__/volume-security-guard.test.ts +3 -2
  276. package/src/__tests__/web-search-history.test.ts +337 -0
  277. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  278. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  279. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  280. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  281. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  282. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  283. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  284. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  285. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  286. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  287. package/src/__tests__/workspace-policy.test.ts +1 -11
  288. package/src/acp/client-handler.ts +1 -2
  289. package/src/agent/image-optimize.ts +24 -12
  290. package/src/agent/loop.ts +251 -19
  291. package/src/avatar/resvg-lazy.test.ts +136 -0
  292. package/src/avatar/resvg-lazy.ts +82 -9
  293. package/src/avatar/traits-png-sync.ts +21 -1
  294. package/src/backup/__tests__/backup-key.test.ts +152 -0
  295. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  296. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  297. package/src/backup/__tests__/local-writer.test.ts +218 -0
  298. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  299. package/src/backup/__tests__/paths.test.ts +300 -0
  300. package/src/backup/__tests__/restore.test.ts +498 -0
  301. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  302. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  303. package/src/backup/backup-key.ts +137 -0
  304. package/src/backup/backup-worker.ts +459 -0
  305. package/src/backup/list-snapshots.ts +147 -0
  306. package/src/backup/local-writer.ts +133 -0
  307. package/src/backup/offsite-writer.ts +222 -0
  308. package/src/backup/paths.ts +226 -0
  309. package/src/backup/restore.ts +322 -0
  310. package/src/backup/snapshot-lock.ts +431 -0
  311. package/src/backup/stream-crypt.ts +263 -0
  312. package/src/browser/__tests__/operations.test.ts +163 -0
  313. package/src/browser/identifiers.ts +51 -0
  314. package/src/browser/operations.ts +660 -0
  315. package/src/browser/types.ts +81 -0
  316. package/src/bundler/package-resolver.ts +4 -0
  317. package/src/calls/audio-store.ts +11 -5
  318. package/src/calls/call-controller.ts +226 -71
  319. package/src/calls/call-domain.ts +9 -0
  320. package/src/calls/call-speech-output.ts +190 -0
  321. package/src/calls/call-transport.ts +77 -0
  322. package/src/calls/guardian-question-copy.ts +2 -2
  323. package/src/calls/media-stream-audio-transcode.ts +173 -0
  324. package/src/calls/media-stream-output.ts +660 -0
  325. package/src/calls/media-stream-parser.ts +300 -0
  326. package/src/calls/media-stream-protocol.ts +166 -0
  327. package/src/calls/media-stream-server.ts +592 -0
  328. package/src/calls/media-stream-stt-session.ts +460 -0
  329. package/src/calls/media-turn-detector.ts +230 -0
  330. package/src/calls/relay-server.ts +90 -75
  331. package/src/calls/resolve-call-tts-provider.ts +136 -0
  332. package/src/calls/telephony-stt-routing.ts +145 -0
  333. package/src/calls/tts-call-strategy.ts +161 -0
  334. package/src/calls/tts-text-sanitizer.ts +32 -16
  335. package/src/calls/twilio-routes.ts +281 -17
  336. package/src/calls/voice-quality.ts +78 -35
  337. package/src/calls/voice-session-bridge.ts +9 -1
  338. package/src/channels/types.ts +16 -0
  339. package/src/cli/AGENTS.md +1 -1
  340. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  341. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  342. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  343. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  344. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  345. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  346. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  347. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  348. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  349. package/src/cli/commands/__tests__/email-list.test.ts +28 -4
  350. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  351. package/src/cli/commands/__tests__/email-send.test.ts +130 -5
  352. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  353. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  354. package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
  355. package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
  356. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  357. package/src/cli/commands/__tests__/task.test.ts +913 -0
  358. package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
  359. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  360. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  361. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  362. package/src/cli/commands/attachment.ts +182 -0
  363. package/src/cli/commands/backup.ts +993 -0
  364. package/src/cli/commands/browser.ts +350 -0
  365. package/src/cli/commands/cache.ts +341 -0
  366. package/src/cli/commands/completions.ts +0 -3
  367. package/src/cli/commands/config.ts +6 -6
  368. package/src/cli/commands/conversations-import.ts +347 -0
  369. package/src/cli/commands/conversations.ts +90 -0
  370. package/src/cli/commands/credentials.ts +0 -1
  371. package/src/cli/commands/domain.ts +210 -0
  372. package/src/cli/commands/email.ts +308 -16
  373. package/src/cli/commands/image-generation.ts +300 -0
  374. package/src/cli/commands/inference.ts +200 -0
  375. package/src/cli/commands/memory.ts +127 -17
  376. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  377. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  378. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  379. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  380. package/src/cli/commands/oauth/mode.ts +12 -3
  381. package/src/cli/commands/oauth/providers.ts +15 -0
  382. package/src/cli/commands/oauth/shared.ts +2 -1
  383. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -10
  384. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -1
  385. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -2
  386. package/src/cli/commands/platform/__tests__/status.test.ts +6 -1
  387. package/src/cli/commands/stt.ts +339 -0
  388. package/src/cli/commands/task.ts +795 -0
  389. package/src/cli/commands/trust.ts +50 -19
  390. package/src/cli/commands/tts.ts +273 -0
  391. package/src/cli/commands/ui.ts +670 -0
  392. package/src/cli/commands/watchers.ts +509 -0
  393. package/src/cli/lib/daemon-credential-client.ts +0 -19
  394. package/src/cli/program.ts +53 -8
  395. package/src/cli.ts +0 -37
  396. package/src/config/__tests__/backup-schema.test.ts +134 -0
  397. package/src/config/assistant-feature-flags.ts +61 -62
  398. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  399. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  400. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
  401. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  402. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  403. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  404. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  405. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  406. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  407. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  408. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  409. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  410. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  411. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  412. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -2
  413. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  414. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  415. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
  416. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  417. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +28 -18
  418. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  419. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  420. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  421. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  422. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  423. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  424. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  425. package/src/config/bundled-tool-registry.ts +0 -167
  426. package/src/config/env-registry.ts +24 -0
  427. package/src/config/env.ts +39 -10
  428. package/src/config/feature-flag-registry.json +63 -15
  429. package/src/config/llm-resolver.ts +128 -0
  430. package/src/config/loader.ts +220 -22
  431. package/src/config/raw-config-utils.ts +30 -2
  432. package/src/config/sanitize-for-transfer.ts +35 -0
  433. package/src/config/schema.ts +65 -51
  434. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  435. package/src/config/schemas/analysis.ts +32 -0
  436. package/src/config/schemas/backup.ts +72 -0
  437. package/src/config/schemas/calls.ts +1 -30
  438. package/src/config/schemas/elevenlabs.ts +0 -59
  439. package/src/config/schemas/filing.ts +49 -14
  440. package/src/config/schemas/heartbeat.ts +27 -10
  441. package/src/config/schemas/host-browser.ts +47 -1
  442. package/src/config/schemas/inference.ts +3 -23
  443. package/src/config/schemas/llm.ts +318 -0
  444. package/src/config/schemas/memory-lifecycle.ts +14 -2
  445. package/src/config/schemas/memory-processing.ts +1 -9
  446. package/src/config/schemas/notifications.ts +4 -11
  447. package/src/config/schemas/platform.ts +3 -9
  448. package/src/config/schemas/security.ts +33 -0
  449. package/src/config/schemas/services.ts +53 -4
  450. package/src/config/schemas/stt.ts +60 -0
  451. package/src/config/schemas/tts.ts +283 -0
  452. package/src/config/schemas/updates.ts +14 -0
  453. package/src/config/schemas/workspace-git.ts +3 -40
  454. package/src/config/skills.ts +6 -2
  455. package/src/config/types.ts +4 -0
  456. package/src/contacts/contact-store.ts +56 -11
  457. package/src/contacts/contacts-write.ts +38 -1
  458. package/src/context/__tests__/compact-prompt.test.ts +45 -0
  459. package/src/context/__tests__/microcompact.test.ts +805 -0
  460. package/src/context/estimator-calibration.ts +136 -0
  461. package/src/context/microcompact.ts +443 -0
  462. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  463. package/src/context/prompts/compact.md +12 -0
  464. package/src/context/token-estimator.ts +61 -3
  465. package/src/context/tool-result-truncation.ts +2 -1
  466. package/src/context/window-manager.ts +272 -35
  467. package/src/credential-execution/approval-bridge.ts +0 -1
  468. package/src/credential-execution/executable-discovery.ts +23 -2
  469. package/src/credential-execution/process-manager.test.ts +109 -0
  470. package/src/credential-execution/process-manager.ts +96 -2
  471. package/src/credential-health/credential-health-service.ts +366 -0
  472. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  473. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  474. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  475. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  476. package/src/daemon/approval-generators.ts +29 -4
  477. package/src/daemon/assistant-attachments.ts +24 -13
  478. package/src/daemon/classifier.ts +2 -2
  479. package/src/daemon/config-watcher.ts +99 -6
  480. package/src/daemon/context-overflow-reducer.ts +4 -1
  481. package/src/daemon/conversation-agent-loop-handlers.ts +85 -12
  482. package/src/daemon/conversation-agent-loop.ts +563 -104
  483. package/src/daemon/conversation-attachments.ts +2 -6
  484. package/src/daemon/conversation-error.ts +46 -0
  485. package/src/daemon/conversation-history.ts +40 -6
  486. package/src/daemon/conversation-launch.ts +220 -0
  487. package/src/daemon/conversation-lifecycle.ts +85 -11
  488. package/src/daemon/conversation-messaging.ts +110 -7
  489. package/src/daemon/conversation-notifiers.ts +5 -0
  490. package/src/daemon/conversation-process.ts +591 -23
  491. package/src/daemon/conversation-queue-manager.ts +27 -0
  492. package/src/daemon/conversation-runtime-assembly.ts +769 -28
  493. package/src/daemon/conversation-slash.ts +38 -2
  494. package/src/daemon/conversation-surfaces.ts +483 -5
  495. package/src/daemon/conversation-tool-setup.ts +35 -5
  496. package/src/daemon/conversation-usage.ts +8 -5
  497. package/src/daemon/conversation.ts +193 -47
  498. package/src/daemon/external-skills-bootstrap.ts +41 -0
  499. package/src/daemon/guardian-action-generators.ts +34 -14
  500. package/src/daemon/handlers/config-model.test.ts +86 -0
  501. package/src/daemon/handlers/config-model.ts +54 -12
  502. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  503. package/src/daemon/handlers/conversations.ts +13 -3
  504. package/src/daemon/handlers/shared.ts +51 -1
  505. package/src/daemon/handlers/skills.ts +323 -79
  506. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  507. package/src/daemon/host-browser-proxy.ts +2 -1
  508. package/src/daemon/lifecycle.ts +185 -26
  509. package/src/daemon/message-protocol.ts +6 -0
  510. package/src/daemon/message-types/conversations.ts +48 -1
  511. package/src/daemon/message-types/home.ts +40 -0
  512. package/src/daemon/message-types/meet.ts +143 -0
  513. package/src/daemon/message-types/messages.ts +23 -1
  514. package/src/daemon/message-types/schedules.ts +34 -2
  515. package/src/daemon/message-types/skills.ts +16 -0
  516. package/src/daemon/message-types/surfaces.ts +2 -0
  517. package/src/daemon/message-types/trust.ts +0 -2
  518. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  519. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  520. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  521. package/src/daemon/pkb-context-tracker.ts +125 -0
  522. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  523. package/src/daemon/pkb-reminder-builder.ts +31 -0
  524. package/src/daemon/providers-setup.ts +6 -0
  525. package/src/daemon/server.ts +463 -10
  526. package/src/daemon/shutdown-handlers.ts +32 -4
  527. package/src/daemon/shutdown-registry.ts +40 -0
  528. package/src/daemon/tool-side-effects.ts +9 -9
  529. package/src/daemon/watch-handler.ts +4 -4
  530. package/src/daemon/web-search-history.ts +126 -0
  531. package/src/email/html-renderer.ts +76 -0
  532. package/src/events/domain-events.ts +0 -1
  533. package/src/filing/filing-service.ts +9 -10
  534. package/src/heartbeat/heartbeat-service.ts +156 -22
  535. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  536. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  537. package/src/home/__tests__/feed-scheduler.test.ts +222 -0
  538. package/src/home/__tests__/feed-types.test.ts +275 -0
  539. package/src/home/__tests__/feed-writer.test.ts +688 -0
  540. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  541. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  542. package/src/home/__tests__/progress-formula.test.ts +213 -0
  543. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  544. package/src/home/__tests__/rollup-producer.test.ts +442 -0
  545. package/src/home/assistant-feed-authoring.ts +128 -0
  546. package/src/home/emit-feed-event.ts +162 -0
  547. package/src/home/feed-scheduler.ts +263 -0
  548. package/src/home/feed-types.ts +235 -0
  549. package/src/home/feed-writer.ts +469 -0
  550. package/src/home/platform-gmail-digest.ts +163 -0
  551. package/src/home/progress-formula.ts +86 -0
  552. package/src/home/relationship-state-writer.ts +824 -0
  553. package/src/home/relationship-state.ts +143 -0
  554. package/src/home/rollup-producer.ts +413 -0
  555. package/src/home/suggested-prompts.ts +101 -0
  556. package/src/hooks/runner.ts +7 -0
  557. package/src/inbound/platform-callback-registration.ts +12 -3
  558. package/src/inbound/public-ingress-urls.ts +12 -0
  559. package/src/instrument.ts +1 -1
  560. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  561. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  562. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  563. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  564. package/src/ipc/__tests__/socket-path.test.ts +73 -0
  565. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  566. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  567. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  568. package/src/ipc/cli-client.ts +152 -0
  569. package/src/ipc/cli-server.ts +252 -0
  570. package/src/ipc/gateway-client.ts +180 -0
  571. package/src/ipc/routes/attachment.ts +114 -0
  572. package/src/ipc/routes/browser-context.ts +61 -0
  573. package/src/ipc/routes/browser.ts +96 -0
  574. package/src/ipc/routes/cache.ts +96 -0
  575. package/src/ipc/routes/index.ts +21 -0
  576. package/src/ipc/routes/task-queue.ts +226 -0
  577. package/src/ipc/routes/task.ts +173 -0
  578. package/src/ipc/routes/ui-request.ts +50 -0
  579. package/src/ipc/routes/wake-conversation.ts +19 -0
  580. package/src/ipc/routes/watcher.ts +203 -0
  581. package/src/ipc/socket-path.ts +100 -0
  582. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  583. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  584. package/src/memory/__tests__/conversation-analyze-job.test.ts +233 -0
  585. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  586. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  587. package/src/memory/admin.ts +18 -0
  588. package/src/memory/app-store.ts +1 -1
  589. package/src/memory/attachments-store.ts +70 -0
  590. package/src/memory/auto-analysis-enqueue.ts +127 -0
  591. package/src/memory/auto-analysis-guard.ts +27 -0
  592. package/src/memory/cleanup-schedule-state.ts +37 -0
  593. package/src/memory/conversation-analyze-job.ts +74 -0
  594. package/src/memory/conversation-attention-store.ts +13 -6
  595. package/src/memory/conversation-crud.ts +199 -0
  596. package/src/memory/conversation-disk-view.ts +7 -0
  597. package/src/memory/conversation-group-migration.ts +65 -1
  598. package/src/memory/conversation-queries.ts +6 -5
  599. package/src/memory/conversation-title-service.ts +7 -4
  600. package/src/memory/db-init.ts +8 -0
  601. package/src/memory/db-maintenance.ts +108 -0
  602. package/src/memory/db.ts +1 -0
  603. package/src/memory/embedding-backend.ts +1 -1
  604. package/src/memory/graph/compaction.ts +299 -0
  605. package/src/memory/graph/consolidation.ts +4 -4
  606. package/src/memory/graph/conversation-graph-memory.ts +104 -29
  607. package/src/memory/graph/extraction.test.ts +295 -2
  608. package/src/memory/graph/extraction.ts +181 -51
  609. package/src/memory/graph/graph-search.test.ts +92 -0
  610. package/src/memory/graph/graph-search.ts +4 -1
  611. package/src/memory/graph/narrative.ts +2 -2
  612. package/src/memory/graph/pattern-scan.ts +2 -2
  613. package/src/memory/graph/retriever.test.ts +459 -0
  614. package/src/memory/graph/retriever.ts +257 -66
  615. package/src/memory/graph/scoring.test.ts +186 -0
  616. package/src/memory/graph/scoring.ts +31 -1
  617. package/src/memory/graph/store.ts +41 -0
  618. package/src/memory/graph/tool-handlers.ts +27 -0
  619. package/src/memory/graph/tools.ts +6 -1
  620. package/src/memory/group-crud.ts +6 -1
  621. package/src/memory/indexer.ts +95 -16
  622. package/src/memory/job-handlers/cleanup.ts +11 -8
  623. package/src/memory/job-handlers/conversation-starters.ts +39 -30
  624. package/src/memory/job-handlers/summarization.ts +2 -2
  625. package/src/memory/job-utils.ts +7 -1
  626. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  627. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  628. package/src/memory/jobs-store.ts +106 -5
  629. package/src/memory/jobs-worker.ts +26 -9
  630. package/src/memory/llm-usage-store.ts +92 -56
  631. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  632. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  633. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  634. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  635. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  636. package/src/memory/migrations/index.ts +7 -0
  637. package/src/memory/migrations/registry.ts +8 -0
  638. package/src/memory/pkb/pkb-index.test.ts +368 -0
  639. package/src/memory/pkb/pkb-index.ts +255 -0
  640. package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
  641. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  642. package/src/memory/pkb/pkb-search.test.ts +438 -0
  643. package/src/memory/pkb/pkb-search.ts +137 -0
  644. package/src/memory/pkb/types.ts +53 -0
  645. package/src/memory/qdrant-client.ts +122 -1
  646. package/src/memory/qdrant-manager.ts +43 -16
  647. package/src/memory/schema/conversations.ts +2 -0
  648. package/src/memory/schema/oauth.ts +3 -0
  649. package/src/memory/slack-thread-store.ts +37 -0
  650. package/src/memory/usage-buckets.ts +396 -0
  651. package/src/messaging/providers/gmail/adapter.ts +6 -16
  652. package/src/messaging/providers/gmail/client.ts +79 -6
  653. package/src/messaging/providers/gmail/types.ts +7 -0
  654. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  655. package/src/messaging/providers/slack/adapter.ts +155 -38
  656. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  657. package/src/messaging/providers/slack/backfill.ts +101 -0
  658. package/src/messaging/providers/slack/client.ts +16 -0
  659. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  660. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  661. package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
  662. package/src/messaging/providers/slack/render-transcript.ts +443 -0
  663. package/src/messaging/providers/slack/types.ts +4 -0
  664. package/src/messaging/style-analyzer.ts +5 -2
  665. package/src/notifications/README.md +9 -5
  666. package/src/notifications/decision-engine.ts +6 -12
  667. package/src/notifications/preference-extractor.ts +2 -6
  668. package/src/notifications/signal.ts +5 -0
  669. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  670. package/src/oauth/byo-connection.test.ts +18 -1
  671. package/src/oauth/byo-connection.ts +3 -1
  672. package/src/oauth/connect-orchestrator.ts +2 -0
  673. package/src/oauth/connection-resolver.ts +6 -2
  674. package/src/oauth/connection.ts +2 -0
  675. package/src/oauth/oauth-store.ts +10 -0
  676. package/src/oauth/platform-connection.test.ts +145 -0
  677. package/src/oauth/platform-connection.ts +62 -31
  678. package/src/oauth/seed-providers.ts +10 -1
  679. package/src/permissions/approval-policy.test.ts +948 -0
  680. package/src/permissions/approval-policy.ts +257 -0
  681. package/src/permissions/bash-risk-classifier.test.ts +1208 -0
  682. package/src/permissions/bash-risk-classifier.ts +707 -0
  683. package/src/permissions/checker.ts +218 -699
  684. package/src/permissions/command-registry.test.ts +535 -0
  685. package/src/permissions/command-registry.ts +825 -0
  686. package/src/permissions/defaults.ts +71 -75
  687. package/src/permissions/file-risk-classifier.test.ts +535 -0
  688. package/src/permissions/file-risk-classifier.ts +274 -0
  689. package/src/permissions/risk-types.ts +205 -0
  690. package/src/permissions/secret-prompter.ts +53 -2
  691. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  692. package/src/permissions/skill-risk-classifier.ts +214 -0
  693. package/src/permissions/trust-client.ts +52 -25
  694. package/src/permissions/trust-store-interface.ts +1 -6
  695. package/src/permissions/trust-store.ts +164 -65
  696. package/src/permissions/types.ts +23 -14
  697. package/src/permissions/web-risk-classifier.test.ts +170 -0
  698. package/src/permissions/web-risk-classifier.ts +89 -0
  699. package/src/permissions/workspace-policy.ts +1 -13
  700. package/src/platform/client.test.ts +10 -0
  701. package/src/platform/client.ts +19 -1
  702. package/src/platform/sync-identity.ts +129 -0
  703. package/src/prompts/persona-resolver.ts +127 -3
  704. package/src/prompts/system-prompt.ts +78 -38
  705. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  706. package/src/prompts/templates/SOUL.md +5 -3
  707. package/src/prompts/templates/channels/slack.md +20 -0
  708. package/src/prompts/update-bulletin-job.ts +190 -0
  709. package/src/prompts/user-reference.ts +20 -17
  710. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  711. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  712. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  713. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  714. package/src/providers/anthropic/client.ts +335 -70
  715. package/src/providers/call-site-routing.ts +71 -0
  716. package/src/providers/fireworks/client.ts +2 -2
  717. package/src/providers/gemini/client.ts +74 -3
  718. package/src/providers/managed-proxy/constants.ts +2 -1
  719. package/src/providers/model-catalog.ts +502 -28
  720. package/src/providers/model-intents.ts +8 -8
  721. package/src/providers/ollama/client.ts +2 -2
  722. package/src/providers/openai/chat-completions-provider.ts +530 -0
  723. package/src/providers/openai/client.ts +25 -440
  724. package/src/providers/openai/responses-provider.ts +579 -0
  725. package/src/providers/openrouter/client.ts +168 -4
  726. package/src/providers/provider-env-vars.ts +56 -0
  727. package/src/providers/provider-secret-catalog.ts +139 -0
  728. package/src/providers/provider-send-message.ts +22 -5
  729. package/src/providers/ratelimit.ts +4 -0
  730. package/src/providers/registry.ts +21 -10
  731. package/src/providers/retry.ts +185 -39
  732. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  733. package/src/providers/speech-to-text/__tests__/resolve.test.ts +883 -0
  734. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  735. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  736. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  737. package/src/providers/speech-to-text/deepgram.ts +115 -0
  738. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  739. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  740. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  741. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  742. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  743. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  744. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  745. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  746. package/src/providers/speech-to-text/provider-catalog.ts +323 -0
  747. package/src/providers/speech-to-text/resolve.ts +393 -6
  748. package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
  749. package/src/providers/speech-to-text/xai-realtime.ts +796 -0
  750. package/src/providers/speech-to-text/xai.test.ts +155 -0
  751. package/src/providers/speech-to-text/xai.ts +97 -0
  752. package/src/providers/types.ts +102 -3
  753. package/src/runtime/AGENTS.md +45 -3
  754. package/src/runtime/__tests__/agent-wake.test.ts +872 -0
  755. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  756. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  757. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  758. package/src/runtime/agent-wake.ts +553 -0
  759. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  760. package/src/runtime/auth/route-policy.ts +34 -5
  761. package/src/runtime/auth/token-service.ts +56 -1
  762. package/src/runtime/btw-sidechain.ts +15 -3
  763. package/src/runtime/capability-tokens.ts +10 -10
  764. package/src/runtime/channel-invite-transport.ts +1 -1
  765. package/src/runtime/channel-invite-transports/email.ts +14 -6
  766. package/src/runtime/channel-readiness-service.ts +12 -22
  767. package/src/runtime/channel-reply-delivery.ts +106 -2
  768. package/src/runtime/chrome-extension-registry.ts +38 -2
  769. package/src/runtime/decision-token.ts +116 -0
  770. package/src/runtime/gateway-client.ts +2 -2
  771. package/src/runtime/http-router.ts +32 -0
  772. package/src/runtime/http-server.ts +447 -11
  773. package/src/runtime/http-types.ts +29 -3
  774. package/src/runtime/interactive-ui.ts +362 -0
  775. package/src/runtime/invite-instruction-generator.ts +2 -2
  776. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  777. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  778. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  779. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  780. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  781. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  782. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  783. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  784. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  785. package/src/runtime/migrations/migration-transport.ts +1 -0
  786. package/src/runtime/migrations/migration-wizard.ts +1 -0
  787. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  788. package/src/runtime/migrations/vbundle-importer.ts +187 -8
  789. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  790. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  791. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  792. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  793. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  794. package/src/runtime/pending-interactions.ts +0 -11
  795. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  796. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +618 -0
  797. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +247 -0
  798. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  799. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  800. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  801. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  802. package/src/runtime/routes/app-management-routes.ts +12 -18
  803. package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
  804. package/src/runtime/routes/approval-routes.ts +12 -17
  805. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  806. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  807. package/src/runtime/routes/attachment-routes.ts +216 -17
  808. package/src/runtime/routes/avatar-routes.ts +20 -4
  809. package/src/runtime/routes/backup-routes.ts +519 -0
  810. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  811. package/src/runtime/routes/btw-routes.ts +9 -10
  812. package/src/runtime/routes/contact-routes.test.ts +298 -0
  813. package/src/runtime/routes/contact-routes.ts +132 -5
  814. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  815. package/src/runtime/routes/conversation-management-routes.ts +133 -0
  816. package/src/runtime/routes/conversation-routes.ts +487 -160
  817. package/src/runtime/routes/debug-routes.ts +1 -1
  818. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  819. package/src/runtime/routes/events-routes.ts +16 -0
  820. package/src/runtime/routes/filing-routes.ts +93 -0
  821. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  822. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  823. package/src/runtime/routes/home-feed-routes.ts +452 -0
  824. package/src/runtime/routes/home-state-routes.ts +138 -0
  825. package/src/runtime/routes/host-browser-routes.ts +3 -14
  826. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  827. package/src/runtime/routes/identity-routes.ts +3 -17
  828. package/src/runtime/routes/inbound-message-handler.ts +912 -2
  829. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  830. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  831. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  832. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  833. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  834. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  835. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  836. package/src/runtime/routes/integrations/slack/channel.ts +36 -6
  837. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  838. package/src/runtime/routes/llm-context-normalization.ts +325 -0
  839. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  840. package/src/runtime/routes/migration-routes.ts +722 -91
  841. package/src/runtime/routes/settings-routes.ts +26 -7
  842. package/src/runtime/routes/skills-routes.ts +76 -7
  843. package/src/runtime/routes/stt-routes.ts +233 -0
  844. package/src/runtime/routes/surface-action-routes.ts +41 -2
  845. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  846. package/src/runtime/routes/tts-routes.ts +108 -24
  847. package/src/runtime/routes/usage-routes.ts +30 -2
  848. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  849. package/src/runtime/routes/user-routes.ts +13 -1
  850. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  851. package/src/runtime/routes/work-items-routes.ts +11 -3
  852. package/src/runtime/runtime-mode.ts +33 -0
  853. package/src/runtime/services/__tests__/analyze-conversation.test.ts +426 -0
  854. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  855. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  856. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  857. package/src/runtime/services/analyze-conversation.ts +340 -0
  858. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  859. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  860. package/src/runtime/skill-route-registry.ts +71 -0
  861. package/src/runtime/slack-block-formatting.ts +437 -10
  862. package/src/schedule/scheduler.ts +58 -0
  863. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  864. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  865. package/src/security/oauth2.ts +122 -37
  866. package/src/security/secure-keys.ts +32 -10
  867. package/src/security/token-manager.ts +35 -13
  868. package/src/security/untrusted-content.ts +102 -0
  869. package/src/sequence/engine.ts +23 -0
  870. package/src/sequence/types.ts +1 -1
  871. package/src/skills/catalog-cache.ts +26 -7
  872. package/src/skills/catalog-files.ts +64 -2
  873. package/src/skills/catalog-install.ts +31 -3
  874. package/src/skills/category-inference.ts +122 -0
  875. package/src/skills/clawhub-files.ts +213 -0
  876. package/src/skills/clawhub.ts +84 -23
  877. package/src/skills/skill-cache-store.ts +97 -0
  878. package/src/skills/skill-file-provider.ts +40 -0
  879. package/src/skills/skillssh-files.ts +395 -0
  880. package/src/skills/skillssh-registry.ts +4 -4
  881. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +468 -0
  882. package/src/stt/__tests__/types.test.ts +89 -0
  883. package/src/stt/daemon-batch-transcriber.ts +228 -0
  884. package/src/stt/stt-stream-session.ts +506 -0
  885. package/src/stt/types.ts +334 -0
  886. package/src/stt/wav-encoder.test.ts +373 -0
  887. package/src/stt/wav-encoder.ts +175 -0
  888. package/src/subagent/manager.ts +79 -27
  889. package/src/tasks/ephemeral-permissions.ts +9 -4
  890. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  891. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  892. package/src/tools/browser/__tests__/browser-status.test.ts +166 -0
  893. package/src/tools/browser/browser-execution.ts +1208 -41
  894. package/src/tools/browser/browser-manager.ts +45 -0
  895. package/src/tools/browser/browser-mode-constants.ts +12 -0
  896. package/src/tools/browser/browser-mode.ts +92 -0
  897. package/src/tools/browser/browser-status-constants.ts +33 -0
  898. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  899. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  900. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  901. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  902. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +205 -17
  903. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  904. package/src/tools/browser/cdp-client/errors.ts +15 -0
  905. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  906. package/src/tools/browser/cdp-client/factory.ts +797 -87
  907. package/src/tools/browser/cdp-client/index.ts +16 -2
  908. package/src/tools/browser/cdp-client/types.ts +68 -0
  909. package/src/tools/credentials/tool-policy.ts +39 -5
  910. package/src/tools/credentials/vault.ts +41 -7
  911. package/src/tools/executor.ts +4 -0
  912. package/src/tools/filesystem/write.ts +52 -0
  913. package/src/tools/host-terminal/host-shell.ts +45 -5
  914. package/src/tools/memory/register.test.ts +185 -0
  915. package/src/tools/memory/register.ts +3 -1
  916. package/src/tools/network/web-fetch.ts +25 -12
  917. package/src/tools/network/web-search.ts +20 -2
  918. package/src/tools/permission-checker.ts +36 -15
  919. package/src/tools/policy-context.ts +25 -8
  920. package/src/tools/registry.ts +55 -3
  921. package/src/tools/shared/shell-output.ts +3 -1
  922. package/src/tools/side-effects.ts +0 -9
  923. package/src/tools/skills/execute.ts +2 -2
  924. package/src/tools/skills/sandbox-runner.ts +6 -2
  925. package/src/tools/terminal/backends/native.ts +51 -2
  926. package/src/tools/terminal/safe-env.ts +11 -2
  927. package/src/tools/terminal/shell.ts +16 -4
  928. package/src/tools/tool-manifest.ts +6 -0
  929. package/src/tools/types.ts +29 -3
  930. package/src/tools/ui-surface/definitions.ts +6 -1
  931. package/src/tools/verification-control-plane-policy.ts +1 -1
  932. package/src/tts/__tests__/provider-adapters.test.ts +1061 -0
  933. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  934. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  935. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  936. package/src/tts/provider-catalog.ts +219 -0
  937. package/src/tts/provider-registry.ts +73 -0
  938. package/src/tts/providers/deepgram-provider.ts +219 -0
  939. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  940. package/src/tts/providers/fish-audio-provider.ts +183 -0
  941. package/src/tts/providers/index.ts +44 -0
  942. package/src/tts/providers/register-builtins.ts +130 -0
  943. package/src/tts/providers/xai-provider.ts +224 -0
  944. package/src/tts/synthesize-text.ts +110 -0
  945. package/src/tts/tts-config-resolver.ts +78 -0
  946. package/src/tts/types.ts +199 -0
  947. package/src/types/onboarding-context.ts +7 -0
  948. package/src/types/tar-stream.d.ts +66 -0
  949. package/src/util/abort-reasons.ts +58 -0
  950. package/src/util/device-id.ts +32 -16
  951. package/src/util/errors.ts +9 -1
  952. package/src/util/json.ts +17 -0
  953. package/src/util/platform.ts +56 -12
  954. package/src/util/pricing.ts +78 -5
  955. package/src/util/spawn.ts +1 -1
  956. package/src/util/truncate.ts +4 -2
  957. package/src/util/unicode.ts +201 -0
  958. package/src/version.ts +19 -24
  959. package/src/watcher/engine.ts +24 -1
  960. package/src/watcher/providers/google-calendar.ts +134 -8
  961. package/src/watcher/providers/outlook-calendar.ts +42 -2
  962. package/src/watcher/watcher-store.ts +31 -0
  963. package/src/workspace/git-service.ts +23 -4
  964. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  965. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  966. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  967. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  968. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  969. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  970. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  971. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  972. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  973. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  974. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  975. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  976. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  977. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  978. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
  979. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  980. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  981. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  982. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  983. package/src/workspace/migrations/AGENTS.md +1 -1
  984. package/src/workspace/migrations/registry.ts +32 -0
  985. package/src/workspace/provider-commit-message-generator.ts +19 -38
  986. package/src/workspace/top-level-renderer.ts +13 -1
  987. package/src/workspace/turn-commit.ts +31 -0
  988. package/src/__tests__/email-cli.test.ts +0 -297
  989. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  990. package/src/__tests__/outlook-attachments.test.ts +0 -301
  991. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  992. package/src/__tests__/outlook-categories.test.ts +0 -212
  993. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  994. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  995. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  996. package/src/__tests__/outlook-trash.test.ts +0 -77
  997. package/src/__tests__/outlook-unsubscribe.test.ts +0 -250
  998. package/src/__tests__/update-bulletin-format.test.ts +0 -122
  999. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  1000. package/src/__tests__/update-bulletin.test.ts +0 -277
  1001. package/src/__tests__/update-template-contract.test.ts +0 -29
  1002. package/src/cli/commands/browser-relay.ts +0 -466
  1003. package/src/cli/commands/doctor.ts +0 -341
  1004. package/src/config/bundled-skills/browser/SKILL.md +0 -63
  1005. package/src/config/bundled-skills/browser/TOOLS.json +0 -393
  1006. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  1007. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  1008. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  1009. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  1010. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  1011. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  1012. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  1013. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  1014. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  1015. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  1016. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  1017. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  1018. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -32
  1019. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  1020. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  1021. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  1022. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  1023. package/src/config/bundled-skills/gmail/SKILL.md +0 -175
  1024. package/src/config/bundled-skills/gmail/TOOLS.json +0 -558
  1025. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -149
  1026. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  1027. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  1028. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  1029. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  1030. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  1031. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  1032. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -220
  1033. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  1034. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -251
  1035. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  1036. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  1037. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  1038. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  1039. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  1040. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  1041. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  1042. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  1043. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  1044. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  1045. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  1046. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  1047. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  1048. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  1049. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  1050. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  1051. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  1052. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  1053. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  1054. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  1055. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  1056. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  1057. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  1058. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  1059. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  1060. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  1061. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  1062. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  1063. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  1064. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  1065. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  1066. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  1067. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  1068. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  1069. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  1070. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  1071. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  1072. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  1073. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  1074. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  1075. package/src/config/bundled-skills/slack/SKILL.md +0 -107
  1076. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  1077. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  1078. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  1079. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  1080. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  1081. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  1082. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  1083. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  1084. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  1085. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  1086. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  1087. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  1088. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  1089. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  1090. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  1091. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  1092. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  1093. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  1094. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  1095. package/src/email/guardrails.ts +0 -221
  1096. package/src/email/provider.ts +0 -117
  1097. package/src/email/providers/agentmail.ts +0 -361
  1098. package/src/email/providers/index.ts +0 -65
  1099. package/src/email/service.ts +0 -384
  1100. package/src/email/types.ts +0 -126
  1101. package/src/prompts/templates/UPDATES.md +0 -38
  1102. package/src/prompts/templates/USER.md +0 -13
  1103. package/src/prompts/update-bulletin-format.ts +0 -68
  1104. package/src/prompts/update-bulletin-state.ts +0 -58
  1105. package/src/prompts/update-bulletin-template-path.ts +0 -13
  1106. package/src/prompts/update-bulletin.ts +0 -128
  1107. package/src/providers/speech-to-text/types.ts +0 -17
  1108. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
  1109. package/src/shared/provider-env-vars.ts +0 -19
  1110. package/src/tools/watcher/create.ts +0 -86
  1111. package/src/tools/watcher/delete.ts +0 -36
  1112. package/src/tools/watcher/digest.ts +0 -54
  1113. package/src/tools/watcher/list.ts +0 -83
  1114. package/src/tools/watcher/update.ts +0 -71
@@ -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
@@ -69,9 +81,11 @@ function writeConfig(obj: unknown): void {
69
81
  describe("AssistantConfigSchema", () => {
70
82
  test("parses empty object with full defaults", () => {
71
83
  const result = AssistantConfigSchema.parse({});
72
- expect(result.services.inference.provider).toBe("anthropic");
73
- expect(result.services.inference.model).toBe("claude-opus-4-6");
84
+ // services.inference now carries only `mode`; provider/model live under
85
+ // llm.default.{provider,model} (see PR 19 of unify-llm-callsites).
74
86
  expect(result.services.inference.mode).toBe("your-own");
87
+ expect(result.llm.default.provider).toBe("anthropic");
88
+ expect(result.llm.default.model).toBe("claude-opus-4-7");
75
89
  expect(result.services["image-generation"].provider).toBe("gemini");
76
90
  expect(result.services["image-generation"].model).toBe(
77
91
  "gemini-3.1-flash-image-preview",
@@ -81,12 +95,12 @@ describe("AssistantConfigSchema", () => {
81
95
  "inference-provider-native",
82
96
  );
83
97
  expect(result.services["web-search"].mode).toBe("your-own");
84
- expect(result.maxTokens).toBe(64000);
85
- expect(result.thinking).toEqual({
98
+ expect(result.llm.default.maxTokens).toBe(64000);
99
+ expect(result.llm.default.thinking).toEqual({
86
100
  enabled: true,
87
101
  streamThinking: true,
88
102
  });
89
- expect(result.contextWindow).toEqual({
103
+ expect(result.llm.default.contextWindow).toEqual({
90
104
  enabled: true,
91
105
  maxInputTokens: 200000,
92
106
  targetBudgetRatio: 0.3,
@@ -122,11 +136,13 @@ describe("AssistantConfigSchema", () => {
122
136
 
123
137
  test("accepts valid complete config", () => {
124
138
  const input = {
125
- services: {
126
- inference: { provider: "openai", model: "gpt-4" },
139
+ llm: {
140
+ default: {
141
+ provider: "openai" as const,
142
+ model: "gpt-4",
143
+ maxTokens: 4096,
144
+ },
127
145
  },
128
- maxTokens: 4096,
129
- thinking: { enabled: true },
130
146
  timeouts: {
131
147
  shellDefaultTimeoutSec: 30,
132
148
  shellMaxTimeoutSec: 300,
@@ -142,13 +158,186 @@ describe("AssistantConfigSchema", () => {
142
158
  auditLog: { retentionDays: 30 },
143
159
  };
144
160
  const result = AssistantConfigSchema.parse(input);
145
- expect(result.services.inference.provider).toBe("openai");
146
- expect(result.services.inference.model).toBe("gpt-4");
147
- expect(result.maxTokens).toBe(4096);
148
- expect(result.thinking.enabled).toBe(true);
161
+ expect(result.llm.default.provider).toBe("openai");
162
+ expect(result.llm.default.model).toBe("gpt-4");
163
+ expect(result.llm.default.maxTokens).toBe(4096);
164
+ expect(result.llm.default.thinking.enabled).toBe(true);
149
165
  expect(result.secretDetection.action).toBe("block");
150
166
  });
151
167
 
168
+ test("applies llm defaults when llm key is omitted", () => {
169
+ const result = AssistantConfigSchema.parse({});
170
+ expect(result.llm).toBeDefined();
171
+ expect(result.llm.default).toEqual({
172
+ provider: "anthropic",
173
+ model: "claude-opus-4-7",
174
+ maxTokens: 64000,
175
+ effort: "max",
176
+ speed: "standard",
177
+ temperature: null,
178
+ thinking: { enabled: true, streamThinking: true },
179
+ contextWindow: {
180
+ enabled: true,
181
+ maxInputTokens: 200000,
182
+ targetBudgetRatio: 0.3,
183
+ compactThreshold: 0.8,
184
+ summaryBudgetRatio: 0.05,
185
+ overflowRecovery: {
186
+ enabled: true,
187
+ safetyMarginRatio: 0.05,
188
+ maxAttempts: 3,
189
+ interactiveLatestTurnCompression: "summarize",
190
+ nonInteractiveLatestTurnCompression: "truncate",
191
+ },
192
+ },
193
+ openrouter: { only: [] },
194
+ });
195
+ expect(result.llm.profiles).toEqual({});
196
+ expect(result.llm.callSites).toEqual({});
197
+ expect(result.llm.pricingOverrides).toEqual([]);
198
+ });
199
+
200
+ test("accepts an explicit llm block with profiles and call sites", () => {
201
+ const input = {
202
+ llm: {
203
+ default: {
204
+ provider: "anthropic" as const,
205
+ model: "claude-opus-4-7",
206
+ maxTokens: 32000,
207
+ effort: "high" as const,
208
+ speed: "fast" as const,
209
+ temperature: null,
210
+ thinking: { enabled: true, streamThinking: false },
211
+ contextWindow: {
212
+ enabled: true,
213
+ maxInputTokens: 200000,
214
+ targetBudgetRatio: 0.3,
215
+ compactThreshold: 0.8,
216
+ summaryBudgetRatio: 0.05,
217
+ overflowRecovery: {
218
+ enabled: true,
219
+ safetyMarginRatio: 0.05,
220
+ maxAttempts: 3,
221
+ interactiveLatestTurnCompression: "summarize" as const,
222
+ nonInteractiveLatestTurnCompression: "truncate" as const,
223
+ },
224
+ },
225
+ },
226
+ profiles: {
227
+ fast: { speed: "fast" as const, effort: "low" as const },
228
+ },
229
+ callSites: {
230
+ mainAgent: { profile: "fast" },
231
+ commitMessage: { maxTokens: 256 },
232
+ },
233
+ pricingOverrides: [],
234
+ },
235
+ };
236
+ const result = AssistantConfigSchema.parse(input);
237
+ expect(result.llm.default.model).toBe("claude-opus-4-7");
238
+ expect(result.llm.default.speed).toBe("fast");
239
+ expect(result.llm.profiles?.fast).toEqual({
240
+ speed: "fast",
241
+ effort: "low",
242
+ });
243
+ expect(result.llm.callSites?.mainAgent).toEqual({ profile: "fast" });
244
+ expect(result.llm.callSites?.commitMessage).toEqual({ maxTokens: 256 });
245
+ });
246
+
247
+ test("rejects an llm.callSites entry that references an undefined profile", () => {
248
+ const input = {
249
+ llm: {
250
+ default: {
251
+ provider: "anthropic" as const,
252
+ model: "claude-opus-4-6",
253
+ maxTokens: 64000,
254
+ effort: "max" as const,
255
+ speed: "standard" as const,
256
+ temperature: null,
257
+ thinking: { enabled: true, streamThinking: true },
258
+ contextWindow: {
259
+ enabled: true,
260
+ maxInputTokens: 200000,
261
+ targetBudgetRatio: 0.3,
262
+ compactThreshold: 0.8,
263
+ summaryBudgetRatio: 0.05,
264
+ overflowRecovery: {
265
+ enabled: true,
266
+ safetyMarginRatio: 0.05,
267
+ maxAttempts: 3,
268
+ interactiveLatestTurnCompression: "summarize" as const,
269
+ nonInteractiveLatestTurnCompression: "truncate" as const,
270
+ },
271
+ },
272
+ },
273
+ callSites: {
274
+ mainAgent: { profile: "missing-profile" },
275
+ },
276
+ },
277
+ };
278
+ expect(() => AssistantConfigSchema.parse(input)).toThrow(/missing-profile/);
279
+ });
280
+
281
+ test("legacy top-level inference keys are ignored after PR 19 cleanup", () => {
282
+ // The legacy keys (top-level maxTokens, effort, speed, thinking,
283
+ // contextWindow, services.inference.{provider,model}) were removed in PR
284
+ // 19. Configs that still carry them parse cleanly because Zod strips
285
+ // unknown fields, and migration 039 erases them from the on-disk file
286
+ // entirely.
287
+ const input = {
288
+ services: {
289
+ inference: { provider: "openai", model: "gpt-4" },
290
+ },
291
+ maxTokens: 8000,
292
+ effort: "medium",
293
+ speed: "fast",
294
+ thinking: { enabled: false, streamThinking: false },
295
+ };
296
+ const result = AssistantConfigSchema.parse(input);
297
+ expect((result as Record<string, unknown>).maxTokens).toBeUndefined();
298
+ expect((result as Record<string, unknown>).effort).toBeUndefined();
299
+ expect((result as Record<string, unknown>).speed).toBeUndefined();
300
+ expect((result as Record<string, unknown>).thinking).toBeUndefined();
301
+ expect(
302
+ (result.services.inference as Record<string, unknown>).provider,
303
+ ).toBeUndefined();
304
+ expect(
305
+ (result.services.inference as Record<string, unknown>).model,
306
+ ).toBeUndefined();
307
+ expect(result.llm.default.provider).toBe("anthropic");
308
+ expect(result.llm.default.model).toBe("claude-opus-4-7");
309
+ });
310
+
311
+ test("partial llm config (empty `llm: {}`) doesn't trigger full config reset", () => {
312
+ // Regression guard: previously LLMConfigBase had no schema-level defaults,
313
+ // so any `llm: {}` block would fail validation and the loader's recovery
314
+ // path would fall through to `cloneDefaultConfig()`, discarding unrelated
315
+ // valid settings (like a custom `llm.default.maxTokens`). With leaf-level
316
+ // defaults, `llm: {}` parses cleanly and the user's other settings are
317
+ // preserved.
318
+ const result = AssistantConfigSchema.parse({
319
+ llm: { default: { maxTokens: 32000 } },
320
+ });
321
+ expect(result.llm.default.maxTokens).toBe(32000);
322
+ expect(result.llm.default.provider).toBe("anthropic");
323
+ expect(result.llm.default.model).toBe("claude-opus-4-7");
324
+ });
325
+
326
+ test("llm.default with one missing field still parses (defaults applied)", () => {
327
+ // A user can override a single field of `llm.default` without specifying
328
+ // the rest — schema-level defaults fill in everything that wasn't set.
329
+ const result = AssistantConfigSchema.parse({
330
+ llm: { default: { model: "claude-haiku-4-5" } },
331
+ });
332
+ expect(result.llm.default.model).toBe("claude-haiku-4-5");
333
+ expect(result.llm.default.provider).toBe("anthropic");
334
+ expect(result.llm.default.maxTokens).toBe(64000);
335
+ expect(result.llm.default.thinking).toEqual({
336
+ enabled: true,
337
+ streamThinking: true,
338
+ });
339
+ });
340
+
152
341
  test("applies rollout defaults for dynamic budget", () => {
153
342
  const result = AssistantConfigSchema.parse({});
154
343
  expect(result.memory.retrieval.dynamicBudget).toEqual({
@@ -166,7 +355,7 @@ describe("AssistantConfigSchema", () => {
166
355
  enqueueIntervalMs: 6 * 60 * 60 * 1000,
167
356
  supersededItemRetentionMs: 30 * 24 * 60 * 60 * 1000,
168
357
  conversationRetentionDays: 0,
169
- llmRequestLogRetentionMs: 1 * 24 * 60 * 60 * 1000,
358
+ llmRequestLogRetentionMs: 1 * 60 * 60 * 1000,
170
359
  });
171
360
  });
172
361
 
@@ -177,15 +366,74 @@ describe("AssistantConfigSchema", () => {
177
366
  expect(result.success).toBe(false);
178
367
  });
179
368
 
369
+ test("accepts memory.cleanup.llmRequestLogRetentionMs at the 365-day boundary", () => {
370
+ const max = 365 * 24 * 60 * 60 * 1000;
371
+ const result = AssistantConfigSchema.safeParse({
372
+ memory: { cleanup: { llmRequestLogRetentionMs: max } },
373
+ });
374
+ expect(result.success).toBe(true);
375
+ if (result.success) {
376
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(max);
377
+ }
378
+ });
379
+
380
+ test("rejects memory.cleanup.llmRequestLogRetentionMs above 365 days", () => {
381
+ // This must match the gateway's MAX_LLM_REQUEST_LOG_RETENTION_MS. Without
382
+ // the Zod .max(), a manually edited config.json with a large value would
383
+ // be silently accepted by the daemon and then truncated by the macOS
384
+ // picker on the next PATCH — a quiet data-loss bug.
385
+ const overMax = 365 * 24 * 60 * 60 * 1000 + 1;
386
+ const result = AssistantConfigSchema.safeParse({
387
+ memory: { cleanup: { llmRequestLogRetentionMs: overMax } },
388
+ });
389
+ expect(result.success).toBe(false);
390
+ if (!result.success) {
391
+ expect(
392
+ result.error.issues.some((i) =>
393
+ i.path.includes("llmRequestLogRetentionMs"),
394
+ ),
395
+ ).toBe(true);
396
+ }
397
+ });
398
+
399
+ test("rejects negative memory.cleanup.llmRequestLogRetentionMs", () => {
400
+ const result = AssistantConfigSchema.safeParse({
401
+ memory: { cleanup: { llmRequestLogRetentionMs: -1 } },
402
+ });
403
+ expect(result.success).toBe(false);
404
+ });
405
+
406
+ test("accepts null memory.cleanup.llmRequestLogRetentionMs (keep forever)", () => {
407
+ const result = AssistantConfigSchema.safeParse({
408
+ memory: { cleanup: { llmRequestLogRetentionMs: null } },
409
+ });
410
+ expect(result.success).toBe(true);
411
+ if (result.success) {
412
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBeNull();
413
+ }
414
+ });
415
+
416
+ test("accepts memory.cleanup.llmRequestLogRetentionMs: 0 (prune immediately)", () => {
417
+ const result = AssistantConfigSchema.safeParse({
418
+ memory: { cleanup: { llmRequestLogRetentionMs: 0 } },
419
+ });
420
+ expect(result.success).toBe(true);
421
+ if (result.success) {
422
+ expect(result.data.memory.cleanup.llmRequestLogRetentionMs).toBe(0);
423
+ }
424
+ });
425
+
180
426
  test("rejects invalid provider", () => {
181
427
  const result = AssistantConfigSchema.safeParse({
182
- services: { inference: { provider: "invalid" } },
428
+ llm: { default: { provider: "invalid" } },
183
429
  });
184
430
  expect(result.success).toBe(false);
185
431
  });
186
432
 
187
- test("rejects negative maxTokens", () => {
188
- const result = AssistantConfigSchema.safeParse({ maxTokens: -100 });
433
+ test("rejects negative llm.default.maxTokens", () => {
434
+ const result = AssistantConfigSchema.safeParse({
435
+ llm: { default: { maxTokens: -100 } },
436
+ });
189
437
  expect(result.success).toBe(false);
190
438
  if (!result.success) {
191
439
  expect(
@@ -194,8 +442,10 @@ describe("AssistantConfigSchema", () => {
194
442
  }
195
443
  });
196
444
 
197
- test("rejects non-integer maxTokens", () => {
198
- const result = AssistantConfigSchema.safeParse({ maxTokens: 3.14 });
445
+ test("rejects non-integer llm.default.maxTokens", () => {
446
+ const result = AssistantConfigSchema.safeParse({
447
+ llm: { default: { maxTokens: 3.14 } },
448
+ });
199
449
  expect(result.success).toBe(false);
200
450
  if (!result.success) {
201
451
  expect(
@@ -204,9 +454,9 @@ describe("AssistantConfigSchema", () => {
204
454
  }
205
455
  });
206
456
 
207
- test("rejects string maxTokens", () => {
457
+ test("rejects string llm.default.maxTokens", () => {
208
458
  const result = AssistantConfigSchema.safeParse({
209
- maxTokens: "not-a-number",
459
+ llm: { default: { maxTokens: "not-a-number" } },
210
460
  });
211
461
  expect(result.success).toBe(false);
212
462
  if (!result.success) {
@@ -232,7 +482,7 @@ describe("AssistantConfigSchema", () => {
232
482
 
233
483
  test("rejects invalid thinking config", () => {
234
484
  const result = AssistantConfigSchema.safeParse({
235
- thinking: { enabled: "yes" },
485
+ llm: { default: { thinking: { enabled: "yes" } } },
236
486
  });
237
487
  expect(result.success).toBe(false);
238
488
  if (!result.success) {
@@ -242,16 +492,21 @@ describe("AssistantConfigSchema", () => {
242
492
 
243
493
  test("rejects contextWindow targetBudgetRatio >= compactThreshold", () => {
244
494
  const result = AssistantConfigSchema.safeParse({
245
- contextWindow: { targetBudgetRatio: 0.8, compactThreshold: 0.8 },
495
+ llm: {
496
+ default: {
497
+ contextWindow: { targetBudgetRatio: 0.8, compactThreshold: 0.8 },
498
+ },
499
+ },
246
500
  });
247
501
  expect(result.success).toBe(false);
248
502
  if (!result.success) {
249
503
  expect(
250
504
  result.error.issues.some(
251
505
  (issue) =>
252
- issue.path.join(".") === "contextWindow.targetBudgetRatio" &&
506
+ issue.path.join(".") ===
507
+ "llm.default.contextWindow.targetBudgetRatio" &&
253
508
  issue.message.includes(
254
- "must be less than contextWindow.compactThreshold",
509
+ "must be less than llm.default.contextWindow.compactThreshold",
255
510
  ),
256
511
  ),
257
512
  ).toBe(true);
@@ -261,7 +516,11 @@ describe("AssistantConfigSchema", () => {
261
516
  test("rejects overflowRecovery safetyMarginRatio out of (0,1) range", () => {
262
517
  for (const bad of [0, 1, -0.1, 1.5]) {
263
518
  const result = AssistantConfigSchema.safeParse({
264
- contextWindow: { overflowRecovery: { safetyMarginRatio: bad } },
519
+ llm: {
520
+ default: {
521
+ contextWindow: { overflowRecovery: { safetyMarginRatio: bad } },
522
+ },
523
+ },
265
524
  });
266
525
  expect(result.success).toBe(false);
267
526
  if (!result.success) {
@@ -276,8 +535,12 @@ describe("AssistantConfigSchema", () => {
276
535
 
277
536
  test("rejects invalid overflowRecovery interactiveLatestTurnCompression", () => {
278
537
  const result = AssistantConfigSchema.safeParse({
279
- contextWindow: {
280
- overflowRecovery: { interactiveLatestTurnCompression: "explode" },
538
+ llm: {
539
+ default: {
540
+ contextWindow: {
541
+ overflowRecovery: { interactiveLatestTurnCompression: "explode" },
542
+ },
543
+ },
281
544
  },
282
545
  });
283
546
  expect(result.success).toBe(false);
@@ -292,8 +555,12 @@ describe("AssistantConfigSchema", () => {
292
555
 
293
556
  test("rejects invalid overflowRecovery nonInteractiveLatestTurnCompression", () => {
294
557
  const result = AssistantConfigSchema.safeParse({
295
- contextWindow: {
296
- overflowRecovery: { nonInteractiveLatestTurnCompression: "nope" },
558
+ llm: {
559
+ default: {
560
+ contextWindow: {
561
+ overflowRecovery: { nonInteractiveLatestTurnCompression: "nope" },
562
+ },
563
+ },
297
564
  },
298
565
  });
299
566
  expect(result.success).toBe(false);
@@ -364,7 +631,7 @@ describe("AssistantConfigSchema", () => {
364
631
  "ollama",
365
632
  ] as const) {
366
633
  const result = AssistantConfigSchema.safeParse({
367
- services: { inference: { provider } },
634
+ llm: { default: { provider } },
368
635
  });
369
636
  expect(result.success).toBe(true);
370
637
  }
@@ -381,13 +648,19 @@ describe("AssistantConfigSchema", () => {
381
648
 
382
649
  test("provides helpful error messages", () => {
383
650
  const result = AssistantConfigSchema.safeParse({
384
- maxTokens: -1,
651
+ llm: { default: { maxTokens: -1 } },
385
652
  secretDetection: { action: "explode" },
386
653
  });
387
654
  expect(result.success).toBe(false);
388
655
  if (!result.success) {
389
656
  const messages = result.error.issues.map((i) => i.message);
390
- expect(messages.some((m) => m.includes("positive"))).toBe(true);
657
+ // The llm.default.maxTokens validation rejects -1 with a "Too small"
658
+ // / "expected number to be >0" message from Zod's default issue text.
659
+ expect(
660
+ messages.some(
661
+ (m) => m.includes("positive") || /expected number to be >0/i.test(m),
662
+ ),
663
+ ).toBe(true);
391
664
  expect(
392
665
  messages.some(
393
666
  (m) =>
@@ -402,6 +675,11 @@ describe("AssistantConfigSchema", () => {
402
675
  expect(result.permissions).toEqual({
403
676
  mode: "workspace",
404
677
  hostAccess: false,
678
+ autoApproveUpTo: {
679
+ conversation: "low",
680
+ background: "medium",
681
+ headless: "none",
682
+ },
405
683
  });
406
684
  });
407
685
 
@@ -438,6 +716,119 @@ describe("AssistantConfigSchema", () => {
438
716
  }
439
717
  });
440
718
 
719
+ test("defaults autoApproveUpTo to per-context object when not specified", () => {
720
+ const result = AssistantConfigSchema.parse({
721
+ permissions: { mode: "workspace" },
722
+ });
723
+ expect(result.permissions.autoApproveUpTo).toEqual({
724
+ conversation: "low",
725
+ background: "medium",
726
+ headless: "none",
727
+ });
728
+ });
729
+
730
+ test("accepts autoApproveUpTo none", () => {
731
+ const result = AssistantConfigSchema.parse({
732
+ permissions: { autoApproveUpTo: "none" },
733
+ });
734
+ expect(result.permissions.autoApproveUpTo).toBe("none");
735
+ });
736
+
737
+ test("accepts autoApproveUpTo low", () => {
738
+ const result = AssistantConfigSchema.parse({
739
+ permissions: { autoApproveUpTo: "low" },
740
+ });
741
+ expect(result.permissions.autoApproveUpTo).toBe("low");
742
+ });
743
+
744
+ test("accepts autoApproveUpTo medium", () => {
745
+ const result = AssistantConfigSchema.parse({
746
+ permissions: { autoApproveUpTo: "medium" },
747
+ });
748
+ expect(result.permissions.autoApproveUpTo).toBe("medium");
749
+ });
750
+
751
+ test("rejects autoApproveUpTo high", () => {
752
+ const result = AssistantConfigSchema.safeParse({
753
+ permissions: { autoApproveUpTo: "high" },
754
+ });
755
+ expect(result.success).toBe(false);
756
+ });
757
+
758
+ test("rejects invalid autoApproveUpTo string", () => {
759
+ const result = AssistantConfigSchema.safeParse({
760
+ permissions: { autoApproveUpTo: "everything" },
761
+ });
762
+ expect(result.success).toBe(false);
763
+ });
764
+
765
+ test("autoApproveUpTo round-trips through JSON serialization", () => {
766
+ const original = AssistantConfigSchema.parse({
767
+ permissions: { autoApproveUpTo: "medium" },
768
+ });
769
+ const json = JSON.stringify(original);
770
+ const parsed = AssistantConfigSchema.parse(JSON.parse(json));
771
+ expect(parsed.permissions.autoApproveUpTo).toBe("medium");
772
+ });
773
+
774
+ test("accepts autoApproveUpTo as per-context object", () => {
775
+ const result = AssistantConfigSchema.parse({
776
+ permissions: {
777
+ autoApproveUpTo: {
778
+ conversation: "low",
779
+ background: "medium",
780
+ headless: "none",
781
+ },
782
+ },
783
+ });
784
+ expect(result.permissions.autoApproveUpTo).toEqual({
785
+ conversation: "low",
786
+ background: "medium",
787
+ headless: "none",
788
+ });
789
+ });
790
+
791
+ test("per-context object applies defaults for omitted keys", () => {
792
+ const result = AssistantConfigSchema.parse({
793
+ permissions: {
794
+ autoApproveUpTo: {},
795
+ },
796
+ });
797
+ expect(result.permissions.autoApproveUpTo).toEqual({
798
+ conversation: "low",
799
+ background: "medium",
800
+ headless: "none",
801
+ });
802
+ });
803
+
804
+ test("per-context object rejects invalid enum values", () => {
805
+ const result = AssistantConfigSchema.safeParse({
806
+ permissions: {
807
+ autoApproveUpTo: { conversation: "high" },
808
+ },
809
+ });
810
+ expect(result.success).toBe(false);
811
+ });
812
+
813
+ test("per-context object round-trips through JSON serialization", () => {
814
+ const original = AssistantConfigSchema.parse({
815
+ permissions: {
816
+ autoApproveUpTo: {
817
+ conversation: "none",
818
+ background: "low",
819
+ headless: "medium",
820
+ },
821
+ },
822
+ });
823
+ const json = JSON.stringify(original);
824
+ const parsed = AssistantConfigSchema.parse(JSON.parse(json));
825
+ expect(parsed.permissions.autoApproveUpTo).toEqual({
826
+ conversation: "none",
827
+ background: "low",
828
+ headless: "medium",
829
+ });
830
+ });
831
+
441
832
  test("applies workspaceGit defaults including interactiveGitTimeoutMs", () => {
442
833
  const result = AssistantConfigSchema.parse({});
443
834
  expect(result.workspaceGit).toEqual({
@@ -451,11 +842,7 @@ describe("AssistantConfigSchema", () => {
451
842
  enrichmentMaxRetries: 2,
452
843
  commitMessageLLM: {
453
844
  enabled: false,
454
- useConfiguredProvider: true,
455
- providerFastModelOverrides: {},
456
845
  timeoutMs: 600,
457
- maxTokens: 120,
458
- temperature: 0.2,
459
846
  maxFilesInPrompt: 30,
460
847
  maxDiffBytes: 12000,
461
848
  minRemainingTurnBudgetMs: 1000,
@@ -509,11 +896,7 @@ describe("AssistantConfigSchema", () => {
509
896
  const result = AssistantConfigSchema.parse({});
510
897
  const llm = result.workspaceGit.commitMessageLLM;
511
898
  expect(llm.enabled).toBe(false);
512
- expect(llm.useConfiguredProvider).toBe(true);
513
- expect(llm.providerFastModelOverrides).toEqual({});
514
899
  expect(llm.timeoutMs).toBe(600);
515
- expect(llm.maxTokens).toBe(120);
516
- expect(llm.temperature).toBe(0.2);
517
900
  expect(llm.maxFilesInPrompt).toBe(30);
518
901
  expect(llm.maxDiffBytes).toBe(12000);
519
902
  expect(llm.minRemainingTurnBudgetMs).toBe(1000);
@@ -526,13 +909,6 @@ describe("AssistantConfigSchema", () => {
526
909
  expect(result.success).toBe(false);
527
910
  });
528
911
 
529
- test("rejects commitMessageLLM.temperature > 2", () => {
530
- const result = AssistantConfigSchema.safeParse({
531
- workspaceGit: { commitMessageLLM: { temperature: 2.5 } },
532
- });
533
- expect(result.success).toBe(false);
534
- });
535
-
536
912
  test("breaker settings have correct defaults", () => {
537
913
  const result = AssistantConfigSchema.parse({});
538
914
  const breaker = result.workspaceGit.commitMessageLLM.breaker;
@@ -547,14 +923,12 @@ describe("AssistantConfigSchema", () => {
547
923
  commitMessageLLM: {
548
924
  enabled: true,
549
925
  timeoutMs: 1000,
550
- temperature: 0.5,
551
926
  breaker: { openAfterFailures: 5 },
552
927
  },
553
928
  },
554
929
  });
555
930
  expect(result.workspaceGit.commitMessageLLM.enabled).toBe(true);
556
931
  expect(result.workspaceGit.commitMessageLLM.timeoutMs).toBe(1000);
557
- expect(result.workspaceGit.commitMessageLLM.temperature).toBe(0.5);
558
932
  expect(result.workspaceGit.commitMessageLLM.breaker.openAfterFailures).toBe(
559
933
  5,
560
934
  );
@@ -564,18 +938,18 @@ describe("AssistantConfigSchema", () => {
564
938
  );
565
939
  });
566
940
 
567
- test("rejects commitMessageLLM.temperature < 0", () => {
568
- const result = AssistantConfigSchema.safeParse({
569
- workspaceGit: { commitMessageLLM: { temperature: -0.1 } },
570
- });
571
- expect(result.success).toBe(false);
572
- });
573
-
574
- test("rejects non-integer commitMessageLLM.maxTokens", () => {
575
- const result = AssistantConfigSchema.safeParse({
576
- workspaceGit: { commitMessageLLM: { maxTokens: 3.5 } },
941
+ test("ignores legacy commitMessageLLM.{maxTokens,temperature} keys", () => {
942
+ // PR 19 removed maxTokens/temperature from the schema; Zod silently
943
+ // strips them on parse. Migration 039 erases them from disk so they
944
+ // don't accumulate over time.
945
+ const result = AssistantConfigSchema.parse({
946
+ workspaceGit: {
947
+ commitMessageLLM: { maxTokens: 200, temperature: 0.5 },
948
+ },
577
949
  });
578
- expect(result.success).toBe(false);
950
+ const cm = result.workspaceGit.commitMessageLLM as Record<string, unknown>;
951
+ expect(cm.maxTokens).toBeUndefined();
952
+ expect(cm.temperature).toBeUndefined();
579
953
  });
580
954
 
581
955
  // ── Calls config ────────────────────────────────────────────────────
@@ -602,8 +976,6 @@ describe("AssistantConfigSchema", () => {
602
976
  },
603
977
  voice: {
604
978
  language: "en-US",
605
- transcriptionProvider: "Deepgram",
606
- ttsProvider: "elevenlabs",
607
979
  hints: [],
608
980
  interruptSensitivity: "low",
609
981
  },
@@ -711,29 +1083,8 @@ describe("AssistantConfigSchema", () => {
711
1083
  test("config without calls.voice parses correctly and produces defaults", () => {
712
1084
  const result = AssistantConfigSchema.parse({});
713
1085
  expect(result.calls.voice.language).toBe("en-US");
714
- expect(result.calls.voice.transcriptionProvider).toBe("Deepgram");
715
- });
716
-
717
- test("elevenlabs tuning params have correct defaults", () => {
718
- const result = AssistantConfigSchema.parse({});
719
- expect(result.elevenlabs.voiceModelId).toBe("");
720
- expect(result.elevenlabs.speed).toBe(1.0);
721
- expect(result.elevenlabs.stability).toBe(0.5);
722
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
723
- });
724
-
725
- test("rejects elevenlabs.speed below 0.7", () => {
726
- const result = AssistantConfigSchema.safeParse({
727
- elevenlabs: { speed: 0.5 },
728
- });
729
- expect(result.success).toBe(false);
730
- });
731
-
732
- test("rejects elevenlabs.speed above 1.2", () => {
733
- const result = AssistantConfigSchema.safeParse({
734
- elevenlabs: { speed: 1.5 },
735
- });
736
- expect(result.success).toBe(false);
1086
+ expect(result.calls.voice.hints).toEqual([]);
1087
+ expect(result.calls.voice.interruptSensitivity).toBe("low");
737
1088
  });
738
1089
 
739
1090
  test("accepts valid calls.voice overrides", () => {
@@ -741,51 +1092,29 @@ describe("AssistantConfigSchema", () => {
741
1092
  calls: {
742
1093
  voice: {
743
1094
  language: "es-ES",
744
- transcriptionProvider: "Google",
745
1095
  },
746
1096
  },
747
- elevenlabs: {
748
- stability: 0.8,
749
- },
750
1097
  });
751
1098
  expect(result.calls.voice.language).toBe("es-ES");
752
- expect(result.calls.voice.transcriptionProvider).toBe("Google");
753
- expect(result.elevenlabs.stability).toBe(0.8);
754
- // Defaults preserved for unset fields
755
- expect(result.elevenlabs.voiceModelId).toBe("");
756
- expect(result.elevenlabs.similarityBoost).toBe(0.75);
757
- });
758
-
759
- test("rejects invalid calls.voice.transcriptionProvider", () => {
760
- const result = AssistantConfigSchema.safeParse({
761
- calls: { voice: { transcriptionProvider: "AWS" } },
762
- });
763
- expect(result.success).toBe(false);
764
- if (!result.success) {
765
- const msgs = result.error.issues.map((i) => i.message);
766
- expect(
767
- msgs.some((m) => m.includes("calls.voice.transcriptionProvider")),
768
- ).toBe(true);
769
- }
770
1099
  });
771
1100
 
772
- test("rejects elevenlabs.stability out of range", () => {
773
- const result = AssistantConfigSchema.safeParse({
774
- elevenlabs: { stability: 1.5 },
1101
+ test("transcriptionProvider is no longer part of the voice config schema", () => {
1102
+ // Zod strips unrecognized keys by default — the legacy field is silently ignored.
1103
+ const result = AssistantConfigSchema.parse({
1104
+ calls: { voice: { transcriptionProvider: "Google" } },
775
1105
  });
776
- expect(result.success).toBe(false);
1106
+ expect(
1107
+ (result.calls.voice as Record<string, unknown>).transcriptionProvider,
1108
+ ).toBeUndefined();
777
1109
  });
778
1110
 
779
- test("accepts optional calls.model", () => {
1111
+ test("legacy calls.model key is stripped after PR 19 cleanup", () => {
1112
+ // calls.model moved to llm.callSites.callAgent.model in PR 4 and the
1113
+ // legacy field was removed in PR 19. Zod silently strips unknown keys.
780
1114
  const result = AssistantConfigSchema.parse({
781
1115
  calls: { model: "claude-haiku-4-5-20251001" },
782
1116
  });
783
- expect(result.calls.model).toBe("claude-haiku-4-5-20251001");
784
- });
785
-
786
- test("calls.model is undefined by default", () => {
787
- const result = AssistantConfigSchema.parse({});
788
- expect(result.calls.model).toBeUndefined();
1117
+ expect((result.calls as Record<string, unknown>).model).toBeUndefined();
789
1118
  });
790
1119
 
791
1120
  // ── Caller identity config ────────────────────────────────────────
@@ -850,6 +1179,10 @@ describe("AssistantConfigSchema", () => {
850
1179
  host: "localhost",
851
1180
  port: 9222,
852
1181
  probeTimeoutMs: 500,
1182
+ desktopAuto: {
1183
+ enabled: true,
1184
+ cooldownMs: 30_000,
1185
+ },
853
1186
  },
854
1187
  });
855
1188
  });
@@ -958,68 +1291,481 @@ describe("AssistantConfigSchema", () => {
958
1291
  });
959
1292
  expect(result.success).toBe(false);
960
1293
  });
961
- });
962
1294
 
963
- // ---------------------------------------------------------------------------
964
- // Tests: Voice quality profile resolver
965
- // ---------------------------------------------------------------------------
1295
+ // ── services.tts config ──────────────────────────────────────────────
966
1296
 
967
- describe("resolveVoiceQualityProfile", () => {
968
- test("always returns ElevenLabs ttsProvider", () => {
969
- const config = AssistantConfigSchema.parse({});
970
- const profile = resolveVoiceQualityProfile(config);
971
- expect(profile.ttsProvider).toBe("ElevenLabs");
972
- expect(profile.transcriptionProvider).toBe("Deepgram");
1297
+ test("applies services.tts defaults when not specified", () => {
1298
+ const result = AssistantConfigSchema.parse({});
1299
+ expect(result.services.tts.mode).toBe("your-own");
1300
+ expect(result.services.tts.provider).toBe("elevenlabs");
1301
+ expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
1302
+ DEFAULT_ELEVENLABS_VOICE_ID,
1303
+ );
1304
+ expect(result.services.tts.providers.elevenlabs.speed).toBe(1.0);
1305
+ expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
1306
+ expect(result.services.tts.providers.elevenlabs.similarityBoost).toBe(0.75);
1307
+ expect(
1308
+ result.services.tts.providers.elevenlabs.conversationTimeoutSeconds,
1309
+ ).toBe(30);
1310
+ expect(result.services.tts.providers["fish-audio"].referenceId).toBe("");
1311
+ expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
1312
+ expect(result.services.tts.providers["fish-audio"].format).toBe("mp3");
1313
+ expect(result.services.tts.providers["fish-audio"].speed).toBe(1.0);
1314
+ expect(result.services.tts.providers.deepgram.model).toBe(
1315
+ "aura-asteria-en",
1316
+ );
1317
+ expect(result.services.tts.providers.deepgram.format).toBe("mp3");
973
1318
  });
974
1319
 
975
- test("uses shared elevenlabs.voiceId for voice", () => {
976
- const config = AssistantConfigSchema.parse({
977
- elevenlabs: { voiceId: "test-voice-id" },
1320
+ test("accepts valid services.tts provider override", () => {
1321
+ const result = AssistantConfigSchema.parse({
1322
+ services: { tts: { provider: "fish-audio" } },
978
1323
  });
979
- const profile = resolveVoiceQualityProfile(config);
980
- expect(profile.ttsProvider).toBe("ElevenLabs");
981
- expect(profile.voice).toBe("test-voice-id");
1324
+ expect(result.services.tts.provider).toBe("fish-audio");
1325
+ expect(result.services.tts.mode).toBe("your-own");
982
1326
  });
983
1327
 
984
- test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
985
- const config = AssistantConfigSchema.parse({});
986
- const profile = resolveVoiceQualityProfile(config);
987
- expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
1328
+ test("accepts deepgram as services.tts.provider", () => {
1329
+ const result = AssistantConfigSchema.parse({
1330
+ services: { tts: { provider: "deepgram" } },
1331
+ });
1332
+ expect(result.services.tts.provider).toBe("deepgram");
1333
+ expect(result.services.tts.mode).toBe("your-own");
988
1334
  });
989
1335
 
990
- test("applies voice tuning params from elevenlabs config", () => {
991
- const config = AssistantConfigSchema.parse({
992
- elevenlabs: {
993
- voiceId: "abc123",
994
- voiceModelId: "turbo_v2_5",
995
- speed: 0.9,
996
- stability: 0.8,
997
- similarityBoost: 0.9,
1336
+ test("accepts valid services.tts.providers.elevenlabs overrides", () => {
1337
+ const result = AssistantConfigSchema.parse({
1338
+ services: {
1339
+ tts: {
1340
+ providers: {
1341
+ elevenlabs: { voiceId: "custom-voice", speed: 0.8 },
1342
+ },
1343
+ },
998
1344
  },
999
1345
  });
1000
- const profile = resolveVoiceQualityProfile(config);
1001
- expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
1346
+ expect(result.services.tts.providers.elevenlabs.voiceId).toBe(
1347
+ "custom-voice",
1348
+ );
1349
+ expect(result.services.tts.providers.elevenlabs.speed).toBe(0.8);
1350
+ // Unset fields preserve defaults
1351
+ expect(result.services.tts.providers.elevenlabs.stability).toBe(0.5);
1002
1352
  });
1003
- });
1004
-
1005
- // ---------------------------------------------------------------------------
1006
- // Tests: buildElevenLabsVoiceSpec
1007
- // ---------------------------------------------------------------------------
1008
1353
 
1009
- describe("buildElevenLabsVoiceSpec", () => {
1010
- test("produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity", () => {
1011
- const spec = buildElevenLabsVoiceSpec({
1012
- voiceId: "abc123",
1013
- voiceModelId: "turbo_v2_5",
1014
- speed: 1.0,
1015
- stability: 0.5,
1016
- similarityBoost: 0.75,
1354
+ test("accepts valid services.tts.providers.fish-audio overrides", () => {
1355
+ const result = AssistantConfigSchema.parse({
1356
+ services: {
1357
+ tts: {
1358
+ providers: {
1359
+ "fish-audio": { referenceId: "my-voice", format: "wav" },
1360
+ },
1361
+ },
1362
+ },
1017
1363
  });
1018
- expect(spec).toBe("abc123-turbo_v2_5-1_0.5_0.75");
1364
+ expect(result.services.tts.providers["fish-audio"].referenceId).toBe(
1365
+ "my-voice",
1366
+ );
1367
+ expect(result.services.tts.providers["fish-audio"].format).toBe("wav");
1368
+ // Defaults preserved
1369
+ expect(result.services.tts.providers["fish-audio"].chunkLength).toBe(200);
1019
1370
  });
1020
1371
 
1021
- test("returns empty string when voiceId is empty", () => {
1022
- const spec = buildElevenLabsVoiceSpec({
1372
+ test("accepts valid services.tts.providers.deepgram overrides", () => {
1373
+ const result = AssistantConfigSchema.parse({
1374
+ services: {
1375
+ tts: {
1376
+ providers: {
1377
+ deepgram: { model: "aura-luna-en", format: "opus" },
1378
+ },
1379
+ },
1380
+ },
1381
+ });
1382
+ expect(result.services.tts.providers.deepgram.model).toBe("aura-luna-en");
1383
+ expect(result.services.tts.providers.deepgram.format).toBe("opus");
1384
+ });
1385
+
1386
+ test("rejects services.tts.mode = managed", () => {
1387
+ const result = AssistantConfigSchema.safeParse({
1388
+ services: { tts: { mode: "managed" } },
1389
+ });
1390
+ expect(result.success).toBe(false);
1391
+ if (!result.success) {
1392
+ const msgs = result.error.issues.map((i) => i.message);
1393
+ expect(
1394
+ msgs.some((m) => m.includes("your-own") || m.includes("managed")),
1395
+ ).toBe(true);
1396
+ }
1397
+ });
1398
+
1399
+ // ── hostBrowser.cdpInspect.desktopAuto config ───────────────────────
1400
+
1401
+ test("applies hostBrowser.cdpInspect.desktopAuto defaults", () => {
1402
+ const result = AssistantConfigSchema.parse({});
1403
+ expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
1404
+ enabled: true,
1405
+ cooldownMs: 30_000,
1406
+ });
1407
+ });
1408
+
1409
+ test("accepts hostBrowser.cdpInspect.desktopAuto overrides", () => {
1410
+ const result = AssistantConfigSchema.parse({
1411
+ hostBrowser: {
1412
+ cdpInspect: {
1413
+ desktopAuto: { enabled: false, cooldownMs: 10_000 },
1414
+ },
1415
+ },
1416
+ });
1417
+ expect(result.hostBrowser.cdpInspect.desktopAuto.enabled).toBe(false);
1418
+ expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(10_000);
1419
+ });
1420
+
1421
+ test("accepts hostBrowser.cdpInspect.desktopAuto.cooldownMs of 0 (disable cooldown)", () => {
1422
+ const result = AssistantConfigSchema.parse({
1423
+ hostBrowser: {
1424
+ cdpInspect: { desktopAuto: { cooldownMs: 0 } },
1425
+ },
1426
+ });
1427
+ expect(result.hostBrowser.cdpInspect.desktopAuto.cooldownMs).toBe(0);
1428
+ });
1429
+
1430
+ test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs below 0", () => {
1431
+ const result = AssistantConfigSchema.safeParse({
1432
+ hostBrowser: {
1433
+ cdpInspect: { desktopAuto: { cooldownMs: -1 } },
1434
+ },
1435
+ });
1436
+ expect(result.success).toBe(false);
1437
+ if (!result.success) {
1438
+ expect(
1439
+ result.error.issues.some((issue) =>
1440
+ issue.path.join(".").includes("cooldownMs"),
1441
+ ),
1442
+ ).toBe(true);
1443
+ }
1444
+ });
1445
+
1446
+ test("rejects invalid services.tts.provider", () => {
1447
+ const result = AssistantConfigSchema.safeParse({
1448
+ services: { tts: { provider: "aws-polly" } },
1449
+ });
1450
+ expect(result.success).toBe(false);
1451
+ if (!result.success) {
1452
+ const msgs = result.error.issues.map((i) => i.message);
1453
+ expect(msgs.some((m) => m.includes("services.tts.provider"))).toBe(true);
1454
+ }
1455
+ });
1456
+
1457
+ test("services.tts.mode only accepts your-own as literal", () => {
1458
+ // Explicit your-own should work
1459
+ const valid = TtsServiceSchema.safeParse({ mode: "your-own" });
1460
+ expect(valid.success).toBe(true);
1461
+
1462
+ // managed should be rejected
1463
+ const invalid = TtsServiceSchema.safeParse({ mode: "managed" });
1464
+ expect(invalid.success).toBe(false);
1465
+
1466
+ // Any other string should be rejected
1467
+ const invalid2 = TtsServiceSchema.safeParse({ mode: "self-hosted" });
1468
+ expect(invalid2.success).toBe(false);
1469
+ });
1470
+
1471
+ // ── services.stt config ──────────────────────────────────────────────
1472
+
1473
+ test("rejects services.stt without explicit provider", () => {
1474
+ const result = AssistantConfigSchema.safeParse({
1475
+ services: { stt: { mode: "your-own" } },
1476
+ });
1477
+ expect(result.success).toBe(false);
1478
+ if (!result.success) {
1479
+ expect(
1480
+ result.error.issues.some((i) => i.path.join(".").includes("provider")),
1481
+ ).toBe(true);
1482
+ }
1483
+ });
1484
+
1485
+ test("applies services.stt structural defaults when provider is explicit", () => {
1486
+ const result = AssistantConfigSchema.parse({
1487
+ services: { stt: { provider: "openai-whisper" } },
1488
+ });
1489
+ expect(result.services.stt.mode).toBe("your-own");
1490
+ expect(result.services.stt.provider).toBe("openai-whisper");
1491
+ // providers defaults to empty sparse map
1492
+ expect(result.services.stt.providers).toEqual({});
1493
+ });
1494
+
1495
+ test("accepts valid services.stt provider override", () => {
1496
+ const result = AssistantConfigSchema.parse({
1497
+ services: { stt: { provider: "openai-whisper" } },
1498
+ });
1499
+ expect(result.services.stt.provider).toBe("openai-whisper");
1500
+ expect(result.services.stt.mode).toBe("your-own");
1501
+ });
1502
+
1503
+ test("accepts valid services.stt.providers.openai-whisper overrides", () => {
1504
+ const result = AssistantConfigSchema.parse({
1505
+ services: {
1506
+ stt: {
1507
+ provider: "openai-whisper",
1508
+ providers: {
1509
+ "openai-whisper": {},
1510
+ },
1511
+ },
1512
+ },
1513
+ });
1514
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1515
+ });
1516
+
1517
+ test("parses when providers map is empty (sparse default)", () => {
1518
+ const result = AssistantConfigSchema.parse({
1519
+ services: { stt: { provider: "deepgram", providers: {} } },
1520
+ });
1521
+ expect(result.services.stt.providers).toEqual({});
1522
+ expect(result.services.stt.provider).toBe("deepgram");
1523
+ });
1524
+
1525
+ test("parses when unknown future provider blobs exist under providers", () => {
1526
+ const result = AssistantConfigSchema.parse({
1527
+ services: {
1528
+ stt: {
1529
+ provider: "openai-whisper",
1530
+ providers: {
1531
+ "openai-whisper": {},
1532
+ "future-provider": { model: "next-gen", lang: "en" },
1533
+ },
1534
+ },
1535
+ },
1536
+ });
1537
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1538
+ expect(result.services.stt.providers["future-provider"]).toEqual({
1539
+ model: "next-gen",
1540
+ lang: "en",
1541
+ });
1542
+ });
1543
+
1544
+ test("rejects services.stt.mode = managed", () => {
1545
+ const result = AssistantConfigSchema.safeParse({
1546
+ services: { stt: { mode: "managed" } },
1547
+ });
1548
+ expect(result.success).toBe(false);
1549
+ if (!result.success) {
1550
+ const msgs = result.error.issues.map((i) => i.message);
1551
+ expect(
1552
+ msgs.some((m) => m.includes("your-own") || m.includes("managed")),
1553
+ ).toBe(true);
1554
+ }
1555
+ });
1556
+
1557
+ test("rejects invalid services.stt.provider", () => {
1558
+ const result = AssistantConfigSchema.safeParse({
1559
+ services: { stt: { provider: "azure-speech" } },
1560
+ });
1561
+ expect(result.success).toBe(false);
1562
+ if (!result.success) {
1563
+ const msgs = result.error.issues.map((i) => i.message);
1564
+ expect(msgs.some((m) => m.includes("services.stt.provider"))).toBe(true);
1565
+ }
1566
+ });
1567
+
1568
+ test("accepts deepgram as services.stt.provider", () => {
1569
+ const result = AssistantConfigSchema.parse({
1570
+ services: { stt: { provider: "deepgram" } },
1571
+ });
1572
+ expect(result.services.stt.provider).toBe("deepgram");
1573
+ expect(result.services.stt.mode).toBe("your-own");
1574
+ });
1575
+
1576
+ test("accepts google-gemini as services.stt.provider", () => {
1577
+ const result = AssistantConfigSchema.parse({
1578
+ services: { stt: { provider: "google-gemini" } },
1579
+ });
1580
+ expect(result.services.stt.provider).toBe("google-gemini");
1581
+ expect(result.services.stt.mode).toBe("your-own");
1582
+ });
1583
+
1584
+ test("applies services.stt structural defaults when google-gemini provider is explicit", () => {
1585
+ const result = AssistantConfigSchema.parse({
1586
+ services: { stt: { provider: "google-gemini" } },
1587
+ });
1588
+ expect(result.services.stt.mode).toBe("your-own");
1589
+ expect(result.services.stt.provider).toBe("google-gemini");
1590
+ expect(result.services.stt.providers).toEqual({});
1591
+ });
1592
+
1593
+ test("accepts valid services.stt.providers.deepgram overrides", () => {
1594
+ const result = AssistantConfigSchema.parse({
1595
+ services: {
1596
+ stt: {
1597
+ provider: "deepgram",
1598
+ providers: {
1599
+ deepgram: {},
1600
+ },
1601
+ },
1602
+ },
1603
+ });
1604
+ expect(result.services.stt.providers.deepgram).toEqual({});
1605
+ });
1606
+
1607
+ test("existing configs with explicit per-provider objects continue to parse", () => {
1608
+ // Configs with explicit per-provider objects must continue to
1609
+ // parse and round-trip successfully.
1610
+ const result = AssistantConfigSchema.parse({
1611
+ services: {
1612
+ stt: {
1613
+ provider: "openai-whisper",
1614
+ providers: {
1615
+ "openai-whisper": {},
1616
+ deepgram: {},
1617
+ },
1618
+ },
1619
+ },
1620
+ });
1621
+ expect(result.services.stt.providers["openai-whisper"]).toEqual({});
1622
+ expect(result.services.stt.providers.deepgram).toEqual({});
1623
+ });
1624
+
1625
+ test("services.stt.provider is required (no implicit default)", () => {
1626
+ const result = AssistantConfigSchema.safeParse({
1627
+ services: { stt: {} },
1628
+ });
1629
+ expect(result.success).toBe(false);
1630
+ });
1631
+
1632
+ test("services.stt.mode only accepts your-own as literal", () => {
1633
+ // Explicit your-own should work
1634
+ const valid = SttServiceSchema.safeParse({
1635
+ mode: "your-own",
1636
+ provider: "openai-whisper",
1637
+ });
1638
+ expect(valid.success).toBe(true);
1639
+
1640
+ // managed should be rejected
1641
+ const invalid = SttServiceSchema.safeParse({
1642
+ mode: "managed",
1643
+ provider: "openai-whisper",
1644
+ });
1645
+ expect(invalid.success).toBe(false);
1646
+
1647
+ // Any other string should be rejected
1648
+ const invalid2 = SttServiceSchema.safeParse({
1649
+ mode: "self-hosted",
1650
+ provider: "openai-whisper",
1651
+ });
1652
+ expect(invalid2.success).toBe(false);
1653
+ });
1654
+
1655
+ test("rejects hostBrowser.cdpInspect.desktopAuto.cooldownMs above 300000", () => {
1656
+ const result = AssistantConfigSchema.safeParse({
1657
+ hostBrowser: {
1658
+ cdpInspect: { desktopAuto: { cooldownMs: 500_000 } },
1659
+ },
1660
+ });
1661
+ expect(result.success).toBe(false);
1662
+ if (!result.success) {
1663
+ expect(
1664
+ result.error.issues.some((issue) =>
1665
+ issue.path.join(".").includes("cooldownMs"),
1666
+ ),
1667
+ ).toBe(true);
1668
+ }
1669
+ });
1670
+
1671
+ test("rejects non-integer hostBrowser.cdpInspect.desktopAuto.cooldownMs", () => {
1672
+ const result = AssistantConfigSchema.safeParse({
1673
+ hostBrowser: {
1674
+ cdpInspect: { desktopAuto: { cooldownMs: 5000.5 } },
1675
+ },
1676
+ });
1677
+ expect(result.success).toBe(false);
1678
+ });
1679
+
1680
+ test("rejects non-boolean hostBrowser.cdpInspect.desktopAuto.enabled", () => {
1681
+ const result = AssistantConfigSchema.safeParse({
1682
+ hostBrowser: {
1683
+ cdpInspect: { desktopAuto: { enabled: "yes" } },
1684
+ },
1685
+ });
1686
+ expect(result.success).toBe(false);
1687
+ });
1688
+
1689
+ test("desktopAuto defaults preserved when only cdpInspect.enabled is set", () => {
1690
+ const result = AssistantConfigSchema.parse({
1691
+ hostBrowser: { cdpInspect: { enabled: true } },
1692
+ });
1693
+ expect(result.hostBrowser.cdpInspect.desktopAuto).toEqual({
1694
+ enabled: true,
1695
+ cooldownMs: 30_000,
1696
+ });
1697
+ });
1698
+ });
1699
+
1700
+ // ---------------------------------------------------------------------------
1701
+ // Tests: Voice quality profile resolver
1702
+ // ---------------------------------------------------------------------------
1703
+
1704
+ describe("resolveVoiceQualityProfile", () => {
1705
+ test("always returns ElevenLabs ttsProvider", () => {
1706
+ const config = AssistantConfigSchema.parse({});
1707
+ const profile = resolveVoiceQualityProfile(config);
1708
+ expect(profile.ttsProvider).toBe("ElevenLabs");
1709
+ });
1710
+
1711
+ test("uses services.tts.providers.elevenlabs.voiceId for voice", () => {
1712
+ const config = AssistantConfigSchema.parse({
1713
+ services: {
1714
+ tts: {
1715
+ providers: { elevenlabs: { voiceId: "test-voice-id" } },
1716
+ },
1717
+ },
1718
+ });
1719
+ const profile = resolveVoiceQualityProfile(config);
1720
+ expect(profile.ttsProvider).toBe("ElevenLabs");
1721
+ expect(profile.voice).toBe("test-voice-id");
1722
+ });
1723
+
1724
+ test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
1725
+ const config = AssistantConfigSchema.parse({});
1726
+ const profile = resolveVoiceQualityProfile(config);
1727
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
1728
+ });
1729
+
1730
+ test("applies voice tuning params from services.tts.providers.elevenlabs config", () => {
1731
+ const config = AssistantConfigSchema.parse({
1732
+ services: {
1733
+ tts: {
1734
+ providers: {
1735
+ elevenlabs: {
1736
+ voiceId: "abc123",
1737
+ voiceModelId: "turbo_v2_5",
1738
+ speed: 0.9,
1739
+ stability: 0.8,
1740
+ similarityBoost: 0.9,
1741
+ },
1742
+ },
1743
+ },
1744
+ },
1745
+ });
1746
+ const profile = resolveVoiceQualityProfile(config);
1747
+ expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
1748
+ });
1749
+ });
1750
+
1751
+ // ---------------------------------------------------------------------------
1752
+ // Tests: buildElevenLabsVoiceSpec
1753
+ // ---------------------------------------------------------------------------
1754
+
1755
+ describe("buildElevenLabsVoiceSpec", () => {
1756
+ test("produces Twilio-compliant voice string: voiceId-model-speed_stability_similarity", () => {
1757
+ const spec = buildElevenLabsVoiceSpec({
1758
+ voiceId: "abc123",
1759
+ voiceModelId: "turbo_v2_5",
1760
+ speed: 1.0,
1761
+ stability: 0.5,
1762
+ similarityBoost: 0.75,
1763
+ });
1764
+ expect(spec).toBe("abc123-turbo_v2_5-1_0.5_0.75");
1765
+ });
1766
+
1767
+ test("returns empty string when voiceId is empty", () => {
1768
+ const spec = buildElevenLabsVoiceSpec({
1023
1769
  voiceId: "",
1024
1770
  voiceModelId: "turbo_v2_5",
1025
1771
  speed: 1.0,
@@ -1042,13 +1788,424 @@ describe("buildElevenLabsVoiceSpec", () => {
1042
1788
 
1043
1789
  test("default config uses a bare voiceId when no model override is set", () => {
1044
1790
  const config = AssistantConfigSchema.parse({
1045
- elevenlabs: { voiceId: "test" },
1791
+ services: {
1792
+ tts: {
1793
+ providers: { elevenlabs: { voiceId: "test" } },
1794
+ },
1795
+ },
1046
1796
  });
1047
- const spec = buildElevenLabsVoiceSpec(config.elevenlabs);
1797
+ const spec = buildElevenLabsVoiceSpec(
1798
+ config.services.tts.providers.elevenlabs,
1799
+ );
1048
1800
  expect(spec).toBe("test");
1049
1801
  });
1050
1802
  });
1051
1803
 
1804
+ // ---------------------------------------------------------------------------
1805
+ // Tests: TTS config resolver
1806
+ // ---------------------------------------------------------------------------
1807
+
1808
+ describe("resolveTtsConfig", () => {
1809
+ test("returns default provider and config from empty config", () => {
1810
+ const config = AssistantConfigSchema.parse({});
1811
+ const resolved = resolveTtsConfig(config);
1812
+ expect(resolved.provider).toBe("elevenlabs");
1813
+ expect(resolved.providerConfig).toMatchObject({
1814
+ voiceId: DEFAULT_ELEVENLABS_VOICE_ID,
1815
+ speed: 1.0,
1816
+ stability: 0.5,
1817
+ similarityBoost: 0.75,
1818
+ });
1819
+ });
1820
+
1821
+ test("uses canonical services.tts.provider when set", () => {
1822
+ const config = AssistantConfigSchema.parse({
1823
+ services: { tts: { provider: "fish-audio" } },
1824
+ });
1825
+ const resolved = resolveTtsConfig(config);
1826
+ expect(resolved.provider).toBe("fish-audio");
1827
+ expect(resolved.providerConfig).toMatchObject({
1828
+ referenceId: "",
1829
+ chunkLength: 200,
1830
+ format: "mp3",
1831
+ speed: 1.0,
1832
+ });
1833
+ });
1834
+
1835
+ test("returns canonical elevenlabs config from services.tts.providers", () => {
1836
+ const config = AssistantConfigSchema.parse({
1837
+ services: {
1838
+ tts: {
1839
+ provider: "elevenlabs",
1840
+ providers: {
1841
+ elevenlabs: { voiceId: "canonical-voice", stability: 0.9 },
1842
+ },
1843
+ },
1844
+ },
1845
+ });
1846
+ const resolved = resolveTtsConfig(config);
1847
+ expect(resolved.provider).toBe("elevenlabs");
1848
+ expect(resolved.providerConfig).toMatchObject({
1849
+ voiceId: "canonical-voice",
1850
+ stability: 0.9,
1851
+ });
1852
+ });
1853
+
1854
+ test("uses canonical elevenlabs config exclusively (no legacy fallback)", () => {
1855
+ const config = AssistantConfigSchema.parse({
1856
+ services: {
1857
+ tts: {
1858
+ providers: {
1859
+ elevenlabs: { voiceId: "canonical-voice", speed: 0.9 },
1860
+ },
1861
+ },
1862
+ },
1863
+ });
1864
+ const resolved = resolveTtsConfig(config);
1865
+ expect(resolved.provider).toBe("elevenlabs");
1866
+ expect(resolved.providerConfig).toMatchObject({
1867
+ voiceId: "canonical-voice",
1868
+ speed: 0.9,
1869
+ });
1870
+ });
1871
+
1872
+ test("uses canonical fish-audio config exclusively (no legacy fallback)", () => {
1873
+ const config = AssistantConfigSchema.parse({
1874
+ services: {
1875
+ tts: {
1876
+ provider: "fish-audio",
1877
+ providers: {
1878
+ "fish-audio": { referenceId: "canonical-ref", format: "wav" },
1879
+ },
1880
+ },
1881
+ },
1882
+ });
1883
+ const resolved = resolveTtsConfig(config);
1884
+ expect(resolved.provider).toBe("fish-audio");
1885
+ expect(resolved.providerConfig).toMatchObject({
1886
+ referenceId: "canonical-ref",
1887
+ format: "wav",
1888
+ });
1889
+ });
1890
+
1891
+ test("returns empty config for unknown provider", () => {
1892
+ // Force an unknown provider via type assertion for coverage.
1893
+ // structuredClone prevents mutation from leaking into Zod's shared
1894
+ // default objects (Zod 4 stores defaults by reference).
1895
+ const config = structuredClone(
1896
+ AssistantConfigSchema.parse({}),
1897
+ ) as AssistantConfig;
1898
+ (config.services.tts as { provider: string }).provider = "aws-polly";
1899
+ const resolved = resolveTtsConfig(config);
1900
+ expect(resolved.provider).toBe("aws-polly");
1901
+ expect(resolved.providerConfig).toEqual({});
1902
+ });
1903
+
1904
+ test("unknown provider resolution is deterministic across repeated calls", () => {
1905
+ const config = structuredClone(
1906
+ AssistantConfigSchema.parse({}),
1907
+ ) as AssistantConfig;
1908
+ (config.services.tts as { provider: string }).provider = "nonexistent";
1909
+ const first = resolveTtsConfig(config);
1910
+ const second = resolveTtsConfig(config);
1911
+ expect(first).toEqual(second);
1912
+ expect(first.providerConfig).toEqual({});
1913
+ });
1914
+ });
1915
+
1916
+ // ---------------------------------------------------------------------------
1917
+ // Tests: TTS provider catalog integration
1918
+ // ---------------------------------------------------------------------------
1919
+
1920
+ describe("TTS provider catalog integration", () => {
1921
+ test("VALID_TTS_SERVICE_PROVIDERS matches catalog provider IDs", () => {
1922
+ const catalogIds = listCatalogProviderIds();
1923
+ expect([...VALID_TTS_SERVICE_PROVIDERS]).toEqual(catalogIds);
1924
+ });
1925
+
1926
+ test("schema accepts all catalog provider IDs as services.tts.provider", () => {
1927
+ for (const providerId of listCatalogProviderIds()) {
1928
+ const result = AssistantConfigSchema.safeParse({
1929
+ services: { tts: { provider: providerId } },
1930
+ });
1931
+ expect(result.success).toBe(true);
1932
+ if (result.success) {
1933
+ expect(result.data.services.tts.provider).toBe(providerId);
1934
+ }
1935
+ }
1936
+ });
1937
+
1938
+ test("TtsProvidersSchema has a key for every catalog provider", () => {
1939
+ const parsed = AssistantConfigSchema.parse({});
1940
+ const providerKeys = Object.keys(parsed.services.tts.providers);
1941
+ for (const providerId of listCatalogProviderIds()) {
1942
+ expect(providerKeys).toContain(providerId);
1943
+ }
1944
+ });
1945
+
1946
+ test("resolveTtsConfig returns correct defaults for each catalog provider", () => {
1947
+ for (const providerId of listCatalogProviderIds()) {
1948
+ const config = AssistantConfigSchema.parse({
1949
+ services: { tts: { provider: providerId } },
1950
+ });
1951
+ const resolved = resolveTtsConfig(config);
1952
+ expect(resolved.provider).toBe(providerId);
1953
+ // Every catalog provider should resolve to a non-empty config object
1954
+ expect(Object.keys(resolved.providerConfig).length).toBeGreaterThan(0);
1955
+ }
1956
+ });
1957
+
1958
+ test("resolveTtsConfig returns overridden values for elevenlabs", () => {
1959
+ const config = AssistantConfigSchema.parse({
1960
+ services: {
1961
+ tts: {
1962
+ provider: "elevenlabs",
1963
+ providers: {
1964
+ elevenlabs: { voiceId: "override-voice", speed: 0.7 },
1965
+ },
1966
+ },
1967
+ },
1968
+ });
1969
+ const resolved = resolveTtsConfig(config);
1970
+ expect(resolved.provider).toBe("elevenlabs");
1971
+ expect(resolved.providerConfig).toMatchObject({
1972
+ voiceId: "override-voice",
1973
+ speed: 0.7,
1974
+ // Defaults still present for unset fields
1975
+ stability: 0.5,
1976
+ similarityBoost: 0.75,
1977
+ });
1978
+ });
1979
+
1980
+ test("resolveTtsConfig returns overridden values for fish-audio", () => {
1981
+ const config = AssistantConfigSchema.parse({
1982
+ services: {
1983
+ tts: {
1984
+ provider: "fish-audio",
1985
+ providers: {
1986
+ "fish-audio": {
1987
+ referenceId: "override-ref",
1988
+ format: "opus",
1989
+ speed: 1.5,
1990
+ },
1991
+ },
1992
+ },
1993
+ },
1994
+ });
1995
+ const resolved = resolveTtsConfig(config);
1996
+ expect(resolved.provider).toBe("fish-audio");
1997
+ expect(resolved.providerConfig).toMatchObject({
1998
+ referenceId: "override-ref",
1999
+ format: "opus",
2000
+ speed: 1.5,
2001
+ // Defaults for unset fields
2002
+ chunkLength: 200,
2003
+ });
2004
+ });
2005
+ });
2006
+
2007
+ // ---------------------------------------------------------------------------
2008
+ // Tests: TTS migration 032
2009
+ // ---------------------------------------------------------------------------
2010
+
2011
+ describe("032-tts-provider-unification migration", () => {
2012
+ const migrationDir = join(WORKSPACE_DIR, "_mig032");
2013
+
2014
+ beforeEach(() => {
2015
+ if (existsSync(migrationDir)) {
2016
+ rmSync(migrationDir, { recursive: true, force: true });
2017
+ }
2018
+ mkdirSync(migrationDir, { recursive: true });
2019
+ });
2020
+
2021
+ afterEach(() => {
2022
+ if (existsSync(migrationDir)) {
2023
+ rmSync(migrationDir, { recursive: true, force: true });
2024
+ }
2025
+ });
2026
+
2027
+ function writeMigConfig(obj: unknown): void {
2028
+ writeFileSync(
2029
+ join(migrationDir, "config.json"),
2030
+ JSON.stringify(obj, null, 2),
2031
+ );
2032
+ }
2033
+
2034
+ function readMigConfig(): Record<string, unknown> {
2035
+ return JSON.parse(
2036
+ readFileSync(join(migrationDir, "config.json"), "utf-8"),
2037
+ ) as Record<string, unknown>;
2038
+ }
2039
+
2040
+ test("backfills provider from calls.voice.ttsProvider", async () => {
2041
+ writeMigConfig({
2042
+ calls: { voice: { ttsProvider: "fish-audio" } },
2043
+ });
2044
+ const { ttsProviderUnificationMigration } =
2045
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2046
+ await ttsProviderUnificationMigration.run(migrationDir);
2047
+ const result = readMigConfig();
2048
+ const tts = (result.services as Record<string, unknown>).tts as Record<
2049
+ string,
2050
+ unknown
2051
+ >;
2052
+ expect(tts.provider).toBe("fish-audio");
2053
+ expect(tts.mode).toBe("your-own");
2054
+ });
2055
+
2056
+ test("backfills elevenlabs provider config from legacy keys", async () => {
2057
+ writeMigConfig({
2058
+ calls: { voice: { ttsProvider: "elevenlabs" } },
2059
+ elevenlabs: { voiceId: "my-voice", speed: 0.8 },
2060
+ });
2061
+ const { ttsProviderUnificationMigration } =
2062
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2063
+ await ttsProviderUnificationMigration.run(migrationDir);
2064
+ const result = readMigConfig();
2065
+ const tts = (result.services as Record<string, unknown>).tts as Record<
2066
+ string,
2067
+ unknown
2068
+ >;
2069
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
2070
+ expect(providers.elevenlabs.voiceId).toBe("my-voice");
2071
+ expect(providers.elevenlabs.speed).toBe(0.8);
2072
+ });
2073
+
2074
+ test("backfills fish-audio provider config from legacy keys", async () => {
2075
+ writeMigConfig({
2076
+ calls: { voice: { ttsProvider: "fish-audio" } },
2077
+ fishAudio: { referenceId: "my-ref", format: "wav" },
2078
+ });
2079
+ const { ttsProviderUnificationMigration } =
2080
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2081
+ await ttsProviderUnificationMigration.run(migrationDir);
2082
+ const result = readMigConfig();
2083
+ const tts = (result.services as Record<string, unknown>).tts as Record<
2084
+ string,
2085
+ unknown
2086
+ >;
2087
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
2088
+ expect(providers["fish-audio"].referenceId).toBe("my-ref");
2089
+ expect(providers["fish-audio"].format).toBe("wav");
2090
+ });
2091
+
2092
+ test("removes legacy fields after migration", async () => {
2093
+ writeMigConfig({
2094
+ calls: { voice: { ttsProvider: "elevenlabs", language: "en-US" } },
2095
+ elevenlabs: { voiceId: "my-voice" },
2096
+ });
2097
+ const { ttsProviderUnificationMigration } =
2098
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2099
+ await ttsProviderUnificationMigration.run(migrationDir);
2100
+ const result = readMigConfig();
2101
+ // Legacy keys removed
2102
+ expect(
2103
+ (
2104
+ (result.calls as Record<string, unknown>).voice as Record<
2105
+ string,
2106
+ unknown
2107
+ >
2108
+ ).ttsProvider,
2109
+ ).toBeUndefined();
2110
+ expect(result.elevenlabs).toBeUndefined();
2111
+ // Other voice fields preserved
2112
+ expect(
2113
+ (
2114
+ (result.calls as Record<string, unknown>).voice as Record<
2115
+ string,
2116
+ unknown
2117
+ >
2118
+ ).language,
2119
+ ).toBe("en-US");
2120
+ });
2121
+
2122
+ test("is idempotent — repeated runs produce no changes", async () => {
2123
+ writeMigConfig({
2124
+ calls: { voice: { ttsProvider: "fish-audio" } },
2125
+ fishAudio: { referenceId: "my-ref" },
2126
+ });
2127
+ const { ttsProviderUnificationMigration } =
2128
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2129
+ await ttsProviderUnificationMigration.run(migrationDir);
2130
+ const afterFirst = readMigConfig();
2131
+ await ttsProviderUnificationMigration.run(migrationDir);
2132
+ const afterSecond = readMigConfig();
2133
+ expect(afterSecond).toEqual(afterFirst);
2134
+ });
2135
+
2136
+ test("does not overwrite existing services.tts.provider", async () => {
2137
+ writeMigConfig({
2138
+ services: { tts: { provider: "elevenlabs" } },
2139
+ calls: { voice: { ttsProvider: "fish-audio" } },
2140
+ });
2141
+ const { ttsProviderUnificationMigration } =
2142
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2143
+ await ttsProviderUnificationMigration.run(migrationDir);
2144
+ const result = readMigConfig();
2145
+ const tts = (result.services as Record<string, unknown>).tts as Record<
2146
+ string,
2147
+ unknown
2148
+ >;
2149
+ // Should keep the existing canonical value, not the legacy one
2150
+ expect(tts.provider).toBe("elevenlabs");
2151
+ });
2152
+
2153
+ test("does not overwrite existing canonical provider config keys", async () => {
2154
+ writeMigConfig({
2155
+ services: {
2156
+ tts: {
2157
+ providers: {
2158
+ elevenlabs: { voiceId: "canonical-voice" },
2159
+ },
2160
+ },
2161
+ },
2162
+ elevenlabs: { voiceId: "legacy-voice", speed: 0.8 },
2163
+ });
2164
+ const { ttsProviderUnificationMigration } =
2165
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2166
+ await ttsProviderUnificationMigration.run(migrationDir);
2167
+ const result = readMigConfig();
2168
+ const tts = (result.services as Record<string, unknown>).tts as Record<
2169
+ string,
2170
+ unknown
2171
+ >;
2172
+ const providers = tts.providers as Record<string, Record<string, unknown>>;
2173
+ // Canonical voiceId preserved, legacy speed backfilled
2174
+ expect(providers.elevenlabs.voiceId).toBe("canonical-voice");
2175
+ expect(providers.elevenlabs.speed).toBe(0.8);
2176
+ // Legacy top-level key removed
2177
+ expect(result.elevenlabs).toBeUndefined();
2178
+ });
2179
+
2180
+ test("skips config without any legacy TTS fields", async () => {
2181
+ writeMigConfig({ maxTokens: 4096 });
2182
+ const { ttsProviderUnificationMigration } =
2183
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2184
+ const before = readMigConfig();
2185
+ await ttsProviderUnificationMigration.run(migrationDir);
2186
+ const after = readMigConfig();
2187
+ // Should remain unchanged (no services.tts added)
2188
+ expect(after).toEqual(before);
2189
+ });
2190
+
2191
+ test("down removes services.tts from config", async () => {
2192
+ writeMigConfig({
2193
+ services: {
2194
+ inference: { provider: "anthropic" },
2195
+ tts: { provider: "elevenlabs", mode: "your-own" },
2196
+ },
2197
+ });
2198
+ const { ttsProviderUnificationMigration } =
2199
+ await import("../workspace/migrations/032-tts-provider-unification.js");
2200
+ await ttsProviderUnificationMigration.down(migrationDir);
2201
+ const result = readMigConfig();
2202
+ const services = result.services as Record<string, unknown>;
2203
+ expect(services.tts).toBeUndefined();
2204
+ // Other services keys preserved
2205
+ expect(services.inference).toBeDefined();
2206
+ });
2207
+ });
2208
+
1052
2209
  // ---------------------------------------------------------------------------
1053
2210
  // Tests: loader integration (config file -> loadConfig with fallback)
1054
2211
  // ---------------------------------------------------------------------------
@@ -1083,28 +2240,27 @@ describe("loadConfig with schema validation", () => {
1083
2240
  // intermittently trigger unhandled ENOENT in CI if the directory is removed.
1084
2241
  test("loads valid config", () => {
1085
2242
  writeConfig({
1086
- services: {
1087
- inference: { provider: "openai", model: "gpt-4" },
2243
+ llm: {
2244
+ default: { provider: "openai", model: "gpt-4", maxTokens: 4096 },
1088
2245
  },
1089
- maxTokens: 4096,
1090
2246
  });
1091
2247
  const config = loadConfig();
1092
- expect(config.services.inference.provider).toBe("openai");
1093
- expect(config.services.inference.model).toBe("gpt-4");
1094
- expect(config.maxTokens).toBe(4096);
2248
+ expect(config.llm.default.provider).toBe("openai");
2249
+ expect(config.llm.default.model).toBe("gpt-4");
2250
+ expect(config.llm.default.maxTokens).toBe(4096);
1095
2251
  });
1096
2252
 
1097
2253
  test("applies defaults for missing fields", () => {
1098
2254
  writeConfig({});
1099
2255
  const config = loadConfig();
1100
- expect(config.services.inference.provider).toBe("anthropic");
1101
- expect(config.services.inference.model).toBe("claude-opus-4-6");
1102
- expect(config.maxTokens).toBe(64000);
1103
- expect(config.thinking).toEqual({
2256
+ expect(config.llm.default.provider).toBe("anthropic");
2257
+ expect(config.llm.default.model).toBe("claude-opus-4-7");
2258
+ expect(config.llm.default.maxTokens).toBe(64000);
2259
+ expect(config.llm.default.thinking).toEqual({
1104
2260
  enabled: true,
1105
2261
  streamThinking: true,
1106
2262
  });
1107
- expect(config.contextWindow).toEqual({
2263
+ expect(config.llm.default.contextWindow).toEqual({
1108
2264
  enabled: true,
1109
2265
  maxInputTokens: 200000,
1110
2266
  targetBudgetRatio: 0.3,
@@ -1122,16 +2278,16 @@ describe("loadConfig with schema validation", () => {
1122
2278
 
1123
2279
  test("falls back to default for invalid provider", () => {
1124
2280
  writeConfig({
1125
- services: { inference: { provider: "invalid-provider" } },
2281
+ llm: { default: { provider: "invalid-provider" } },
1126
2282
  });
1127
2283
  const config = loadConfig();
1128
- expect(config.services.inference.provider).toBe("anthropic");
2284
+ expect(config.llm.default.provider).toBe("anthropic");
1129
2285
  });
1130
2286
 
1131
2287
  test("falls back to default for invalid maxTokens", () => {
1132
- writeConfig({ maxTokens: -100 });
2288
+ writeConfig({ llm: { default: { maxTokens: -100 } } });
1133
2289
  const config = loadConfig();
1134
- expect(config.maxTokens).toBe(64000);
2290
+ expect(config.llm.default.maxTokens).toBe(64000);
1135
2291
  });
1136
2292
 
1137
2293
  test("falls back to defaults for invalid nested values", () => {
@@ -1146,23 +2302,26 @@ describe("loadConfig with schema validation", () => {
1146
2302
 
1147
2303
  test("preserves valid fields when other fields are invalid", () => {
1148
2304
  writeConfig({
1149
- services: {
1150
- inference: { provider: "openai", model: "gpt-4" },
2305
+ llm: {
2306
+ default: {
2307
+ provider: "openai",
2308
+ model: "gpt-4",
2309
+ maxTokens: -1,
2310
+ thinking: { enabled: true },
2311
+ },
1151
2312
  },
1152
- maxTokens: -1,
1153
- thinking: { enabled: true },
1154
2313
  });
1155
2314
  const config = loadConfig();
1156
- expect(config.services.inference.provider).toBe("openai");
1157
- expect(config.services.inference.model).toBe("gpt-4");
1158
- expect(config.thinking.enabled).toBe(true);
1159
- expect(config.maxTokens).toBe(64000);
2315
+ expect(config.llm.default.provider).toBe("openai");
2316
+ expect(config.llm.default.model).toBe("gpt-4");
2317
+ expect(config.llm.default.thinking.enabled).toBe(true);
2318
+ expect(config.llm.default.maxTokens).toBe(64000);
1160
2319
  });
1161
2320
 
1162
2321
  test("handles no config file", () => {
1163
2322
  const config = loadConfig();
1164
- expect(config.services.inference.provider).toBe("anthropic");
1165
- expect(config.maxTokens).toBe(64000);
2323
+ expect(config.llm.default.provider).toBe("anthropic");
2324
+ expect(config.llm.default.maxTokens).toBe(64000);
1166
2325
  });
1167
2326
 
1168
2327
  test("partial nested objects get defaults for missing fields", () => {
@@ -1183,11 +2342,15 @@ describe("loadConfig with schema validation", () => {
1183
2342
 
1184
2343
  test("falls back for invalid contextWindow relationship", () => {
1185
2344
  writeConfig({
1186
- contextWindow: { targetBudgetRatio: 0.8, compactThreshold: 0.8 },
2345
+ llm: {
2346
+ default: {
2347
+ contextWindow: { targetBudgetRatio: 0.8, compactThreshold: 0.8 },
2348
+ },
2349
+ },
1187
2350
  });
1188
2351
  const config = loadConfig();
1189
- expect(config.contextWindow.targetBudgetRatio).toBe(0.3);
1190
- expect(config.contextWindow.compactThreshold).toBe(0.8);
2352
+ expect(config.llm.default.contextWindow.targetBudgetRatio).toBe(0.3);
2353
+ expect(config.llm.default.contextWindow.compactThreshold).toBe(0.8);
1191
2354
  });
1192
2355
 
1193
2356
  test("falls back for invalid rateLimit values", () => {
@@ -1210,6 +2373,11 @@ describe("loadConfig with schema validation", () => {
1210
2373
  expect(config.permissions).toEqual({
1211
2374
  mode: "workspace",
1212
2375
  hostAccess: false,
2376
+ autoApproveUpTo: {
2377
+ conversation: "low",
2378
+ background: "medium",
2379
+ headless: "none",
2380
+ },
1213
2381
  });
1214
2382
  });
1215
2383
 
@@ -1244,6 +2412,93 @@ describe("loadConfig with schema validation", () => {
1244
2412
  expect(config.calls.provider).toBe("twilio");
1245
2413
  });
1246
2414
 
2415
+ test("recovers from partial filing.activeHours without wiping unrelated fields", () => {
2416
+ // Only activeHoursStart is set. The superRefine must emit the issue so
2417
+ // the loader's delete-and-retry can strip the set field; otherwise the
2418
+ // mismatch persists and the config falls back to full defaults (which
2419
+ // would reset llm.default.maxTokens below to 64000).
2420
+ writeConfig({
2421
+ llm: { default: { maxTokens: 4096 } },
2422
+ filing: { activeHoursStart: 8 },
2423
+ });
2424
+ const config = loadConfig();
2425
+ expect(config.llm.default.maxTokens).toBe(4096);
2426
+ expect(config.filing.activeHoursStart).toBeNull();
2427
+ expect(config.filing.activeHoursEnd).toBeNull();
2428
+ });
2429
+
2430
+ test("recovers from partial heartbeat.activeHours without wiping unrelated fields", () => {
2431
+ // activeHoursStart is explicitly nulled while activeHoursEnd defaults to
2432
+ // 22 — a mismatch. Dual-emit strips both sides; both defaults restore
2433
+ // (8, 22). llm.default.maxTokens is unaffected.
2434
+ writeConfig({
2435
+ llm: { default: { maxTokens: 4096 } },
2436
+ heartbeat: { activeHoursStart: null },
2437
+ });
2438
+ const config = loadConfig();
2439
+ expect(config.llm.default.maxTokens).toBe(4096);
2440
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2441
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2442
+ });
2443
+
2444
+ test("recovers from heartbeat.activeHours null-mismatch where explicit value equals opposite default", () => {
2445
+ // { start: null, end: 8 } — single-emit on the null side would strip
2446
+ // start, the default 8 would restore it, and the equal-hours check would
2447
+ // fire, cascading to a full defaults reset that wipes llm.default.maxTokens.
2448
+ // Dual-emit strips both sides in one pass.
2449
+ writeConfig({
2450
+ llm: { default: { maxTokens: 4096 } },
2451
+ heartbeat: { activeHoursStart: null, activeHoursEnd: 8 },
2452
+ });
2453
+ const config = loadConfig();
2454
+ expect(config.llm.default.maxTokens).toBe(4096);
2455
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2456
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2457
+ });
2458
+
2459
+ test("recovers from heartbeat.activeHours null-mismatch on the end side", () => {
2460
+ // { start: 22, end: null } — same cascade class as above, mirrored.
2461
+ writeConfig({
2462
+ llm: { default: { maxTokens: 4096 } },
2463
+ heartbeat: { activeHoursStart: 22, activeHoursEnd: null },
2464
+ });
2465
+ const config = loadConfig();
2466
+ expect(config.llm.default.maxTokens).toBe(4096);
2467
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2468
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2469
+ });
2470
+
2471
+ test("recovers from equal heartbeat.activeHours without wiping unrelated fields", () => {
2472
+ // { start: 22, end: 22 } — both equal to the default for end. Single-emit
2473
+ // on one path would strip one side, the default would recreate the
2474
+ // equal-hours mismatch, and the loader would fall back to full defaults,
2475
+ // wiping llm.default.maxTokens. Dual-emit strips both sides at once.
2476
+ writeConfig({
2477
+ llm: { default: { maxTokens: 4096 } },
2478
+ heartbeat: { activeHoursStart: 22, activeHoursEnd: 22 },
2479
+ });
2480
+ const config = loadConfig();
2481
+ expect(config.llm.default.maxTokens).toBe(4096);
2482
+ expect(config.heartbeat.activeHoursStart).toBe(8);
2483
+ expect(config.heartbeat.activeHoursEnd).toBe(22);
2484
+ });
2485
+
2486
+ test("recovers from equal filing.activeHours without wiping unrelated fields", () => {
2487
+ // activeHoursStart === activeHoursEnd is invalid (empty window). Filing's
2488
+ // defaults are null/null, so single-emit on one path would strip one side
2489
+ // and the null default would recreate a mismatch — cascading to a full
2490
+ // defaults reset that wipes llm.default.maxTokens. Dual-emit strips both
2491
+ // sides so both defaults restore to null.
2492
+ writeConfig({
2493
+ llm: { default: { maxTokens: 1234 } },
2494
+ filing: { activeHoursStart: 5, activeHoursEnd: 5 },
2495
+ });
2496
+ const config = loadConfig();
2497
+ expect(config.llm.default.maxTokens).toBe(1234);
2498
+ expect(config.filing.activeHoursStart).toBeNull();
2499
+ expect(config.filing.activeHoursEnd).toBeNull();
2500
+ });
2501
+
1247
2502
  test("applies calls defaults when not specified", () => {
1248
2503
  writeConfig({});
1249
2504
  const config = loadConfig();
@@ -1253,8 +2508,13 @@ describe("loadConfig with schema validation", () => {
1253
2508
  expect(config.calls.disclosure.enabled).toBe(true);
1254
2509
  expect(config.calls.safety.denyCategories).toEqual([]);
1255
2510
  expect(config.calls.voice.language).toBe("en-US");
1256
- expect(config.calls.voice.transcriptionProvider).toBe("Deepgram");
1257
- expect(config.calls.model).toBeUndefined();
2511
+ expect(
2512
+ (config.calls.voice as Record<string, unknown>).transcriptionProvider,
2513
+ ).toBeUndefined();
2514
+ expect(
2515
+ (config.calls.voice as Record<string, unknown>).ttsProvider,
2516
+ ).toBeUndefined();
2517
+ expect((config.calls as Record<string, unknown>).model).toBeUndefined();
1258
2518
  expect(config.calls.callerIdentity).toEqual({
1259
2519
  allowPerCallOverride: true,
1260
2520
  });