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:
- Heartbeat initiation — Sends
:\n\nkeepalive comments every 30 seconds to prevent proxy timeouts - Connection handshake — Publishes a
connectedevent once the agent reports ready state - Snapshot delivery — Optionally sends a complete message snapshot plus any buffered events that occurred before snapshot generation
- 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:
- Cancels any existing grace timer via
cancelEventStreamGrace - Sets
eventStreamGraceActiveRef.current = true - Schedules
checkServerIdleto run after 30 seconds
Server Verification Before Closure
The checkServerIdle function polls /api/agent/:sid before finalizing disconnection:
- If
state.isStreamingorstate.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.tsgenerates aReadableStreamwith 30-second heartbeats, snapshot deduplication, and structured error events AgentEventConnectionprovides resilient client-side handling with exponential backoff and categorical error responses- The 30-second grace window in
useAgentSession.tspolls 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →