# SSE Reconnection for Page Refresh Mid-Stream in the Chat Window: How It Works

> Learn how SSE reconnection maintains chat continuity during page refreshes using retry-aware logic and grace-period hooks. Discover the agegr/pi-web solution.

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

---

**The ChatWindow component preserves streaming continuity during page refreshes through a retry-aware `AgentEventConnection` helper and a `useAgentSession` hook with grace-period logic.**

When users refresh the page while an AI response is still streaming, the application must seamlessly resume without losing in-progress generation. This article explores how the `agegr/pi-web` repository implements robust **SSE reconnection for page refresh mid-stream** using a lightweight connection manager and guarded lifecycle predicates.

## Core Architecture: AgentEventConnection and useAgentSession

The reconnection system depends on two coordinated components. `AgentEventConnection` handles raw `EventSource` operations and implements passive retry logic, while `useAgentSession` provides the hook-level context and "maintain" predicates that determine when reconnection should occur.

This separation keeps transport concerns isolated from React lifecycle management. The connection manager never directly accesses component state; instead, it receives configuration callbacks that encapsulate session-specific logic.

### Error Handling and Scheduled Retries

When the `EventSource` emits an error, `AgentEventConnection` marks the connection as failed and invokes `scheduleRetry` in [`lib/agent-event-connection.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-connection.ts) [lines 151-190]:

```typescript
private scheduleRetry(sessionId: string): void {
  if (!this.options.shouldMaintain(sessionId)) return;
  const timer = setTimeout(() => {
    this.retry = null;
    this.maintain(sessionId);            // re-creates the EventSource
  }, this.options.reconnectDelayMs);
  this.retry = { sessionId, timer };
}

```

The retry only proceeds if `shouldMaintain(sessionId)` returns true. Any successful connection clears pending retries via `stopRetrying` [lines 191-197].

## Grace-Period Handling for Component Remounting

The critical challenge for **SSE reconnection during page refresh** is that the component unmounts and remounts, potentially destroying and recreating the connection object. The solution uses a grace-period flag that persists across remounts.

In [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), when `state.isStreaming === true`, the hook sets `eventStreamGraceActiveRef.current = true` [lines 54-62]. This flag extends the maintenance window even when the component briefly disappears.

The `shouldMaintain` predicate evaluates three conditions:

```typescript
shouldMaintain: (sid) =>
  sessionHookMountedRef.current &&
  sessionIdRef.current === sid &&
  (agentRunningRef.current || eventStreamGraceActiveRef.current),

```

By including `eventStreamGraceActiveRef.current`, the predicate returns true during the grace period, allowing `scheduleRetry` to proceed with reconnection.

## Reconnection Flow After Page Refresh

The complete **SSE reconnection for page refresh mid-stream** sequence works as follows:

1. **Pre-refresh state**: An active `EventSource` streams events for a given session ID. `eventStreamGraceActiveRef` is true because streaming is in progress.

2. **Refresh triggered**: The browser tears down the page. The old `EventSource` connection closes, triggering `source.onerror` in the dying instance.

3. **Retry scheduled**: The final error handler calls `scheduleRetry`, which checks `shouldMaintain`. Because the grace flag is still true, a timer is set for `EVENT_STREAM_RECONNECT_DELAY_MS`.

4. **Post-refresh remount**: The new page instance runs `useAgentSession`, creating a fresh `AgentEventConnection`. The scheduled retry fires, or the new instance detects the active session and calls `maintain(sessionId)`.

5. **Stream resumes**: A new `EventSource` opens to `/api/agent/{sessionId}/events`. The server, tracking the session, returns remaining events from the continuation point.

This behavior is documented in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) under "SSE reconnect on page refresh mid-stream".

## Configuration Parameters

The hook configures the connection with two timing constants in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) [lines 63-65]:

| Parameter | Purpose | Default Context |
|-----------|---------|---------------|
| `readinessTimeoutMs` | How long to wait for server readiness before failing | Initial connection |
| `reconnectDelayMs` | Delay before retry attempt after connection failure | Error recovery |

These values balance responsiveness against server load. Immediate retries risk thundering herds; excessive delays hurt perceived responsiveness.

## Hook Initialization Pattern

The `useAgentSession` hook lazily instantiates `AgentEventConnection` using a ref to preserve the instance across renders:

```tsx
const eventConnectionRef = useRef<AgentEventConnection | null>(null);
if (!eventConnectionRef.current) {
  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,
    reconnectDelayMs: EVENT_STREAM_RECONNECT_DELAY_MS,
    onUnexpectedError: (err) => console.error(err),
  });
}

```

The `createSource` callback ensures each reconnection uses a fresh `EventSource` instance, as the standard API does not support reuse after closure.

## Key Source Files

| File | Responsibility |
|------|--------------|
| [`lib/agent-event-connection.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-connection.ts) | Raw SSE wrapper, error handling, retry scheduling with `scheduleRetry` and `stopRetrying` |
| [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) | Hook lifecycle, grace-period flag management, `shouldMaintain` predicate definition |
| [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) | Architectural documentation for SSE reconnection behavior |

## Summary

- **`AgentEventConnection`** provides transport-agnostic SSE management with configurable retry delays and maintenance predicates.

- **`useAgentSession`** supplies the "maintain" logic that keeps connections alive during page refreshes through a grace-period flag.

- **Grace-period flags** bridge component remounts, ensuring scheduled retries execute even when the original connection object is destroyed.

- **Session ID persistence** allows the server to resume streaming from the correct continuation point after reconnection.

- The entire mechanism operates without explicit user action—refreshing the page automatically triggers recovery within `EVENT_STREAM_RECONNECT_DELAY_MS`.

## Frequently Asked Questions

### How does the system know when to retry versus give up?

The `shouldMaintain` predicate controls this decision. It returns false if the hook is unmounted without the grace flag, the session ID mismatches, or the agent has stopped. Only when all conditions hold will `scheduleRetry` proceed. This prevents retries for abandoned sessions.

### What happens if the server is slow to respond after refresh?

The `readinessTimeoutMs` parameter places an upper bound on initial connection attempts. If exceeded, the connection fails and enters the standard retry loop. Subsequent retries use `reconnectDelayMs` to pace attempts.

### Is the grace period time-bounded or event-bounded?

According to the [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) documentation and [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) implementation, the grace flag is event-bounded—it activates when streaming begins and deactivates once the stream concludes normally. There is no explicit timeout; the flag clears when the final event arrives.

### Can multiple tabs streaming the same session interfere with each other?

Each tab maintains its own `AgentEventConnection` instance with independent `EventSource` connections. The server treats these as separate consumers of the same session event stream. No client-side coordination prevents duplicate output across tabs; this is by design for simplicity.