@vellumai/assistant 0.4.22 → 0.4.25

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 (72) hide show
  1. package/bun.lock +3 -0
  2. package/package.json +2 -1
  3. package/scripts/ipc/check-swift-decoder-drift.ts +55 -44
  4. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -90
  5. package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
  6. package/src/__tests__/config-schema.test.ts +38 -178
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
  8. package/src/__tests__/credential-security-invariants.test.ts +0 -2
  9. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
  10. package/src/__tests__/headless-browser-interactions.test.ts +0 -4
  11. package/src/__tests__/ipc-snapshot.test.ts +0 -63
  12. package/src/__tests__/onboarding-template-contract.test.ts +10 -20
  13. package/src/__tests__/relay-server.test.ts +3 -3
  14. package/src/__tests__/resolve-guardian-trust-class.test.ts +61 -0
  15. package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
  16. package/src/__tests__/runtime-events-sse.test.ts +7 -0
  17. package/src/__tests__/session-init.benchmark.test.ts +0 -4
  18. package/src/__tests__/session-runtime-assembly.test.ts +34 -8
  19. package/src/__tests__/system-prompt.test.ts +7 -1
  20. package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
  21. package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
  22. package/src/__tests__/twilio-routes.test.ts +2 -3
  23. package/src/__tests__/voice-quality.test.ts +21 -132
  24. package/src/calls/relay-server.ts +11 -5
  25. package/src/calls/twilio-routes.ts +4 -38
  26. package/src/calls/voice-quality.ts +7 -63
  27. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
  28. package/src/config/bundled-skills/messaging/SKILL.md +3 -5
  29. package/src/config/bundled-skills/phone-calls/SKILL.md +143 -82
  30. package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
  31. package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
  32. package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
  33. package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
  34. package/src/config/calls-schema.ts +3 -53
  35. package/src/config/elevenlabs-schema.ts +33 -0
  36. package/src/config/schema.ts +183 -137
  37. package/src/config/types.ts +0 -1
  38. package/src/daemon/daemon-control.ts +3 -0
  39. package/src/daemon/handlers/browser.ts +2 -53
  40. package/src/daemon/ipc-contract/browser.ts +5 -84
  41. package/src/daemon/ipc-contract/surfaces.ts +51 -48
  42. package/src/daemon/ipc-contract-inventory.json +0 -9
  43. package/src/daemon/session-agent-loop-handlers.ts +3 -0
  44. package/src/daemon/session-agent-loop.ts +2 -1
  45. package/src/daemon/session-runtime-assembly.ts +9 -7
  46. package/src/daemon/session-tool-setup.ts +27 -13
  47. package/src/mcp/client.ts +2 -1
  48. package/src/memory/conversation-crud.ts +339 -166
  49. package/src/memory/migrations/102-alter-table-columns.ts +254 -37
  50. package/src/memory/schema.ts +1227 -1035
  51. package/src/runtime/routes/events-routes.ts +7 -0
  52. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  53. package/src/schedule/scheduler.ts +159 -45
  54. package/src/security/secure-keys.ts +3 -3
  55. package/src/tools/browser/browser-execution.ts +314 -331
  56. package/src/tools/browser/browser-handoff.ts +11 -37
  57. package/src/tools/browser/browser-manager.ts +203 -352
  58. package/src/tools/browser/browser-screencast.ts +15 -76
  59. package/src/tools/network/script-proxy/certs.ts +7 -237
  60. package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
  61. package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
  62. package/src/tools/network/script-proxy/logging.ts +12 -196
  63. package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
  64. package/src/tools/network/script-proxy/policy.ts +4 -152
  65. package/src/tools/network/script-proxy/router.ts +2 -60
  66. package/src/tools/network/script-proxy/server.ts +5 -137
  67. package/src/tools/network/script-proxy/types.ts +19 -125
  68. package/src/tools/system/voice-config.ts +23 -1
  69. package/src/util/logger.ts +4 -1
  70. package/src/__tests__/elevenlabs-config.test.ts +0 -95
  71. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
  72. package/src/calls/elevenlabs-config.ts +0 -32
package/bun.lock CHANGED
@@ -11,6 +11,7 @@
11
11
  "@modelcontextprotocol/sdk": "^1.15.1",
12
12
  "@qdrant/js-client-rest": "^1.16.2",
13
13
  "@sentry/node": "^10.38.0",
