# How SSE Event Streaming Works for Agent Events in Pi-Web: Understanding the 30-Second Grace Window

> Learn how Pi-Web uses SSE event streaming for agent events and its 30-second grace window to ensure reliable real-time updates despite network latency or UI interruptions.

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

---

**Pi-Web uses Server-Sent Events (SSE) with a 30-second idle grace window to deliver real-time agent updates while preventing premature connection closure during brief UI interruptions or network latency.**

This article explains the complete SSE event streaming architecture in the `agegr/pi-web` repository, from server-side stream generation to client-side connection management and the critical 30-second grace window that ensures reliable event delivery.

## The Three-Component SSE Architecture

Pi-Web's SSE implementation spans three coordinated layers: a server-side stream generator, a resilient client connection manager, and a React hook that orchestrates the UI lifecycle.

### Server-Side: `createAgentEventStream` in [`lib/agent-event-stream.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-stream.ts)

The server creates SSE streams through `createAgentEventStream`, which returns a `ReadableStream<Uint8Array>` that writes properly formatted SSE data (`data: …\n\n`). The function performs four sequential operations:

1. **Heartbeat initiation** — Sends `:\n\n` keepalive comments every 30 seconds to prevent proxy timeouts
2. **Connection handshake** — Publishes a `connected` event once the agent reports ready state
3. **Snapshot delivery** — Optionally sends a complete message snapshot plus any buffered events that occurred before snapshot generation
4. **Event forwarding** — Streams subsequent agent events, filtering duplicates via `isEventIncludedInSnapshot`

Startup failures trigger a `startup_error` event rather than silent stream termination.

```ts
// lib/agent-event-stream.ts
export function createAgentEventStream(
  req: Request,
  sessionId: string,
  sessionPromise: Promise<AgentEventStreamSession>,
): ReadableStream<Uint8Array> {
  // … set up heartbeat, publish "connected" event, forward agent events …
}

```

### Client-Side: `AgentEventConnection` in [`lib/agent-event-connection.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-connection.ts)

`AgentEventConnection` wraps the native `EventSource` with production-grade reliability features. It implements exponential backoff reconnection and categorical error handling:

| Error Status | Trigger | Behavior |
|-------------|---------|----------|
| `ready_timeout` | No `connected` event within `readinessTimeoutMs` (default 10s) | Retry with backoff |
| `startup_error` | Agent process failed to start | **Stop reconnection permanently** |
| `closed` | Any other disconnection | Retry after `reconnectDelayMs` |

The connection remains active only while `shouldMaintain(sessionId)` returns `true`, allowing UI state to control stream lifecycle.

```ts
// lib/agent-event-connection.ts
const connection = new AgentEventConnection({
  createSource: (id) => new EventSource(`/api/agent/${encodeURIComponent(id)}/events`),
  onEvent: handleAgentEvent,
  shouldMaintain: (id) => activeSessionId === id,
  readinessTimeoutMs: 10_000,
  reconnectDelayMs: 1_000,
});
await connection.ensureConnected(sessionId);

```

### UI Integration: `useAgentSession` in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)

The `useAgentSession` hook bridges the connection manager to React state. It schedules the critical 30-second grace window when agent activity concludes.

## The 30-Second Grace Window Explained

The **30-second idle grace window** (`EVENT_STREAM_IDLE_GRACE_MS = 30_000`) prevents race conditions between server-side event completion and client-side reception. Without this buffer, rapid connection closure would drop events delayed by network latency or browser throttling.

### When the Grace Window Activates

The grace timer starts when `agent_end` → `agent_settled` fires, signaling prompt completion. The hook calls `scheduleEventStreamClose`, which:

1. Cancels any existing grace timer via `cancelEventStreamGrace`
2. Sets `eventStreamGraceActiveRef.current = true`
3. Schedules `checkServerIdle` to run after 30 seconds

### Server Verification Before Closure

The `checkServerIdle` function polls `/api/agent/:sid` before finalizing disconnection:

- **If `state.isStreaming` or `state.isPromptRunning`** — Cancel grace period, keep SSE open
- **If `state.isCompacting`** — Extend grace period (compaction indicates background activity)
- **If idle with no compaction** — Close via `closeEvents()`

```ts
// hooks/useAgentSession.ts – start the 30 s grace timer
const scheduleEventStreamClose = useCallback((sid: string) => {
  cancelEventStreamGrace();
  eventStreamGraceActiveRef.current = true;
  const generation = eventStreamGraceGenerationRef.current;

  const checkServerIdle = async () => {
    // … fetch /api/agent/:sid …
    if (promptActive) { /* keep stream open */ return; }
    if (data.running && state?.isCompacting) { /* wait longer */ }
    // otherwise close
    eventStreamGraceActiveRef.current = false;
    closeEvents();
  };

  eventStreamGraceTimerRef.current = setTimeout(
    () => void checkServerIdle(),
    EVENT_STREAM_IDLE_GRACE_MS,
  );
}, [cancelEventStreamGrace, closeEvents]);

```

### Why 30 Seconds Specifically

The 30-second value balances three requirements according to the Pi-Web source code:

- **Above maximum proxy idle timeout** — Most reverse proxies (Nginx, Cloudflare) close idle connections at 60s; 30s heartbeats keep underlying TCP alive
- **Below user-perceived delay** — Windows longer than 30s feel sluggish when users explicitly end sessions
- **Sufficient for event propagation** — Covers P99 latency tails and brief browser background tab freezing

## Key Implementation Files

| File | Responsibility |
|------|--------------|
| [`lib/agent-event-stream.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-stream.ts) | SSE `ReadableStream` generation, heartbeat, snapshot logic |
| [`lib/agent-event-connection.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-connection.ts) | `EventSource` lifecycle, reconnection, error categorization |
| [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) | React integration, 30s grace window orchestration |
| [`lib/agent-event-wire.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-wire.ts) | Wire format definitions (`AgentEventLike`) |
| `app/api/agent/[id]/events/route.ts` | Next.js route handler for SSE endpoint |

## Summary

- Pi-Web delivers real-time agent updates via **Server-Sent Events** implemented across three coordinated components
- The server in [`lib/agent-event-stream.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-stream.ts) generates a `ReadableStream` with 30-second heartbeats, snapshot deduplication, and structured error events
- `AgentEventConnection` provides resilient client-side handling with exponential backoff and categorical error responses
- The **30-second grace window** in [`useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/useAgentSession.ts) polls server state before closing, preventing event loss from network latency or UI interruptions
- This architecture ensures low-latency delivery while gracefully handling the unpredictable conditions of production browser environments

## Frequently Asked Questions

### What triggers the 30-second grace window in Pi-Web's SSE implementation?

The grace window activates when the `agent_settled` event follows `agent_end`, indicating prompt completion. The `useAgentSession` hook schedules `checkServerIdle` to run after `EVENT_STREAM_IDLE_GRACE_MS` (30 seconds), polling `/api/agent/:sid` to verify the server has truly finished before closing the `EventSource` connection.

### How does Pi-Web prevent duplicate events when reconnecting to an SSE stream?

The server-side `createAgentEventStream` sends a snapshot of current state plus buffered events, then uses `isEventIncludedInSnapshot` to filter subsequent events. Clients receive complete state on reconnection without duplicate processing, as implemented in [`lib/agent-event-stream.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-event-stream.ts).

### What happens if the agent fails to start during SSE stream initialization?

`createAgentEventStream` emits a `startup_error` event through the SSE channel. The client-side `AgentEventConnection` recognizes this status and **permanently stops reconnection attempts**, distinguishing unrecoverable failures from transient network errors that merit retry with exponential backoff.

### Why does Pi-Web use SSE instead of WebSockets for agent event streaming?

According to the `agegr/pi-web` architecture, SSE provides unidirectional server-to-client streaming with automatic reconnection, native browser `EventSource` support, and seamless HTTP/2 compatibility—sufficient for agent→UI updates without the complexity of bidirectional WebSocket handshakes and state management.