@vellumai/assistant 0.5.5 → 0.5.7

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 (382) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +4 -5
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +1 -2
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +3 -3
  26. package/src/__tests__/context-window-manager.test.ts +78 -0
  27. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  28. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  29. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  30. package/src/__tests__/conversation-title-service.test.ts +117 -1
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  33. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  34. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  35. package/src/__tests__/credentials-cli.test.ts +78 -0
  36. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  37. package/src/__tests__/docker-signing-key-bootstrap.test.ts +98 -0
  38. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  39. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  40. package/src/__tests__/host-shell-tool.test.ts +6 -7
  41. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  42. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  43. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  44. package/src/__tests__/intent-routing.test.ts +0 -13
  45. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  46. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  47. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  48. package/src/__tests__/memory-regressions.test.ts +8 -30
  49. package/src/__tests__/migration-export-http.test.ts +2 -2
  50. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  51. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  52. package/src/__tests__/migration-validate-http.test.ts +2 -2
  53. package/src/__tests__/non-member-access-request.test.ts +0 -5
  54. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  55. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  56. package/src/__tests__/permission-types.test.ts +1 -0
  57. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  58. package/src/__tests__/qdrant-manager.test.ts +28 -2
  59. package/src/__tests__/registry.test.ts +0 -6
  60. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  62. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  63. package/src/__tests__/secure-keys.test.ts +83 -263
  64. package/src/__tests__/shell-identity.test.ts +96 -6
  65. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  66. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  67. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  68. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  69. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  70. package/src/__tests__/skill-load-tool.test.ts +0 -2
  71. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  72. package/src/__tests__/skills.test.ts +0 -2
  73. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  74. package/src/__tests__/suggestion-routes.test.ts +1 -32
  75. package/src/__tests__/system-prompt.test.ts +0 -1
  76. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  77. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  78. package/src/__tests__/tool-executor.test.ts +4 -0
  79. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  80. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  81. package/src/__tests__/update-bulletin.test.ts +0 -2
  82. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  83. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  84. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  85. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  86. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  87. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  88. package/src/calls/audio-store.test.ts +97 -0
  89. package/src/calls/audio-store.ts +205 -0
  90. package/src/calls/call-controller.ts +85 -7
  91. package/src/calls/call-domain.ts +3 -0
  92. package/src/calls/call-store.ts +10 -3
  93. package/src/calls/fish-audio-client.ts +117 -0
  94. package/src/calls/relay-server.ts +27 -0
  95. package/src/calls/twilio-routes.ts +2 -1
  96. package/src/calls/types.ts +1 -0
  97. package/src/calls/voice-ingress-preflight.ts +0 -42
  98. package/src/calls/voice-quality.ts +26 -5
  99. package/src/calls/voice-session-bridge.ts +6 -12
  100. package/src/cli/commands/config.ts +1 -4
  101. package/src/cli/commands/conversations.ts +0 -18
  102. package/src/cli/commands/credentials.ts +34 -4
  103. package/src/cli/commands/oauth/index.ts +7 -0
  104. package/src/cli/commands/oauth/platform.ts +179 -0
  105. package/src/cli/commands/platform.ts +3 -3
  106. package/src/config/assistant-feature-flags.ts +186 -5
  107. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  108. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  109. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  110. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  111. package/src/config/bundled-tool-registry.ts +1 -11
  112. package/src/config/env-registry.ts +1 -1
  113. package/src/config/env.ts +16 -16
  114. package/src/config/feature-flag-registry.json +48 -16
  115. package/src/config/loader.ts +98 -31
  116. package/src/config/schema.ts +4 -25
  117. package/src/config/schemas/calls.ts +13 -0
  118. package/src/config/schemas/fish-audio.ts +39 -0
  119. package/src/config/schemas/memory.ts +0 -4
  120. package/src/config/schemas/platform.ts +1 -1
  121. package/src/config/schemas/security.ts +4 -4
  122. package/src/config/types.ts +0 -1
  123. package/src/contacts/contact-store.ts +39 -0
  124. package/src/contacts/types.ts +2 -0
  125. package/src/context/window-manager.ts +53 -2
  126. package/src/credential-execution/approval-bridge.ts +1 -0
  127. package/src/credential-execution/executable-discovery.ts +28 -4
  128. package/src/credential-execution/feature-gates.ts +16 -0
  129. package/src/credential-execution/process-manager.ts +38 -0
  130. package/src/daemon/assistant-attachments.ts +9 -0
  131. package/src/daemon/config-watcher.ts +6 -4
  132. package/src/daemon/conversation-agent-loop.ts +0 -60
  133. package/src/daemon/conversation-memory.ts +0 -117
  134. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  135. package/src/daemon/conversation-tool-setup.ts +0 -105
  136. package/src/daemon/conversation.ts +10 -1
  137. package/src/daemon/handlers/config-vercel.ts +92 -0
  138. package/src/daemon/handlers/conversations.ts +0 -11
  139. package/src/daemon/handlers/skills.ts +2 -15
  140. package/src/daemon/install-symlink.ts +195 -0
  141. package/src/daemon/lifecycle.ts +229 -96
  142. package/src/daemon/message-types/conversations.ts +3 -4
  143. package/src/daemon/message-types/diagnostics.ts +3 -22
  144. package/src/daemon/message-types/messages.ts +0 -2
  145. package/src/daemon/message-types/upgrades.ts +8 -0
  146. package/src/daemon/server.ts +30 -92
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/followups/followup-store.ts +5 -2
  149. package/src/inbound/platform-callback-registration.ts +3 -3
  150. package/src/instrument.ts +8 -5
  151. package/src/memory/conversation-crud.ts +0 -236
  152. package/src/memory/conversation-title-service.ts +76 -11
  153. package/src/memory/db-init.ts +15 -11
  154. package/src/memory/indexer.ts +15 -106
  155. package/src/memory/items-extractor.ts +15 -1
  156. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  157. package/src/memory/job-handlers/embedding.ts +0 -79
  158. package/src/memory/job-utils.ts +1 -1
  159. package/src/memory/jobs-store.ts +30 -13
  160. package/src/memory/jobs-worker.ts +31 -27
  161. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  162. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  163. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  164. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  165. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  166. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  167. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  168. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  169. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  170. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  171. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  172. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  173. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  174. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  175. package/src/memory/migrations/116-messages-fts.ts +106 -1
  176. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  177. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  178. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  179. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  180. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  181. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  182. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  183. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  184. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  185. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  186. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  187. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  188. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  189. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  190. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  191. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  192. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  193. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  194. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  195. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  196. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  197. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  198. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  199. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  200. package/src/memory/migrations/index.ts +5 -3
  201. package/src/memory/migrations/registry.ts +90 -0
  202. package/src/memory/migrations/validate-migration-state.ts +137 -11
  203. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  204. package/src/memory/qdrant-client.ts +4 -6
  205. package/src/memory/qdrant-manager.ts +64 -7
  206. package/src/memory/schema/calls.ts +1 -0
  207. package/src/memory/schema/contacts.ts +1 -0
  208. package/src/memory/schema/conversations.ts +0 -3
  209. package/src/memory/schema/index.ts +0 -2
  210. package/src/messaging/draft-store.ts +2 -2
  211. package/src/notifications/decision-engine.ts +4 -1
  212. package/src/oauth/connection-resolver.ts +6 -4
  213. package/src/permissions/checker.ts +0 -38
  214. package/src/permissions/defaults.ts +3 -3
  215. package/src/permissions/shell-identity.ts +76 -22
  216. package/src/permissions/trust-client.ts +2 -13
  217. package/src/permissions/trust-store.ts +8 -3
  218. package/src/permissions/types.ts +4 -2
  219. package/src/platform/client.ts +35 -7
  220. package/src/prompts/persona-resolver.ts +138 -0
  221. package/src/prompts/system-prompt.ts +36 -4
  222. package/src/prompts/templates/users/default.md +1 -0
  223. package/src/providers/registry.ts +27 -40
  224. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  225. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  226. package/src/runtime/auth/external-assistant-id.ts +13 -59
  227. package/src/runtime/auth/route-policy.ts +29 -1
  228. package/src/runtime/auth/token-service.ts +53 -15
  229. package/src/runtime/channel-readiness-service.ts +1 -16
  230. package/src/runtime/http-server.ts +29 -2
  231. package/src/runtime/middleware/error-handler.ts +1 -9
  232. package/src/runtime/routes/audio-routes.ts +40 -0
  233. package/src/runtime/routes/btw-routes.ts +0 -17
  234. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  235. package/src/runtime/routes/conversation-query-routes.ts +106 -2
  236. package/src/runtime/routes/conversation-routes.ts +4 -43
  237. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  238. package/src/runtime/routes/identity-routes.ts +18 -29
  239. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  240. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  241. package/src/runtime/routes/integrations/vercel.ts +89 -0
  242. package/src/runtime/routes/log-export-routes.ts +5 -0
  243. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  244. package/src/runtime/routes/memory-item-routes.ts +144 -4
  245. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  246. package/src/runtime/routes/migration-routes.ts +17 -1
  247. package/src/runtime/routes/notification-routes.ts +58 -0
  248. package/src/runtime/routes/schedule-routes.ts +65 -0
  249. package/src/runtime/routes/settings-routes.ts +41 -1
  250. package/src/runtime/routes/tts-routes.ts +86 -0
  251. package/src/runtime/routes/upgrade-broadcast-routes.ts +175 -0
  252. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  253. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  254. package/src/runtime/routes/workspace-routes.ts +1 -1
  255. package/src/runtime/routes/workspace-utils.ts +86 -2
  256. package/src/schedule/schedule-store.ts +0 -21
  257. package/src/security/ces-credential-client.ts +59 -22
  258. package/src/security/ces-rpc-credential-backend.ts +85 -0
  259. package/src/security/credential-backend.ts +12 -88
  260. package/src/security/keychain-broker-client.ts +10 -2
  261. package/src/security/secure-keys.ts +94 -113
  262. package/src/skills/catalog-install.ts +13 -7
  263. package/src/skills/inline-command-render.ts +5 -1
  264. package/src/skills/inline-command-runner.ts +30 -2
  265. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  266. package/src/tools/calls/call-start.ts +1 -0
  267. package/src/tools/executor.ts +0 -4
  268. package/src/tools/memory/handlers.ts +1 -129
  269. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  270. package/src/tools/network/web-fetch.ts +3 -1
  271. package/src/tools/permission-checker.ts +18 -0
  272. package/src/tools/skills/execute.ts +1 -1
  273. package/src/tools/skills/load.ts +9 -2
  274. package/src/tools/types.ts +0 -8
  275. package/src/util/errors.ts +0 -12
  276. package/src/util/platform.ts +8 -55
  277. package/src/util/xml.ts +8 -0
  278. package/src/workspace/git-service.ts +5 -2
  279. package/src/workspace/heartbeat-service.ts +5 -24
  280. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  281. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  282. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  283. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  284. package/src/workspace/migrations/006-services-config.ts +49 -0
  285. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  286. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  287. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  288. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  289. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  290. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  291. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  292. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  293. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  294. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  295. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  296. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  297. package/src/workspace/migrations/registry.ts +8 -0
  298. package/src/workspace/migrations/runner.ts +106 -2
  299. package/src/workspace/migrations/types.ts +4 -0
  300. package/src/__tests__/archive-recall.test.ts +0 -560
  301. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  302. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  303. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  304. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  305. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  306. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  307. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  308. package/src/__tests__/diagnostics-export.test.ts +0 -288
  309. package/src/__tests__/local-gateway-health.test.ts +0 -209
  310. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  311. package/src/__tests__/memory-brief-time.test.ts +0 -285
  312. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  313. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  314. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  315. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  316. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  317. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  318. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  319. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  320. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  321. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  322. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  323. package/src/__tests__/memory-reducer.test.ts +0 -704
  324. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  325. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  326. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  327. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  328. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  329. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  330. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  331. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  332. package/src/__tests__/swarm-recursion.test.ts +0 -197
  333. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  334. package/src/__tests__/swarm-tool.test.ts +0 -185
  335. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  336. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  337. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  338. package/src/commands/cc-command-registry.ts +0 -248
  339. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  340. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  341. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  342. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  343. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  344. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  345. package/src/config/schemas/memory-simplified.ts +0 -101
  346. package/src/config/schemas/swarm.ts +0 -82
  347. package/src/logfire.ts +0 -135
  348. package/src/memory/archive-recall.ts +0 -516
  349. package/src/memory/archive-store.ts +0 -400
  350. package/src/memory/brief-formatting.ts +0 -33
  351. package/src/memory/brief-open-loops.ts +0 -266
  352. package/src/memory/brief-time.ts +0 -162
  353. package/src/memory/brief.ts +0 -75
  354. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  355. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  356. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  357. package/src/memory/migrations/186-memory-archive.ts +0 -109
  358. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  359. package/src/memory/reducer-scheduler.ts +0 -242
  360. package/src/memory/reducer-store.ts +0 -271
  361. package/src/memory/reducer-types.ts +0 -106
  362. package/src/memory/reducer.ts +0 -467
  363. package/src/memory/schema/memory-archive.ts +0 -121
  364. package/src/memory/schema/memory-brief.ts +0 -55
  365. package/src/runtime/local-gateway-health.ts +0 -275
  366. package/src/security/secret-ingress.ts +0 -68
  367. package/src/swarm/backend-claude-code.ts +0 -225
  368. package/src/swarm/checkpoint.ts +0 -137
  369. package/src/swarm/graph-utils.ts +0 -53
  370. package/src/swarm/index.ts +0 -55
  371. package/src/swarm/limits.ts +0 -66
  372. package/src/swarm/orchestrator.ts +0 -424
  373. package/src/swarm/plan-validator.ts +0 -117
  374. package/src/swarm/router-planner.ts +0 -162
  375. package/src/swarm/router-prompts.ts +0 -39
  376. package/src/swarm/synthesizer.ts +0 -81
  377. package/src/swarm/types.ts +0 -72
  378. package/src/swarm/worker-backend.ts +0 -131
  379. package/src/swarm/worker-prompts.ts +0 -80
  380. package/src/swarm/worker-runner.ts +0 -170
  381. package/src/tools/claude-code/claude-code.ts +0 -610
  382. package/src/tools/swarm/delegate.ts +0 -205
