@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -9,8 +9,10 @@
9
9
  */
10
10
 
11
11
  import { getGatewayInternalBaseUrl } from "../config/env.js";
12
+ import { loadConfig } from "../config/loader.js";
12
13
  import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
13
14
  import type { ServerMessage } from "../daemon/message-protocol.js";
15
+ import { getPublicBaseUrl } from "../inbound/public-ingress-urls.js";
14
16
  import {
15
17
  expireCanonicalGuardianRequest,
16
18
  getCanonicalRequestByPendingQuestionId,
@@ -22,6 +24,7 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
22
24
  import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
23
25
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
24
26
  import { getLogger } from "../util/logger.js";
27
+ import { createStreamingEntry } from "./audio-store.js";
25
28
  import {
26
29
  getMaxCallDurationMs,
27
30
  getSilenceTimeoutMs,
@@ -42,10 +45,12 @@ import {
42
45
  updateCallSession,
43
46
  } from "./call-store.js";
44
47
  import { finalizeCall } from "./finalize-call.js";
48
+ import { synthesizeWithFishAudio } from "./fish-audio-client.js";
45
49
  import { sendGuardianExpiryNotices } from "./guardian-action-sweep.js";
46
50
  import { dispatchGuardianQuestion } from "./guardian-dispatch.js";
47
51
  import type { RelayConnection } from "./relay-server.js";
48
52
  import type { PromptSpeakerContext } from "./speaker-identification.js";
53
+ import { sanitizeForTts } from "./tts-text-sanitizer.js";
49
54
  import {
50
55
  ASK_GUARDIAN_CAPTURE_REGEX,
51
56
  CALL_OPENING_ACK_MARKER,
@@ -56,6 +61,7 @@ import {
56
61
  extractBalancedJson,
57
62
  stripInternalSpeechMarkers,
58
63
  } from "./voice-control-protocol.js";
64
+ import { isFishAudioTts } from "./voice-quality.js";
59
65
  import {
60
66
  startVoiceTurn,
61
67
  type VoiceTurnHandle,
@@ -101,6 +107,8 @@ export class CallController {
101
107
  private task: string | null;
102
108
  /** True when the call session was created via the inbound path (no outbound task). */
103
109
  private isInbound: boolean;
110
+ /** When true, the disclosure announcement is skipped for this call. */
111
+ private skipDisclosure: boolean;
104
112
  /** Instructions queued while an LLM turn is in-flight or during pending guardian input */
105
113
  private pendingInstructions: string[] = [];
106
114
  /** Ensures the call opener is triggered at most once per call. */
@@ -131,6 +139,8 @@ export class CallController {
131
139
  * without blocking the caller.
132
140
  */
133
141
  private guardianUnavailableForCall = false;
142
+ /** Active Fish Audio session — tracked so interrupt handling can close it. */
143
+ private activeFishAbort: AbortController | null = null;
134
144
 
135
145
  constructor(
136
146
  callSessionId: string,
@@ -150,9 +160,10 @@ export class CallController {
150
160
  this.assistantId = opts?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
151
161
  this.trustContext = opts?.trustContext ?? null;
152
162
 
153
- // Resolve the conversation ID from the call session
163
+ // Resolve the conversation ID and skipDisclosure from the call session
154
164
  const session = getCallSession(callSessionId);
155
165
  this.conversationId = session?.conversationId ?? callSessionId;
166
+ this.skipDisclosure = session?.skipDisclosure ?? false;
156
167
 
157
168
  this.startDurationTimer();
158
169
  this.resetSilenceTimer();
@@ -340,6 +351,11 @@ export class CallController {
340
351
  const wasSpeaking = this.state === "speaking";
341
352
  this.abortCurrentTurn();
342
353
  this.llmRunVersion++;
354
+ // Cancel in-flight Fish Audio synthesis on barge-in
355
+ if (this.activeFishAbort) {
356
+ this.activeFishAbort.abort();
357
+ this.activeFishAbort = null;
358
+ }
343
359
  // Explicitly terminate the in-progress TTS turn so the relay can
344
360
  // immediately hand control back to the caller after barge-in.
345
361
  if (wasSpeaking) {
@@ -370,6 +386,10 @@ export class CallController {
370
386
  this.pendingInstructions = [];
371
387
  this.llmRunVersion++;
372
388
  this.abortCurrentTurn();
389
+ if (this.activeFishAbort) {
390
+ this.activeFishAbort.abort();
391
+ this.activeFishAbort = null;
392
+ }
373
393
  this.currentTurnPromise = null;
374
394
  unregisterCallController(this.callSessionId);
375
395
 
@@ -516,24 +536,45 @@ export class CallController {
516
536
  runVersion: number,
517
537
  runSignal: AbortSignal,
518
538
  ): Promise<string> {
539
+ // Fish Audio TTS routing: when configured, buffer text by sentence
540
+ // boundaries and synthesize via Fish Audio instead of streaming text
541
+ // tokens for ElevenLabs TTS.
542
+ const config = loadConfig();
543
+ const useFishAudio = isFishAudioTts(config);
544
+
519
545
  // Buffer incoming tokens so we can strip control markers ([ASK_GUARDIAN:...], [END_CALL])
520
546
  // before they reach TTS. We hold text whenever an unmatched '[' appears, since it
521
547
  // could be the start of a control marker.
522
548
  let ttsBuffer = "";
523
549
  let fullResponseText = "";
524
550
 
551
+ // When using Fish Audio, we accumulate all text and synthesize
552
+ // the complete response at the end of the turn (better prosody).
553
+ let fishAudioTextBuffer = "";
554
+
555
+ /** Emit a chunk of safe text to the appropriate TTS backend. */
556
+ const emitSafeChunk = (safeText: string): void => {
557
+ const cleaned = sanitizeForTts(safeText);
558
+ if (cleaned.length === 0) return;
559
+ if (useFishAudio) {
560
+ fishAudioTextBuffer += cleaned;
561
+ } else {
562
+ this.relay.sendTextToken(cleaned, false);
563
+ }
564
+ };
565
+
525
566
  const flushSafeText = (): void => {
526
567
  if (!this.isCurrentRun(runVersion)) return;
527
568
  if (ttsBuffer.length === 0) return;
528
569
  const bracketIdx = ttsBuffer.indexOf("[");
529
570
  if (bracketIdx === -1) {
530
571
  // No bracket at all — safe to flush everything
531
- this.relay.sendTextToken(ttsBuffer, false);
572
+ emitSafeChunk(ttsBuffer);
532
573
  ttsBuffer = "";
533
574
  } else {
534
575
  // Flush everything before the bracket
535
576
  if (bracketIdx > 0) {
536
- this.relay.sendTextToken(ttsBuffer.slice(0, bracketIdx), false);
577
+ emitSafeChunk(ttsBuffer.slice(0, bracketIdx));
537
578
  ttsBuffer = ttsBuffer.slice(bracketIdx);
538
579
  }
539
580
 
@@ -547,10 +588,10 @@ export class CallController {
547
588
  // Not a control marker prefix — flush up to the next '[' (if any)
548
589
  const nextBracket = ttsBuffer.indexOf("[", 1);
549
590
  if (nextBracket === -1) {
550
- this.relay.sendTextToken(ttsBuffer, false);
591
+ emitSafeChunk(ttsBuffer);
551
592
  ttsBuffer = "";
552
593
  } else {
553
- this.relay.sendTextToken(ttsBuffer.slice(0, nextBracket), false);
594
+ emitSafeChunk(ttsBuffer.slice(0, nextBracket));
554
595
  ttsBuffer = ttsBuffer.slice(nextBracket);
555
596
  }
556
597
  }
@@ -585,6 +626,7 @@ export class CallController {
585
626
  trustContext: this.trustContext ?? undefined,
586
627
  isInbound: this.isInbound,
587
628
  task: this.task,
629
+ skipDisclosure: this.skipDisclosure,
588
630
  onTextDelta,
589
631
  onComplete,
590
632
  onError,
@@ -625,10 +667,50 @@ export class CallController {
625
667
  // Final sweep: strip any remaining control markers from the buffer
626
668
  ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
627
669
  if (ttsBuffer.length > 0) {
628
- this.relay.sendTextToken(ttsBuffer, false);
670
+ emitSafeChunk(ttsBuffer);
671
+ }
672
+
673
+ // When using Fish Audio, synthesize the complete response text in a
674
+ // single REST API call. The full text gives Fish Audio better context
675
+ // for prosody and intonation. Audio streams back via chunked transfer
676
+ // encoding and is forwarded to Twilio as it arrives.
677
+ const sanitizedFishText = sanitizeForTts(fishAudioTextBuffer.trim());
678
+ if (useFishAudio && sanitizedFishText.length > 0) {
679
+ if (!this.isCurrentRun(runVersion)) return fullResponseText;
680
+ let handle: ReturnType<typeof createStreamingEntry> | null = null;
681
+ try {
682
+ const format = config.fishAudio.format ?? "mp3";
683
+ handle = createStreamingEntry(format as "mp3" | "wav" | "opus");
684
+ const baseUrl = getPublicBaseUrl(config);
685
+ const url = `${baseUrl}/v1/audio/${handle.audioId}`;
686
+ this.relay.sendPlayUrl(url);
687
+ const abortController = new AbortController();
688
+ this.activeFishAbort = abortController;
689
+ await synthesizeWithFishAudio(
690
+ sanitizedFishText,
691
+ config.fishAudio,
692
+ {
693
+ onChunk: (chunk) => handle!.push(chunk),
694
+ signal: abortController.signal,
695
+ },
696
+ );
697
+ } catch (err) {
698
+ if (err instanceof DOMException && err.name === "AbortError") {
699
+ log.debug("Fish Audio synthesis aborted (barge-in)");
700
+ } else {
701
+ log.error({ err }, "Fish Audio synthesis failed — skipping");
702
+ }
703
+ } finally {
704
+ this.activeFishAbort = null;
705
+ handle?.finalize();
706
+ }
629
707
  }
630
708
 
631
- // Signal end of this turn's speech
709
+ // Signal end of this turn's speech. An empty token with `last: true`
710
+ // tells ConversationRelay to start listening — it does NOT trigger TTS
711
+ // synthesis. This is required even when Fish Audio handled all audio
712
+ // playback, because ConversationRelay still needs the end-of-turn signal
713
+ // to transition from "assistant speaking" to "caller speaking" state.
632
714
  this.relay.sendTextToken("", true);
633
715
 
634
716
  // Mark the greeting's first response as awaiting ack
@@ -652,7 +734,7 @@ export class CallController {
652
734
  recordCallEvent(this.callSessionId, "assistant_spoke", {
653
735
  text: responseText,
654
736
  });
655
- const spokenText = stripInternalSpeechMarkers(responseText).trim();
737
+ const spokenText = sanitizeForTts(stripInternalSpeechMarkers(responseText)).trim();
656
738
  if (spokenText.length > 0) {
657
739
  const session = getCallSession(this.callSessionId);
658
740
  if (session) {
@@ -85,6 +85,7 @@ export type StartCallInput = {
85
85
  conversationId: string;
86
86
  assistantId?: string;
87
87
  callerIdentityMode?: "assistant_number" | "user_number";
88
+ skipDisclosure?: boolean;
88
89
  };
89
90
 
90
91
  export type CancelCallInput = {
@@ -364,6 +365,7 @@ export async function startCall(
364
365
  context: callContext,
365
366
  conversationId,
366
367
  callerIdentityMode,
368
+ skipDisclosure,
367
369
  assistantId = DAEMON_INTERNAL_ASSISTANT_ID,
368
370
  } = input;
369
371
 
@@ -440,6 +442,7 @@ export async function startCall(
440
442
  task: callContext ? `${task}\n\nContext: ${callContext}` : task,
441
443
  callerIdentityMode: identityResult.mode,
442
444
  callerIdentitySource: identityResult.source,
445
+ skipDisclosure,
443
446
  initiatedFromConversationId: conversationId,
444
447
  });
445
448
  sessionId = session.id;
@@ -41,6 +41,10 @@ const parseCallSession = createRowMapper<
41
41
  inviteGuardianName: "inviteGuardianName",
42
42
  callerIdentityMode: "callerIdentityMode",
43
43
  callerIdentitySource: "callerIdentitySource",
44
+ skipDisclosure: {
45
+ from: "skipDisclosure",
46
+ transform: (v: unknown) => v === 1,
47
+ },
44
48
  initiatedFromConversationId: "initiatedFromConversationId",
45
49
  startedAt: "startedAt",
46
50
  endedAt: "endedAt",
@@ -87,11 +91,13 @@ export function createCallSession(opts: {
87
91
  inviteGuardianName?: string;
88
92
  callerIdentityMode?: string;
89
93
  callerIdentitySource?: string;
94
+ skipDisclosure?: boolean;
90
95
  initiatedFromConversationId?: string;
91
96
  }): CallSession {
92
97
  const db = getDb();
93
98
  const now = Date.now();
94
- const session = {
99
+ const skipDisclosure = opts.skipDisclosure ?? false;
100
+ const row = {
95
101
  id: uuid(),
96
102
  conversationId: opts.conversationId,
97
103
  provider: opts.provider,
@@ -106,6 +112,7 @@ export function createCallSession(opts: {
106
112
  inviteGuardianName: opts.inviteGuardianName ?? null,
107
113
  callerIdentityMode: opts.callerIdentityMode ?? null,
108
114
  callerIdentitySource: opts.callerIdentitySource ?? null,
115
+ skipDisclosure: skipDisclosure ? 1 : 0,
109
116
  initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
110
117
  startedAt: null,
111
118
  endedAt: null,
@@ -113,8 +120,8 @@ export function createCallSession(opts: {
113
120
  createdAt: now,
114
121
  updatedAt: now,
115
122
  };
116
- db.insert(callSessions).values(session).run();
117
- return session;
123
+ db.insert(callSessions).values(row).run();
124
+ return { ...row, skipDisclosure };
118
125
  }
119
126
 
120
127
  export function getCallSession(id: string): CallSession | null {
@@ -0,0 +1,129 @@
1
+ import type { FishAudioConfig } from "../config/schemas/fish-audio.js";
2
+ import { credentialKey } from "../security/credential-key.js";
3
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
4
+ import { getLogger } from "../util/logger.js";
5
+
6
+ const log = getLogger("fish-audio-client");
7
+
8
+ /** Timeout waiting for the first chunk from Fish Audio (ms). */
9
+ const FIRST_CHUNK_TIMEOUT_MS = 10_000;
10
+
11
+ /** Timeout waiting between consecutive chunks (ms). */
12
+ const IDLE_TIMEOUT_MS = 5_000;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fish Audio REST API (POST /v1/tts)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ interface SynthesizeOptions {
19
+ onChunk?: (chunk: Uint8Array) => void;
20
+ signal?: AbortSignal;
21
+ }
22
+
23
+ /**
24
+ * Synthesize text to audio using the Fish Audio REST API with the s2-pro
25
+ * model. Streams audio chunks via the optional `onChunk` callback as they
26
+ * arrive from the server's chunked transfer-encoded response. Returns the
27
+ * complete audio buffer when the response finishes.
28
+ *
29
+ * Pass an `AbortSignal` to cancel in-flight synthesis (e.g. on barge-in).
30
+ */
31
+ export async function synthesizeWithFishAudio(
32
+ text: string,
33
+ config: FishAudioConfig,
34
+ options?: SynthesizeOptions,
35
+ ): Promise<Buffer> {
36
+ const apiKey = await getSecureKeyAsync(
37
+ credentialKey("fish-audio", "api_key"),
38
+ );
39
+ if (!apiKey) {
40
+ throw new Error(
41
+ "Fish Audio API key not configured. Store it via: assistant credentials set --service fish-audio --field api_key <key>",
42
+ );
43
+ }
44
+
45
+ const body = {
46
+ text,
47
+ reference_id: config.referenceId || undefined,
48
+ model: "s2-pro",
49
+ format: config.format,
50
+ mp3_bitrate: 192,
51
+ chunk_length: config.chunkLength,
52
+ normalize: true,
53
+ latency: config.latency,
54
+ temperature: 1.0,
55
+ prosody: config.speed !== 1.0 ? { speed: config.speed } : undefined,
56
+ };
57
+
58
+ log.info(
59
+ {
60
+ referenceId: config.referenceId,
61
+ format: config.format,
62
+ textLength: text.length,
63
+ },
64
+ "Starting Fish Audio synthesis",
65
+ );
66
+
67
+ const response = await fetch("https://api.fish.audio/v1/tts", {
68
+ method: "POST",
69
+ headers: {
70
+ Authorization: `Bearer ${apiKey}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: JSON.stringify(body),
74
+ signal: options?.signal,
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const errorText = await response.text();
79
+ throw new Error(`Fish Audio API error (${response.status}): ${errorText}`);
80
+ }
81
+
82
+ if (!response.body) {
83
+ throw new Error("Fish Audio API returned no body");
84
+ }
85
+
86
+ const chunks: Uint8Array[] = [];
87
+ const reader = response.body.getReader();
88
+ let isFirstChunk = true;
89
+
90
+ try {
91
+ while (true) {
92
+ const timeoutMs = isFirstChunk ? FIRST_CHUNK_TIMEOUT_MS : IDLE_TIMEOUT_MS;
93
+ let timerId: ReturnType<typeof setTimeout>;
94
+ const timeout = new Promise<never>((_, reject) => {
95
+ timerId = setTimeout(
96
+ () => reject(new Error(`Fish Audio read timed out after ${timeoutMs}ms`)),
97
+ timeoutMs,
98
+ );
99
+ });
100
+ let done: boolean;
101
+ let value: Uint8Array | undefined;
102
+ try {
103
+ ({ done, value } = await Promise.race([reader.read(), timeout]));
104
+ } finally {
105
+ clearTimeout(timerId!);
106
+ }
107
+ if (done) break;
108
+ if (value) {
109
+ isFirstChunk = false;
110
+ chunks.push(value);
111
+ options?.onChunk?.(value);
112
+ }
113
+ }
114
+ } catch (err) {
115
+ try { await reader.cancel(); } catch { /* Ignore cancellation errors */ }
116
+ throw err;
117
+ }
118
+
119
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
120
+ const merged = new Uint8Array(totalLength);
121
+ let offset = 0;
122
+ for (const chunk of chunks) {
123
+ merged.set(chunk, offset);
124
+ offset += chunk.byteLength;
125
+ }
126
+
127
+ log.debug({ bytes: totalLength }, "Fish Audio synthesis complete");
128
+ return Buffer.from(merged);
129
+ }
@@ -139,6 +139,12 @@ export interface RelayEndMessage {
139
139
  handoffData?: string;
140
140
  }
141
141
 
142
+ export interface RelayPlayMessage {
143
+ type: "play";
144
+ source: string;
145
+ interruptible: boolean;
146
+ }
147
+
142
148
  // ── WebSocket data type ──────────────────────────────────────────────
143
149
 
144
150
  export interface RelayWebSocketData {
@@ -323,6 +329,27 @@ export class RelayConnection {
323
329
  }
324
330
  }
325
331
 
332
+ /**
333
+ * Send a play-audio URL to the caller. Used when the assistant handles
334
+ * TTS synthesis itself (e.g. Fish Audio) instead of relying on
335
+ * ConversationRelay's built-in TTS.
336
+ */
337
+ sendPlayUrl(url: string): void {
338
+ const message: RelayPlayMessage = {
339
+ type: "play",
340
+ source: url,
341
+ interruptible: true,
342
+ };
343
+ try {
344
+ this.ws.send(JSON.stringify(message));
345
+ } catch (err) {
346
+ log.error(
347
+ { err, callSessionId: this.callSessionId },
348
+ "Failed to send play URL",
349
+ );
350
+ }
351
+ }
352
+
326
353
  /**
327
354
  * End the ConversationRelay session.
328
355
  */
@@ -0,0 +1,189 @@
1
+ import {
2
+ findContactByAddress,
3
+ findGuardianForChannel,
4
+ listContacts,
5
+ listGuardianChannels,
6
+ } from "../contacts/contact-store.js";
7
+ import { getAssistantName } from "../daemon/identity-helpers.js";
8
+ import { DEFAULT_USER_REFERENCE, resolveGuardianName } from "../prompts/user-reference.js";
9
+ import { getLogger } from "../util/logger.js";
10
+
11
+ const logger = getLogger("stt-hints");
12
+
13
+ export interface SttHintsInput {
14
+ staticHints: string[];
15
+ assistantName: string | null;
16
+ guardianName: string | null;
17
+ taskDescription: string | null;
18
+ targetContactName: string | null;
19
+ callerContactName: string | null;
20
+ inviteFriendName: string | null;
21
+ inviteGuardianName: string | null;
22
+ recentContactNames: string[];
23
+ }
24
+
25
+ const MAX_HINTS_LENGTH = 500;
26
+
27
+ /**
28
+ * Assemble STT vocabulary hints from multiple sources into a single
29
+ * comma-separated string suitable for speech-to-text provider hint APIs.
30
+ *
31
+ * Pure function — no DB or filesystem dependencies.
32
+ */
33
+ export function buildSttHints(input: SttHintsInput): string {
34
+ const hints: string[] = [...input.staticHints];
35
+
36
+ if (input.assistantName != null && input.assistantName.trim().length > 0) {
37
+ hints.push(input.assistantName.trim());
38
+ }
39
+
40
+ if (
41
+ input.guardianName != null &&
42
+ input.guardianName.trim().length > 0 &&
43
+ input.guardianName.trim() !== DEFAULT_USER_REFERENCE
44
+ ) {
45
+ hints.push(input.guardianName.trim());
46
+ }
47
+
48
+ if (input.inviteFriendName != null && input.inviteFriendName.trim().length > 0) {
49
+ hints.push(input.inviteFriendName.trim());
50
+ }
51
+
52
+ if (input.inviteGuardianName != null && input.inviteGuardianName.trim().length > 0) {
53
+ hints.push(input.inviteGuardianName.trim());
54
+ }
55
+
56
+ if (input.targetContactName != null && input.targetContactName.trim().length > 0) {
57
+ hints.push(input.targetContactName.trim());
58
+ }
59
+
60
+ if (input.callerContactName != null && input.callerContactName.trim().length > 0) {
61
+ hints.push(input.callerContactName.trim());
62
+ }
63
+
64
+ // Extract potential proper nouns from task description.
65
+ // Split on sentence boundaries, then for each sentence take words
66
+ // after the first that start with an uppercase letter.
67
+ if (input.taskDescription != null && input.taskDescription.trim().length > 0) {
68
+ // Split on sentence-ending punctuation followed by whitespace, but avoid
69
+ // splitting on periods after common abbreviations (Dr., Mr., etc.) so that
70
+ // names like "Dr. Smith" aren't fragmented and dropped by the first-word skip.
71
+ const sentences = input.taskDescription.split(
72
+ /(?<!\b(?:Mr|Mrs|Ms|Dr|Jr|Sr|St|Rev|Prof|Gen|Sgt|Lt|Col))[.]\s+|[!?]\s+/,
73
+ );
74
+ for (const sentence of sentences) {
75
+ const words = sentence.trim().split(/\s+/);
76
+ // Skip the first word (always capitalized at sentence start)
77
+ for (let i = 1; i < words.length; i++) {
78
+ // Use Unicode-aware \p{L} to preserve accented/non-Latin letters (José, Łukasz, etc.)
79
+ const word = words[i].replace(/[^\p{L}'-]/gu, "");
80
+ if (word.length > 0 && /^\p{Lu}/u.test(word)) {
81
+ hints.push(word);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ hints.push(...input.recentContactNames);
88
+
89
+ // Deduplicate (case-insensitive), filter empty/whitespace-only, trim each
90
+ const seen = new Set<string>();
91
+ const deduped: string[] = [];
92
+ for (const hint of hints) {
93
+ const trimmed = hint.trim();
94
+ if (trimmed.length === 0) continue;
95
+ const key = trimmed.toLowerCase();
96
+ if (seen.has(key)) continue;
97
+ seen.add(key);
98
+ deduped.push(trimmed);
99
+ }
100
+
101
+ const joined = deduped.join(",");
102
+
103
+ if (joined.length <= MAX_HINTS_LENGTH) {
104
+ return joined;
105
+ }
106
+
107
+ // Truncate at the last comma before the limit to avoid partial words
108
+ const truncated = joined.slice(0, MAX_HINTS_LENGTH);
109
+ const lastComma = truncated.lastIndexOf(",");
110
+ if (lastComma === -1) {
111
+ // Single hint that exceeds the limit — return it truncated
112
+ return truncated;
113
+ }
114
+ return truncated.slice(0, lastComma);
115
+ }
116
+
117
+ /**
118
+ * Wire real data sources (contacts DB, identity helpers, config) into
119
+ * {@link buildSttHints}. All DB lookups are best-effort — errors are
120
+ * logged but never propagate so hints can never fail a call.
121
+ */
122
+ export function resolveCallHints(
123
+ session: {
124
+ task: string | null;
125
+ toNumber: string;
126
+ fromNumber: string;
127
+ direction: "inbound" | "outbound";
128
+ inviteFriendName: string | null;
129
+ inviteGuardianName: string | null;
130
+ } | null,
131
+ staticHints: string[],
132
+ ): string {
133
+ const assistantName = getAssistantName();
134
+
135
+ // Look up the guardian contact for a displayName fallback (mirrors relay-server pattern)
136
+ let guardianDisplayName: string | undefined;
137
+ try {
138
+ const voiceGuardian = findGuardianForChannel("phone");
139
+ const guardianChannels = voiceGuardian ? null : listGuardianChannels();
140
+ const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
141
+ guardianDisplayName = guardianContact?.displayName;
142
+ } catch (err) {
143
+ logger.warn({ err }, "Failed to look up guardian contact for STT hints");
144
+ }
145
+ const guardianName = resolveGuardianName(guardianDisplayName);
146
+
147
+ let targetContactName: string | null = null;
148
+ let callerContactName: string | null = null;
149
+ let recentContactNames: string[] = [];
150
+
151
+ // For inbound calls, fromNumber is the caller (the interesting party);
152
+ // toNumber is the assistant's own Twilio number (not useful for contact lookup).
153
+ // For outbound calls, toNumber is who we're calling.
154
+ try {
155
+ if (session) {
156
+ const otherPartyNumber =
157
+ session.direction === "inbound" ? session.fromNumber : session.toNumber;
158
+ const otherPartyContact = findContactByAddress("phone", otherPartyNumber);
159
+ if (otherPartyContact) {
160
+ if (session.direction === "inbound") {
161
+ callerContactName = otherPartyContact.displayName;
162
+ } else {
163
+ targetContactName = otherPartyContact.displayName;
164
+ }
165
+ }
166
+ }
167
+ } catch (err) {
168
+ logger.warn({ err }, "Failed to look up contact for STT hints");
169
+ }
170
+
171
+ try {
172
+ const recentContacts = listContacts(15);
173
+ recentContactNames = recentContacts.map((c) => c.displayName);
174
+ } catch (err) {
175
+ logger.warn({ err }, "Failed to list recent contacts for STT hints");
176
+ }
177
+
178
+ return buildSttHints({
179
+ staticHints,
180
+ assistantName,
181
+ guardianName,
182
+ taskDescription: session?.task ?? null,
183
+ targetContactName,
184
+ callerContactName,
185
+ inviteFriendName: session?.inviteFriendName ?? null,
186
+ inviteGuardianName: session?.inviteGuardianName ?? null,
187
+ recentContactNames,
188
+ });
189
+ }