Summary
ConversationProvider from @elevenlabs/react is broken on iOS Safari 18.7. When using useConversation() + useConversationClientTool() inside a ConversationProvider, the session connects at the WebSocket level (the onConnect callback fires, audio streams in both directions), but React state never updates:
status reports disconnected within 1ms of briefly showing connected
isSpeaking never updates
- Client tools registered via
useConversationClientTool() never fire — the SDK logs "Client tool not defined" errors for every tool call
The same agent, same device, same browser works perfectly when calling Conversation.startSession() from @elevenlabs/client directly, bypassing the React provider entirely.
Environment
| Component |
Version |
| Device |
iPhone 16 Pro (iOS 18.7) |
| Browser |
Safari 26.4 / WebKit |
@elevenlabs/react |
1.0.3 |
@elevenlabs/client |
1.1.2 |
| React |
19.x |
| Vite |
8.x |
Desktop browsers (Chrome, Firefox, Safari on macOS) work correctly with ConversationProvider.
Steps to reproduce
// main.tsx
import { ConversationProvider } from '@elevenlabs/react';
createRoot(document.getElementById('root')!).render(
<ConversationProvider>
<App />
</ConversationProvider>
);
// App.tsx
import { useConversation, useConversationClientTool } from '@elevenlabs/react';
function App() {
const { status, isSpeaking, startSession, endSession } = useConversation();
useConversationClientTool('navigate_to_planet', (params) => {
console.log('Tool called:', params); // Never fires on iOS
return 'OK';
});
return (
<button onClick={() => {
if (status === 'connected') endSession();
else startSession({ agentId: 'your-agent-id' });
}}>
{status} {isSpeaking ? '(speaking)' : ''}
</button>
);
}
- Deploy to a public URL (localhost won't have mic permissions on iOS)
- Open on iPhone running iOS 18.7 in Safari
- Tap the button to start a session
- Observe: button shows "disconnected" even though the agent is audibly speaking
- Ask the agent to use a tool — observe
"Client tool not defined" in the console
Expected behavior
status transitions: disconnected → connecting → connected
isSpeaking toggles as the agent speaks
- Client tools fire when the agent invokes them
Actual behavior (iOS Safari 18.7)
startSession() is called
status briefly flashes connected, then immediately reverts to disconnected (~1ms)
- The WebSocket connection IS alive — the agent speaks, audio plays (after the first message), transcripts appear in
onMessage
isSpeaking never changes from false
- When the agent tries to call a client tool, the SDK logs:
"Client tool not defined: navigate_to_planet" — the tool registrations from useConversationClientTool() are not reaching the live session
- The session is effectively headless: audio works, but all React state and tool dispatch is broken
Root cause analysis
The provider's internal state management breaks on iOS Safari. Based on extensive debugging:
ConversationProvider wraps Conversation.startSession() in a .then(success, failure) chain that manages React context state
- On iOS Safari, either:
- The promise rejects silently (the failure handler in
ConversationProvider.js:85 runs but only sets lockRef.current = null without logging), or
- The provider's internal state machine enters an inconsistent state where the WebSocket session is alive but the React context layer believes it's disconnected
- Because the context reports
disconnected, the tool dispatch layer (which is gated on connection status) never forwards tool calls to the registered handlers
- The
useConversationClientTool() registrations are stored in React context state that is stale/disconnected from the live Conversation instance
Evidence
When using ConversationProvider on iOS Safari:
onConnect fired (+3982ms) ← SDK callback fires
onStatusChange: {status: "disconnected"} ← React state immediately reverts (same ms)
isSpeaking: false (never changes)
"Client tool not defined: navigate_to_planet" ← tools not registered on live session
When bypassing the provider via Conversation.startSession() from @elevenlabs/client on the SAME device:
onConnect fired (+3982ms)
onStatusChange: {status: "connected"} ← stays connected
onModeChange: {mode: "speaking"} ← updates correctly
onMessage: {source: "user", message: "Go to Mars."}
navigate_to_planet called: {name: "Mars"} ← tool works
This confirms the bug is in ConversationProvider's state management, not in the underlying client SDK or iOS Safari's WebSocket support.
Workaround
Bypass ConversationProvider and use Conversation from @elevenlabs/client directly:
import { Conversation } from '@elevenlabs/client';
const convRef = useRef<Conversation | null>(null);
const [status, setStatus] = useState('disconnected');
const start = async () => {
setStatus('connecting');
const conv = await Conversation.startSession({
agentId: 'your-agent-id',
clientTools: {
navigate_to_planet: (params) => {
// Works reliably on iOS
return 'OK';
},
},
onConnect: () => setStatus('connected'),
onDisconnect: () => { setStatus('disconnected'); convRef.current = null; },
onModeChange: (m) => setIsSpeaking(m.mode === 'speaking'),
});
convRef.current = conv;
};
Related issues
Additional iOS issue (separate)
There is a second, unrelated issue on iOS Safari: the first agent message audio is inaudible. Subsequent messages play normally.
AudioContext.state is running
- The SDK's hidden
<audio> element has currentTime advancing past 15+ seconds
paused is false, volume is 1, readyState is 4
- No audible output from the speaker
This appears to be related to MediaDeviceOutput.create() in output.ts using audioElement.autoplay = true with srcObject = MediaStream but never calling audioElement.play() explicitly. On iOS Safari, autoplay with srcObject does not produce audible output without an explicit play() call in the user gesture chain. Additionally, getUserMedia() switches iOS to AVAudioSessionCategoryPlayAndRecord which routes initial audio output to the earpiece at near-zero volume.
Summary
ConversationProviderfrom@elevenlabs/reactis broken on iOS Safari 18.7. When usinguseConversation()+useConversationClientTool()inside aConversationProvider, the session connects at the WebSocket level (theonConnectcallback fires, audio streams in both directions), but React state never updates:statusreportsdisconnectedwithin 1ms of briefly showingconnectedisSpeakingnever updatesuseConversationClientTool()never fire — the SDK logs"Client tool not defined"errors for every tool callThe same agent, same device, same browser works perfectly when calling
Conversation.startSession()from@elevenlabs/clientdirectly, bypassing the React provider entirely.Environment
@elevenlabs/react@elevenlabs/clientDesktop browsers (Chrome, Firefox, Safari on macOS) work correctly with
ConversationProvider.Steps to reproduce
"Client tool not defined"in the consoleExpected behavior
statustransitions:disconnected→connecting→connectedisSpeakingtoggles as the agent speaksActual behavior (iOS Safari 18.7)
startSession()is calledstatusbriefly flashesconnected, then immediately reverts todisconnected(~1ms)onMessageisSpeakingnever changes fromfalse"Client tool not defined: navigate_to_planet"— the tool registrations fromuseConversationClientTool()are not reaching the live sessionRoot cause analysis
The provider's internal state management breaks on iOS Safari. Based on extensive debugging:
ConversationProviderwrapsConversation.startSession()in a.then(success, failure)chain that manages React context stateConversationProvider.js:85runs but only setslockRef.current = nullwithout logging), ordisconnected, the tool dispatch layer (which is gated on connection status) never forwards tool calls to the registered handlersuseConversationClientTool()registrations are stored in React context state that is stale/disconnected from the liveConversationinstanceEvidence
When using
ConversationProvideron iOS Safari:When bypassing the provider via
Conversation.startSession()from@elevenlabs/clienton the SAME device:This confirms the bug is in
ConversationProvider's state management, not in the underlying client SDK or iOS Safari's WebSocket support.Workaround
Bypass
ConversationProviderand useConversationfrom@elevenlabs/clientdirectly:Related issues
isSpeakingnot updating on iOS (same root cause)Additional iOS issue (separate)
There is a second, unrelated issue on iOS Safari: the first agent message audio is inaudible. Subsequent messages play normally.
AudioContext.stateisrunning<audio>element hascurrentTimeadvancing past 15+ secondspausedisfalse,volumeis1,readyStateis4This appears to be related to
MediaDeviceOutput.create()inoutput.tsusingaudioElement.autoplay = truewithsrcObject = MediaStreambut never callingaudioElement.play()explicitly. On iOS Safari, autoplay withsrcObjectdoes not produce audible output without an explicitplay()call in the user gesture chain. Additionally,getUserMedia()switches iOS toAVAudioSessionCategoryPlayAndRecordwhich routes initial audio output to the earpiece at near-zero volume.