# How Pi-Web Session Auto-Naming Works: A Complete Technical Guide

> Discover how pi-web session auto-naming works. Learn how it creates readable titles by analyzing conversation history without impacting the real session.

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

---

**Pi-Web automatically generates human-readable session titles by spawning an isolated shadow Agent that analyzes conversation history without affecting the real session state or invoking tools.**

The `agegr/pi-web` repository implements an intelligent session auto-naming system that eliminates manual title management. When users start a conversation, the system analyzes the initial messages to generate a concise, context-aware title that reflects the user's intent and language. This process leverages the Pi coding-agent core to create safe, non-destructive analysis runs that execute in parallel to the main session.

## The Architecture of Session Auto-Naming

The session auto-naming workflow centers on the `generateSessionTitle()` function in [`lib/session-title.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-title.ts). This utility creates a temporary sandboxed environment that mirrors the current session's configuration while preventing any side effects.

### Triggering Title Generation

The system initiates auto-naming immediately after session creation or following the first user message. In [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), the RPC manager detects when a session requires naming and invokes `generateSessionTitle()` with the current `AgentSession` object. The function first ensures the source agent is idle via `await sourceAgent.waitForIdle()` before proceeding.

### Sanitizing the Message Stream

Before sending data to the language model, `sanitizeTitleMessages()` processes the conversation history to remove tool-call blocks and internal metadata. This sanitization ensures the title generation prompt contains only user-visible content, preventing tool syntax from confusing the model and keeping the focus on the actual conversation topic.

### Building the Shadow Agent

The core safety mechanism is `buildSessionTitleAgentOptions()`, which clones the current session's complete state—including the system prompt, model configuration, and message history—into a new options object. Crucially, it replaces every tool implementation with a stub function that throws an error upon invocation. This **shadow agent** approach guarantees that the title generation run cannot mutate the real session, access external resources, or execute any tool side-effects.

## The Title Generation Pipeline

Once the shadow agent is configured, Pi-Web executes a controlled generation sequence with strict safeguards.

### Appending the Title Prompt

If the sanitized message history ends with a user message, the system calls `appendTitleRequestToTrailingUser()` to fold the `TITLE_PROMPT` constant into that final message. This technique presents the title instruction as a natural continuation of the user's input rather than a separate system intervention, improving context coherence.

### Executing with Timeout Protection

The system creates a new `Agent` instance with the shadow options and executes either `temporaryAgent.continue()` (if the last message was from the user) or `temporaryAgent.prompt(TITLE_PROMPT)`. To prevent hangs, the entire operation is raced against `TITLE_TIMEOUT_MS` (90 seconds):

```typescript
await Promise.race([
  run,
  new Promise<never>((_, reject) => setTimeout(() => {
    tempAgent.abort();
    reject(new Error("Session title generation timed out"));
  }, TITLE_TIMEOUT_MS)),
]);

```

If the timeout expires, the temporary agent is forcibly aborted and the promise rejects, ensuring the main application thread never blocks indefinitely.

### Parsing and Validating Results

After the shadow run completes, `getAssistantResult()` extracts the latest assistant message from the temporary agent's history. The raw text passes through `parseGeneratedSessionTitle()`, which performs rigorous cleaning:

- Strips markdown fences (`` ```json `` or `` ```text `` blocks)
- Extracts values from JSON wrappers like `{ "title": "..." }`
- Removes common prefixes such as "Session title:" or "Title:"
- Strips surrounding quotes and trailing punctuation
- Collapses multiple whitespace characters

The validator ensures the result contains at least one letter or number using the Unicode regex `[\p{L}\p{N}]`, and enforces the `MAX_TITLE_LENGTH` limit of 80 characters by truncating excess content. If validation fails, the function throws an error rather than returning unusable titles.

## Integration with Session Storage and UI

Once generated, titles persist through the session lifecycle and surface in the user interface.

### Persisting Titles to session_info

The [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) file handles the storage phase. After receiving the `GeneratedSessionTitle` object, the manager writes a `session_info` entry to the session's `.jsonl` log file. This entry acts as the canonical source of truth for the session's display name, separate from the message content itself.

### Displaying Auto-Generated Titles

On the client side, [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) reads the `.jsonl` file and extracts the `session_info` entry to supply the title to React components. The `useAgentSession` hook and [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx) component subscribe to these updates, automatically reflecting the new title in the sidebar navigation and browser tab bar without requiring a page refresh. Because the shadow agent inherits the original session's model and system prompt, the generated title naturally matches the user's language and conversation context.

## Code Implementation Examples

The following patterns demonstrate practical usage of the session auto-naming API:

```typescript
// Triggering title generation after first user message
import { generateSessionTitle } from "@/lib/session-title";

async function maybeNameSession(session: AgentSession) {
  // Only attempt naming if user messages exist
  if (!session.agent.state.messages.some(m => m.role === "user")) return;

  try {
    const { title } = await generateSessionTitle(session);
    console.log("Auto-generated title:", title);
    // RPC manager automatically persists this to session_info
  } catch (e) {
    console.warn("Failed to auto-name session:", e);
  }
}

```

```typescript
// Core title generation logic (simplified from lib/session-title.ts)
export async function generateSessionTitle(
  source: AgentSession
): Promise<GeneratedSessionTitle> {
  const sourceAgent = source.agent;
  await sourceAgent.waitForIdle();

  const sanitized = sanitizeTitleMessages(sourceAgent.state.messages);
  const opts = buildSessionTitleAgentOptions(sourceAgent);
  opts.initialState!.messages = sanitized;

  // Append prompt to trailing user message when present
  if (sanitized.at(-1)?.role === "user") {
    opts.initialState!.messages = appendTitleRequestToTrailingUser(sanitized);
  }

  const tempAgent = new Agent(opts);
  const run = sanitized.at(-1)?.role === "user"
    ? tempAgent.continue()
    : tempAgent.prompt(TITLE_PROMPT);

  // 90-second safety timeout
  await Promise.race([
    run,
    new Promise<never>((_, reject) => setTimeout(() => {
      tempAgent.abort();
      reject(new Error("Session title generation timed out"));
    }, TITLE_TIMEOUT_MS)),
  ]);

  return getAssistantResult(tempAgent, sanitized.length);
}

```

```typescript
// Parsing logic with strict validation
export function parseGeneratedSessionTitle(raw: string): string {
  let value = raw.trim();

  // Extract content from markdown fences
  const fenced = value.match(/^```(?:json|text)?\s*([\s\S]*?)\s*```$/i);
  if (fenced) value = fenced[1].trim();

  // Handle JSON objects
  if (value.startsWith("{")) {
    try {
      const parsed = JSON.parse(value) as { title?: unknown };
      if (typeof parsed.title === "string") value = parsed.title.trim();
    } catch { /* fall through */ }
  }

  // Clean prefixes and normalize whitespace
  value = value.split(/\r?\n/, 1)[0] ?? "";
  value = value.replace(/^(?:session\s+title|title|标题)\s*[:：-]\s*/i, "");
  value = stripWrappingQuotes(value).replace(/\s+/g, " ").trim();
  value = value.replace(/[。.!]+$/u, "").trim();

  // Validation: must contain alphanumeric characters
  if (!/[\p{L}\p{N}]/u.test(value)) {
    throw new Error("The model did not return a usable session title");
  }

  // Enforce 80-character limit
  if (Array.from(value).length > MAX_TITLE_LENGTH) {
    value = Array.from(value).slice(0, MAX_TITLE_LENGTH).join("").trim();
  }

  return value;
}

```

## Summary

- **Shadow Agent Isolation**: Pi-Web creates a sanitized clone of the session state where all tools are replaced with error-throwing stubs, ensuring title generation never affects the real conversation.
- **Timeout Safety**: Every auto-naming attempt is capped at 90 seconds (`TITLE_TIMEOUT_MS`), with automatic abort if the model hangs.
- **Rigorous Parsing**: The `parseGeneratedSessionTitle()` function in [`lib/session-title.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-title.ts) handles markdown fences, JSON wrappers, and multi-language prefixes while enforcing an 80-character limit.
- **Automatic Persistence**: Generated titles write to a `session_info` entry in the session's `.jsonl` file, read by [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) and displayed in [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx).
- **Context Awareness**: By reusing the original session's model and system prompt, the generated titles match the user's language and accurately reflect conversation goals.

