Reconnects
How the server transparently rotates the upstream model session, what the reconnecting wire event 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 wire event 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.
The reconnecting wire event
{
"type": "reconnecting",
"id": "<message-uuid>",
"seconds_remaining": 4.2
}seconds_remaining is optional — when present, it gives an estimate of how long the rotation will take. The voice pauses during this window; the agent resumes from where it left off once the new upstream session is ready.
TypeScript
Two signals, two subscriptions:
session.on('reconnecting', …)— the server-side upstream rotation (the wire event 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';
function ConnectionStatus() {
const state = useTransportState();
const error = useRealtimeError();
if (state === 'reconnecting') {
return <div style={{ color: '#92400e' }}>Reconnecting…</div>;
}
if (state === 'failed' && error) {
return (
<div style={{ color: '#b91c1c' }}>
{error.code}: {error.message}
</div>
);
}
if (state === 'ready') {
return <div style={{ color: '#15803d' }}>Connected</div>;
}
return <div style={{ color: '#6b7280' }}>{state}</div>;
}The SDK does not retry a failed session itself. To resume a conversation after a terminal disconnect, start a new session with agent.start({ resumeSessionId }).
Python
agent.start(on_state_change=...) observes the same lifecycle; the RealtimeReconnecting event on the stream is the server-side rotation notice:
from cosmo_ai import CosmoRealtime, RealtimeReconnecting, 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 CosmoRealtime(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, RealtimeReconnecting):
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 cannot be recovered finishes the stream with a terminal RealtimeSessionEnded 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: CosmoRealtime, 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 backoffSwift
session.states carries the transport lifecycle (.reconnecting / .reconnected bracket a transient recovery); the .reconnecting(_:) event on session.events is the server-side rotation notice:
Task {
for await state in session.states {
switch state {
case .reconnecting:
print("transport recovering…")
case .reconnected:
print("recovered — same session, same thread")
case .disconnected(let reason):
print("session over: \(reason)")
default:
break
}
}
}
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 constructing a new session via RealtimeSession.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 / .reconnected):
- 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.
Next steps
- 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