How Nodeterm Replays Scrollback Data on Cold Start: Terminal Session Persistence Explained

Nodeterm replays scrollback data on cold start by detecting when a tmux session no longer exists, then retrieving a pre-saved snapshot from disk via the pty.readScrollback IPC call and writing it directly to the xterm instance before recreating the underlying session.

Nodeterm is an open-source terminal management application that maintains persistent terminal sessions across machine reboots. When the application launches after a cold start and discovers the underlying tmux session has been destroyed, it seamlessly reconstructs the previous terminal output by replaying stored scrollback data. This article examines the implementation details of how nodeterm captures, stores, and restores terminal scrollback to ensure users never lose their session context.

Understanding the Cold Start Problem in Terminal Multiplexers

Why Tmux Sessions Are Lost on Reboot

Nodeterm relies on tmux to manage persistent terminal sessions, but tmux itself is an in-memory process. When a machine reboots or the tmux server is terminated, all active panes and their scrollback buffers are destroyed. Without a dedicated persistence layer, users would return to empty terminals after restarting their computer, losing critical command history and output context.

The Warm Attach vs. Fresh Start Distinction

In src/main/pty-manager.ts, the PtyManager class distinguishes between two initialization scenarios:

  • Warm attach: A tmux session with the expected name (nt-${sessionId}) already exists. The terminal connects to the live session, which immediately redraws its current state. No scrollback replay is necessary because tmux preserves the active buffer.
  • Cold start (Fresh): The tmux has-session check fails, indicating the session was destroyed. The fresh flag is set to true, triggering the scrollback restoration workflow before a new tmux session is created.

Capturing Scrollback Snapshots Before Shutdown

To ensure data availability after a reboot, nodeterm periodically persists terminal output to disk. The captureScrollback function in src/core/scrollback-store.ts handles this by executing tmux commands directly against the active pane.

The snapshot generation process follows these steps:

  1. Capture raw output: Execute tmux capture-pane -e -t nt-${sessionId} to grab the current pane content, including escape sequences for color and formatting.
  2. Apply byte limits: Truncate the output to SCROLLBACK_MAX_BYTES (256 KB by default) to prevent excessive disk usage.
  3. Atomic write: Store the data in the user data directory under terminal-scrollback/${sessionId}.log using atomic file writes to prevent corruption during unexpected shutdowns.
// src/core/scrollback-store.ts
export async function captureScrollback(sessionId: string) {
  // Capture current pane content via tmux, preserving escape sequences
  const raw = await execTmux(['capture-pane', '-t', `nt-${sessionId}`, '-e']);
  
  // Truncate to configured limit (256 KB default)
  const truncated = raw.slice(0, SCROLLBACK_MAX_BYTES);
  
  // Write to persistent storage
  await writeFileAtomic(
    path.join(scrollbackDir, `${sessionId}.log`), 
    truncated
  );
}

Detecting Cold Starts and Reading Stored Data

When the renderer process detects a cold start, it requests the stored scrollback via the pty:readScrollback IPC channel defined in src/shared/ipc.ts. The main process handles this request in src/core/scrollback-store.ts by reading the corresponding file from the terminal-scrollback directory and returning the raw buffer to the renderer.

// src/core/scrollback-store.ts (IPC handler)
ipcMain.handle('pty:readScrollback', async (_, sessionId: string) => {
  const filePath = path.join(scrollbackDir, `${sessionId}.log`);
  return readFile(filePath, 'utf8');
});

This separation of concerns ensures that file system access remains in the main process while the renderer handles UI updates, adhering to Electron's security model.

Replaying Scrollback into the Terminal UI

The restoration logic resides in src/renderer/nodes/TerminalNode.tsx, the React component responsible for mounting the xterm.js instance. When the component initializes with fresh = true, it executes the replay sequence before attaching to the new tmux session.

The component performs the following actions:

  1. Calls window.nodeTerminal.pty.readScrollback(sessionId) to fetch the historical output.
  2. Writes the retrieved data to the xterm instance using term.write(scrollback), which parses the ANSI escape sequences to reconstruct colors and formatting.
  3. Appends a visual separator (--- session restored ---) to indicate the boundary between historical replay and new output.
  4. Proceeds to create the new tmux session and attach the pty.
// src/renderer/nodes/TerminalNode.tsx (simplified logic)
useEffect(() => {
  if (fresh) {
    // Fetch the stored scrollback from main process
    const scrollback = await window.nodeTerminal.pty.readScrollback(sessionId);
    
    // Replay into xterm instance
    term.write(scrollback);
    term.writeln('\r\n--- session restored ---\r\n');
  }
  
  // Create or attach to tmux session after replay
  await initializePtySession(sessionId);
}, [sessionId, fresh]);

Handling Agent Session Continuation

For AI agent sessions (Claude, Codex, Gemini), the scrollback replay provides the visual context, but the agent process itself must also resume. After writing the scrollback to xterm, TerminalNode.tsx checks if the node represents a resumable agent. If so, it launches the agent CLI with the --resume ${sessionId} flag, ensuring the agent's internal state synchronizes with the displayed terminal history.

// Agent resume logic following scrollback replay
if (isAgent && fresh) {
  const resumeCommand = `claude --resume ${sessionId}`;
  await window.nodeTerminal.pty.write(sessionId, resumeCommand);
}

Summary

  • Scrollback persistence: Nodeterm captures terminal output using tmux capture-pane -e and stores it in src/core/scrollback-store.ts, capped at 256 KB per session.
  • Cold start detection: The PtyManager in src/main/pty-manager.ts checks for existing tmux sessions; absence indicates a cold start requiring scrollback restoration.
  • IPC retrieval: The renderer requests historical data via the pty:readScrollback channel, while the main process reads from the terminal-scrollback/ directory.
  • UI reconstruction: TerminalNode.tsx writes the retrieved scrollback directly to the xterm instance before creating a new tmux session, ensuring visual continuity.
  • Agent synchronization: Resumable agents are restarted with session-specific flags after the scrollback replay completes, maintaining workflow continuity.

Frequently Asked Questions

Where does nodeterm store scrollback snapshots?

Nodeterm stores scrollback snapshots in the user's application data directory under a terminal-scrollback/ subdirectory. Each session receives its own file named ${sessionId}.log, written atomically by the captureScrollback function in src/core/scrollback-store.ts to prevent data corruption during crashes.

What is the maximum size of a scrollback snapshot?

By default, nodeterm caps scrollback snapshots at 256 KB per session, configurable via the SCROLLBACK_MAX_BYTES constant. The captureScrollback function truncates the raw tmux output to this limit before writing to disk, balancing historical fidelity with storage efficiency.

How does nodeterm distinguish between a cold start and a warm attach?

In src/main/pty-manager.ts, the application executes tmux has-session -t nt-${sessionId} before initializing the terminal. If the command returns successfully, the session is alive and the connection proceeds as a warm attach. If the check fails, the fresh flag is set to true, triggering the scrollback replay workflow for a cold start.

Does scrollback replay work for AI agent sessions?

Yes. When a terminal node hosts a resumable agent (Claude, Codex, or Gemini), the scrollback replay restores the visual history first. Immediately after, TerminalNode.tsx launches the agent CLI with the --resume ${sessionId} flag, allowing the agent to synchronize its internal state with the displayed terminal history and continue the conversation seamlessly.

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 →