# How SSE Reconnection Works on Page Refresh Mid-Stream in Pi-Web

> Discover how Pi-Web's SSE reconnection seamlessly restores streaming UI after a page refresh mid-stream, ensuring uninterrupted data flow.

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

---

**When a user reloads the browser during an active generation, Pi-Web automatically re-establishes the Server-Sent Events connection and restores the streaming UI by detecting the server's current state through the `connected` event handshake.**

The pi-web repository implements a resilient SSE architecture that survives page refreshes without losing active session state. Through the `useAgentSession` hook and the `AgentEventConnection` utility, the frontend ensures seamless continuity by re-establishing streams and reconciling missed events via HTTP polling fallbacks.

## The Reconnection Architecture

The reconnection flow centers on [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), which orchestrates the `EventSource` lifecycle and state synchronization.

### Initializing the AgentEventConnection

On the first render, the hook constructs an `AgentEventConnection` configured to handle automatic reconnections. This wrapper is initialized with specific timing parameters and a factory function that points to the streaming endpoint.

```typescript
// hooks/useAgentSession.ts (lines 49-63)
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,000ms
  reconnectDelayMs: EVENT_STREAM_RECONNECT_DELAY_MS,   // 1,000ms
});

```

The `shouldMaintain` callback ensures the connection stays alive only while the session remains active, preventing resource leaks from orphaned connections.

### Surviving the Page Refresh

When the user refreshes the page, the React hook executes again and creates a fresh `AgentEventConnection` instance. The `useEffect` immediately invokes `ensureEventsConnected` (lines 77-80), which opens a new `EventSource` to the same session ID.

```typescript
// hooks/useAgentSession.ts
useEffect(() => {
  if (!session?.id || !sessionRunning) return;
  maintainEventsConnected(session.id);   // Opens a fresh EventSource
}, [maintainEventsConnected, session?.id, sessionRunning]);

```

Because browser `EventSource` connections do not survive page reloads, this recreation is mandatory. The hook relies on the server-side session persistence to maintain continuity across the break in connectivity.

### The Connected Event Handshake

Once the new SSE channel opens, the server emits a `connected` event containing an `isStreaming` boolean. The handler at lines 212-226 processes this signal to determine whether to resume the streaming UI or enter an idle state.

```typescript
// hooks/useAgentSession.ts (lines 212-226)
const handleAgentEvent = useCallback((event) => {
  if (event.type === 'connected') {
    dispatch({ type: 'end' });
    if (event.isStreaming) {
      cancelEventStreamGrace();
      sdkAgentActiveRef.current = true;
      setAgentRunning(true);
      setAgentPhase({ kind: 'waiting_model' });
    }
  }
  // …additional event handling…
}, [cancelEventStreamGrace]);

```

If `isStreaming` is true, the client cancels the idle-grace timer, marks the agent as active, and restores the "waiting for model" bubble. This creates the illusion that the stream never interrupted, even though the underlying TCP connection is entirely new.

## State Synchronization Mechanisms

To handle edge cases where SSE messages are lost during the refresh window, Pi-Web implements multiple fallback mechanisms that guarantee UI consistency.

### Grace Period for Idle Streams

If the generation completed while the page was unloading, a grace timer prevents the connection from closing immediately. The `scheduleEventStreamClose` function (lines 190-210) starts a `EVENT_STREAM_IDLE_GRACE_MS` timer (30,000ms) that polls `/api/agent/<sessionId>` until the server confirms the session is idle.

```typescript
// Conceptual flow from lines 190-210
if (!isStreaming && !eventStreamGraceActiveRef.current) {
  scheduleEventStreamClose(sessionId);  // 30s grace period
}

```

This prevents the client from closing the stream prematurely if the server is still finalizing the response.

### Periodic State Reconciliation

The `reconcileAgentState` function (lines 155-156) performs periodic HTTP GET requests to `/api/agent/<sessionId>` to verify the server-side state. If the server reports that the session is neither streaming nor prompting, the client invokes `finishPromptWithoutStream` to load the final session state and close the SSE channel.