@@ -54,6 +54,7 @@ mock.module("node:fs", () => ({
54
54
  // Import after mocking
55
55
  import {
56
56
  loadCheckpoints,
57
+ rollbackWorkspaceMigrations,
57
58
  runWorkspaceMigrations,
58
59
  } from "../workspace/migrations/runner.js";
59
60
 
@@ -68,6 +69,7 @@ function makeMigration(id: string): WorkspaceMigration {
68
69
  id,
69
70
  description: `Migration ${id}`,
70
71
  run: mock(() => {}),
72
+ down: mock(() => {}),
71
73
  };
72
74
  }
73
75
 
@@ -244,6 +246,7 @@ describe("runWorkspaceMigrations", () => {
244
246
  // Simulate async work
245
247
  await Promise.resolve();
246
248
  }),
249
+ down: mock(() => {}),
247
250
  };
248
251
 
249
252
  await runWorkspaceMigrations(WORKSPACE_DIR, [asyncMigration]);
@@ -291,3 +294,114 @@ describe("runWorkspaceMigrations", () => {
291
294
  );
292
295
  });
293
296
  });
297
+
298
+ describe("rollbackWorkspaceMigrations", () => {
299
+ beforeEach(() => {
300
+ mockCheckpointContents = null;
301
+ readTextFileSyncFn.mockClear();
302
+ ensureDirFn.mockClear();
303
+ writeFileSyncFn.mockClear();
304
+ renameSyncFn.mockClear();
305
+ logWarnFn.mockClear();
306
+ logInfoFn.mockClear();
307
+ logErrorFn.mockClear();
308
+ });
309
+
310
+ test("rolls back migrations in reverse order", async () => {
311
+ const m1 = makeMigration("001");
312
+ const m2 = makeMigration("002");
313
+ const m3 = makeMigration("003");
314
+
315
+ // All three migrations are marked as completed in checkpoints
316
+ mockCheckpointContents = JSON.stringify({
317
+ applied: {
318
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "completed" },
319
+ "002": { appliedAt: "2025-01-02T00:00:00.000Z", status: "completed" },
320
+ "003": { appliedAt: "2025-01-03T00:00:00.000Z", status: "completed" },
321
+ },
322
+ });
323
+
324
+ const callOrder: string[] = [];
325
+ (m2.down as ReturnType<typeof mock>).mockImplementation(() => {
326
+ callOrder.push("002");
327
+ });
328
+ (m3.down as ReturnType<typeof mock>).mockImplementation(() => {
329
+ callOrder.push("003");
330
+ });
331
+
332
+ // Roll back to m1 — should reverse m3 then m2, but not m1
333
+ await rollbackWorkspaceMigrations(WORKSPACE_DIR, [m1, m2, m3], "001");
334
+
335
+ expect(m3.down).toHaveBeenCalledTimes(1);
336
+ expect(m2.down).toHaveBeenCalledTimes(1);
337
+ expect(m1.down).not.toHaveBeenCalled();
338
+ expect(callOrder).toEqual(["003", "002"]);
339
+ });
340
+
341
+ test("handles crash during rollback (rolling_back status)", async () => {
342
+ const m1 = makeMigration("001");
343
+
344
+ // Simulate a crash during a previous rollback — m1 is left in rolling_back state
345
+ mockCheckpointContents = JSON.stringify({
346
+ applied: {
347
+ "001": {
348
+ appliedAt: "2025-01-01T00:00:00.000Z",
349
+ status: "rolling_back",
350
+ },
351
+ },
352
+ });
353
+
354
+ // runWorkspaceMigrations should clear the rolling_back status and re-run forward
355
+ await runWorkspaceMigrations(WORKSPACE_DIR, [m1]);
356
+
357
+ // The runner treats "rolling_back" like "started" — it clears the entry and re-runs
358
+ expect(m1.run).toHaveBeenCalledTimes(1);
359
+ });
360
+
361
+ test("removes checkpoints for rolled-back migrations", async () => {
362
+ const m1 = makeMigration("001");
363
+ const m2 = makeMigration("002");
364
+ const m3 = makeMigration("003");
365
+
366
+ mockCheckpointContents = JSON.stringify({
367
+ applied: {
368
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "completed" },
369
+ "002": { appliedAt: "2025-01-02T00:00:00.000Z", status: "completed" },
370
+ "003": { appliedAt: "2025-01-03T00:00:00.000Z", status: "completed" },
371
+ },
372
+ });
373
+
374
+ await rollbackWorkspaceMigrations(WORKSPACE_DIR, [m1, m2, m3], "001");
375
+
376
+ // The last checkpoint write should only contain m1 (002 and 003 were rolled back)
377
+ const lastWriteCall = writeFileSyncFn.mock.calls.at(-1) as unknown[];
378
+ const finalCheckpoint = JSON.parse(lastWriteCall[1] as string);
379
+ expect(finalCheckpoint.applied["001"]).toBeDefined();
380
+ expect(finalCheckpoint.applied["002"]).toBeUndefined();
381
+ expect(finalCheckpoint.applied["003"]).toBeUndefined();
382
+ });
383
+
384
+ test("no-op when already at target", async () => {
385
+ const m1 = makeMigration("001");
386
+ const m2 = makeMigration("002");
387
+ const m3 = makeMigration("003");
388
+
389
+ mockCheckpointContents = JSON.stringify({
390
+ applied: {
391
+ "001": { appliedAt: "2025-01-01T00:00:00.000Z", status: "completed" },
392
+ "002": { appliedAt: "2025-01-02T00:00:00.000Z", status: "completed" },
393
+ "003": { appliedAt: "2025-01-03T00:00:00.000Z", status: "completed" },
394
+ },
395
+ });
396
+
397
+ // Target is the last migration — nothing to roll back
398
+ await rollbackWorkspaceMigrations(WORKSPACE_DIR, [m1, m2, m3], "003");
399
+
400
+ expect(m1.down).not.toHaveBeenCalled();
401
+ expect(m2.down).not.toHaveBeenCalled();
402
+ expect(m3.down).not.toHaveBeenCalled();
403
+
404
+ // No checkpoint writes should have occurred (no rollback happened)
405
+ expect(writeFileSyncFn).not.toHaveBeenCalled();
406
+ });
407
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { getAudio, storeAudio } from "./audio-store.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Reset module-level state between tests by re-importing.
11
+ * Since the store uses module-level variables, we isolate via fresh imports
12
+ * where needed, but for most tests the shared module state is fine as long
13
+ * as we account for it.
14
+ */
15
+
16
+ function makeBuffer(sizeBytes: number): Buffer {
17
+ return Buffer.alloc(sizeBytes, 0x42);
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Tests
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe("audio-store", () => {
25
+ describe("storeAudio / getAudio", () => {
26
+ test("stores and retrieves audio by id", () => {
27
+ const buf = makeBuffer(1024);
28
+ const id = storeAudio(buf, "mp3");
29
+ const result = getAudio(id);
30
+ expect(result).not.toBeNull();
31
+ expect(result!.type).toBe("buffer");
32
+ if (result!.type === "buffer") {
33
+ expect(result!.buffer).toEqual(buf);
34
+ }
35
+ expect(result!.contentType).toBe("audio/mpeg");
36
+ });
37
+
38
+ test("returns correct content type for each format", () => {
39
+ const buf = makeBuffer(64);
40
+
41
+ const mp3Id = storeAudio(buf, "mp3");
42
+ expect(getAudio(mp3Id)!.contentType).toBe("audio/mpeg");
43
+
44
+ const wavId = storeAudio(buf, "wav");
45
+ expect(getAudio(wavId)!.contentType).toBe("audio/wav");
46
+
47
+ const opusId = storeAudio(buf, "opus");
48
+ expect(getAudio(opusId)!.contentType).toBe("audio/opus");
49
+ });
50
+
51
+ test("returns null for unknown id", () => {
52
+ expect(getAudio("nonexistent-id")).toBeNull();
53
+ });
54
+ });
55
+
56
+ describe("TTL expiration", () => {
57
+ test("expired entries return null", () => {
58
+ const buf = makeBuffer(128);
59
+ const id = storeAudio(buf, "wav");
60
+
61
+ // Fast-forward time past TTL (60s)
62
+ const originalNow = Date.now;
63
+ Date.now = () => originalNow() + 61_000;
64
+ try {
65
+ const result = getAudio(id);
66
+ expect(result).toBeNull();
67
+ } finally {
68
+ Date.now = originalNow;
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("capacity eviction", () => {
74
+ test("evicts oldest entries when capacity is exceeded", () => {
75
+ // The store has a 50MB cap. Fill it with entries, then add one more
76
+ // that would exceed the cap. The oldest should be evicted.
77
+ const chunkSize = 10 * 1024 * 1024; // 10MB per chunk
78
+ const ids: string[] = [];
79
+
80
+ // Store 5 x 10MB = 50MB (at capacity)
81
+ for (let i = 0; i < 5; i++) {
82
+ ids.push(storeAudio(makeBuffer(chunkSize), "opus"));
83
+ }
84
+
85
+ // All 5 should be retrievable
86
+ for (const id of ids) {
87
+ expect(getAudio(id)).not.toBeNull();
88
+ }
89
+
90
+ // Add one more 10MB entry — should evict the oldest
91
+ const newId = storeAudio(makeBuffer(chunkSize), "mp3");
92
+ expect(getAudio(newId)).not.toBeNull();
93
+ // The first entry should have been evicted
94
+ expect(getAudio(ids[0]!)).toBeNull();
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,205 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ interface AudioEntry {
4
+ buffer: Buffer;
5
+ contentType: string;
6
+ expiresAt: number;
7
+ }
8
+
9
+ interface StreamingAudioEntry {
10
+ contentType: string;
11
+ expiresAt: number;
12
+ chunks: Uint8Array[];
13
+ totalBytes: number;
14
+ complete: boolean;
15
+ subscribers: Set<ReadableStreamDefaultController<Uint8Array>>;
16
+ }
17
+
18
+ const store = new Map<string, AudioEntry>();
19
+ const streamingStore = new Map<string, StreamingAudioEntry>();
20
+ const MAX_STORE_BYTES = 50 * 1024 * 1024; // 50MB cap
21
+ const TTL_MS = 60_000; // 60 seconds
22
+
23
+ let currentBytes = 0;
24
+
25
+ export function storeAudio(
26
+ buffer: Buffer,
27
+ format: "mp3" | "wav" | "opus",
28
+ ): string {
29
+ evictExpired();
30
+ // Evict oldest if over capacity
31
+ while (currentBytes + buffer.length > MAX_STORE_BYTES && store.size > 0) {
32
+ const oldest = store.keys().next().value;
33
+ if (oldest) removeEntry(oldest);
34
+ }
35
+ const id = randomUUID();
36
+ const contentType = contentTypeForFormat(format);
37
+ store.set(id, { buffer, contentType, expiresAt: Date.now() + TTL_MS });
38
+ currentBytes += buffer.length;
39
+ return id;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Streaming entries — audio is pushed chunk-by-chunk while being served
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export interface StreamingAudioHandle {
47
+ audioId: string;
48
+ push: (chunk: Uint8Array) => void;
49
+ finalize: () => void;
50
+ }
51
+
52
+ export function createStreamingEntry(
53
+ format: "mp3" | "wav" | "opus",
54
+ ): StreamingAudioHandle {
55
+ evictExpired();
56
+ const id = randomUUID();
57
+ const contentType = contentTypeForFormat(format);
58
+ const entry: StreamingAudioEntry = {
59
+ contentType,
60
+ expiresAt: Date.now() + TTL_MS,
61
+ chunks: [],
62
+ totalBytes: 0,
63
+ complete: false,
64
+ subscribers: new Set(),
65
+ };
66
+ streamingStore.set(id, entry);
67
+
68
+ return {
69
+ audioId: id,
70
+ push(chunk: Uint8Array) {
71
+ entry.chunks.push(chunk);
72
+ entry.totalBytes += chunk.byteLength;
73
+ for (const controller of entry.subscribers) {
74
+ try {
75
+ controller.enqueue(chunk);
76
+ } catch {
77
+ entry.subscribers.delete(controller);
78
+ }
79
+ }
80
+ },
81
+ finalize() {
82
+ entry.complete = true;
83
+ for (const controller of entry.subscribers) {
84
+ try {
85
+ controller.close();
86
+ } catch {
87
+ // Already closed
88
+ }
89
+ }
90
+ entry.subscribers.clear();
91
+ },
92
+ };
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Retrieval — handles both regular and streaming entries
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export type AudioResult =
100
+ | { type: "buffer"; buffer: Buffer; contentType: string }
101
+ | { type: "stream"; stream: ReadableStream<Uint8Array>; contentType: string };
102
+
103
+ export function getAudio(id: string): AudioResult | null {
104
+ evictExpired();
105
+
106
+ // Check streaming store first
107
+ const streamingEntry = streamingStore.get(id);
108
+ if (streamingEntry) {
109
+ if (Date.now() > streamingEntry.expiresAt) {
110
+ streamingStore.delete(id);
111
+ return null;
112
+ }
113
+
114
+ if (streamingEntry.complete) {
115
+ // Synthesis finished — serve the complete buffer
116
+ const merged = mergeChunks(streamingEntry.chunks);
117
+ return {
118
+ type: "buffer",
119
+ buffer: Buffer.from(merged),
120
+ contentType: streamingEntry.contentType,
121
+ };
122
+ }
123
+
124
+ // Still streaming — return a ReadableStream that replays existing
125
+ // chunks and subscribes for future ones.
126
+ let ctrl: ReadableStreamDefaultController<Uint8Array>;
127
+ const stream = new ReadableStream<Uint8Array>({
128
+ start(controller) {
129
+ ctrl = controller;
130
+ for (const chunk of streamingEntry.chunks) {
131
+ controller.enqueue(chunk);
132
+ }
133
+ if (streamingEntry.complete) {
134
+ controller.close();
135
+ } else {
136
+ streamingEntry.subscribers.add(controller);
137
+ }
138
+ },
139
+ cancel() {
140
+ streamingEntry.subscribers.delete(ctrl);
141
+ },
142
+ });
143
+
144
+ return { type: "stream", stream, contentType: streamingEntry.contentType };
145
+ }
146
+
147
+ // Check regular store
148
+ const entry = store.get(id);
149
+ if (!entry) return null;
150
+ if (Date.now() > entry.expiresAt) {
151
+ removeEntry(id);
152
+ return null;
153
+ }
154
+ return { type: "buffer", buffer: entry.buffer, contentType: entry.contentType };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Internal helpers
159
+ // ---------------------------------------------------------------------------
160
+
161
+ function contentTypeForFormat(format: "mp3" | "wav" | "opus"): string {
162
+ return format === "mp3"
163
+ ? "audio/mpeg"
164
+ : format === "wav"
165
+ ? "audio/wav"
166
+ : "audio/opus";
167
+ }
168
+
169
+ function mergeChunks(chunks: Uint8Array[]): Uint8Array {
170
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
171
+ const merged = new Uint8Array(totalLength);
172
+ let offset = 0;
173
+ for (const chunk of chunks) {
174
+ merged.set(chunk, offset);
175
+ offset += chunk.byteLength;
176
+ }
177
+ return merged;
178
+ }
179
+
180
+ function removeEntry(id: string): void {
181
+ const entry = store.get(id);
182
+ if (entry) {
183
+ currentBytes -= entry.buffer.length;
184
+ store.delete(id);
185
+ }
186
+ }
187
+
188
+ function evictExpired(): void {
189
+ const now = Date.now();
190
+ for (const [id, entry] of store) {
191
+ if (now > entry.expiresAt) removeEntry(id);
192
+ }
193
+ for (const [id, entry] of streamingStore) {
194
+ if (now > entry.expiresAt) {
195
+ for (const controller of entry.subscribers) {
196
+ try {
197
+ controller.close();
198
+ } catch {
199
+ // noop
200
+ }
201
+ }
202
+ streamingStore.delete(id);
203
+ }
204
+ }
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,6 +45,7 @@ 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";
@@ -56,6 +60,7 @@ import {
56
60
  extractBalancedJson,
57
61
  stripInternalSpeechMarkers,
58
62
  } from "./voice-control-protocol.js";
63
+ import { isFishAudioTts } from "./voice-quality.js";
59
64
  import {
60
65
  startVoiceTurn,
61
66
  type VoiceTurnHandle,
@@ -101,6 +106,8 @@ export class CallController {
101
106
  private task: string | null;
102
107
  /** True when the call session was created via the inbound path (no outbound task). */
103
108
  private isInbound: boolean;
109
+ /** When true, the disclosure announcement is skipped for this call. */
110
+ private skipDisclosure: boolean;
104
111
  /** Instructions queued while an LLM turn is in-flight or during pending guardian input */
105
112
  private pendingInstructions: string[] = [];
106
113
  /** Ensures the call opener is triggered at most once per call. */
@@ -131,6 +138,8 @@ export class CallController {
131
138
  * without blocking the caller.
132
139
  */
133
140
  private guardianUnavailableForCall = false;
141
+ /** Active Fish Audio session — tracked so interrupt handling can close it. */
142
+ private activeFishAbort: AbortController | null = null;
134
143
 
135
144
  constructor(
136
145
  callSessionId: string,
@@ -150,9 +159,10 @@ export class CallController {
150
159
  this.assistantId = opts?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
151
160
  this.trustContext = opts?.trustContext ?? null;
152
161
 
153
- // Resolve the conversation ID from the call session
162
+ // Resolve the conversation ID and skipDisclosure from the call session
154
163
  const session = getCallSession(callSessionId);
155
164
  this.conversationId = session?.conversationId ?? callSessionId;
165
+ this.skipDisclosure = session?.skipDisclosure ?? false;
156
166
 
157
167
  this.startDurationTimer();
158
168
  this.resetSilenceTimer();
@@ -340,6 +350,11 @@ export class CallController {
340
350
  const wasSpeaking = this.state === "speaking";
341
351
  this.abortCurrentTurn();
342
352
  this.llmRunVersion++;
353
+ // Cancel in-flight Fish Audio synthesis on barge-in
354
+ if (this.activeFishAbort) {
355
+ this.activeFishAbort.abort();
356
+ this.activeFishAbort = null;
357
+ }
343
358
  // Explicitly terminate the in-progress TTS turn so the relay can
344
359
  // immediately hand control back to the caller after barge-in.
345
360
  if (wasSpeaking) {
@@ -370,6 +385,10 @@ export class CallController {
370
385
  this.pendingInstructions = [];
371
386
  this.llmRunVersion++;
372
387
  this.abortCurrentTurn();
388
+ if (this.activeFishAbort) {
389
+ this.activeFishAbort.abort();
390
+ this.activeFishAbort = null;
391
+ }
373
392
  this.currentTurnPromise = null;
374
393
  unregisterCallController(this.callSessionId);
375
394
 
@@ -516,24 +535,43 @@ export class CallController {
516
535
  runVersion: number,
517
536
  runSignal: AbortSignal,
518
537
  ): Promise<string> {
538
+ // Fish Audio TTS routing: when configured, buffer text by sentence
539
+ // boundaries and synthesize via Fish Audio instead of streaming text
540
+ // tokens for ElevenLabs TTS.
541
+ const config = loadConfig();
542
+ const useFishAudio = isFishAudioTts(config);
543
+
519
544
  // Buffer incoming tokens so we can strip control markers ([ASK_GUARDIAN:...], [END_CALL])
520
545
  // before they reach TTS. We hold text whenever an unmatched '[' appears, since it
521
546
  // could be the start of a control marker.
522
547
  let ttsBuffer = "";
523
548
  let fullResponseText = "";
524
549
 
550
+ // When using Fish Audio, we accumulate all text and synthesize
551
+ // the complete response at the end of the turn (better prosody).
552
+ let fishAudioTextBuffer = "";
553
+
554
+ /** Emit a chunk of safe text to the appropriate TTS backend. */
555
+ const emitSafeChunk = (safeText: string): void => {
556
+ if (useFishAudio) {
557
+ fishAudioTextBuffer += safeText;
558
+ } else {
559
+ this.relay.sendTextToken(safeText, false);
560
+ }
561
+ };
562
+
525
563
  const flushSafeText = (): void => {
526
564
  if (!this.isCurrentRun(runVersion)) return;
527
565
  if (ttsBuffer.length === 0) return;
528
566
  const bracketIdx = ttsBuffer.indexOf("[");
529
567
  if (bracketIdx === -1) {
530
568
  // No bracket at all — safe to flush everything
531
- this.relay.sendTextToken(ttsBuffer, false);
569
+ emitSafeChunk(ttsBuffer);
532
570
  ttsBuffer = "";
533
571
  } else {
534
572
  // Flush everything before the bracket
535
573
  if (bracketIdx > 0) {
536
- this.relay.sendTextToken(ttsBuffer.slice(0, bracketIdx), false);
574
+ emitSafeChunk(ttsBuffer.slice(0, bracketIdx));
537
575
  ttsBuffer = ttsBuffer.slice(bracketIdx);
538
576
  }
539
577
 
@@ -547,10 +585,10 @@ export class CallController {
547
585
  // Not a control marker prefix — flush up to the next '[' (if any)
548
586
  const nextBracket = ttsBuffer.indexOf("[", 1);
549
587
  if (nextBracket === -1) {
550
- this.relay.sendTextToken(ttsBuffer, false);
588
+ emitSafeChunk(ttsBuffer);
551
589
  ttsBuffer = "";
552
590
  } else {
553
- this.relay.sendTextToken(ttsBuffer.slice(0, nextBracket), false);
591
+ emitSafeChunk(ttsBuffer.slice(0, nextBracket));
554
592
  ttsBuffer = ttsBuffer.slice(nextBracket);
555
593
  }
556
594
  }
@@ -585,6 +623,7 @@ export class CallController {
585
623
  trustContext: this.trustContext ?? undefined,
586
624
  isInbound: this.isInbound,
587
625
  task: this.task,
626
+ skipDisclosure: this.skipDisclosure,
588
627
  onTextDelta,
589
628
  onComplete,
590
629
  onError,
@@ -625,10 +664,49 @@ export class CallController {
625
664
  // Final sweep: strip any remaining control markers from the buffer
626
665
  ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
627
666
  if (ttsBuffer.length > 0) {
628
- this.relay.sendTextToken(ttsBuffer, false);
667
+ emitSafeChunk(ttsBuffer);
668
+ }
669
+
670
+ // When using Fish Audio, synthesize the complete response text in a
671
+ // single REST API call. The full text gives Fish Audio better context
672
+ // for prosody and intonation. Audio streams back via chunked transfer
673
+ // encoding and is forwarded to Twilio as it arrives.
674
+ if (useFishAudio && fishAudioTextBuffer.trim().length > 0) {
675
+ if (!this.isCurrentRun(runVersion)) return fullResponseText;
676
+ let handle: ReturnType<typeof createStreamingEntry> | null = null;
677
+ try {
678
+ const format = config.fishAudio.format ?? "mp3";
679
+ handle = createStreamingEntry(format as "mp3" | "wav" | "opus");
680
+ const baseUrl = getPublicBaseUrl(config);
681
+ const url = `${baseUrl}/v1/audio/${handle.audioId}`;
682
+ this.relay.sendPlayUrl(url);
683
+ const abortController = new AbortController();
684
+ this.activeFishAbort = abortController;
685
+ await synthesizeWithFishAudio(
686
+ fishAudioTextBuffer.trim(),
687
+ config.fishAudio,
688
+ {
689
+ onChunk: (chunk) => handle!.push(chunk),
690
+ signal: abortController.signal,
691
+ },
692
+ );
693
+ } catch (err) {
694
+ if (err instanceof DOMException && err.name === "AbortError") {
695
+ log.debug("Fish Audio synthesis aborted (barge-in)");
696
+ } else {
697
+ log.error({ err }, "Fish Audio synthesis failed — skipping");
698
+ }
699
+ } finally {
700
+ this.activeFishAbort = null;
701
+ handle?.finalize();
702
+ }
629
703
  }
630
704
 
631
- // Signal end of this turn's speech
705
+ // Signal end of this turn's speech. An empty token with `last: true`
706
+ // tells ConversationRelay to start listening — it does NOT trigger TTS
707
+ // synthesis. This is required even when Fish Audio handled all audio
708
+ // playback, because ConversationRelay still needs the end-of-turn signal
709
+ // to transition from "assistant speaking" to "caller speaking" state.
632
710
  this.relay.sendTextToken("", true);
633
711
 
634
712
  // Mark the greeting's first response as awaiting ack
@@ -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;