@vellumai/assistant 0.5.6 → 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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  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 +0 -114
  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 +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. 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;