Connection Lifecycle
How the widget manages its WebSocket connection over the life of a run — active/dormant/hibernating states, wake triggers, jittered reconnects, and participant-token expiry recovery.
Overview
The widget scales connections with active conversations, not page views. A run parked at a HITL input doesn't need a live socket — user→server traffic (sendMessage/submitHandback) is REST, and the server replays full state (timeline + any open HITL session) on every connect, so a socketless widget loses nothing; it just hears about new events late.
Connection states
active ──idle N min at a HITL input──▶ dormant ──~1 h idle / tab hidden──▶ hibernating
▲ (poll finds events, interaction, submit) │
└────────────────────────────◀──────────────────────────────────────────────┘
dormant/hibernating ──poll sees a terminal run──▶ completed | error
wake fails repeatedly ──▶ offline wake gets a 401 ──▶ expired ──re-mint──▶ active- active — the run is executing, streaming, or was recently interacted with. The socket stays open, kept alive with server-side pings (~25 s) so intermediary proxies can never cut an in-flight conversation.
- dormant — parked at a HITL input with no interaction for a while (default 5 min). The widget deliberately disconnects (no reconnect loop) and lazily polls
GET …/timeline?since_seq=every 30–60 s. The UI is unchanged; a new poll result wakes the widget. - hibernating — dormant for longer (default ~1 h) or the tab is hidden. Polling stops too; the widget wakes only on interaction/visibility/online — a widget can sit hibernating indefinitely at zero background cost.
- offline (public status) — reserved for genuine failures: wake was attempted and reconnecting failed repeatedly. Dormancy/hibernation are never demoted to offline on a timer.
- expired(public status) — the participant token's TTL lapsed; see Token expiry & re-mint below.
active/dormant/hibernating are internal — hosts never see them via onStatusChange. Only the public RunStatus values change (connecting, running, completed, error, offline, expired).Wake triggers
The widget wakes from dormant/hibernating on: focus/keydown/pointerdown anywhere inside the widget, the tab becoming visible, navigator.onLine restoration, and always immediately before sendMessage/submitHandback (it must be connected to hear the response).
connectionPolicy
<ChatterflyWidget
{...props}
connectionPolicy={{ idleDisconnectMs: 5 * 60_000, pollIntervalMs: 45_000, hibernateAfterMs: 60 * 60_000 }}
// or: connectionPolicy="always-on"
/>| Name | Description |
|---|---|
| idleDisconnectMs | Idle time at a parked HITL input before going dormant. Default 5 min. |
| pollIntervalMs | Poll cadence while dormant. Default 30–60 s. |
| hibernateAfterMs | Idle time before dormant → hibernating (polling stops). Default ~1 h. |
"always-on"instead of an options object to opt out of dormancy entirely — the socket stays open for the run's whole lifetime. Use this for surfaces that need instant push (e.g. an internal multi-participant dashboard) where the extra open connections are an acceptable cost.Reconnects
- Reconnects while the timeline already has content never blank the UI (no full-screen connecting/replaying render) — only the very first connect shows that. Style an optional "reconnecting…" pill via
strings.reconnecting. - Retries use jittered exponential backoff (2 s → ~30 s cap), not a fixed interval, so a backend deploy doesn't trigger a thundering herd across every embed on your site simultaneously.
Token expiry & re-mint
Participant tokens are short-lived JWTs (~1 h default). On a wake or poll, a 401 triggers an automatic call to POST /api/public/runs/{runId}/token (re-mint) using the expired token itself — signature still valid, only exp lapsed — which mints a fresh token for the same participant. This is transparent to the host in the common case, and makes hibernation effectively indefinite for serverless embeds.
Re-mint only applies to public deployments (i.e. the widget minted its own run via workflowId/deploymentId) and only for non-terminal runs. If re-mint is unavailable (privately-minted participantToken) or fails, the widget surfaces the expired status plus WidgetError("token_expired") so you can offer ref.current?.restart().
Anti-patterns
- Don't remount the widget (e.g. via a changing
key) ononStatusChange("connecting")— reconnects are expected and self-healing; remounting discards the in-memory timeline and forces a full replay. - Don't build your own keep-alive ping from the host page — the widget already manages its own connection lifecycle; extra traffic only defeats the dormancy scaling story.
- Don't treat
expiredas a hard failure requiring an immediate restart — the widget already attempts a silent re-mint first; only show a recovery affordance onceexpiredactually surfaces.
