How Running-State Polling and Reconciliation Work in π-Web
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, 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, which inspects the global registry managed by 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 theisStreamingflag and currentrunId. - Visibility changes: When the
visibilitychangeevent 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
onlineevent 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, 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.
// 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 ?? [];
}
// 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/runningevery 2.5 seconds while the tab is visible, pausing automatically when the document is hidden to conserve resources. - The
useAgentSessionhook inhooks/useAgentSession.tsperforms reconciliation on initial mount, visibility changes, and network restoration to recover from missed SSE events. - Monotonic
runIdtracking 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.tsmaintains the global registry ofAgentSessionWrapperinstances 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. The API route at 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.
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 →