How SSE Streaming Handles Reconnection After Page Refresh in Pi-Web

When a user reloads the page mid-stream, the Pi-Web frontend automatically re-establishes the EventSource connection, checks the server’s streaming status via the connected event, and reconciles the UI state using grace timers and HTTP polling fallbacks.

The agegr/pi-web repository implements a resilient Server‑Sent Events (SSE) architecture that survives browser refreshes without losing session context. By leveraging the AgentEventConnection class configured in the React hook layer, the application ensures that streaming agent sessions remain intact even when users navigate away and return.

Reconnection Architecture Overview

The SSE lifecycle management lives primarily in hooks/useAgentSession.ts. This hook instantiates an AgentEventConnection—a wrapper around the native EventSource API that handles automatic reconnection with configurable timeouts.

The connection is initialized with two critical timing parameters:

  • EVENT_STREAM_RECONNECT_DELAY_MS (1,000 ms): The backoff delay before attempting to reopen the stream after a disconnect.
  • EVENT_STREAM_READY_TIMEOUT_MS (60,000 ms): The maximum time to wait for the server to acknowledge the SSE handshake before considering the connection failed.

The shouldMaintain callback ensures the stream stays alive only while the session remains active, preventing zombie connections when the user actually leaves the page.

The Reconnection Flow

When the page reloads with an active sessionId, the hook executes ensureEventsConnected(sessionId), which creates a fresh EventSource pointing to /api/agent/<sessionId>/events. The server then drives the client through a state recovery protocol.

1. Detecting Active Streams on Reconnect

Upon successful handshake, the server emits a connected event containing an isStreaming boolean. The handler in hooks/useAgentSession.ts (lines 212–226) processes this signal:

If isStreaming is true, the client immediately:

  • Cancels the idle grace timer via cancelEventStreamGrace().
  • Sets sdkAgentActiveRef.current = true.
  • Transitions the UI to the waiting_model phase, restoring the streaming bubble that was present before the refresh.

This ensures the user sees the generation resume instantly without manual intervention.

2. Grace Period Management for Idle Sessions

If the stream finished while the page was unloading, the client starts a EVENT_STREAM_IDLE_GRACE_MS timer (30,000 ms) via scheduleEventStreamClose. This safety mechanism periodically polls the /api/agent/<sessionId> endpoint to check session status.

When the server reports an idle state, the client closes the SSE connection cleanly. This prevents stray HTTP connections from lingering after the user has abandoned the session.

3. State Reconciliation After Reconnection

After the new SSE channel is established, the client runs reconcileAgentState to fetch the canonical session state from /api/agent/<sessionId>. If the server indicates the session is neither streaming nor prompting, the client invokes finishPromptWithoutStream to load the final state and close the stream.

This HTTP fallback guarantees that missed SSE events—whether from a network glitch or a rapid page refresh sequence—cannot leave the UI stuck in a loading state.

4. Prompt Settlement Fallback

In cases where the server never reports an idle state (e.g., the stream is still active but SSE messages were lost), the client executes waitForPromptSettlement. This function polls the session endpoint until the prompt finishes or the PROMPT_SETTLE_MAX_MS timeout (20,000 ms) expires.

Once settled, the UI loads the latest session file and optionally terminates the SSE stream, ensuring consistency between server and client state.

Implementation Deep Dive

The following excerpts from hooks/useAgentSession.ts demonstrate the core reconnection logic:

// Initialize the SSE connection with reconnection config
eventConnectionRef.current = new AgentEventConnection({
  createSource: (sid) => new EventSource(`/api/agent/${encodeURIComponent(sid)}/events`),
  onEvent: (e) => handleAgentEventRef.current?.(e as AgentEvent),
  shouldMaintain: (sid) => (
    sessionHookMountedRef.current &&
    sessionIdRef.current === sid &&
    (agentRunningRef.current || eventStreamGraceActiveRef.current)
  ),
  readinessTimeoutMs: EVENT_STREAM_READY_TIMEOUT_MS,   // 60 s
  reconnectDelayMs: EVENT_STREAM_RECONNECT_DELAY_MS,   // 1 s
});

On mount or session change, the hook ensures connectivity:

useEffect(() => {
  if (!session?.id || !sessionRunning) return;
  maintainEventsConnected(session.id);   // reopens EventSource after refresh
}, [maintainEventsConnected, session?.id, sessionRunning]);

The event handler reconciles server state with UI state:

const handleAgentEvent = useCallback((event) => {
  if (event.type === 'connected') {
    dispatch({ type: 'end' });
    if (event.isStreaming) {
      cancelEventStreamGrace();
      sdkAgentActiveRef.current = true;
      setAgentRunning(true);
      setAgentPhase({ kind: 'waiting_model' });
    }
  }
  // ...other event types...
}, [cancelEventStreamGrace]);

Summary

  • AgentEventConnection in hooks/useAgentSession.ts manages the EventSource lifecycle with automatic reconnection delays and readiness timeouts.
  • The connected event transmits the isStreaming flag, allowing the client to immediately resume the UI if generation was ongoing.
  • An idle grace timer (EVENT_STREAM_IDLE_GRACE_MS) watches for completed streams after a refresh and closes the connection when the server reports an idle state.
  • State reconciliation via reconcileAgentState and waitForPromptSettlement uses HTTP polling to recover any events lost during the disconnect window.
  • All timing constants (1000ms reconnect delay, 20000ms settlement timeout, 30000ms grace period) are configurable but default to values that balance responsiveness with server load.

Frequently Asked Questions

What happens if the server is still streaming when the page refreshes?

The client creates a new EventSource connection to the same session ID. When the server emits the connected event with isStreaming: true, the client cancels the grace timer, sets the agent to active, and restores the waiting_model UI phase—effectively resuming the stream exactly where it left off.

How does the client handle completed streams after a refresh?

If the stream finished while the page was away, the grace timer (scheduleEventStreamClose) polls the session endpoint. Once the server reports an idle status, the client closes the SSE connection and loads the final session state via finishPromptWithoutStream, ensuring the user sees the completed result without a streaming UI.

What is the purpose of the grace timer in SSE reconnection?

The EVENT_STREAM_IDLE_GRACE_MS (30-second) timer acts as a safety valve. It prevents the frontend from immediately closing the connection on refresh, giving the server time to report whether the previous stream actually completed. This avoids premature UI dismissal when the user rapidly reloads the page.

How long does the client wait for the SSE connection to become ready?

The connection must become ready within EVENT_STREAM_READY_TIMEOUT_MS (60 seconds). If the server fails to send the connected event within this window, the AgentEventConnection considers the attempt failed and will retry after the standard reconnect delay of 1,000 ms.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →