# How Pi-Web Handles SSE Reconnection After Page Refresh

> Discover how Pi-Web ensures seamless SSE reconnection after a page refresh. Learn about its event source management, connection acknowledgments, and intelligent retry logic for robust data streaming.

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

---

**When the page refreshes, Pi-Web instantly creates a fresh `EventSource` connection, waits for a server-side "connected" acknowledgment, and implements intelligent retry logic that distinguishes between fatal errors requiring manual reconnection and transient network blips handled by the browser.**

The **pi-web** repository implements a robust chat interface using Server-Sent Events (SSE) to stream real-time updates from the server to the browser. Understanding how **SSE reconnection after page refresh** works is critical for maintaining session continuity, as a full page reload tears down all existing network connections and requires the client to rebuild its event stream from scratch.

## Rebuilding the EventSource Connection on Mount

When the React component remounts after a refresh, the `useAgentSession` hook immediately invokes the `connectEvents` function to establish a new SSE channel.

### Creating a Fresh EventSource Instance

The hook clears any stale references and instantiates a new `EventSource` pointing to the agent-specific endpoint:

```typescript
const es = new EventSource(`/api/agent/${encodeURIComponent(sid)}/events`);

```

This logic resides in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) (lines 599-600). By creating a brand new instance rather than attempting to reuse the destroyed connection from the previous page lifecycle, the client ensures a clean slate for the HTTP handshake.

### The Connected Handshake Protocol

Unlike standard SSE implementations that begin streaming immediately, Pi-Web implements an acknowledgment protocol. The server sends a JSON event with `type: "connected"` once the stream is fully established:

```typescript
es.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event.type === "connected") settle("connected");
  handleAgentEventRef.current?.(event);
};

```

The hook resolves its internal promise only after receiving this message (see [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), lines 614-616). A configurable timeout (`EVENT_STREAM_CONNECT_TIMEOUT_MS`, defaulting to 5 seconds) prevents indefinite hanging if the server fails to respond.

## Distinguishing Fatal Errors from Transient Failures

The reconnection strategy bifurcates based on error severity, leveraging both browser-native behavior and custom retry loops.

### Fatal Errors and Manual Retry Logic

When the browser reports the stream as `CLOSED`—typically due to HTTP 404/500 errors or content-type mismatches—the hook settles the promise with status **"closed"** and initiates a manual recovery sequence:

```typescript
if (es.readyState === EventSource.CLOSED) {
  settle("closed");
  if (eventSourceRef.current === es && agentRunningRef.current) {
    eventSourceRef.current = null;
    setTimeout(() => {
      if (agentRunningRef.current) void connectEvents(sid);
    }, 1_000);
  }
}

```

This code from [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) (lines 622-630) checks that the session is still active (`agentRunningRef.current`) before waiting 1,000 milliseconds and recursively calling `connectEvents` again.

### Browser Auto-Reconnect for Transient Issues

For network interruptions where the `EventSource` remains in the `CONNECTING` state, the code deliberately takes no action. The native `EventSource` implementation automatically attempts to reconnect with exponential backoff. The hook respects this mechanism, only intervening when the browser definitively marks the connection as closed.

## Guaranteeing Connection Before User Interaction

Higher-level UI components depend on the `ensureEventsConnected` function to prevent actions during disconnected states. This wrapper invokes `connectEvents` and throws an `EventStreamConnectionError` if the stream fails to reach the "connected" state within the timeout window:

```typescript
const ensureEventsConnected = async (sid: string) => {
  const result = await connectEvents(sid);
  if (result.status !== "connected") {
    throw new EventStreamConnectionError(`Failed to connect: ${result.status}`);
  }
  return result;
};

```

This protective layer ensures that message sending, session forking, and other chat operations only proceed when the SSE channel is verified alive, preventing data loss during the reconnection window after a page refresh.

## Summary

- Pi-Web rebuilds the SSE connection from scratch after every page refresh by creating a new `EventSource` instance in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)
- The client waits for an explicit `connected` message from the server before resolving the connection promise, with a 5-second timeout safeguard
- Fatal errors trigger a manual retry loop with 1-second delays, while transient network issues rely on the browser's native auto-reconnect capability
- The `ensureEventsConnected` function blocks UI actions until the SSE channel is confirmed active, maintaining data integrity during reconnection

## Frequently Asked Questions

### Why does Pi-Web create a new EventSource instead of resuming the old connection after a page refresh?

Browser security constraints and the nature of HTTP connections prevent resuming a destroyed EventSource. When the user refreshes the page, all JavaScript state and network connections are terminated. Pi-Web acknowledges this by always instantiating a fresh `EventSource` via `connectEvents` in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), ensuring a clean HTTP handshake with the server endpoint `/api/agent/[id]/events`.

### What triggers the manual retry logic versus the browser's auto-reconnect?

The code checks `es.readyState === EventSource.CLOSED` to distinguish fatal errors from temporary network blips. When the state is `CLOSED`—indicating the server rejected the connection or returned an error status—the hook manually retries after 1,000 milliseconds. For transient issues where the state remains `CONNECTING`, Pi-Web relies on the browser's native SSE reconnection behavior without interference.

### How long does Pi-Web wait for the server to acknowledge the SSE connection?

The hook enforces a 5-second timeout configurable via the `EVENT_STREAM_CONNECT_TIMEOUT_MS` constant. If the server endpoint `app/api/agent/[id]/events/route.ts` fails to send the `{type: "connected"}` event within this window, the promise resolves with status `"timeout"`, allowing the UI to fall back to polling or display an error state.

### Can users interact with the chat while the SSE connection is reconnecting?

No. The `ensureEventsConnected` function acts as a gatekeeper by throwing an `EventStreamConnectionError` if the stream fails to reach the "connected" state. This prevents message sending and other session actions until the SSE channel is verified active, ensuring the client doesn't attempt operations while disconnected from the server-side `AgentSession` managed by [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts).