# How Pi Web Manages Agent Sessions: Global Registry and Lifecycle Architecture

> Discover how Pi Web manages agent sessions with its global registry and lifecycle architecture. Learn about persistent sessions, hot-reloads, and SSE updates.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: architecture
- Published: 2026-07-30

---

**Pi Web maintains a resilient in-process registry in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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:

1. Instantiates an **`AgentSessionWrapper`** containing the actual `AgentSession` and its unique ID
2. Registers the wrapper in the global map keyed by the session ID
3. Initializes the session by loading its `.jsonl` file, 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`](https://github.com/agegr/pi-web/blob/main/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 `.jsonl` file for the forked session
- Returns the new session ID to the client  
- Calls **`destroy()`** on the old wrapper to remove it from `globalThis.__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`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) publishes events to **[`app/api/agent/running/events/route.ts`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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

```typescript
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

```typescript
// Send fork command to existing session
await post('/api/agent/12345', {
  cmd: 'fork',
});

```

### Consuming Live Updates

```typescript
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

```typescript
// Delete session and remove from registry
await post('/api/sessions/12345', { method: 'DELETE' });
// Internally calls destroyRpcSessionsForCwd()

```

## Summary

- **Global Registry**: [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) maintains `globalThis.__piSessions` to preserve Agent Sessions across Next.js hot-reloads
- **Wrapper Pattern**: Each session uses an `AgentSessionWrapper` that manages lifecycle methods and prevents state leakage
- **Fork Safety**: Forking creates fresh `.jsonl` files and explicitly destroys old wrappers to prevent ID collisions
- **Real-Time Updates**: SSE endpoints at `/api/agent/running/events` and `/api/agent/[id]/events` provide live session status
- **File Persistence**: Sessions store history as `.jsonl` files in `~/.pi/agent/sessions/`, read and normalized by [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/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`](https://github.com/agegr/pi-web/blob/main/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.