Cosmo Realtime SDK
Guides

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 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';

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 backoff

Swift

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

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

  • 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.

Next steps

On this page