How Session Export to HTML Handles Deep Tree Traversal in Pi Web

Pi Web uses an iterative patch to replace recursive rendering functions, preventing stack overflow when exporting deeply nested chat sessions to HTML.

Pi Web is an open-source chat interface that stores conversations as .jsonl files. When users export sessions, the application must transform these structured logs into self-contained HTML documents. The challenge lies in rendering conversation trees of arbitrary depth without crashing the browser. According to the agegr/pi-web source code, the export system solves this through a server-side post-processing step that replaces recursive JavaScript with iterative equivalents.

The Deep Tree Problem in Session Export

Browser JavaScript has a call stack limit typically ranging from 10,000 to 50,000 frames depending on the engine. A recursive function walking a linear chat history of 1,000+ messages would exhaust this limit, causing a RangeError: Maximum call stack size exceeded.

The Pi SDK's default export (exportSessionToHtml) generates HTML containing a renderTreeRecursive function. For standard conversation depths, this works fine. However, Pi Web specifically targets long-form conversations where linear threads commonly exceed hundreds of messages.

How the Export Route Applies the Iterative Patch

The export flow in app/api/sessions/[id]/export/route.ts follows a four-stage pipeline:

  1. Fetch the session file — the handler invokes lib/session-reader.ts to parse the .jsonl entries for the requested session ID.

  2. Generate raw HTML — the Pi SDK's exportSessionToHtml creates a document embedding renderTreeRecursive for tree visualization.

  3. Apply iterative transformation — Pi Web post-processes the HTML string, substituting the recursive implementation with renderTreeIterative before streaming the response.

  4. Return patched document — the modified HTML reaches the client with guaranteed O(1) stack depth regardless of message count.

This server-side patching strategy means clients never execute vulnerable recursive code.

The Iterative Rendering Implementation

The replacement function uses an explicit stack data structure to simulate recursion without growing the call stack:

function renderTreeIterative(root) {
  const stack = [{ node: root, depth: 0 }];
  while (stack.length) {
    const { node, depth } = stack.pop();
    // render node …
    for (let i = node.children.length - 1; i >= 0; i--) {
      stack.push({ node: node.children[i], depth: depth + 1 });
    }
  }
}

Key characteristics of this implementation:

  • LIFO stack ordering — children are pushed in reverse order so they render left-to-right when popped
  • Constant memory overhead — stack depth equals tree breadth, not tree depth
  • No tail-call optimization dependency — works reliably across all browser engines

The patch preserves all rendering behavior while eliminating stack consumption proportional to conversation length.

Complete Export Flow Example

Triggering an export from a React component:

// Export session as downloadable HTML file
const exportSession = async (sessionId: string) => {
  const res = await fetch(`/api/sessions/${sessionId}/export`);
  const blob = await res.blob();
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `session-${sessionId}.html`;
  a.click();
};

The fetch targets the patched endpoint in app/api/sessions/[id]/export/route.ts, ensuring the returned blob contains the iterative renderer.

Source Files and Architecture

File Responsibility
app/api/sessions/[id]/export/route.ts API endpoint coordinating fetch, generation, patch, and response
lib/session-reader.ts .jsonl parser extracting message entries by session ID
lib/export-html.ts (Pi SDK) Raw HTML generator requiring post-processing for deep trees

The separation of concerns allows Pi Web to upgrade SDK versions while maintaining its deep-tree safeguard through the patching layer.

Performance and Scalability

The iterative approach provides predictable performance characteristics:

  • Time complexity: O(n) for n messages — identical to recursive traversal
  • Space complexity: O(w) where w is maximum tree width (typically small for chat threads)
  • Stack guarantee: Exactly one frame regardless of thread depth

This design intentionally trades marginal code complexity for hard reliability guarantees across all session sizes.

Summary

  • Pi Web exports sessions via app/api/sessions/[id]/export/route.ts using the Pi SDK's HTML generator
  • Deep linear conversations would crash with default recursive rendering due to JavaScript stack limits
  • The export route patches generated HTML to replace renderTreeRecursive with renderTreeIterative
  • Iterative traversal uses an explicit stack and while loop for O(1) call stack depth
  • This server-side transformation ensures exported HTML works for sessions of any depth

Frequently Asked Questions

What causes stack overflow in HTML session exports?

Recursive JavaScript functions consume one stack frame per nested call. A 5,000-message linear chat creates 5,000 nested invocations, exceeding browser limits. The renderTreeRecursive function in unpatched SDK output is vulnerable to this failure mode for sufficiently deep sessions.

How does Pi Web modify the SDK's HTML output?

The export route in app/api/sessions/[id]/export/route.ts performs a string replacement on the generated HTML before streaming it to the client. This substitutes the recursive function definition with an iterative equivalent that uses a manual stack array, preserving all rendering logic while eliminating deep call stacks.

Can the iterative approach handle branching conversation trees?

Yes. The renderTreeIterative function uses a stack that processes all children of each node, making it suitable for both linear threads and complex branched conversations. The space requirement scales with tree width (number of siblings at a given level) rather than depth.

Where is the session data stored before export?

Sessions persist as .jsonl files containing one JSON object per line, read by lib/session-reader.ts. This append-only format supports efficient streaming and reconstruction of conversation history regardless of total session size.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →