Reconnects
How the server transparently rotates the upstream model session, what the reconnecting frame means, and how to surface reconnecting state in each SDK.
Upstream realtime model sessions have a finite lifetime. When the provider session expires, the Cosmo server transparently rotates to a fresh upstream session — the room, voice tracks, and conversation state survive the rotation. Clients receive a reconnecting frame during the swap.
Separately, the media transport itself can hit a transient drop (network blip, client briefly offline). The transport layer attempts to recover the same connection; each SDK surfaces this window through its lifecycle state (reconnecting, then back to connected). If recovery fails, the session ends terminally — sessions are single-attempt in every SDK, and starting a new session is the way back.
Both recovery layers belong to the WebRTC transport. A session on the websocket transport has neither: the upstream session is never rotated and a dropped socket is not recovered — either one ends the session, and the app starts a new one.
The reconnecting frame
{
"type": "reconnecting",
"version": "1.0",
"seconds_remaining": 4.2
}seconds_remaining is optional — when present, it estimates how long the rotation takes. The voice pauses during this window; the agent resumes from where it left off once the new upstream session is ready.
Reconnect handling in your app
Each SDK surfaces the same three layers; only the idiom differs.
Two signals, two subscriptions:
session.on('reconnecting', …)— the server-side upstream rotation (the frame above).session.on('lifecycle', …)— the formal connection state machine (idle → connecting → connected ↔ reconnecting → disconnected), which entersreconnectingwhen the media transport is recovering from a transient drop.
const session = await client.agent({ /* config */ }).start();
session.on('reconnecting', ({ secondsRemaining }) => {
console.log('server rotating upstream session', { secondsRemaining });
});
session.on('lifecycle', (state) => {
if (state.kind === 'reconnecting') {
console.log('transport recovering…');
}
if (state.kind === 'disconnected') {
console.log('session over:', state.disconnectReason, state.detail);
}
});In React, useTransportState() reads the same transitions — the transport axis reports 'reconnecting' during transient recovery and returns to 'ready' on success:
import { useTransportState, useRealtimeError } from 'cosmo-ai/react';
function ConnectionStatus() {
const state = useTransportState();
const error = useRealtimeError();
if (state === 'reconnecting') {
return <div style={{ color: '#92400e' }}>Reconnecting…</div>;
}
if (state === 'failed') {
// A transport that gives up reports itself on the transport axis; the
// error axis carries a server error only when the server sent one.
return (
<div style={{ color: '#b91c1c' }}>
{error ? `${error.code}: ${error.message}` : 'Connection lost.'}
</div>
);
}
if (state === 'ready') {
return <div style={{ color: '#15803d' }}>Connected</div>;
}
return <div style={{ color: '#6b7280' }}>{state}</div>;
}The SDK doesn't retry a failed session itself. To resume a conversation after a terminal disconnect, start a new session with agent.start({ resumeSessionId }).
agent.start(on_state_change=...) observes the same lifecycle; the ReconnectingEvent event on the stream is the server-side rotation notice:
from cosmo_ai import RealtimeClient, ReconnectingEvent, SessionState, SessionStateKind
def on_state(state: SessionState) -> None:
if state.kind is SessionStateKind.RECONNECTING:
print("transport recovering…")
elif state.kind is SessionStateKind.DISCONNECTED:
print(f"session over: {state.disconnect_reason} ({state.detail})")
async with RealtimeClient(api_key="cosmo_...") as client:
agent = client.agent()
async with agent.start(on_state_change=on_state) as session:
async for event in session:
if isinstance(event, ReconnectingEvent):
print(f"server rotating upstream session (eta={event.seconds_remaining}s)")On a transient transport drop the SDK re-asserts session state automatically once the transport recovers — the input binding and the last mute state are re-sent, so a recovered session behaves like it never dropped. A drop that can't be recovered finishes the stream with a terminal SessionEndedEvent and a DISCONNECTED state carrying disconnect_reason.
To retry after a terminal disconnect, start a new session (optionally with resume_session_id= to continue the conversation):
async def run_with_retry(client: RealtimeClient, max_attempts: int = 3) -> None:
agent = client.agent()
last_session_id: str | None = None
for attempt in range(1, max_attempts + 1):
print(f"Connecting (attempt {attempt})…")
try:
async with agent.start(resume_session_id=last_session_id) as session:
last_session_id = session.session_id
async for event in session:
... # handle events; iteration ends when the session does
reason = session.state.disconnect_reason
if reason is not None and reason.value in ("client_ended", "server_ended"):
return # clean end — don't retry
except Exception as exc:
print(f"Session error: {exc}")
await asyncio.sleep(2**attempt) # exponential backoffThe onStateChange handler on agent.start carries the transport lifecycle (.reconnecting, then .connected again on recovery); the .reconnecting(_:) event on session.events is the server-side rotation notice:
let onStateChange: @Sendable (SessionState) -> Void = { state in
switch state {
case .reconnecting:
print("transport recovering…")
case .connected:
print("live — first connect or a completed recovery")
case .disconnected(let reason, let detail):
print("session over: \(reason) \(detail ?? "")")
default:
break
}
}
let session = try await agent.start(onStateChange: onStateChange)
for try await event in session.events {
if case .reconnecting(let notice) = event {
print("server rotating upstream session (eta=\(notice.secondsRemaining ?? 0)s)")
}
}Like the other SDKs, a Swift session is single-attempt: any terminal path finishes the streams, and reconnecting means opening a new session with agent.start().
What survives a reconnect
When the server sends reconnecting (upstream model rotation):
- The room stays connected and audio tracks keep publishing.
- Conversation state is preserved server-side; the agent resumes mid-conversation.
- The Cosmo
session_idis unchanged — the rotation is internal to the server.
When the transport recovers a transient drop (reconnecting → connected):
- The same session continues; no new
readyfires. - The SDK re-asserts the input binding and the last mute state on the recovered connection.
- Transcript history already delivered to your app is untouched (the React provider keeps its accumulated snapshot).
When recovery fails, the session is over: the stream finishes with its terminal ended event and the lifecycle reports disconnected with a typed reason. Start a new session — with resumeSessionId / resume_session_id if you want the conversation to continue.
See also
- Debugging — correlate session IDs across sessions in logs
- Build a voice React app — full lifecycle including error + reconnect UI
- Session limits — server-enforced duration caps end sessions cleanly