How pi-web Reconciles Running State SSE to Prevent Stale Events

pi-web prevents stale running-state events by combining Server-Sent Events (SSE) with a periodic reconciliation loop that verifies the actual session state via GET /api/agent/[id] whenever the UI suspects it has missed an update.

pi-web is an open-source agent runtime that maintains strict synchronization between server-side execution and client-side interface states. When network interruptions or browser throttling cause missed SSE notifications, the system must ensure that UI indicators like "thinking" bubbles accurately reflect the server's actual running state. The reconciliation mechanism implemented in the useAgentSession hook guarantees that stale events never persist by cross-referencing the SSE feed against the ground-truth session endpoint.

The Dual-Feed Synchronization Strategy

pi-web employs a two-pronged approach to maintain a reliable view of an agent's running state. This architecture combines push-based real-time updates with pull-based verification to eliminate drift between the server and client.

Real-Time Updates via SSE

The primary data feed originates from the Server-Sent Events endpoint at GET /api/agent/running/events. This stream broadcasts the current set of running session IDs whenever any session's status changes.

In app/api/agent/running/events/route.ts, the endpoint retrieves the active sessions from the RPC manager and encodes them for the event stream:

// app/api/agent/running/events/route.ts
export async function GET(request: Request) {
  const { encode } = await import("@/lib/sse");
  const ids = getRunningRpcSessionIds();          // current set
  return new Response(encode({ type: "running", runningSessionIds: ids }), {
    headers: { "Content-Type": "text/event-stream" },
  });
}

Whenever any session's isRunning flag changes, the encode function broadcasts the updated set to all connected clients. This provides immediate UI updates during normal network conditions.

Periodic State Reconciliation

SSE connections can drop due to network hiccups, background tab throttling, or browser sleep modes. To guard against missed notifications, useAgentSession implements a reconciliation loop that polls the definitive session state endpoint GET /api/agent/[id].

The hook fetches the AgentStateResponse from app/api/agent/[id]/route.ts to obtain the latest running flag and full session state. This polling occurs both on a schedule and in response to specific browser events, ensuring the UI never lags behind reality.

Reconciliation Logic in useAgentSession

The core reconciliation logic resides in hooks/useAgentSession.ts (lines 818-852). After each SSE-driven update or periodic check, the client compares its local state against the server's authoritative response:

// hooks/useAgentSession.ts (excerpt, lines 818-852)
if (serverState.running && state) {
  // Server says still running → keep UI busy
  setAgentPhase(state.isStreaming ? { kind: "waiting_model" } : { kind: "running_command" });
} else if (!serverState.running) {
  // Server says idle but UI still thinks we're busy → clean up
  if (state?.isStreaming) {
    // Finish the streaming bubble
    setAgentPhase({ kind: "waiting_model" });
  }
  // Ensure SSE is re-connected if needed
  reconnectSSE();
}

Key reconciliation behaviors:

  • Stale UI cleanup: When serverState.running is false but the UI still displays a streaming indicator, the hook forces the end of the streaming bubble by calling setAgentPhase and clears the running flag.
  • Missed event recovery: If the UI believes the session is idle but the server reports running: true, the client re-opens the SSE stream via reconnectSSE() and resumes listening for events.
  • State synchronization: The hook updates the local runningSessionIds set to match the server's ground truth, ensuring subsequent operations reference the correct execution status.

Handling Browser Throttling and Network Recovery

The reconciliation loop activates aggressively when the user returns to the application after periods of inactivity. In hooks/useAgentSession.ts, event listeners trigger immediate state verification:

// hooks/useAgentSession.ts (excerpt)
useEffect(() => {
  const handle = () => reconcileRunningState(); // calls the logic above
  window.addEventListener("visibilitychange", handle);
  window.addEventListener("online", handle);
  return () => {
    window.removeEventListener("visibilitychange", handle);
    window.removeEventListener("online", handle);
  };
}, []);

When the visibilitychange event fires (user returns to the tab) or the online event fires (network connectivity restored), the hook immediately calls reconcileRunningState(). This catches any SSE messages missed while the tab was backgrounded or offline, preventing the "stale thinking bubble" problem where the UI incorrectly indicates an active session.

Summary

  • Dual-feed architecture: pi-web combines SSE streams from /api/agent/running/events with periodic polling of /api/agent/[id] to maintain accurate state synchronization.
  • Automatic drift correction: The useAgentSession hook (lines 818-852) compares local UI state against the server's running flag, forcing cleanup of stale indicators when the server reports idle status.
  • Resilient to network issues: Browser events like visibilitychange and online trigger immediate reconciliation, recovering missed SSE updates caused by tab throttling or disconnections.
  • Centralized session tracking: The lib/rpc-manager.ts module maintains the source of truth via getRunningRpcSessionIds(), which both the SSE endpoint and reconciliation logic reference.

Frequently Asked Questions

How does pi-web detect missed SSE events?

pi-web detects missed SSE events through a reconciliation loop in useAgentSession that periodically fetches the full session state from GET /api/agent/[id]. When the browser fires visibilitychange or online events, or when the hook's internal timer triggers, it compares the server's running flag against the local UI state. Any discrepancy indicates a missed SSE update, prompting immediate state synchronization.

What happens when the server reports idle but the UI shows running?

When the server reports running: false but the UI still displays active indicators, the reconciliation logic in hooks/useAgentSession.ts forces an immediate cleanup. It calls setAgentPhase({ kind: "waiting_model" }) to terminate the streaming bubble and clears the local running flag. This prevents "zombie" loading states that persist after the underlying agent session has completed.

How does the SSE endpoint track which sessions are running?

The SSE endpoint in app/api/agent/running/events/route.ts imports getRunningRpcSessionIds() from lib/rpc-manager.ts. This function queries the central RPC manager registry that tracks all AgentSessionWrapper lifecycles. Whenever any session's isRunning status changes, the manager updates its internal set, and the SSE endpoint broadcasts the new collection to connected clients.

Why does pi-web use both SSE and polling instead of just SSE?

pi-web uses both mechanisms because SSE connections are unreliable in real-world browser environments. Background tabs face throttling, mobile browsers drop connections to save battery, and network interruptions occur without notice. The periodic polling acts as a safety net that guarantees eventual consistency, ensuring that even if the SSE stream misses updates, the UI will correct itself within seconds of the next reconciliation check.

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 →