Lifecycle
The session lifecycle is identical to the web widget except reconnect: on mobile a dropped socket comes back as a fresh guest session, never a resume — the server binds a mobile session to the merchant, not the individual shopper, so per-subject continuity is not currently scheduled. Everything else — minimize, teardown, close codes — matches web. The single most important behavior to get right:
minimize() is UI-only — the session and WebSocket stay alive. endSession() ends the session
(sends {type:"end"}) and tears everything down. In your custom UI, wire your "collapse" control to
minimize() and your "X" to endSession().
The lifecycle table
| You do | What happens on the wire | Backend |
|---|---|---|
startVoiceSession() / startTextSession() | issue token → open WS → handshake (ready) | ready → session_state |
minimize() | nothing — UI-only collapse; WS + session stay alive, no end sent | session stays active |
restore() | nothing — un-collapse | — |
| app backgrounded | WS may drop | socket drop noted |
| → return (any time, even < 60s) | reconnect → fresh guest session — no resume, no recap | new session, bound to the merchant not the shopper |
endSession() | sends {type:"end", reason:"user_closed"} → real end + teardown | end_session(reason="client_end") |
| bot ends / fatal close | SDK emits ended / a failed state, tears down | end_session(...) |
screen unmount / destroy() | hard teardown (no end sent) | client_disconnect |
Backgrounding → fresh guest session
When the app is backgrounded the OS may drop the socket. On the mobile SDK, returning to the foreground
always starts a fresh guest session — there is no resume and no recap, even if you reconnect within
60 seconds with the same conversation_id. The server binds a mobile session to the merchant, not the
individual shopper, so per-subject continuity is not currently scheduled. The SDK still sends
your current app context on the new session (see Context enrichment); only the
prior conversation is not carried over.
The 60s Redis reattach window (reattach_session → resume, DB recap after 60s) is a web-widget behavior
— it does not apply to the mobile SDK.
This is not an end. You never send end on backgrounding.
foreground ──tap launcher──▶ session live (WS open)
│
├─ minimize() ──────────────────────────────▶ session live (UI collapsed, WS open, NO end)
│
├─ app backgrounded ──▶ WS drops ──▶ return (any time) ─▶ fresh guest session (no resume, no recap)
│
└─ endSession() ─────────────────────────────▶ {type:"end"} ─▶ real end + teardown
Reconnect policy
The transport reconnects on transient drops with exponential backoff + jitter, capped at ≤3 attempts, then settles into a graceful state without crashing. A heartbeat ping runs about every 25s; if no pong is seen in ~30s the SDK closes and reconnects.
Fatal close codes are NOT retried — they mean the session cannot continue:
| Code | Meaning |
|---|---|
4400 | malformed request |
4401 | invalid/expired token (or key blocked by the billing kill-switch) |
4403 | app_id not allowed |
4503 | voice provider down |
1008 | session limit |
1011 | server crash |
All other drops are reconnected (within the ≤3 cap). See the wire protocol §8.
Observing state
- React Native
- Flutter
const session = await client.startVoiceSession({ lang: 'uk' });
session.on('status', (s) => console.log(s.status)); // idle | connecting | … | ended
session.on('ended', (e) => console.log('ended:', e.reason));
session.on('error', (e) => console.warn(e.code, e.message));
session.minimize(); // UI-only — session stays alive
session.restore();
await session.endSession(); // the X — real end
session.destroy(); // hard teardown on unmount (no end sent)
final session = client.startVoiceSession(config: const SessionConfig(lang: 'uk'));
// Coarse status, or the sealed states for richer handling.
session.status.listen((s) => print(s.name));
session.states.listen((s) {
// SessionConnecting | SessionReady | SessionReconnecting | SessionEnded | SessionFailed
});
session.minimize(); // UI-only — session stays alive
session.restore();
await session.endSession(); // the X — real end
await session.dispose(); // hard teardown
await client.destroy();
Parity note. The web widget does not yet wire an explicit "X to end" button (the backend frame exists but is unwired on web today). The mobile SDKs expose it as
endSession().