# How Pi-Web Handles Running State Polling and Reconciliation During Visibility Changes

> Discover how Pi-Web manages state polling and reconciliation during visibility changes. Learn how `visibilitychange` and `online` events ensure UI sync with backend AgentSessions.

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

---

**Pi-Web keeps the UI synchronized with backend AgentSessions by combining 15-second polling intervals with `visibilitychange` and `online` event listeners that trigger immediate state reconciliation whenever users return to backgrounded tabs or recover from network loss.**

Pi-Web implements a robust running state polling and reconciliation system to maintain consistency between the client interface and server-side agent processes. When browser tabs are hidden or network connectivity drops, the application risks missing critical Server-Sent Events (SSE) that update streaming status. The reconciliation architecture in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) eliminates this drift by proactively querying the server whenever environmental conditions change.

## The Three-Layer Reconciliation Architecture

The system employs a "reconciliation net" consisting of periodic polling, visibility detection, and network recovery handlers. Together, these mechanisms ensure that [`components/ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatWindow.tsx) never displays stale streaming bubbles after the underlying agent has finished processing.

### Periodic Background Polling

When `agentRunning` is true, a `useEffect` hook establishes a baseline polling mechanism using `setInterval`. Every `AGENT_STATE_RECONCILE_MS` (15 seconds), the system invokes `reconcileAgentState` to fetch the current status from `app/api/agent/[id]/route.ts`. This interval provides a safety net during active sessions, ensuring the client does not diverge from the server truth even when the tab remains continuously visible.

```tsx
// Inside hooks/useAgentSession.ts
useEffect(() => {
  if (!agentRunning) return;

  const reconcile = () => {
    const sid = sessionIdRef.current;
    if (sid) void reconcileAgentState(sid);
  };

  // Run every 15 seconds
  const interval = setInterval(reconcile, AGENT_STATE_RECONCILE_MS);

  // Run immediately when the tab becomes visible again
  const onVisible = () => {
    if (document.visibilityState === "visible") reconcile();
  };
  document.addEventListener("visibilitychange", onVisible);

  // Run when the browser goes online
  window.addEventListener("online", reconcile);

  return () => {
    clearInterval(interval);
    document.removeEventListener("visibilitychange", onVisible);
    window.removeEventListener("online", reconcile);
  };
}, [agentRunning, reconcileAgentState]);

```

### Visibility Change Detection

The system registers a `document.addEventListener("visibilitychange", onVisible)` handler that checks `document.visibilityState` whenever the user switches tabs or minimizes the browser. Upon detecting the `"visible"` state, the handler immediately executes `reconcileAgentState` without waiting for the next 15-second polling cycle. This catches SSE events that were dropped while the tab was backgrounded, preventing the UI from displaying a "streaming" indicator for an agent that has already completed its task.

### Network Recovery Handling

Pi-Web also listens for `window.addEventListener("online", reconcile)` events. When the browser detects restored connectivity after an interruption, it forces an immediate call to `reconcileAgentState`. This ensures that temporary disconnections do not leave [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) state references out of sync with the actual `AgentSession` status on the server.

## The reconcileAgentState Implementation

The core reconciliation logic resides in the `reconcileAgentState` function, which queries `/api/agent/${encodeURIComponent(sid)}` to retrieve the `AgentStateResponse`. It compares the server-returned `running` flag and detailed `state` object (including `isStreaming`, `isPromptRunning`, and `isCompacting` flags) against the client's current references.

If the server reports that the agent is idle while the client still believes streaming is active, the function invokes `finishPromptWithoutStream` to cleanly close the UI state:

```ts
// Core reconciliation logic (simplified)
async function reconcileAgentState(sid: string) {
  const res = await fetch(`/api/agent/${encodeURIComponent(sid)}`);
  if (!res.ok) return;
  const { running, state } = await res.json();

  // If server says idle but client still thinks streaming, finish prompt
  if (!running || !(state?.isStreaming || state?.isPromptRunning || state?.isCompacting)) {
    await finishPromptWithoutStream(sid, promptRunIdRef.current);
  }
}

```

## Preventing State Drift

This running state polling and reconciliation strategy specifically addresses the risk of state divergence caused by browser throttling of background tabs. When a user returns to a tab after an extended absence, the visibility-change trigger ensures Pi-Web immediately contacts the `/api/agent/<sid>` endpoint rather than waiting for the next poll. By comparing the server's authoritative state against local refs and calling `finishPromptWithoutStream` when discrepancies are detected, the system guarantees that [`components/ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatWindow.tsx) cannot remain stuck in a loading state.

## Summary

- **15-second polling** via `setInterval` provides baseline state synchronization during active agent sessions.
- **`visibilitychange` event listener** triggers immediate `reconcileAgentState` calls when users return to backgrounded tabs.
- **`online` event listener** forces reconciliation upon network recovery to handle missed SSE events during outages.
- **`reconcileAgentState`** in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) queries the agent endpoint and invokes `finishPromptWithoutStream` to resolve stale streaming states.
- This three-layer approach ensures the UI never displays incorrect "streaming" status when agents have completed while the tab was hidden.

## Frequently Asked Questions

### How often does Pi-Web poll the running agent state?

The system polls every 15 seconds via `setInterval` when `agentRunning` is true, as defined by the `AGENT_STATE_RECONCILE_MS` constant. This interval provides a balance between real-time accuracy and server load, acting as a fallback when visibility or network events do not trigger earlier reconciliation.

### What happens when a user switches back to a backgrounded tab?

When the `visibilitychange` event fires and `document.visibilityState` equals `"visible"`, Pi-Web immediately executes `reconcileAgentState` without waiting for the next polling interval. This fetches the current status from `/api/agent/<sid>` and corrects any UI state that drifted due to missed SSE events while the tab was inactive.

### How does Pi-Web handle network interruptions?

The application listens for the `online` event on the window object. When connectivity is restored, it forces an immediate reconciliation by querying the server endpoint, ensuring the client state resynchronizes with the backend `AgentSession` before the user interacts with the interface again.

### What endpoint does the reconciliation logic query?

The `reconcileAgentState` function queries `/api/agent/<sid>` (implemented in `app/api/agent/[id]/route.ts`), which returns an `AgentStateResponse` containing the `running` boolean and detailed state flags. The client uses this response to determine whether to invoke `finishPromptWithoutStream` and close stale streaming indicators.