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

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, 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.

// 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.

// 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.

// 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.

// 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.

// 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: Core React hook managing AgentEventConnection lifecycle, reconnection timing, and UI state reconciliation.
  • 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 (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 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.

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 →