Cosmo Realtime SDK
Guides

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 enters reconnecting when 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 backoff

The 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_id is unchanged — the rotation is internal to the server.

When the transport recovers a transient drop (reconnecting → connected):

  • The same session continues; no new ready fires.
  • 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

On this page