## Frequently Asked Questions

### How does Pi-Web prevent title generation from affecting the main session?

The `buildSessionTitleAgentOptions()` function in [`lib/session-title.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-title.ts) creates a shadow configuration that clones the session's messages and settings but replaces every tool implementation with a stub that throws an error. This ensures the temporary Agent runs in complete isolation, unable to modify files, call APIs, or alter the parent session's state.

### What happens if the LLM fails to generate a valid title?

If the model returns content without alphanumeric characters (validated via `[\p{L}\p{N}]`), exceeds the 90-second timeout window, or produces unparseable output, `generateSessionTitle()` throws an error. The calling code in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) typically catches these failures and leaves the session with a default placeholder title rather than crashing the application.

### How long can auto-generated session titles be?

The system enforces a hard limit of 80 characters (`MAX_TITLE_LENGTH`). If the model returns a longer string, `parseGeneratedSessionTitle()` truncates it to the first 80 characters and trims trailing whitespace, ensuring titles fit cleanly in UI elements like sidebar navigation and browser tabs.

### Which file handles the actual invocation of the title generation?

The [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) file orchestrates the process. It detects when a new session receives its first user message, calls `generateSessionTitle()` from [`lib/session-title.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-title.ts), and writes the resulting title to the `session_info` entry in the session's `.jsonl` log file.