14
+ "@vellumai/proxy-sidecar": "file:../proxy-sidecar",
14
15
  "agentmail": "^0.1.0",
15
16
  "archiver": "^7.0.1",
16
17
  "commander": "^13.1.0",
@@ -517,6 +518,8 @@
517
518
 
518
519
  "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
519
520
 
521
+ "@vellumai/proxy-sidecar": ["@vellumai/proxy-sidecar@file:../proxy-sidecar", { "devDependencies": { "@types/bun": "^1.2.4", "@types/node": "^25.2.2", "typescript": "^5.7.3" } }],
522
+
520
523
  "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
521
524
 
522
525
  "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.22",
3
+ "version": "0.4.25",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "^0.2.42",
30
+ "@vellumai/proxy-sidecar": "file:../proxy-sidecar",
30
31
  "@anthropic-ai/sdk": "^0.39.0",
31
32
  "@google/genai": "^1.40.0",
32
33
  "@modelcontextprotocol/sdk": "^1.15.1",
@@ -12,16 +12,16 @@
12
12
  * bun run ipc:check-swift-drift # check for drift
13
13
  */
14
14
 
15
- import * as fs from 'fs';
16
- import * as path from 'path';
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
17
 
18
- import { extractInventory } from '../../src/daemon/ipc-contract-inventory.js';
18
+ import { extractInventory } from "../../src/daemon/ipc-contract-inventory.js";
19
19
 
20
- const ROOT = path.resolve(import.meta.dirname ?? __dirname, '../..');
21
- const CONTRACT_PATH = path.join(ROOT, 'src/daemon/ipc-contract.ts');
20
+ const ROOT = path.resolve(import.meta.dirname ?? __dirname, "../..");
21
+ const CONTRACT_PATH = path.join(ROOT, "src/daemon/ipc-contract.ts");
22
22
  const SWIFT_PATH = path.resolve(
23
23
  ROOT,
24
- '../clients/shared/IPC/IPCMessages.swift',
24
+ "../clients/shared/IPC/IPCMessages.swift",
25
25
  );
26
26
 
27
27
  /**
@@ -31,37 +31,35 @@ const SWIFT_PATH = path.resolve(
31
31
  */
32
32
  const SWIFT_OMIT_ALLOWLIST = new Set<string>([
33
33
  // Server-internal events not surfaced to macOS client
34
- 'context_compacted',
35
- 'memory_recalled',
36
- 'model_info',
37
- 'secret_detected',
38
- 'sessions_clear_response',
39
- 'usage_response',
40
- 'usage_update',
34
+ "context_compacted",
35
+ "memory_recalled",
36
+ "model_info",
37
+ "secret_detected",
38
+ "sessions_clear_response",
39
+ "usage_response",
40
+ "usage_update",
41
41
  // Gallery and cloud sharing — not yet consumed by the macOS client
42
- 'gallery_install_response',
43
- 'gallery_list_response',
44
- 'share_app_cloud_response',
42
+ "gallery_install_response",
43
+ "gallery_list_response",
44
+ "share_app_cloud_response",
45
45
  // Page publishing — not yet consumed by the macOS client
46
- 'publish_page_response',
47
- 'unpublish_page_response',
46
+ "publish_page_response",
47
+ "unpublish_page_response",
48
48
  // Heartbeat alerts — not yet consumed by the macOS client
49
- 'heartbeat_alert',
50
- // Browser handoff — not yet consumed by the macOS client
51
- 'browser_handoff_request',
49
+ "heartbeat_alert",
52
50
  // Guardian verification — daemon-internal for Telegram channel setup
53
- 'guardian_verification_response',
51
+ "guardian_verification_response",
54
52
  // Ingress invite/member management — not yet consumed by the macOS client
55
- 'ingress_invite_response',
56
- 'ingress_member_response',
53
+ "ingress_invite_response",
54
+ "ingress_member_response",
57
55
  // Inbox escalation — not yet consumed by the macOS client
58
- 'assistant_inbox_escalation_response',
56
+ "assistant_inbox_escalation_response",
59
57
  // Work item messages — not yet consumed by the macOS client
60
- 'work_item_get_response',
61
- 'work_item_run_task_response',
62
- 'work_item_status_changed',
63
- 'work_item_update_response',
64
- 'work_items_list_response',
58
+ "work_item_get_response",
59
+ "work_item_run_task_response",
60
+ "work_item_status_changed",
61
+ "work_item_update_response",
62
+ "work_items_list_response",
65
63
  ]);
