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

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

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.

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

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.

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

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()
// 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 SSE ReadableStream generation, heartbeat, snapshot logic
lib/agent-event-connection.ts EventSource lifecycle, reconnection, error categorization
hooks/useAgentSession.ts React integration, 30s grace window orchestration
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 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 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.

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.

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 →