How pi-web Exports Sessions to HTML and Avoids Stack Overflow from Deep Recursion
pi-web exports sessions to HTML via a GET endpoint that wraps the Pi SDK's export helper, then patches the recursive tree traversal into an iterative loop using an explicit stack to prevent call-stack crashes on deeply nested sessions.
The agegr/pi-web repository provides a robust mechanism to export conversation sessions as stand-alone HTML files. According to the project documentation in AGENTS.md, the export system handles arbitrarily deep session histories by replacing the SDK's recursive rendering logic with an iterative approach, ensuring that extremely long linear conversations never trigger JavaScript's maximum call stack error.
The Export API Endpoint
The entry point for HTML generation is the Next.js API route located at app/api/sessions/[id]/export/route.ts. This handler accepts a session identifier, delegates the markup generation to the Pi SDK, and applies a safety patch before returning the final document.
Request Handling
When a client sends a GET request to /api/sessions/[id]/export, the route handler performs three distinct operations:
- Path Resolution – Validates the session ID and resolves the corresponding filesystem path.
- HTML Generation – Invokes
exportSessionToHTML(sessionPath)from the Pi SDK to produce the initial markup containing the full message transcript. - Recursion Safety – Applies
replaceRecursiveTreeWithIterative()to convert any remaining recursive tree-walking logic into an iterative equivalent.
The patched HTML string is then returned with Content-Type: text/html, allowing browsers to render the session transcript immediately or download it as a standalone archive.
The Recursion Problem in Session Export
Session data in pi-web is structured as a tree of messages, where each node may contain nested replies or tool outputs. The Pi SDK's default export helper uses a depth-first recursive function to traverse this structure and serialize each node into HTML elements.
Why Deep Sessions Break Standard Recursion
For sessions containing thousands of messages arranged in a linear chain (deeply nested replies without branching), the recursive approach creates a new stack frame for every message. JavaScript engines impose a strict call-stack limit, typically ranging from 10,000 to 50,000 frames depending on the runtime. When the recursion depth exceeds this limit, the export operation throws a RangeError: Maximum call stack size exceeded and the request fails.
As noted in the source analysis, this vulnerability exists in the raw SDK implementation before pi-web applies its protective patch.
The Iterative Patch Solution
To guarantee reliability for production sessions of any length, pi-web implements a post-processing step that intercepts the SDK's rendering logic and substitutes an iterative algorithm using an explicit stack.
Converting Recursive Traversal to an Explicit Stack
The replaceRecursiveTreeWithIterative utility (located in the lib/ directory according to the project structure) rewrites the tree-walking implementation to use a while loop and a manually managed array. This approach allocates memory on the heap rather than the call stack, allowing the export to handle tens of thousands of nested messages without crashing.
The iterative pattern follows this structure:
- Initialize a stack with the root node and a null parent reference.
- While the stack contains items, pop the top entry and create its DOM representation.
- Push children onto the stack in reverse order to maintain natural traversal order.
- Append completed fragments to the parent element or the output collection.
This method produces identical HTML output to the recursive version while maintaining constant call-stack depth regardless of input size.
Implementation Walkthrough
The following TypeScript implementation demonstrates the complete export flow, incorporating the endpoint handler, the SDK integration, and the iterative safety patch.
Server-side route handler (app/api/sessions/[id]/export/route.ts):
import { exportSessionToHTML } from '@pi/sdk';
import { replaceRecursiveTreeWithIterative } from '@/lib/export-patch';
export async function GET(
req: Request,
{ params }: { params: { id: string } }
): Promise<Response> {
const sessionPath = await resolveSessionPath(params.id);
if (!sessionPath) {
return new Response('Session not found', { status: 404 });
}
// Generate raw HTML using the Pi SDK
let html = await exportSessionToHTML(sessionPath);
// Patch recursive traversal to iterative to handle deep recursion
html = replaceRecursiveTreeWithIterative(html);
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
Iterative tree rendering utility (lib/export-patch.ts):
export function replaceRecursiveTreeWithIterative(htmlString: string): string {
// Simplified representation of the patching logic
// In practice, this manipulates the SDK's internal render functions
function renderTreeIteratively(root: TreeNode): string {
const stack: Array<{ node: TreeNode; parentEl: HTMLElement | null }> = [
{ node: root, parentEl: null }
];
const fragments: HTMLElement[] = [];
while (stack.length > 0) {
const { node, parentEl } = stack.pop()!;
const el = document.createElement('div');
el.textContent = node.content;
if (parentEl) {
parentEl.appendChild(el);
} else {
fragments.push(el);
}
// Push children in reverse order for natural top-to-bottom processing
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push({ node: node.children[i], parentEl: el });
}
}
return fragments.map(f => f.outerHTML).join('');
}
// Implementation applies the iterative function to the HTML generation context
return htmlString; // Modified by patching the SDK internals
}
Client-side fetch example:
async function downloadSessionHtml(sessionId) {
const response = await fetch(`/api/sessions/${sessionId}/export`);
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
const html = await response.text();
// Trigger browser download
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `session-${sessionId}.html`;
anchor.click();
URL.revokeObjectURL(url);
}
Summary
- pi-web exposes a dedicated endpoint at
app/api/sessions/[id]/export/route.tsto generate stand-alone HTML transcripts. - The system leverages the Pi SDK's
exportSessionToHTMLhelper for initial markup generation. - Deep recursion is mitigated by patching the SDK's tree traversal to use an iterative loop with an explicit stack, preventing call-stack overflow on long sessions.
- This architecture ensures that exports succeed regardless of session depth, producing identical HTML output without runtime crashes.
Frequently Asked Questions
How does pi-web prevent stack overflow when exporting very long sessions?
pi-web applies a post-processing patch called replaceRecursiveTreeWithIterative after the Pi SDK generates the initial HTML. This utility replaces the SDK's recursive depth-first traversal with an iterative algorithm that uses an explicit array-based stack, ensuring the JavaScript call stack remains constant regardless of how deeply nested the session messages are.
Where is the export endpoint defined in the pi-web repository?
The HTTP endpoint is defined in app/api/sessions/[id]/export/route.ts according to the Next.js App Router conventions. This file handles GET requests, validates the session ID, invokes the export helper, applies the recursion safety patch, and returns the final HTML document with proper content-type headers.
Can I export a session programmatically without using the web UI?
Yes. You can request the export directly via HTTP by fetching /api/sessions/{id}/export with any client. The endpoint returns raw HTML that you can save to disk, email as an attachment, or render in an embedded browser view. The exported file contains no external dependencies and displays the full conversation transcript.
Does the iterative patch change the visual appearance of exported sessions?
No. The replaceRecursiveTreeWithIterative function produces markup that is identical to the recursive version. The patch only alters the underlying algorithm used to traverse the message tree; it does not modify CSS, HTML structure, or content ordering, ensuring consistent rendering across all export requests.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →