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

> Learn how Pi-Web's SSE streaming automatically reconnects after a page refresh. Discover UI state reconciliation, grace timers, and HTTP polling fallbacks.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-17

---

**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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) demonstrate the core reconnection logic:

```typescript
// 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:

```typescript
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:

```typescript
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`](https://github.com/agegr/pi-web/blob/main/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.