How Pi Web Manages Agent Sessions: Global Registry and Lifecycle Architecture
Pi Web maintains a resilient in-process registry in lib/rpc-manager.ts that maps session IDs to AgentSessionWrapper instances, enabling persistent Agent Sessions across Next.js hot-reloads while supporting forking, destruction, and real-time SSE updates.
Pi Web, the open-source AI coding assistant from the agegr/pi-web repository, handles Agent Sessions through a centralized RPC manager that survives development server restarts. The architecture wraps each session in a dedicated wrapper class stored in a global JavaScript object, ensuring that chat contexts persist as long as the Node.js process runs. This article explores the complete lifecycle of Agent Sessions, from creation and forking to cleanup and real-time streaming.
The Global Session Registry
The heart of Pi Web's session management lives in lib/rpc-manager.ts, which implements a global registry at globalThis.__piSessions. This design allows sessions to survive Next.js hot-module replacements without losing in-memory state.
When startRpcSession() is invoked (typically via POST /api/agent/new), the system:
- Instantiates an
AgentSessionWrappercontaining the actualAgentSessionand its unique ID - Registers the wrapper in the global map keyed by the session ID
- Initializes the session by loading its
.jsonlfile, discovering available tools, and setting the system prompt
The getRpcSession() function retrieves existing wrappers from this registry, while destroyRpcSessionsForCwd() handles bulk cleanup when worktrees are removed.
Session Lifecycle: Creation, Forking, and Destruction
Creating New Sessions
New sessions originate at app/api/agent/new/route.ts. The endpoint calls startRpcSession() with parameters including the working directory (cwd), initial message, and tool names. The wrapper immediately persists state to ~/.pi/agent/sessions/ as .jsonl files, which provide append-only storage for the conversation history.
Forking Sessions
The "Fork" functionality creates isolated copies of existing conversations. When the UI sends a "fork" command to app/api/agent/[id]/route.ts, the current AgentSessionWrapper:
- Creates a brand-new
.jsonlfile for the forked session - Returns the new session ID to the client
- Calls
destroy()on the old wrapper to remove it fromglobalThis.__piSessions
This prevents the registry from holding stale entries and ensures that subsequent requests cannot accidentally reuse the pre-fork state.
Destroying Sessions
Session destruction triggers destroyRpcSessionsForCwd(), which iterates through the global registry to find wrappers matching the specified working directory. The function removes these entries and cleans up in-memory resources, though the .jsonl files persist on disk for future reference.
Real-Time Session Tracking with SSE
Pi Web implements Server-Sent Events (SSE) for live UI updates without polling. The subscribeRunningSessions() function in rpc-manager.ts publishes events to app/api/agent/running/events/route.ts, broadcasting currently active session IDs.
Individual session streams are available at app/api/agent/[id]/events/route.ts, pushing updates for assistant messages, tool calls, and compaction events. The React hook hooks/useAgentSession.ts coordinates these streams, fetching initial state via GET /api/agent/[id] while maintaining the SSE connection for real-time updates.
Session Persistence and Normalization
The lib/session-reader.ts module handles disk operations, specifically reading .jsonl files from ~/.pi/agent/sessions/. Its normalizeToolCalls() function standardizes tool-call fields across different model providers, producing a consistent SessionContext that the UI consumes.
This separation of concerns allows the RPC manager to focus on in-memory state while the session reader handles persistence logic, creating a robust architecture where sessions can be reconstructed from disk if the process restarts.
Code Examples
Creating a Session
import { post } from '@/lib/agent-client';
// Initialize a new session in a specific working directory
await post('/api/agent/new', {
cwd: '/home/user/project',
message: 'Explain the repo structure',
toolNames: ['fileBrowser', 'gitStatus'],
});
Forking a Session
// Send fork command to existing session
await post('/api/agent/12345', {
cmd: 'fork',
});
Consuming Live Updates
import { useAgentSession } from '@/hooks/useAgentSession';
export function SessionViewer({ sessionId }: { sessionId: string }) {
const { state } = useAgentSession(sessionId);
useEffect(() => {
const evtSource = new EventSource(`/api/agent/${sessionId}/events`);
evtSource.onmessage = (e) => {
const ev = JSON.parse(e.data);
// Handle ev.types: 'assistant', 'toolCall', 'compaction'
};
return () => evtSource.close();
}, [sessionId]);
return <div>{/* Render session state */}</div>;
}
Cleaning Up Sessions
// Delete session and remove from registry
await post('/api/sessions/12345', { method: 'DELETE' });
// Internally calls destroyRpcSessionsForCwd()
Summary
- Global Registry:
lib/rpc-manager.tsmaintainsglobalThis.__piSessionsto preserve Agent Sessions across Next.js hot-reloads - Wrapper Pattern: Each session uses an
AgentSessionWrapperthat manages lifecycle methods and prevents state leakage - Fork Safety: Forking creates fresh
.jsonlfiles and explicitly destroys old wrappers to prevent ID collisions - Real-Time Updates: SSE endpoints at
/api/agent/running/eventsand/api/agent/[id]/eventsprovide live session status - File Persistence: Sessions store history as
.jsonlfiles in~/.pi/agent/sessions/, read and normalized bylib/session-reader.ts
Frequently Asked Questions
How does Pi Web prevent session data loss during hot-reloads?
Pi Web stores active AgentSessionWrapper instances in globalThis.__piSessions, a global JavaScript object that persists across Next.js hot-module replacements. This in-process registry ensures that chat sessions remain in memory as long as the Node.js process runs, even when development code changes trigger a server reload.
What happens to the original session when I fork a conversation?
When you fork a session, the AgentSessionWrapper creates a new .jsonl file with a unique ID for the forked conversation, then calls destroy() on its own instance to remove itself from the global registry. This ensures the original session ID cannot be reused accidentally while the new fork operates independently with its own persisted state.
Where does Pi Web store conversation history?
Session history is stored as append-only .jsonl files in the ~/.pi/agent/sessions/ directory. The lib/session-reader.ts module reads these files and uses normalizeToolCalls() to standardize tool invocation records before constructing the SessionContext used by the frontend.
How does the UI know which sessions are currently active?
The UI subscribes to Server-Sent Events from /api/agent/running/events, which broadcasts active session IDs via subscribeRunningSessions() in the RPC manager. Additionally, the useAgentSession hook opens dedicated SSE streams to /api/agent/[id]/events for real-time updates on specific conversation threads.
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 →