How Pi-Web Handles SSE Reconnection After Page Refresh
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:
const es = new EventSource(`/api/agent/${encodeURIComponent(sid)}/events`);
This logic resides in 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:
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, 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:
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 (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:
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
EventSourceinstance inhooks/useAgentSession.ts - The client waits for an explicit
connectedmessage 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
ensureEventsConnectedfunction 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, 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →