From 16a04ea560a1116f77e3528a9ab07c38135c1c10 Mon Sep 17 00:00:00 2001 From: Steven Diaz Date: Sat, 18 Oct 2025 21:16:18 -0700 Subject: [PATCH 1/3] feat: add "reconnect" method --- api.ts | 9 + example/src/App.tsx | 139 +++++++++- package.json | 3 +- scripts/re-build-local-example.sh | 17 ++ vapi.ts | 424 ++++++++++++++++++++++++++++++ 5 files changed, 584 insertions(+), 8 deletions(-) create mode 100755 scripts/re-build-local-example.sh diff --git a/api.ts b/api.ts index c9defae89..bf472b88b 100644 --- a/api.ts +++ b/api.ts @@ -18143,6 +18143,15 @@ export interface CreateWebCallDTO { workflow?: CreateWorkflowDTO; /** These are the overrides for the `workflow` or `workflowId`'s settings and template variables. */ workflowOverrides?: WorkflowOverrides; + /** + * This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room. + * If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room. + * If set to `true`, the room will be deleted and reconnection will not be allowed. + * + * Defaults to `true`. + * @example true + */ + roomDeleteOnUserLeaveEnabled?: boolean; } export interface UpdateCallDTO { diff --git a/example/src/App.tsx b/example/src/App.tsx index 6378520dc..265ca4f8a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import Vapi from '@vapi-ai/web'; const VAPI_PUBLIC_KEY = import.meta.env.VITE_VAPI_PUBLIC_KEY; +const VAPI_API_BASE_URL = import.meta.env.VITE_VAPI_API_BASE_URL; if (!VAPI_PUBLIC_KEY) { throw new Error('VITE_VAPI_PUBLIC_KEY is required. Please set it in your .env.local file.'); @@ -14,7 +15,7 @@ interface Message { } function App() { - const [vapi] = useState(() => new Vapi(VAPI_PUBLIC_KEY)); + const [vapi] = useState(() => new Vapi(VAPI_PUBLIC_KEY, VAPI_API_BASE_URL)); const [connected, setConnected] = useState(false); const [assistantIsSpeaking, setAssistantIsSpeaking] = useState(false); const [volumeLevel, setVolumeLevel] = useState(0); @@ -25,8 +26,21 @@ function App() { const [interruptionsEnabled, setInterruptionsEnabled] = useState(true); const [interruptAssistantEnabled, setInterruptAssistantEnabled] = useState(true); const [endCallAfterSay, setEndCallAfterSay] = useState(false); + const [storedWebCall, setStoredWebCall] = useState(null); useEffect(() => { + // Check for stored webCall on component mount + const stored = localStorage.getItem('vapi-webcall'); + if (stored) { + try { + const parsedWebCall = JSON.parse(stored); + setStoredWebCall(parsedWebCall); + } catch (error) { + console.error('Error parsing stored webCall:', error); + localStorage.removeItem('vapi-webcall'); + } + } + // Update current time every second const timer = setInterval(() => { setCurrentTime(new Date().toLocaleTimeString()); @@ -34,7 +48,7 @@ function App() { // Set up Vapi event listeners vapi.on('call-start', () => { - console.log('Call started'); + console.log('Call started - call-start event fired'); setConnected(true); addMessage('system', 'Call connected'); }); @@ -44,7 +58,7 @@ function App() { setConnected(false); setAssistantIsSpeaking(false); setVolumeLevel(0); - addMessage('system', 'Call ended'); + addMessage('system', 'Call ended - webCall data preserved for reconnection'); }); vapi.on('speech-start', () => { @@ -84,7 +98,7 @@ function App() { console.error('Vapi error:', error); addMessage('system', `Error: ${error.message || error}`); }); - + return () => { clearInterval(timer); vapi.stop(); @@ -104,7 +118,7 @@ function App() { addMessage('system', 'Starting call...'); // Start call with assistant configuration - await vapi.start({ + const webCall = await vapi.start({ // Basic assistant configuration model: { provider: "openai", @@ -137,8 +151,23 @@ function App() { // Max call duration (in seconds) - 10 minutes maxDurationSeconds: 600 + }, undefined, undefined, undefined, undefined, { + roomDeleteOnUserLeaveEnabled: false }); + // Store webCall in localStorage if it was created successfully + if (webCall) { + const webCallToStore = { + webCallUrl: (webCall as any).webCallUrl, + id: webCall.id, + artifactPlan: webCall.artifactPlan, + assistant: webCall.assistant + }; + localStorage.setItem('vapi-webcall', JSON.stringify(webCallToStore)); + setStoredWebCall(webCallToStore); + addMessage('system', 'Call data stored for reconnection'); + } + } catch (error) { console.error('Error starting call:', error); addMessage('system', `Failed to start call: ${error}`); @@ -149,6 +178,41 @@ function App() { vapi.stop(); }; + const reconnectCall = async () => { + if (!storedWebCall) { + addMessage('system', 'No stored call data found'); + return; + } + + try { + addMessage('system', 'Reconnecting to previous call...'); + console.log('Attempting reconnect with data:', storedWebCall); + await vapi.reconnect(storedWebCall); + addMessage('system', 'Reconnect method completed successfully'); + + // Add a small delay to allow events to propagate + setTimeout(() => { + if (!connected) { + addMessage('system', 'Warning: Reconnect completed but connected state not updated. This may indicate an issue with event handling.'); + } + }, 1000); + + } catch (error) { + console.error('Error reconnecting:', error); + addMessage('system', `Failed to reconnect: ${error}`); + + // Clear invalid stored data + localStorage.removeItem('vapi-webcall'); + setStoredWebCall(null); + } + }; + + const clearStoredCall = () => { + localStorage.removeItem('vapi-webcall'); + setStoredWebCall(null); + addMessage('system', 'Cleared stored call data'); + }; + const toggleMute = () => { const newMutedState = !isMuted; vapi.setMuted(newMutedState); @@ -216,7 +280,7 @@ function App() { borderRadius: '8px', marginBottom: '20px' }}> -
+
Status: {connected ? 'Connected' : 'Disconnected'} + {storedWebCall && !connected && ( + + (Reconnect Available) + + )}
Current Time: {currentTime}
+ {storedWebCall && ( +
+ Stored Call: ID {storedWebCall.id || 'Unknown'} - + {connected ? ' Currently active' : ' Ready to reconnect'} +
+ )} + {connected && (
@@ -261,7 +348,8 @@ function App() { display: 'flex', gap: '10px', justifyContent: 'center', - marginBottom: '20px' + marginBottom: '20px', + flexWrap: 'wrap' }}> + + {storedWebCall && !connected && ( + + )} + + {storedWebCall && ( + + )}
{/* Manual Say Controls */} @@ -539,6 +661,9 @@ function App() {
  • Use "Mute" to temporarily disable your microphone
  • Say "goodbye" or "end call" to end the conversation
  • Click "Stop Call" to manually end the call
  • +
  • Persistent Storage: Call data is automatically saved and persists even after calls end
  • +
  • Reconnection: Use "Reconnect to Stored Call" to rejoin your previous session anytime
  • +
  • Use "Clear Stored Call" to permanently remove saved call data when no longer needed
  • diff --git a/package.json b/package.json index 06dad22d3..12c03cb22 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test:example": "npm run pack:local && cd example && npm install && npm run build", "pack:local": "./scripts/build-latest.sh", "clean-builds": "rm -f vapi-ai-web-*.tgz && echo '🧹 Cleaned up all build tarballs'", - "dev:example": "npm run pack:local && cd example && npm install && npm run dev" + "dev:example": "npm run pack:local && cd example && npm install && npm run dev", + "re-build-local-example": "./scripts/re-build-local-example.sh" }, "repository": { "type": "git", diff --git a/scripts/re-build-local-example.sh b/scripts/re-build-local-example.sh new file mode 100755 index 000000000..2f147477a --- /dev/null +++ b/scripts/re-build-local-example.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +# Clean up the example node_modules and package-lock.json +rm -rf example/node_modules +rm -f example/package-lock.json + +# Clean up the local build clean-builds +npm run clean-builds + +# Re-pack local build +npm run pack:local + +# Re-install the dependencies +cd example +npm install \ No newline at end of file diff --git a/vapi.ts b/vapi.ts index 3eff1e514..c91cf0367 100644 --- a/vapi.ts +++ b/vapi.ts @@ -111,6 +111,18 @@ type VapiEventListeners = { 'call-start-failed': (event: CallStartFailedEvent) => void; }; +type StartCallOptions = { + /** + * This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room. + * If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room. + * If set to `true`, the room will be deleted and reconnection will not be allowed. + * + * Defaults to `true`. + * @example true + */ + roomDeleteOnUserLeaveEnabled?: boolean; +} + async function startAudioPlayer( player: HTMLAudioElement, track: MediaStreamTrack, @@ -246,6 +258,7 @@ export default class Vapi extends VapiEventEmitter { squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides, + options?: StartCallOptions ): Promise { const startTime = Date.now(); @@ -304,6 +317,7 @@ export default class Vapi extends VapiEventEmitter { workflow: typeof workflow === 'string' ? undefined : workflow, workflowId: typeof workflow === 'string' ? workflow : undefined, workflowOverrides, + roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled, }) ).data; @@ -889,4 +903,414 @@ export default class Vapi extends VapiEventEmitter { public stopScreenSharing() { this.call?.stopScreenShare(); } + + async reconnect(webCall: { + webCallUrl: string; + id?: string; + artifactPlan?: { videoRecordingEnabled?: boolean }; + assistant?: { voice?: { provider?: string } }; + }): Promise { + const startTime = Date.now(); + + if (this.started) { + throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.'); + } + + if (!webCall.webCallUrl) { + throw new Error('webCallUrl is required for reconnection.'); + } + + this.emit('call-start-progress', { + stage: 'reconnect-initialization', + status: 'started', + timestamp: new Date().toISOString(), + metadata: { + callId: webCall.id || 'unknown', + hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled, + voiceProvider: webCall?.assistant?.voice?.provider || 'unknown' + } + }); + + this.started = true; + + try { + // Clean up any existing call object + if (this.call) { + this.emit('call-start-progress', { + stage: 'cleanup-existing-call', + status: 'started', + timestamp: new Date().toISOString() + }); + await this.cleanup(); + this.emit('call-start-progress', { + stage: 'cleanup-existing-call', + status: 'completed', + timestamp: new Date().toISOString() + }); + } + + const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false; + const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus'; + + // Stage 1: Create Daily call object + this.emit('call-start-progress', { + stage: 'daily-call-object-creation', + status: 'started', + timestamp: new Date().toISOString(), + metadata: { + audioSource: this.dailyCallObject.audioSource ?? true, + videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled, + isVideoRecordingEnabled, + isVideoEnabled + } + }); + + const dailyCallStartTime = Date.now(); + this.call = DailyIframe.createCallObject({ + audioSource: this.dailyCallObject.audioSource ?? true, + videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled, + dailyConfig: this.dailyCallConfig, + }); + + const dailyCallDuration = Date.now() - dailyCallStartTime; + this.emit('call-start-progress', { + stage: 'daily-call-object-creation', + status: 'completed', + duration: dailyCallDuration, + timestamp: new Date().toISOString() + }); + + this.call.iframe()?.style.setProperty('display', 'none'); + + // Set up event listeners + this.call.on('left-meeting', () => { + this.emit('call-end'); + if (!this.hasEmittedCallEndedStatus) { + this.emit('message', { + type: 'status-update', + status: 'ended', + 'endedReason': 'customer-ended-call', + }); + this.hasEmittedCallEndedStatus = true; + } + if (isVideoRecordingEnabled) { + this.call?.stopRecording(); + } + this.cleanup().catch(console.error); + }); + + this.call.on('error', (error: any) => { + this.emit('error', error); + if (isVideoRecordingEnabled) { + this.call?.stopRecording(); + } + }); + + this.call.on('camera-error', (error: any) => { + this.emit('camera-error', error); + }); + + this.call.on('network-quality-change', (event: any) => { + this.emit('network-quality-change', event); + }); + + this.call.on('network-connection', (event: any) => { + this.emit('network-connection', event); + }); + + this.call.on('track-started', async (e) => { + if (!e || !e.participant) { + return; + } + if (e.participant?.local) { + return; + } + if (e.participant?.user_name !== 'Vapi Speaker') { + return; + } + if (e.track.kind === 'video') { + this.emit('video', e.track); + } + if (e.track.kind === 'audio') { + await buildAudioPlayer(e.track, e.participant.session_id); + } + this.call?.sendAppMessage('playable'); + }); + + this.call.on('participant-joined', (e) => { + if (!e || !this.call) return; + subscribeToTracks( + e, + this.call, + isVideoRecordingEnabled, + isVideoEnabled, + ); + }); + + this.call.on('participant-updated', (e) => { + if (!e) { + return; + } + this.emit('daily-participant-updated', e.participant); + }); + + this.call.on('participant-left', (e) => { + if (!e) { + return; + } + destroyAudioPlayer(e.participant.session_id); + }); + + this.call.on('remote-participants-audio-level', (e) => { + if (e) this.handleRemoteParticipantsAudioLevel(e); + }); + + this.call.on('app-message', (e) => this.onAppMessage(e)); + + this.call.on('nonfatal-error', (e) => { + // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error + if (e?.type === 'audio-processor-error') { + this.call + ?.updateInputSettings({ + audio: { + processor: { + type: 'none', + }, + }, + }) + .then(() => { + safeSetLocalAudio(this.call, true); + }); + } + }); + + // Stage 2: Mobile device handling and permissions + const isMobile = this.isMobileDevice(); + this.emit('call-start-progress', { + stage: 'mobile-permissions', + status: 'started', + timestamp: new Date().toISOString(), + metadata: { isMobile } + }); + + if (isMobile) { + const mobileWaitStartTime = Date.now(); + await this.sleep(1000); + const mobileWaitDuration = Date.now() - mobileWaitStartTime; + this.emit('call-start-progress', { + stage: 'mobile-permissions', + status: 'completed', + duration: mobileWaitDuration, + timestamp: new Date().toISOString(), + metadata: { action: 'permissions-wait' } + }); + } else { + this.emit('call-start-progress', { + stage: 'mobile-permissions', + status: 'completed', + timestamp: new Date().toISOString(), + metadata: { action: 'skipped-not-mobile' } + }); + } + + // Stage 3: Join the call + this.emit('call-start-progress', { + stage: 'daily-call-join', + status: 'started', + timestamp: new Date().toISOString() + }); + + const joinStartTime = Date.now(); + await this.call.join({ + url: webCall.webCallUrl, + subscribeToTracksAutomatically: false, + }); + + const joinDuration = Date.now() - joinStartTime; + this.emit('call-start-progress', { + stage: 'daily-call-join', + status: 'completed', + duration: joinDuration, + timestamp: new Date().toISOString() + }); + + // Stage 4: Video recording setup (if enabled) + if (isVideoRecordingEnabled) { + this.emit('call-start-progress', { + stage: 'video-recording-setup', + status: 'started', + timestamp: new Date().toISOString() + }); + + const recordingStartTime = Date.now(); + const recordingRequestedTime = new Date().getTime(); + + try { + this.call.startRecording({ + width: 1280, + height: 720, + backgroundColor: '#FF1F2D3D', + layout: { + preset: 'default', + }, + }); + + const recordingSetupDuration = Date.now() - recordingStartTime; + this.emit('call-start-progress', { + stage: 'video-recording-setup', + status: 'completed', + duration: recordingSetupDuration, + timestamp: new Date().toISOString() + }); + + this.call.on('recording-started', () => { + const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000; + this.emit('call-start-progress', { + stage: 'video-recording-started', + status: 'completed', + timestamp: new Date().toISOString(), + metadata: { delaySeconds: totalRecordingDelay } + }); + + this.send({ + type: 'control', + control: 'say-first-message', + videoRecordingStartDelaySeconds: totalRecordingDelay, + }); + }); + } catch (error) { + const recordingSetupDuration = Date.now() - recordingStartTime; + this.emit('call-start-progress', { + stage: 'video-recording-setup', + status: 'failed', + duration: recordingSetupDuration, + timestamp: new Date().toISOString(), + metadata: { error: error?.toString() } + }); + // Don't throw here, video recording is optional + } + } else { + this.emit('call-start-progress', { + stage: 'video-recording-setup', + status: 'completed', + timestamp: new Date().toISOString(), + metadata: { action: 'skipped-not-enabled' } + }); + } + + // Stage 5: Audio level observer setup + this.emit('call-start-progress', { + stage: 'audio-observer-setup', + status: 'started', + timestamp: new Date().toISOString() + }); + + const audioObserverStartTime = Date.now(); + + try { + this.call.startRemoteParticipantsAudioLevelObserver(100); + const audioObserverDuration = Date.now() - audioObserverStartTime; + this.emit('call-start-progress', { + stage: 'audio-observer-setup', + status: 'completed', + duration: audioObserverDuration, + timestamp: new Date().toISOString() + }); + } catch (error) { + const audioObserverDuration = Date.now() - audioObserverStartTime; + this.emit('call-start-progress', { + stage: 'audio-observer-setup', + status: 'failed', + duration: audioObserverDuration, + timestamp: new Date().toISOString(), + metadata: { error: error?.toString() } + }); + // Don't throw here, this is non-critical + } + + // Stage 6: Audio processing setup + this.emit('call-start-progress', { + stage: 'audio-processing-setup', + status: 'started', + timestamp: new Date().toISOString() + }); + + const audioProcessingStartTime = Date.now(); + + try { + this.call.updateInputSettings({ + audio: { + processor: { + type: 'noise-cancellation', + }, + }, + }); + + const audioProcessingDuration = Date.now() - audioProcessingStartTime; + this.emit('call-start-progress', { + stage: 'audio-processing-setup', + status: 'completed', + duration: audioProcessingDuration, + timestamp: new Date().toISOString() + }); + } catch (error) { + const audioProcessingDuration = Date.now() - audioProcessingStartTime; + this.emit('call-start-progress', { + stage: 'audio-processing-setup', + status: 'failed', + duration: audioProcessingDuration, + timestamp: new Date().toISOString(), + metadata: { error: error?.toString() } + }); + // Don't throw here, this is non-critical + } + + const totalDuration = Date.now() - startTime; + this.emit('call-start-success', { + totalDuration, + callId: webCall?.id || 'unknown', + timestamp: new Date().toISOString() + }); + + // For reconnection, manually emit call-start since 'listening' message may not be sent + console.log('Reconnect completed successfully - manually emitting call-start event'); + this.emit('call-start'); + + } catch (e) { + const totalDuration = Date.now() - startTime; + + this.emit('call-start-failed', { + stage: 'reconnect', + totalDuration, + error: e?.toString() || 'Unknown error occurred', + errorStack: e instanceof Error ? e.stack : 'No stack trace available', + timestamp: new Date().toISOString(), + context: { + isReconnect: true, + callId: webCall?.id || 'unknown', + hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled, + voiceProvider: webCall?.assistant?.voice?.provider || 'unknown', + isMobile: this.isMobileDevice() + } + }); + + // Also emit the generic error event for backward compatibility + this.emit('error', { + type: 'reconnect-error', + error: e, + totalDuration, + timestamp: new Date().toISOString(), + context: { + isReconnect: true, + callId: webCall?.id || 'unknown', + hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled, + voiceProvider: webCall?.assistant?.voice?.provider || 'unknown', + isMobile: this.isMobileDevice() + } + }); + + await this.cleanup(); + throw e; + } + } } From e0dd1367dd3207f9ef8601775178178fd9dbab8b Mon Sep 17 00:00:00 2001 From: Steven Diaz Date: Sat, 18 Oct 2025 21:34:56 -0700 Subject: [PATCH 2/3] add: "end" method for force ending call + jsdocs --- example/src/App.tsx | 2 +- vapi.ts | 70 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 265ca4f8a..eb012bff3 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -175,7 +175,7 @@ function App() { }; const stopCall = () => { - vapi.stop(); + vapi.end(); }; const reconnectCall = async () => { diff --git a/vapi.ts b/vapi.ts index c91cf0367..e4db53d5f 100644 --- a/vapi.ts +++ b/vapi.ts @@ -28,6 +28,10 @@ import { safeSetInputDevicesAsync, } from './daily-guards'; +export interface EndCallMessage { + type: 'end-call'; +} + export interface AddMessageMessage { type: 'add-message'; message: ChatCompletionMessageParam; @@ -51,7 +55,8 @@ export interface SayMessage { type VapiClientToServerMessage = | AddMessageMessage | ControlMessages - | SayMessage; + | SayMessage + | EndCallMessage; type VapiEventNames = | 'call-end' @@ -123,6 +128,31 @@ type StartCallOptions = { roomDeleteOnUserLeaveEnabled?: boolean; } +type WebCall = { + /** + * The Vapi WebCall URL. This is the URL that the call will be joined on. + * + * call.webCallUrl or call.transport.callUrl + */ + webCallUrl: string; + /** + * The Vapi WebCall ID. This is the ID of the call. + * + * call.id + */ + id?: string; + /** + * The Vapi WebCall artifact plan. This is the artifact plan of the call. + */ + artifactPlan?: { videoRecordingEnabled?: boolean }; + /** + * The Vapi WebCall assistant. This is the assistant of the call. + * + * call.assistant + */ + assistant?: { voice?: { provider?: string } }; +} + async function startAudioPlayer( player: HTMLAudioElement, track: MediaStreamTrack, @@ -816,6 +846,12 @@ export default class Vapi extends VapiEventEmitter { }, 1000); } + /** + * Stops the call by destroying the Daily call object. + * + * If `roomDeleteOnUserLeaveEnabled` is set to `false`, the Vapi call will be kept alive, allowing reconnections to the same call using the `reconnect` method. + * If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections. + */ async stop(): Promise { this.started = false; if (this.call) { @@ -825,6 +861,11 @@ export default class Vapi extends VapiEventEmitter { this.speakingTimeout = null; } + /** + * Sends a Live Call Control message to the Vapi server. + * + * Docs: https://docs.vapi.ai/calls/call-features + */ send(message: VapiClientToServerMessage): void { this.call?.sendAppMessage(JSON.stringify(message)); } @@ -851,6 +892,18 @@ export default class Vapi extends VapiEventEmitter { }); } + /** + * Ends the call immediately by sending a `end-call` message using Live Call Control, and destroys the Daily call object. + * + * This method always ends the call, regardless of the `roomDeleteOnUserLeaveEnabled` option. + */ + public end() { + this.send({ + type: 'end-call', + }); + this.stop(); + } + public setInputDevicesAsync( options: Parameters[0], ) { @@ -904,12 +957,13 @@ export default class Vapi extends VapiEventEmitter { this.call?.stopScreenShare(); } - async reconnect(webCall: { - webCallUrl: string; - id?: string; - artifactPlan?: { videoRecordingEnabled?: boolean }; - assistant?: { voice?: { provider?: string } }; - }): Promise { + /** + * Reconnects to an active call. + * + * + * @param webCall + */ + async reconnect(webCall: WebCall): Promise { const startTime = Date.now(); if (this.started) { @@ -1272,8 +1326,6 @@ export default class Vapi extends VapiEventEmitter { timestamp: new Date().toISOString() }); - // For reconnection, manually emit call-start since 'listening' message may not be sent - console.log('Reconnect completed successfully - manually emitting call-start event'); this.emit('call-start'); } catch (e) { From d62c7504eff3a2fb64226e4c2807e00116f109f1 Mon Sep 17 00:00:00 2001 From: Steven Diaz Date: Sat, 18 Oct 2025 22:02:21 -0700 Subject: [PATCH 3/3] chore: add ts-expect-error as api property is hidden --- vapi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vapi.ts b/vapi.ts index e4db53d5f..da4caa95a 100644 --- a/vapi.ts +++ b/vapi.ts @@ -347,6 +347,7 @@ export default class Vapi extends VapiEventEmitter { workflow: typeof workflow === 'string' ? undefined : workflow, workflowId: typeof workflow === 'string' ? workflow : undefined, workflowOverrides, + // @ts-expect-error: API hidden property roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled, }) ).data;