This reconciliation loop ensures that missed SSE events—caused by network glitches or the refresh itself—cannot leave the UI stuck in a loading state.

### Prompt Settlement Fallback

For cases where the server continues streaming after reconnection, `waitForPromptSettlement` (lines 100-112) polls the session endpoint with a `PROMPT_SETTLE_MAX_MS` timeout of 20,000ms. Once the prompt status indicates completion, the UI loads the latest session file and optionally terminates the SSE connection.

```typescript
// hooks/useAgentSession.ts (lines 100-112)
const waitForPromptSettlement = async (sessionId: string) => {
  const start = Date.now();
  while (Date.now() - start < PROMPT_SETTLE_MAX_MS) {
    const status = await fetchSessionStatus(sessionId);
    if (status.isSettled) return status;
    await delay(1000);
  }
  throw new Error('Prompt settlement timeout');
};

```

## Implementation Details

The complete reconnection logic spans four key files:

- **[`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)**: Core React hook managing `AgentEventConnection` lifecycle, reconnection timing, and UI state reconciliation.
- **[`lib/agent-event-connection.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-connection.ts)**: Wrapper around the native `EventSource` implementing exponential backoff, readiness timeouts, and the `shouldMaintain` guard.
- **`api/agent/[id]/events/route.ts`**: Next.js API route that maintains the SSE stream and emits `connected`, `agent_start`, and `agent_end` events.
- **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)** (via `AgentSessionWrapper`): Server-side emitter that broadcasts the SSE events consumed by the frontend.

## Summary

- **Fresh `EventSource` creation**: On every page load, `useAgentSession` instantiates a new `AgentEventConnection` pointing to `/api/agent/<sessionId>/events`.
- **Streaming detection**: The `connected` event's `isStreaming` flag determines whether to resume the UI bubble or enter an idle grace period.
- **Graceful degradation**: A 30-second idle grace timer (`EVENT_STREAM_IDLE_GRACE_MS`) ensures completed streams close cleanly without leaving orphaned connections.
- **HTTP reconciliation**: `reconcileAgentState` and `waitForPromptSettlement` poll the REST API to recover any events missed during the refresh window.
- **Configurable timeouts**: The system uses `EVENT_STREAM_RECONNECT_DELAY_MS` (1,000ms), `EVENT_STREAM_READY_TIMEOUT_MS` (60,000ms), and `PROMPT_SETTLE_MAX_MS` (20,000ms) to balance responsiveness against server load.

## Frequently Asked Questions

### What triggers the SSE reconnection after a page refresh?

The reconnection is triggered automatically by the `useEffect` hook inside [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) when the component mounts. It calls `maintainEventsConnected(session.id)`, which creates a fresh `EventSource` instance. Unlike WebSockets, the browser does not persist `EventSource` connections across page reloads, so this explicit re-initialization is required.

### How does the client know if the server is still streaming?

The server emits a `connected` event immediately upon establishing the SSE channel. This event includes an `isStreaming` boolean field. If true, the client cancels the idle grace timer, sets `sdkAgentActiveRef.current = true`, and transitions the UI to the `waiting_model` phase, effectively resuming the streaming bubble where the user left off.

### What happens if the stream finished while the page was reloading?

If the generation completes during the refresh window, the client enters a grace period defined by `EVENT_STREAM_IDLE_GRACE_MS` (30 seconds). The `scheduleEventStreamClose` function polls the `/api/agent/<sessionId>` endpoint until the server confirms an idle state, then closes the SSE connection. This prevents the client from displaying a "streaming" UI for a completed generation.

### How long will the client wait for a prompt to settle?

The `waitForPromptSettlement` function enforces a maximum wait time of `PROMPT_SETTLE_MAX_MS` (20,000ms). It polls the session status endpoint every second until the server reports that the prompt has finished processing. If the timeout expires, the client throws an error and closes the connection to prevent indefinite loading states.