# How the Fork Operation Handles Wrapper Destruction to Prevent Corrupt Parent-Session Chains

> Learn how the fork operation in agegr/pi-web destroys AgentSessionWrapper to prevent corrupt parentSession chains, ensuring metadata integrity.

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

---

**The fork operation immediately destroys the `AgentSessionWrapper` via `shutdown()` after creating the new session, forcing a fresh wrapper instantiation for the original session and preserving its parent-metadata integrity.**

When a user forks a session in **Pi Web** (`agegr/pi-web`), the system must spawn a new branch while leaving the original session completely untouched. The critical safeguard preventing chain corruption lives in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), where wrapper destruction is orchestrated right at the fork point.

## Fork Command Handling in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)

The `AgentSessionWrapper` class processes the `"fork"` command starting around **line 43**. The method performs three sequential phases: validation, session creation, and wrapper teardown.

### Validation and Entry Lookup

Before any file operations, the wrapper verifies prerequisites:

```ts
// Lines 44-56 in lib/rpc-manager.ts
if (this.isBashRunning) {
  return { cancelled: true, reason: "Cannot fork while bash is running" };
}
if (!this.isPersisted) {
  return { cancelled: true, reason: "Session must be persisted to fork" };
}
const entry = this.getEntry(command.entryId);
if (!entry) {
  return { cancelled: true, reason: "Entry not found" };
}

```

These guards ensure the session state is stable enough to duplicate.

### Creating the Forked Session

The branch creation logic distinguishes between root entries and nested ones:

- **Root entry** (`!entry.parentId`): Creates a blank session with `parentSession` pointing to the current file
- **Nested entry**: Copies history from the original `.jsonl` file up to (but not including) the fork point

```ts
// Session file creation and ID retrieval
const newSessionId = SessionManager.open(newSessionFile, sessionDir).getSessionId();
cacheSessionPath(newSessionId, newSessionFile);

```

The `SessionManager.open()` call returns a session instance whose ID differs from the original—this distinction becomes crucial in the next step.

## The `shutdown()` Destruction Sequence

Immediately after obtaining `newSessionId`, the wrapper invokes self-destruction:

```ts
case "fork": {
  // ... session creation logic ...
  const newSessionId = SessionManager.open(newSessionFile, sessionDir).getSessionId();
  cacheSessionPath(newSessionId, newSessionFile);
  
  await this.shutdown(); // Line ~75
  return { cancelled: false, newSessionId };
}

```

The `shutdown()` method (defined later in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)) performs comprehensive cleanup:

| Cleanup Action | Purpose |
|--------------|---------|
| **Clears all listeners** | Prevents event handlers from firing on a stale wrapper |
| **Cancels pending UI responses** | Aborts any in-flight `ask()` or `customUI()` calls |
| **Resets custom UI state and extension widgets** | Removes visual components tied to this session instance |
| **Cancels idle timer** | Stops background timeout logic |
| **Removes from `globalThis.__piSessions`** | Deletes the global registry entry keyed by the old `sessionId` |
| **Sets `_alive = false`** | Blocks any subsequent command routing via guard checks |

## Why Wrapper Destruction Prevents Chain Corruption

The destruction step is not merely cleanup—it is **architecturally mandatory** for parent-session chain integrity. Here's the mechanism:

The `AgentSession` instance stored inside `AgentSessionWrapper` mutates its `sessionId` property during fork. The `inner.sessionId` field transitions from the original ID to `newSessionId` to reflect the branched context.

If the wrapper remained registered in `globalThis.__piSessions` under the original key:

1. A later UI request for the original session would retrieve the **same wrapper instance**
2. That wrapper now reports `newSessionId` internally, misleading the session manager
3. The `.jsonl` file's actual `parentSession` metadata would be ignored or overwritten
4. **Result**: A corrupt chain where the original session appears to have been replaced by its fork

By calling `shutdown()` and purging the registry entry, the next request for the original session forces `SessionManager` to instantiate a **fresh `AgentSessionWrapper`** from the untouched original `.jsonl` file. This new wrapper correctly loads and preserves the original `parentSession` field.

## Visualizing the Safe Fork Flow

```

User clicks Fork
       ↓
AgentSessionWrapper receives "fork" command
       ↓
Create new .jsonl file with copied/blank history
       ↓
Obtain newSessionId via SessionManager.open()
       ↓
await this.shutdown()
   ├─ Clear listeners, timers, UI state
   ├─ Delete from globalThis.__piSessions
   └─ Mark _alive = false
       ↓
Return { cancelled: false, newSessionId }
       ↓
UI loads new session; original session untouched
Next request for original → fresh wrapper → correct parent chain

```

## Client-Side Fork Invocation

The UI layer triggers this flow through the RPC interface. In [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts):

```ts
// Initiating fork from the React hook
const result = await agentSession.send({ 
  type: "fork", 
  entryId: selectedEntryId 
});

if (!result.cancelled && result.newSessionId) {
  // Navigate to or load the new branched session
  window.location.href = `/session/${result.newSessionId}`;
}

```

The `send()` method serializes the command to the RPC manager, which executes the fork-and-destroy sequence atomically.

## Summary

- **Validation gates** in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) (lines 44‑56) ensure safe fork preconditions
- **`shutdown()`** immediately after session creation removes the wrapper from `globalThis.__piSessions` and invalidates internal state
- **Fresh wrapper instantiation** on next original-session access preserves correct `parentSession` metadata from the `.jsonl` file
- **`_alive` flag and registry deletion** prevent command routing to the mutated, stale instance

## Frequently Asked Questions

### What happens if `shutdown()` is not called after forking?

Without `shutdown()`, the wrapper remains in `globalThis.__piSessions` indexed by the old session ID. Since the internal `AgentSession` mutated to the new ID, subsequent lookups for the original session would return a wrapper reporting the wrong identity. This corrupts the parent-session chain and can cause data loss or UI inconsistencies.

### Where is the `parentSession` metadata actually stored?

The `parentSession` field lives in the `.jsonl` session file itself, not in memory. When `SessionManager` opens a session file, it reads this metadata to construct the sidebar hierarchy. Fresh wrapper instantiation guarantees this file-level metadata is respected.

### Can a fork operation be cancelled mid-process?

Yes. The validation phase (lines 44‑56) returns `{ cancelled: true, reason: "..." }` for blocked conditions like running bash commands or unpersisted sessions. Once past validation and into file creation, the operation commits—there is no rollback, but the original file is never modified, so atomicity is preserved through copy-on-write semantics.

### How does the UI distinguish forked sessions from root sessions in the sidebar?

The `parentSession` field (display-only) determines visual nesting. Root sessions have no `parentSession`; forked sessions point to their origin file. The sidebar renderer in the Pi Web frontend uses this field to indent and link entries, independent of the runtime `sessionId`.