# How Nodeterm Ensures Terminal Session Continuity with tmux: Architecture Explained

> Discover how Nodeterm uses tmux to achieve terminal session continuity. Learn about the architecture that preserves processes, scrollback, and agent conversations across reloads and reboots.

- Repository: [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm)
- Tags: architecture
- Published: 2026-08-23

---

**TLDR: Nodeterm persists every terminal node inside a NaCl tmux session named `nt-<nodeId>`, so processes, scrollback, and agent conversations survive canvas reloads, app restarts, and machine reboots — replayed exactly on cold start via [`src/core/scrollback-store.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/scrollback-store.ts) and resumed agent IDs.**

Nodeterm, an open-source project by [eneskirca](https://github.com/eneskirca/nodeterm), builds its terminal nodes as thin xterm clients that attach to long-lived tmux sessions. This design is what allows it to offer **true terminal session continuity** across project switches, reloads, and full restarts. Let’s dig into the source to see exactly how the pieces fit together.

## The Core Strategy: Every Terminal Node Is a tmux Session

The entire continuity story is anchored in one decision: **each terminal node is backed by a persistent tmux session named `nt-<nodeId>`**, where `nodeId` is a stable identifier assigned when the node is created. That session is never killed when the UI closes it or when the canvas switches projects — the PTY client detaches, but the tmux session (and everything running inside it) stays alive.

The session name is generated in [`src/core/tmux-naming.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/tmux-naming.ts). It also defines the socket name Nodeterm uses for both local and remote tmux servers, which is what allows the same session to be reattached from a different client later.

## Session Creation: Attach or Create, Forever

In [`src/core/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/pty-manager.ts), the `PtyManager.create()` method spawns a new PTY by running tmux in **attach-or-create mode**:

```ts
tmux new-session -A -D -s nt-<nodeId>

```

- `-A` means “attach if the session exists, otherwise create it.”
- `-D` detaches any other client currently attached, so the node always grabs the session.

The `create()` call returns a `PtyCreateResult` containing a `fresh` boolean. **`fresh: true`** means the session was just created (cold start). **`fresh: false`** means the session already existed (warm re-attach), and tmux itself redraws the pane.

This simple flag drives the entire continuity experience at the renderer level, as documented in [`src/shared/types.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/types.ts).

### Warm Re-attach (Fast Path)

If the tmux server is still alive, the renderer just attaches a new PTY client — no scrollback replay is needed. tmux redraws the live pane instantly, which is why switching back to a terminal node feels immediate.

Even if the canvas was refreshed or the project was switched, the tmux session persists in the background, waiting to be attached again.

### Cold Restore Path (After Reboot or Server Loss)

If the machine rebooted or the tmux server died, the session is gone. `fresh` becomes `true`, and Nodeterm performs a **cold restore**:

1. Reads a persisted snapshot of the terminal scrollback from [`src/core/scrollback-store.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/scrollback-store.ts) (stored in `terminal-scrollback` directory).
2. Re-plays the snapshot into the xterm instance, prefixed with a “session restored” separator so the user knows where the replay starts.
3. If the node is an **agent** (Claude, Codex, Geminka, etc.), the persisted `sessionId` is used to launch the agentCLI with `--resume <id>`, so the agent can continue its conversation.

The exact logic that decides between “seed scrollback” and “do nothing” lives in [`src/renderer/terminal/terminal-config.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/terminal-config.ts).

## Lifecycle: Keep Alive and Clean Up Explicitly

Nodeterm never kills the tmux session just because the node goes off-screen. The only way a session is removed is:

- **Explicit node deletion** — clicking the ✕ on the node header triggers `PtyManager.destroy(nodeId)`, which runs `tmux kill-session -t nt-<nodeId>`.
- **Batch cleanup** — the session-memory panel exposes a “kill all sessions” button that runs the same kill command for every node.

This explicit lifecycle ensures nothing gets orphaned on the process table, while still preserving continuity as long as the user keeps the node around.

## Why tmux Is the Right Backing Store