66
64
 
67
65
  /**
@@ -72,7 +70,7 @@ const SWIFT_OMIT_ALLOWLIST = new Set<string>([
72
70
  const INVENTORY_UNEXTRACTABLE = new Set<string>([
73
71
  // UiSurfaceShow is a union of UiSurfaceShowCard | UiSurfaceShowForm | ...
74
72
  // The shared wire type 'ui_surface_show' comes from UiSurfaceShowBase.
75
- 'ui_surface_show',
73
+ "ui_surface_show",
76
74
  ]);
77
75
 
78
76
  /**
@@ -82,9 +80,9 @@ const INVENTORY_UNEXTRACTABLE = new Set<string>([
82
80
  */
83
81
  const SWIFT_AHEAD_ALLOWLIST = new Set<string>([
84
82
  // Defined in Swift LayoutConfig.swift ahead of daemon implementation
85
- 'ui_layout_config',
83
+ "ui_layout_config",
86
84
  // Defined in Swift HTTPDaemonClient ahead of daemon token rotation endpoint
87
- 'token_rotated',
85
+ "token_rotated",
88
86
  ]);
89
87
 
90
88
  // --- Extract Swift decode cases ---
@@ -97,9 +95,13 @@ function extractSwiftDecodeCases(swiftSource: string): Set<string> {
97
95
  let match: RegExpExecArray | null;
98
96
 
99
97
  // Only scan inside the ServerMessage init(from decoder:) block
100
- const decoderStart = swiftSource.indexOf('public init(from decoder: Decoder) throws');
98
+ const decoderStart = swiftSource.indexOf(
99
+ "public init(from decoder: Decoder) throws",
100
+ );
101
101
  if (decoderStart === -1) {
102
- throw new Error('Could not find ServerMessage decoder in IPCMessages.swift');
102
+ throw new Error(
103
+ "Could not find ServerMessage decoder in IPCMessages.swift",
104
+ );
103
105
  }
104
106
 
105
107
  const decoderSection = swiftSource.slice(decoderStart);
@@ -120,7 +122,7 @@ const contractServerTypes = new Set([
120
122
  ...INVENTORY_UNEXTRACTABLE,
121
123
  ]);
122
124
 
123
- const swiftSource = fs.readFileSync(SWIFT_PATH, 'utf-8');
125
+ const swiftSource = fs.readFileSync(SWIFT_PATH, "utf-8");
124
126
  const swiftDecodeCases = extractSwiftDecodeCases(swiftSource);
125
127
 
126
128
  const diffs: string[] = [];
@@ -134,7 +136,10 @@ for (const wireType of contractServerTypes) {
134
136
 
135
137
  // Types decoded in Swift but not in contract
136
138
  for (const wireType of swiftDecodeCases) {
137
- if (!contractServerTypes.has(wireType) && !SWIFT_AHEAD_ALLOWLIST.has(wireType)) {
139
+ if (
140
+ !contractServerTypes.has(wireType) &&
141
+ !SWIFT_AHEAD_ALLOWLIST.has(wireType)
142
+ ) {
138
143
  diffs.push(` - Swift decodes "${wireType}" but it is not in the contract`);
139
144
  }
140
145
  }
@@ -142,30 +147,36 @@ for (const wireType of swiftDecodeCases) {
142
147
  // Stale allowlist entries
143
148
  for (const wireType of SWIFT_OMIT_ALLOWLIST) {
144
149
  if (!contractServerTypes.has(wireType)) {
145
- diffs.push(` ? Omit-allowlist entry "${wireType}" is not in the contract (stale?)`);
150
+ diffs.push(
151
+ ` ? Omit-allowlist entry "${wireType}" is not in the contract (stale?)`,
152
+ );
146
153
  }
147
154
  }
148
155
  for (const wireType of INVENTORY_UNEXTRACTABLE) {
149
156
  if (!swiftDecodeCases.has(wireType)) {
150
- diffs.push(` ? Unextractable entry "${wireType}" is not decoded in Swift (stale?)`);
157
+ diffs.push(
158
+ ` ? Unextractable entry "${wireType}" is not decoded in Swift (stale?)`,
159
+ );
151
160
  }
152
161
  }
153
162
  for (const wireType of SWIFT_AHEAD_ALLOWLIST) {
154
163
  if (contractServerTypes.has(wireType)) {
155
- diffs.push(` ? Ahead-allowlist entry "${wireType}" is now in the contract (remove from allowlist)`);
164
+ diffs.push(
165
+ ` ? Ahead-allowlist entry "${wireType}" is now in the contract (remove from allowlist)`,
166
+ );
156
167
  }
157
168
  }
158
169
 
159
170
  if (diffs.length > 0) {
160
- console.error('IPC Swift decoder drift detected:\n');
171
+ console.error("IPC Swift decoder drift detected:\n");
161
172
  for (const line of diffs) {
162
173
  console.error(line);
163
174
  }
164
175
  console.error(
165
- '\nFix: update IPCMessages.swift decode cases, the contract, or the',
166
- 'allowlist in check-swift-decoder-drift.ts.',
176
+ "\nFix: update IPCMessages.swift decode cases, the contract, or the",
177
+ "allowlist in check-swift-decoder-drift.ts.",
167
178
  );
168
179
  process.exit(1);
169
180
  }
170
181
 
171
- console.log('IPC Swift decoder is in sync with the contract.');
182
+ console.log("IPC Swift decoder is in sync with the contract.");
@@ -768,54 +768,6 @@ exports[`IPC message snapshots ClientMessage types oauth_connect_start serialize
768
768
  }
769
769
  `;
770
770
 
771
- exports[`IPC message snapshots ClientMessage types browser_cdp_response serializes to expected JSON 1`] = `
772
- {
773
- "sessionId": "test-session",
774
- "success": true,
775
- "type": "browser_cdp_response",
776
- }
777
- `;
778
-
779
- exports[`IPC message snapshots ClientMessage types browser_user_click serializes to expected JSON 1`] = `
780
- {
781
- "sessionId": "test-session",
782
- "surfaceId": "test-surface",
783
- "type": "browser_user_click",
784
- "x": 100,
785
- "y": 200,
786
- }
787
- `;
788
-
789
- exports[`IPC message snapshots ClientMessage types browser_user_scroll serializes to expected JSON 1`] = `
790
- {
791
- "deltaX": 0,
792
- "deltaY": -100,
793
- "sessionId": "test-session",
794
- "surfaceId": "test-surface",
795
- "type": "browser_user_scroll",
796
- "x": 100,
797
- "y": 200,
798
- }
799
- `;
800
-
801
- exports[`IPC message snapshots ClientMessage types browser_user_keypress serializes to expected JSON 1`] = `
802
- {
803
- "key": "Enter",
804
- "sessionId": "test-session",
805
- "surfaceId": "test-surface",
806
- "type": "browser_user_keypress",
807
- }
808
- `;
809
-
810
- exports[`IPC message snapshots ClientMessage types browser_interactive_mode serializes to expected JSON 1`] = `
811
- {
812
- "enabled": true,
813
- "sessionId": "test-session",
814
- "surfaceId": "test-surface",
815
- "type": "browser_interactive_mode",
816
- }
817
- `;
818
-
819
771
  exports[`IPC message snapshots ClientMessage types work_items_list serializes to expected JSON 1`] = `
820
772
  {
821
773
  "status": "queued",
@@ -2437,22 +2389,6 @@ exports[`IPC message snapshots ServerMessage types app_files_changed serializes
2437
2389
  }
2438
2390
  `;
2439
2391
 
2440
- exports[`IPC message snapshots ServerMessage types browser_frame serializes to expected JSON 1`] = `
2441
- {
2442
- "frame": "base64-jpeg-data",
2443
- "metadata": {
2444
- "offsetTop": 0,
2445
- "pageScaleFactor": 1,
2446
- "scrollOffsetX": 0,
2447
- "scrollOffsetY": 0,
2448
- "timestamp": 1700000000,
2449
- },
2450
- "sessionId": "sess-001",
2451
- "surfaceId": "surface-001",
2452
- "type": "browser_frame",
2453
- }
2454
- `;
2455
-
2456
2392
  exports[`IPC message snapshots ServerMessage types diagnostics_export_response serializes to expected JSON 1`] = `
2457
2393
  {
2458
2394
  "filePath": "/tmp/diagnostics-conv-001.zip",
@@ -2511,32 +2447,6 @@ exports[`IPC message snapshots ServerMessage types oauth_connect_result serializ
2511
2447
  }
2512
2448
  `;
2513
2449
 
2514
- exports[`IPC message snapshots ServerMessage types browser_cdp_request serializes to expected JSON 1`] = `
2515
- {
2516
- "sessionId": "test-session",
2517
- "type": "browser_cdp_request",
2518
- }
2519
- `;
2520
-
2521
- exports[`IPC message snapshots ServerMessage types browser_interactive_mode_changed serializes to expected JSON 1`] = `
2522
- {
2523
- "enabled": true,
2524
- "sessionId": "test-session",
2525
- "surfaceId": "test-surface",
2526
- "type": "browser_interactive_mode_changed",
2527
- }
2528
- `;
2529
-
2530
- exports[`IPC message snapshots ServerMessage types browser_handoff_request serializes to expected JSON 1`] = `
2531
- {
2532
- "message": "Login required",
2533
- "reason": "auth",
2534
- "sessionId": "test-session",
2535
- "surfaceId": "test-surface",
2536
- "type": "browser_handoff_request",
2537
- }
2538
- `;
2539
-
2540
2450
  exports[`IPC message snapshots ServerMessage types document_editor_show serializes to expected JSON 1`] = `
2541
2451
  {
2542
2452
  "initialContent": "# Hello World",
@@ -193,9 +193,15 @@ describe("SSE route — capacity limit", () => {
193
193
  expect(res2.status).toBe(200);
194
194
  expect(hub.subscriberCount()).toBe(1); // evicted 1, added 1
195
195
 
196
- // First stream should be closed by onEvict.
197
- const { done } = await reader1.read();
198
- expect(done).toBe(true);
196
+ // First stream: the immediate heartbeat was enqueued during start(),
197
+ // then eviction closed the controller. Read past any buffered data
198
+ // until the stream signals done.
199
+ let evictDone = false;
200
+ while (!evictDone) {
201
+ const result = await reader1.read();
202
+ evictDone = result.done;
203
+ }
204
+ expect(evictDone).toBe(true);
199
205
 
200
206
  ac2.abort();
201
207
  });
@@ -658,10 +658,10 @@ describe("AssistantConfigSchema", () => {
658
658
  userConsultTimeoutSeconds: 120,
659
659
  ttsPlaybackDelayMs: 3000,
660
660
  accessRequestPollIntervalMs: 500,
661
- guardianWaitUpdateInitialIntervalMs: 5000,
661
+ guardianWaitUpdateInitialIntervalMs: 15000,
662
662
  guardianWaitUpdateInitialWindowMs: 30000,
663
- guardianWaitUpdateSteadyMinIntervalMs: 7000,
664
- guardianWaitUpdateSteadyMaxIntervalMs: 10000,
663
+ guardianWaitUpdateSteadyMinIntervalMs: 20000,
664
+ guardianWaitUpdateSteadyMaxIntervalMs: 30000,
665
665
  disclosure: {
666
666
  enabled: true,
667
667
  text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the person you represent. Do not say "AI assistant".',
@@ -670,21 +670,8 @@ describe("AssistantConfigSchema", () => {
670
670
  denyCategories: [],
671
671
  },
672
672
  voice: {
673
- mode: "twilio_standard",
674
673
  language: "en-US",
675
674
  transcriptionProvider: "Deepgram",
676
- fallbackToStandardOnError: true,
677
- elevenlabs: {
678
- voiceId: "",
679
- voiceModelId: "",
680
- speed: 1.0,
681
- stability: 0.5,
682
- similarityBoost: 0.75,
683
- useSpeakerBoost: true,
684
- agentId: "",
685
- apiBaseUrl: "https://api.elevenlabs.io",
686
- registerCallTimeoutMs: 5000,
687
- },
688
675
  },
689
676
  callerIdentity: {
690
677
  allowPerCallOverride: true,
@@ -789,43 +776,28 @@ describe("AssistantConfigSchema", () => {
789
776
 
790
777
  test("config without calls.voice parses correctly and produces defaults", () => {
791
778
  const result = AssistantConfigSchema.parse({});
792
- expect(result.calls.voice.mode).toBe("twilio_standard");
793
779
  expect(result.calls.voice.language).toBe("en-US");
794
780
  expect(result.calls.voice.transcriptionProvider).toBe("Deepgram");
795
- expect(result.calls.voice.fallbackToStandardOnError).toBe(true);
796
- expect(result.calls.voice.elevenlabs.voiceId).toBe("");
797
- expect(result.calls.voice.elevenlabs.voiceModelId).toBe("");
798
- expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
799
- expect(result.calls.voice.elevenlabs.stability).toBe(0.5);
800
- expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
801
- expect(result.calls.voice.elevenlabs.useSpeakerBoost).toBe(true);
802
- expect(result.calls.voice.elevenlabs.agentId).toBe("");
803
- expect(result.calls.voice.elevenlabs.apiBaseUrl).toBe(
804
- "https://api.elevenlabs.io",
805
- );
806
- expect(result.calls.voice.elevenlabs.registerCallTimeoutMs).toBe(5000);
807
781
  });
808
782
 
809
- test("legacy style field is silently stripped by schema", () => {
810
- const result = AssistantConfigSchema.parse({
811
- calls: { voice: { elevenlabs: { style: 0.5 } } },
812
- });
813
- expect(
814
- (result.calls.voice.elevenlabs as Record<string, unknown>).style,
815
- ).toBeUndefined();
816
- expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
783
+ test("elevenlabs tuning params have correct defaults", () => {
784
+ const result = AssistantConfigSchema.parse({});
785
+ expect(result.elevenlabs.voiceModelId).toBe("");
786
+ expect(result.elevenlabs.speed).toBe(1.0);
787
+ expect(result.elevenlabs.stability).toBe(0.5);
788
+ expect(result.elevenlabs.similarityBoost).toBe(0.75);
817
789
  });
818
790
 
819
- test("rejects calls.voice.elevenlabs.speed below 0.7", () => {
791
+ test("rejects elevenlabs.speed below 0.7", () => {
820
792
  const result = AssistantConfigSchema.safeParse({
821
- calls: { voice: { elevenlabs: { speed: 0.5 } } },
793
+ elevenlabs: { speed: 0.5 },
822
794
  });
823
795
  expect(result.success).toBe(false);
824
796
  });
825
797
 
826
- test("rejects calls.voice.elevenlabs.speed above 1.2", () => {
798
+ test("rejects elevenlabs.speed above 1.2", () => {
827
799
  const result = AssistantConfigSchema.safeParse({
828
- calls: { voice: { elevenlabs: { speed: 1.5 } } },
800
+ elevenlabs: { speed: 1.5 },
829
801
  });
830
802
  expect(result.success).toBe(false);
831
803
  });
@@ -834,37 +806,20 @@ describe("AssistantConfigSchema", () => {
834
806
  const result = AssistantConfigSchema.parse({
835
807
  calls: {
836
808
  voice: {
837
- mode: "twilio_elevenlabs_tts",
838
809
  language: "es-ES",
839
810
  transcriptionProvider: "Google",
840
- fallbackToStandardOnError: false,
841
- elevenlabs: {
842
- voiceId: "abc123",
843
- stability: 0.8,
844
- },
845
811
  },
846
812
  },
813
+ elevenlabs: {
814
+ stability: 0.8,
815
+ },
847
816
  });
848
- expect(result.calls.voice.mode).toBe("twilio_elevenlabs_tts");
849
817
  expect(result.calls.voice.language).toBe("es-ES");
850
818
  expect(result.calls.voice.transcriptionProvider).toBe("Google");
851
- expect(result.calls.voice.fallbackToStandardOnError).toBe(false);
852
- expect(result.calls.voice.elevenlabs.voiceId).toBe("abc123");
853
- expect(result.calls.voice.elevenlabs.stability).toBe(0.8);
819
+ expect(result.elevenlabs.stability).toBe(0.8);
854
820
  // Defaults preserved for unset fields
855
- expect(result.calls.voice.elevenlabs.voiceModelId).toBe("");
856
- expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
857
- });
858
-
859
- test("rejects invalid calls.voice.mode", () => {
860
- const result = AssistantConfigSchema.safeParse({
861
- calls: { voice: { mode: "invalid_mode" } },
862
- });
863
- expect(result.success).toBe(false);
864
- if (!result.success) {
865
- const msgs = result.error.issues.map((i) => i.message);
866
- expect(msgs.some((m) => m.includes("calls.voice.mode"))).toBe(true);
867
- }
821
+ expect(result.elevenlabs.voiceModelId).toBe("");
822
+ expect(result.elevenlabs.similarityBoost).toBe(0.75);
868
823
  });
869
824
 
870
825
  test("rejects invalid calls.voice.transcriptionProvider", () => {
@@ -880,23 +835,9 @@ describe("AssistantConfigSchema", () => {
880
835
  }
881
836
  });
882
837
 
883
- test("rejects calls.voice.elevenlabs.stability out of range", () => {
838
+ test("rejects elevenlabs.stability out of range", () => {
884
839
  const result = AssistantConfigSchema.safeParse({
885
- calls: { voice: { elevenlabs: { stability: 1.5 } } },
886
- });
887
- expect(result.success).toBe(false);
888
- });
889
-
890
- test("rejects calls.voice.elevenlabs.registerCallTimeoutMs below 1000", () => {
891
- const result = AssistantConfigSchema.safeParse({
892
- calls: { voice: { elevenlabs: { registerCallTimeoutMs: 500 } } },
893
- });
894
- expect(result.success).toBe(false);
895
- });
896
-
897
- test("rejects calls.voice.elevenlabs.registerCallTimeoutMs above 15000", () => {
898
- const result = AssistantConfigSchema.safeParse({
899
- calls: { voice: { elevenlabs: { registerCallTimeoutMs: 20000 } } },
840
+ elevenlabs: { stability: 1.5 },
900
841
  });
901
842
  expect(result.success).toBe(false);
902
843
  });
@@ -972,114 +913,40 @@ describe("AssistantConfigSchema", () => {
972
913
  // ---------------------------------------------------------------------------
973
914
 
974
915
  describe("resolveVoiceQualityProfile", () => {
975
- test("returns correct profile for twilio_standard", () => {
916
+ test("always returns ElevenLabs ttsProvider", () => {
976
917
  const config = AssistantConfigSchema.parse({});
977
918
  const profile = resolveVoiceQualityProfile(config);
978
- expect(profile.mode).toBe("twilio_standard");
979
- expect(profile.ttsProvider).toBe("Google");
980
- expect(profile.voice).toBe("Google.en-US-Journey-O");
919
+ expect(profile.ttsProvider).toBe("ElevenLabs");
981
920
  expect(profile.transcriptionProvider).toBe("Deepgram");
982
- expect(profile.fallbackToStandardOnError).toBe(true);
983
- expect(profile.validationErrors).toEqual([]);
984
921
  });
985
922
 
986
- test("returns correct profile for twilio_elevenlabs_tts with valid voiceId", () => {
923
+ test("uses shared elevenlabs.voiceId for voice", () => {
987
924
  const config = AssistantConfigSchema.parse({
988
- calls: {
989
- voice: {
990
- mode: "twilio_elevenlabs_tts",
991
- elevenlabs: { voiceId: "test-voice-id" },
992
- },
993
- },
925
+ elevenlabs: { voiceId: "test-voice-id" },
994
926
  });
995
927
  const profile = resolveVoiceQualityProfile(config);
996
- expect(profile.mode).toBe("twilio_elevenlabs_tts");
997
928
  expect(profile.ttsProvider).toBe("ElevenLabs");
998
929
  expect(profile.voice).toBe("test-voice-id");
999
- expect(profile.validationErrors).toEqual([]);
1000
930
  });
1001
931
 
1002
- test("falls back for twilio_elevenlabs_tts with empty voiceId and fallback enabled", () => {
1003
- const config = AssistantConfigSchema.parse({
1004
- calls: {
1005
- voice: {
1006
- mode: "twilio_elevenlabs_tts",
1007
- fallbackToStandardOnError: true,
1008
- elevenlabs: { voiceId: "" },
1009
- },
1010
- },
1011
- });
1012
- const profile = resolveVoiceQualityProfile(config);
1013
- expect(profile.mode).toBe("twilio_standard");
1014
- expect(profile.ttsProvider).toBe("Google");
1015
- expect(profile.voice).toBe("Google.en-US-Journey-O");
1016
- expect(profile.validationErrors.length).toBe(1);
1017
- expect(profile.validationErrors[0]).toContain("falling back");
1018
- });
1019
-
1020
- test("returns errors for twilio_elevenlabs_tts with empty voiceId and fallback disabled", () => {
1021
- const config = AssistantConfigSchema.parse({
1022
- calls: {
1023
- voice: {
1024
- mode: "twilio_elevenlabs_tts",
1025
- fallbackToStandardOnError: false,
1026
- elevenlabs: { voiceId: "" },
1027
- },
1028
- },
1029
- });
1030
- const profile = resolveVoiceQualityProfile(config);
1031
- expect(profile.mode).toBe("twilio_elevenlabs_tts");
1032
- expect(profile.validationErrors.length).toBe(1);
1033
- expect(profile.validationErrors[0]).toContain("voiceId is required");
1034
- });
1035
-
1036
- test("returns correct profile for elevenlabs_agent with valid agentId", () => {
1037
- const config = AssistantConfigSchema.parse({
1038
- calls: {
1039
- voice: {
1040
- mode: "elevenlabs_agent",
1041
- elevenlabs: { agentId: "agent-123", voiceId: "v1" },
1042
- },
1043
- },
1044
- });
1045
- const profile = resolveVoiceQualityProfile(config);
1046
- expect(profile.mode).toBe("elevenlabs_agent");
1047
- expect(profile.ttsProvider).toBe("ElevenLabs");
1048
- expect(profile.voice).toBe("v1");
1049
- expect(profile.agentId).toBe("agent-123");
1050
- expect(profile.validationErrors).toEqual([]);
1051
- });
1052
-
1053
- test("falls back for elevenlabs_agent with empty agentId and fallback enabled", () => {
1054
- const config = AssistantConfigSchema.parse({
1055
- calls: {
1056
- voice: {
1057
- mode: "elevenlabs_agent",
1058
- fallbackToStandardOnError: true,
1059
- elevenlabs: { agentId: "" },
1060
- },
1061
- },
1062
- });
932
+ test("defaults to Rachel voice ID when elevenlabs.voiceId is not set", () => {
933
+ const config = AssistantConfigSchema.parse({});
1063
934
  const profile = resolveVoiceQualityProfile(config);
1064
- expect(profile.mode).toBe("twilio_standard");
1065
- expect(profile.validationErrors.length).toBe(1);
1066
- expect(profile.validationErrors[0]).toContain("agentId is empty");
935
+ expect(profile.voice).toBe("21m00Tcm4TlvDq8ikWAM");
1067
936
  });
1068
937
 
1069
- test("returns errors for elevenlabs_agent with empty agentId and fallback disabled", () => {
938
+ test("applies voice tuning params from elevenlabs config", () => {
1070
939
  const config = AssistantConfigSchema.parse({
1071
- calls: {
1072
- voice: {
1073
- mode: "elevenlabs_agent",
1074
- fallbackToStandardOnError: false,
1075
- elevenlabs: { agentId: "" },
1076
- },
940
+ elevenlabs: {
941
+ voiceId: "abc123",
942
+ voiceModelId: "turbo_v2_5",
943
+ speed: 0.9,
944
+ stability: 0.8,
945
+ similarityBoost: 0.9,
1077
946
  },
1078
947
  });
1079
948
  const profile = resolveVoiceQualityProfile(config);
1080
- expect(profile.mode).toBe("elevenlabs_agent");
1081
- expect(profile.validationErrors.length).toBe(1);
1082
- expect(profile.validationErrors[0]).toContain("agentId is required");
949
+ expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
1083
950
  });
1084
951
  });
1085
952
 
@@ -1123,14 +990,9 @@ describe("buildElevenLabsVoiceSpec", () => {
1123
990
 
1124
991
  test("default config uses a bare voiceId when no model override is set", () => {
1125
992
  const config = AssistantConfigSchema.parse({
1126
- calls: {
1127
- voice: {
1128
- mode: "twilio_elevenlabs_tts",
1129
- elevenlabs: { voiceId: "test" },
1130
- },
1131
- },
993
+ elevenlabs: { voiceId: "test" },
1132
994
  });
1133
- const spec = buildElevenLabsVoiceSpec(config.calls.voice.elevenlabs);
995
+ const spec = buildElevenLabsVoiceSpec(config.elevenlabs);
1134
996
  expect(spec).toBe("test");
1135
997
  });
1136
998
  });
@@ -1375,10 +1237,8 @@ describe("loadConfig with schema validation", () => {
1375
1237
  expect(config.calls.userConsultTimeoutSeconds).toBe(120);
1376
1238
  expect(config.calls.disclosure.enabled).toBe(true);
1377
1239
  expect(config.calls.safety.denyCategories).toEqual([]);
1378
- expect(config.calls.voice.mode).toBe("twilio_standard");
1379
1240
  expect(config.calls.voice.language).toBe("en-US");
1380
1241
  expect(config.calls.voice.transcriptionProvider).toBe("Deepgram");
1381
- expect(config.calls.voice.elevenlabs.voiceId).toBe("");
1382
1242
  expect(config.calls.model).toBeUndefined();
1383
1243
  expect(config.calls.callerIdentity).toEqual({
1384
1244
  allowPerCallOverride: true,