# How the Session Context API Loads Branch-Specific Message Trees in pi-web

> Learn how the session context API in pi-web loads branch specific message trees using the leafId parameter and Pi SDK functions for efficient conversation filtering and UI display.

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

---

**The session context API in pi-web loads branch-specific message trees by accepting a `leafId` query parameter that identifies the target conversation branch, delegating to the Pi SDK's `piBuildSessionContext` function to filter entries, and converting the resulting entries into UI-ready messages via `buildSessionContext` in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts).**

The pi-web repository implements a sophisticated branching conversation interface for AI coding sessions. When users navigate between different branches of a session tree, the **session context API** efficiently loads only the messages specific to that branch by leveraging the Pi SDK's tree-walking capabilities and transforming raw session entries into UI-ready formats.

## Initiating the Context Request from the Client

The request originates in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), where the **`loadContext`** callback constructs the API endpoint whenever the active leaf changes:

```typescript
// hooks/useAgentSession.ts – lines 502-508
const loadContext = useCallback(async (sid: string, leafId: string | null) => {
  const params = new URLSearchParams({ deferThinking: "1", deferMedia: "1" });
  if (leafId) params.set("leafId", leafId);
  const url = `/api/sessions/${encodeURIComponent(sid)}/context?${params}`;
  const res = await fetch(url);
  const d = await res.json() as { context: { messages: AgentMessage[]; entryIds: string[] } };
  setMessages(d.context.messages);
  setEntryIds(d.context.entryIds ?? []);
}, []);

```

The **`leafId`** query parameter identifies the tip of the branch the UI wants to display. When provided, the API returns only the ancestor entries leading to that leaf, rather than the entire session history.

## API Route Handling and Session Resolution

The server-side handler resides in `app/api/sessions/[id]/context/route.ts`. This route extracts the `leafId` from the query string, resolves the session's on-disk file path, and initializes a session manager instance:

```typescript
// app/api/sessions/[id]/context/route.ts – lines 10-27
const leafId = url.searchParams.get("leafId") ?? undefined;
const deferThinking = url.searchParams.has("deferThinking");
const deferToolResultImages = url.searchParams.has("deferMedia");

const filePath = await resolveSessionPath(id);
const sm = SessionManager.open(filePath);
const context = buildSessionContext(
  sm.getEntries() as never,
  leafId,
  { deferThinking, deferToolResultImages }
);
return NextResponse.json({ context });

```

The **`resolveSessionPath`** function maps the public session ID to the underlying JSONL file. **`SessionManager.open`** provides access to the raw session entries via the Pi SDK, which are then passed to **`buildSessionContext`** along with the target leaf identifier.

## Building the Branch-Specific Message Tree

The [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) file contains the core logic for transforming raw SDK entries into a branch-specific UI context. The **`buildSessionContext`** function orchestrates three distinct phases: entry indexing, branch selection, and message conversion.

### Indexing Entries for Branch Lookup

The function begins by indexing all session entries by their ID to enable efficient tree traversal:

```typescript
// lib/session-reader.ts – lines 4-8
const byId = new Map<string, SessionEntry>();
for (const e of entries) byId.set(e.id, e);

```

This `byId` map is passed to the Pi SDK functions to resolve parent-child relationships during branch selection.

### Delegating Branch Selection to the Pi SDK

After indexing, the function invokes the Pi SDK to determine which entries belong to the requested branch:

```typescript
// lib/session-reader.ts – lines 206-219
const piCtx = piBuildSessionContext(
  piEntries,
  leafId,
  byId as unknown as Map<string, PiSessionEntry>
);

// Build the ordered list of context entries
const contextEntries = piBuildContextEntries(
  piEntries,
  leafId,
  byId as unknown as Map<string, PiSessionEntry>,
);

```

The **`piBuildSessionContext`** and **`piBuildContextEntries`** functions (from the `@earendil-works/pi-coding-agent` SDK) walk the session tree using the provided `leafId` as the target node. They return only the entries that form the path from the session root to the specified leaf, handling compaction markers and branching rules internally.

### Converting Entries to UI-Ready Messages

After obtaining the filtered entry list, the function converts each SDK entry into the `AgentMessage` format expected by the React UI. The **`entryToUiMessage`** helper filters out non-message entries and processes content based on the deferral options:

```typescript
// lib/session-reader.ts – lines 93-130 (excerpt)
function entryToUiMessage(
  entry: SessionEntry,
  options: { deferThinking?: boolean; deferToolResultImages?: boolean },
): AgentMessage | null {
  switch (entry.type) {
    case "message": {
      const message = options.deferToolResultImages
        ? omitToolResultBase64Images(normalizeToolCalls(entry.message))
        : normalizeToolCalls(entry.message);
      if (!options.deferThinking || message.role !== "assistant") return message;
      // Strip "thinking" blocks if defer-thinking is requested...
    }
    default:
      return null;
  }
}

```

The conversion logic uses **`normalizeToolCalls`** to standardize tool payloads and **`omitToolResultBase64Images`** to strip heavy image data when `deferMedia` is enabled. The resulting `messages` array together with `entryIds` is returned to the API route.

## Synchronizing UI State After Response

Upon receiving the JSON response, the client updates the React state to display the branch-specific conversation:

```typescript
// hooks/useAgentSession.ts – lines 511-515
const d = await res.json() as { context: { messages: AgentMessage[]; entryIds: string[] } };
setMessages(d.context.messages);
setEntryIds(d.context.entryIds ?? []);

```

This state update triggers a re-render of the chat interface, displaying only the messages belonging to the selected branch while maintaining the full session tree in the sidebar navigation.

## Summary

- The **session context API** accepts a `leafId` parameter at `GET /api/sessions/[id]/context` to identify specific conversation branches.
- **`buildSessionContext`** in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) delegates branch filtering to the Pi SDK's `piBuildSessionContext` and `piBuildContextEntries` functions.
- Raw session entries are converted to UI-ready `AgentMessage` objects by **`entryToUiMessage`**, which handles tool call normalization and optional media deferral via `normalizeToolCalls` and `omitToolResultBase64Images`.
- The client-side **`loadContext`** function in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) manages the request lifecycle and state synchronization using the `deferThinking` and `deferMedia` flags.
- This architecture allows pi-web to display isolated branch views without reloading the entire session file or processing unrelated conversation history.

## Frequently Asked Questions

### What happens if no leafId is provided to the session context API?

If the `leafId` query parameter is omitted, the Pi SDK defaults to returning the currently active branch of the session (typically the most recent leaf). The `buildSessionContext` function passes `undefined` to the SDK, which then selects the default branch path based on session state rather than a specific target node.

### How does the Pi SDK determine which entries belong to a specific branch?

The **`piBuildSessionContext`** function walks the session's entry graph using the provided `leafId` as the terminal node. It traverses parent references backward to the session root, collecting only entries that form the direct ancestry chain of the target leaf, while respecting compaction and branching rules defined in the `@earendil-works/pi-coding-agent` SDK.

### What types of session entries are excluded from the messages array?

The **`entryToUiMessage`** function filters out entries with types other than `"message"`, such as compaction markers, branch summaries, and custom metadata entries. These internal entries are used for session management but are not displayed in the UI conversation view, ensuring only actual message content reaches the client.

### Why does the client request deferThinking and deferMedia parameters?

The `deferThinking` and `deferMedia` flags allow the client to request lightweight message payloads. When `deferThinking` is true, the server strips internal reasoning blocks from assistant messages. When `deferMedia` is true, `entryToUiMessage` calls `omitToolResultBase64Images` to exclude base64-encoded images from tool results, reducing payload size for initial page loads.