Using tmux does more than give the session a name — it provides the underlying infrastructure that makes continuity possible:

- **Persistent panes** that outlive any client UI.
- **Shared scrollback** that can be attached by multiple clients, which is crucial for replaying cold starts.
- **Native mouse selection and bracketed paste** handling, so xterm and tmux work together without extra bridge code.
- **OSC 52 clipboard integration**, making copy-paste work cross-platform and across SSH projects.

Because Nodem leverages tmux’s native features, the renderer stays thin, and the session semantics stay predictable.

## Practical Code Example

Here’s what a cold-start restore looks like at the application level:

```ts
import { ptyManager } from "./src/core/pty-manager";

// Create (or attach) a tmux-backed PTY for a terminal node
const result = await ptyManager.create({
  clientId,
  persistKey: nodeId, // becomes tmux session "nt-<nodeId>"
  cwd,
});

if (result.fresh) {
  // Cold start – replay saved scrollback, then resume a resumable agent CLI
  const scrollback = await ptyManager.readScrollback(nodeId);
  xterm.write(scrollback);
  if (isAgentNode && result.sessionId) {
    ptyManager.sendText(`claude --resume ${result.sessionId}\n`);
  }
}

```

### And that’s everything the user needs to know.

```ts
// Destroy a terminal node and its tmux session
await ptyManager.destroy(nodeId); // runs: tmux kill-session -t nt-<nodeId>

```

### Scrollback persistence on detach (called by the PTY manager)

```ts
await scrollbackStore.save(nodeId, capturedScreen);

```

## Key Files That Implement Continuity

The following files do the heavy lifting — a quick reference for anyone exploring the codebase:

| File | Role |
|------|------|
| [`src/core/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/pty-manager.ts) | Creates/attaches tmux sessions, returns the `fresh` flag, and handles session destruction. |
| [`src/core/tmux-naming.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/tmux-naming.ts) | Generates stable tmux session names (`nt-<nodeId>` and defines socket names. |
| [`src/core/scrollback-store.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/scrollback-store.ts) | Persists scrollback snapshots to `<userData>/terminal-scrollback/` and loads them on cold restore. |
| [`src/shared/types.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/types.ts) | Defines `PtyCreateResult` with the `fresh` boolean. |
| [`src/renderer/terminal/terminal-config.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/terminal-config.ts) | Decides whether to seed xterm with saved scrollback or leave the warm re-attach untouched. |
| [`src/session-host/host.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/session-host/host.ts) | Server-side counterpart that ensures the same continuity for the Server edition. |

## Summary

- **Terminal session continuity is a tmux-session model, not a UI trick**: tmux is the truth, xterm is just a client.
- **`fresh` signals the restore path** — `false` means warm re-attach (no replay), `true` means cold restore with saved scrollback and `--resume` agents.
- **Lifecycle is explicit**: sessions only die on user-delete or explicit kill-all, never on UI blur or project switch.
- **Scrollback is snapshotted** per node to disk, so even reboot isn’t fatal.

## Frequently Asked Questions

### Does the terminal session survive a machine reboot?

Yes, as long as xterm was closed gracefully — mardim keeps scrollback and agent session IDs persisted to disk. On reboot, `fresh` becomes `true` and tilderm replays the scrollback and resumes the agent with `--resume`.

### What happens to scrollback when I restart the app?

Scrollback is saved to `<userData>/terminal-scrollback/` by `scrollback-store`. On cold start in, the stored snapshot is replayed into the xterm buffer, so recent scrollback is restored.

### Can two terminals attach to the same session?

Yes — the `-D` flag in `tmux new-session -A -D` ensures pending clients attach to the same session, but only one client owns it at a time. The renderer uses this to let multiple UI projects access the same node.

### What happens to agents like Claude Code when I close a container?

Claude Code CLI sessions are resume-able via persisted IDs. With the session never killed, the agent keeps running like a ghost process; on post-server restart, Nodder launches `--resume <id>` to get the full conversational context back.