# How Running-State Polling and Reconciliation Work in π-Web

> Understand running-state polling and reconciliation in π-Web. Discover how a 2.5-second heartbeat and fallback checks sync UI with backend AgentSessions, recovering from missed events and visibility changes.

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

---

**Running-state polling and reconciliation in π-Web synchronize the UI with backend AgentSessions through a 2.5-second heartbeat endpoint and multi-layered fallback checks that recover from missed Server-Sent Events and browser visibility changes.**

The `agegr/pi-web` repository implements a robust real-time synchronization system to keep the chat interface aligned with backend agent sessions. Understanding how running-state polling and reconciliation work is essential for maintaining UI consistency when Server-Sent Events (SSE) drop or browser tabs lose focus. This article examines the exact mechanisms implemented in the π-Web codebase, from the lightweight polling loop to the defensive reconciliation triggers.

## Running-State Polling Mechanism

### The Periodic Heartbeat

The system relies on a lightweight polling loop that queries the backend for active sessions. The client calls **GET `/api/agent/running`** every **2.5 seconds** while the browser tab remains visible, returning the list of session IDs with active `AgentSessionWrapper` instances on the server.

When the tab enters the background, the poller automatically pauses to prevent unnecessary network load and server overhead. This logic resides in the sidebar component that drives the "running" badge indicators.

### Sidebar State Management

The polling response updates the UI state in **[`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx)**, which maps the returned session IDs to visual indicators. Each entry displays an active status badge based on whether its ID appears in the polled list, providing users with immediate visibility into which agent sessions are currently processing.

The API endpoint implementation is located at **[`app/api/agent/running/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/agent/running/route.ts)**, which inspects the global registry managed by **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)** to determine which wrappers are currently executing.

## Reconciliation Strategies

Polling alone cannot guarantee consistency when SSE streams drop or prompts complete while connections are unstable. The **`useAgentSession` hook** implements multiple reconciliation triggers to ensure the chat view reflects the ground truth.

### Event-Driven Fallbacks

The hook attaches listeners to critical browser events to re-synchronize state:

- **Initial mount**: Calls **GET `/api/agent/[id]`** to fetch the latest session state, including the `isStreaming` flag and current `runId`.
- **Visibility changes**: When the `visibilitychange` event fires as a tab regains focus, the hook re-issues the GET request to "wake up" the session and retrieve any missed updates.
- **Network restoration**: The `online` event triggers an immediate reconciliation request to recover from connection losses.
- **SSE timeout handling**: After a prompt ends, the server maintains the SSE channel for a **30-second grace window**. If the channel closes prematurely, the hook falls back to periodic GET requests to capture the final state.

### SSE Event Deduplication

Each SSE event carries a monotonic **`runId`** field. The hook tracks the most recent `runId` seen in a ref and ignores any incoming events with older identifiers. This prevents stale message bubbles from reappearing when late events arrive after a reconciliation has already updated the UI with fresher data.

The reconciliation logic is centralized in **[`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)**, which orchestrates the SSE listener, the periodic GET fallback, and the UI state updates.

## Implementation Details

The following code demonstrates the polling and reconciliation patterns used throughout the codebase.

```typescript
// components/SessionSidebar.tsx
import { useEffect, useState } from 'react';
import useSWR from 'swr';

export function useRunningSessions() {
  const { data } = useSWR(
    () => (document.hidden ? null : '/api/agent/running'),
    url => fetch(url).then(r => r.json()),
    { refreshInterval: 2500 }   // 2.5s poll when tab is visible
  );
  return data?.sessions ?? [];
}

```

```typescript
// hooks/useAgentSession.ts
import { useEffect, useRef } from 'react';
import { useSSE } from '@/lib/sse';
import { fetchSessionState } from '@/lib/agent-client';

export function useAgentSession(sessionId: string) {
  const { data, error, mutate } = useSSE(`/api/agent/${sessionId}/events`);
  const latestRunId = useRef<number>(0);

  // Reconcile on visibility / network changes
  useEffect(() => {
    const reconcile = async () => {
      const state = await fetchSessionState(sessionId);
      if (state.runId > latestRunId.current) {
        latestRunId.current = state.runId;
        mutate(state);               // push fresh state to UI
      }
    };
    window.addEventListener('visibilitychange', reconcile);
    window.addEventListener('online', reconcile);
    return () => {
      window.removeEventListener('visibilitychange', reconcile);
      window.removeEventListener('online', reconcile);
    };
  }, [sessionId, mutate]);

  // …handle SSE messages, ignore stale runIds, etc.
}

```

## Summary

- **Running-state polling** queries `/api/agent/running` every 2.5 seconds while the tab is visible, pausing automatically when the document is hidden to conserve resources.
- The **`useAgentSession` hook** in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) performs reconciliation on initial mount, visibility changes, and network restoration to recover from missed SSE events.
- **Monotonic `runId` tracking** prevents stale SSE events from overwriting fresh state after reconciliation has occurred.
- The **30-second SSE grace window** allows the server to keep channels open briefly after prompt completion, with GET fallback ensuring the client captures final states if the connection drops early.
- **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)** maintains the global registry of `AgentSessionWrapper` instances that the running-state endpoint inspects to generate the active session list.

## Frequently Asked Questions

### What triggers a reconciliation check in π-Web?

Reconciliation fires on four specific triggers: when the component initially mounts, when the browser tab regains visibility (visibilitychange event), when the network connection restores (online event), and when SSE connections timeout or close unexpectedly. These triggers ensure the UI resynchronizes with the backend whenever the client might have missed real-time events.

### How does π-Web prevent duplicate messages after reconnection?

The system uses a monotonic `runId` counter attached to every SSE event and API response. The `useAgentSession` hook stores the highest seen `runId` in a React ref and discards any incoming events with older run IDs. This deduplication prevents stale messages from appearing after the client has already fetched fresh state through a reconciliation request.

### Why does the running-state poll pause when the tab is hidden?

The polling mechanism checks `document.hidden` before issuing requests to `/api/agent/running`. When the tab enters the background, the poller returns `null` to SWR, halting the 2.5-second interval. This prevents unnecessary server load and battery drain while ensuring the sidebar immediately refreshes when the user returns to the tab.

### Where is the active session registry maintained?

The global registry of active `AgentSessionWrapper` instances is maintained in **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)**. The API route at [`app/api/agent/running/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/agent/running/route.ts) queries this registry to determine which sessions are currently running, while the sidebar component consumes this data to render active status badges.