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

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. 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, 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):

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 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 reads the .jsonl file and extracts the session_info entry to supply the title to React components. The useAgentSession hook and 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:

// 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);
  }
}
// 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);
}
// 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 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 and displayed in 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 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 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 file orchestrates the process. It detects when a new session receives its first user message, calls generateSessionTitle() from lib/session-title.ts, and writes the resulting title to the session_info entry in the session's .jsonl log file.

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 →