# How `globalThis.__piSessions` Survives Next.js Hot-Reload in Pi Web

> Discover how globalThis__piSessions survives Nextjs hot reload in Pi Web. Pi Web keeps active sessions in a global object that persists through module updates, ensuring continuity.

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

---

**`globalThis.__piSessions` persists across Next.js hot-reloads because Pi Web stores active sessions in a JavaScript global object that survives module replacement while the Node process stays alive.**

Pi Web is an open-source framework that maintains `AgentSessionWrapper` instances across development server refreshes. The implementation relies on a simple but effective pattern: attaching session state to `globalThis` rather than module-level variables.

## Why Module-Level State Disappears on Hot-Reload

Next.js hot-reload replaces a module's code without restarting the entire Node process. Module-level variables get reinitialized, but the global object remains intact. This behavior creates a challenge for maintaining long-lived connections like agent sessions.

In [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), Pi Web solves this by moving session storage from module scope to the global object.

## The Global Registry Implementation

### Declaring the Global Variable

The type declaration establishes TypeScript support for the custom global property:

```typescript
// lib/rpc-manager.ts#L1371
declare global {
  var __piSessions: Map<string, AgentSessionWrapper> | undefined;
}

```

This declaration allows the codebase to reference `globalThis.__piSessions` with proper typing.

### Lazy Initialization Pattern

The registry initializes only on first access:

```typescript
// lib/rpc-manager.ts#L1378-L1380
if (!globalThis.__piSessions) {
  globalThis.__piSessions = new Map();
  // ... registration logic
}

```

**Critical behavior:** On subsequent module loads after hot-reload, `globalThis.__piSessions` already exists. The condition evaluates to `false`, and the existing `Map` is reused.

### Centralized Access Through getRegistry()

All session operations route through a single helper:

```typescript
// lib/rpc-manager.ts#L1377-L1386
function getRegistry(): Map<string, AgentSessionWrapper> {
  if (!globalThis.__piSessions) {
    globalThis.__piSessions = new Map();
    // shutdown hook registration ...
  }
  return globalThis.__piSessions;
}

```

This guarantees every caller receives the same global map reference.

## Process Lifecycle vs. Hot-Reload

Pi Web registers cleanup handlers for actual process termination:

```typescript
// lib/rpc-manager.ts#L1380-L1384
const cleanup = () => globalThis.__piSessions?.forEach((s) => s.destroy());
process.once("exit", cleanup);
// additional signals: SIGINT, SIGTERM, SIGHUP

```

**Key distinction:** These hooks fire when the Node process exits—not during hot-reload. The sessions remain alive and accessible to freshly loaded code.

## Practical Usage Examples

### Retrieving an Existing Session

After hot-reload, previously created sessions remain accessible:

```typescript
import { getRpcSession } from '@/lib/rpc-manager';

const session = getRpcSession('session-id');
if (session?.isAlive()) {
  // Same AgentSessionWrapper instance from before reload
  await session.inner.prompt({ message: 'Ask again after reload' });
}

```

### Creating New Sessions

New sessions automatically join the persistent registry:

```typescript
import { startRpcSession } from '@/lib/rpc-manager';

const wrapper = await startRpcSession({ cwd: '/home/user/project' });
console.log('New session id:', wrapper.sessionId);
// Stored in globalThis.__piSessions for future access

```

## Source File Architecture

| File | Responsibility |
|------|---------------|
| [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) | Global session registry, `getRegistry()`, `getRpcSession()`, `startRpcSession()` |
| [`lib/agent-client.ts`](https://github.com/agegr/pi-web/blob/main/lib/agent-client.ts) | Typed fetch client for `/api/agent` endpoints |
| `app/api/agent/[id]/route.ts` | API route forwarding HTTP to `AgentSessionWrapper` |

## Summary

- **`globalThis.__piSessions`** stores session state outside module scope
- **Lazy initialization** creates the Map once, reuses it thereafter
- **Hot-reload** replaces code but preserves the global object
- **Process exit hooks** clean up sessions only on actual server shutdown
- **`getRegistry()`** centralizes access to guarantee consistent references

## Frequently Asked Questions

### Does this pattern work with Next.js App Router?

Yes. The `globalThis` object is shared across all server-side code in the same Node process. Whether using Pages Router or App Router, the registry persists as long as the development server keeps running.

### What happens to sessions on actual server restart?

Sessions are destroyed. The `process.once("exit", cleanup)` handler and related signal listeners iterate through all entries in `globalThis.__piSessions` and call `s.destroy()` on each `AgentSessionWrapper` before the process terminates.

### Could this cause memory leaks during development?

Unlikely in practice. The registry only grows when new sessions are explicitly created via `startRpcSession()`. Each session occupies memory, but typical development workflows create sessions in response to user actions. The Map structure itself has minimal overhead.

### Is this pattern specific to Pi Web or widely applicable?

The `globalThis` persistence pattern applies to any Node.js application with hot-reload requirements. Popular frameworks like Next.js, Nuxt, and Vite all preserve `globalThis` across module replacement. Pi Web's implementation in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) demonstrates a clean, production-ready application of this technique.