# The Mental Model: One Daemon, Many Surfaces

> RMUX has exactly one moving part at runtime: a single-socket daemon (Unix socket or Windows Named Pipe) that owns all session/window/pane state. Every other surface — CLI, SDK, ratatui widget, AI agent — sends typed request frames to that daemon and reads typed response frames back. The binary doubles as that daemon via a hidden re-exec path (`--__internal-daemon`), so no separate install is needed. The key invariant: if the daemon is not running, the CLI auto-starts it on first command; if it is already running, every subsequent client reuses the same socket without negotiation.

- Repository: Helvesec/rmux
- GitHub: https://github.com/Helvesec/rmux
- Human wiki: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181
- Complete Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms-full.txt

## Source Files

- `README.md`
- `src/main.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-ipc/src/endpoint.rs`
- `Cargo.toml`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/main.rs](src/main.rs)
- [crates/rmux-client/src/auto_start.rs](crates/rmux-client/src/auto_start.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [crates/rmux-server/src/daemon.rs](crates/rmux-server/src/daemon.rs)
- [crates/rmux-server/src/listener.rs](crates/rmux-server/src/listener.rs)
- [crates/rmux-client/src/connection.rs](crates/rmux-client/src/connection.rs)
- [crates/rmux-client/src/lib.rs](crates/rmux-client/src/lib.rs)
- [crates/rmux-ipc/src/lib.rs](crates/rmux-ipc/src/lib.rs)
- [crates/rmux-proto/src/envelope.rs](crates/rmux-proto/src/envelope.rs)
- [crates/rmux-proto/src/lib.rs](crates/rmux-proto/src/lib.rs)
- [crates/rmux-sdk/src/bootstrap/startup_unix.rs](crates/rmux-sdk/src/bootstrap/startup_unix.rs)
- [crates/rmux-sdk/src/lib.rs](crates/rmux-sdk/src/lib.rs)
- [Cargo.toml](Cargo.toml)
- [README.md](README.md)
</details>

# The Mental Model: One Daemon, Many Surfaces

RMUX's runtime has exactly one moving part: a long-lived daemon process that owns all session, window, and pane state. Every other actor — the `rmux` CLI, the `rmux-sdk` Rust crate, a `ratatui-rmux` widget, or an AI agent calling the SDK — is a short-lived client that opens a socket connection, sends typed request frames, reads typed response frames, and exits. The daemon is never a separate install: the same `rmux` binary doubles as both the public CLI and the hidden daemon, selected by a secret re-exec flag.

This page explains how that single invariant shapes every part of the system: how the daemon starts (and why it starts at most once), how clients find and connect to it, what the wire protocol looks like, and what breaks if any piece of this contract changes.

---

## The Single Daemon Invariant

All session, window, and pane state lives inside one daemon process per user endpoint. Clients are stateless — they carry no session data between invocations. This means:

- Two CLI invocations run against identical state because they talk to the same daemon.
- An SDK program can inspect or drive a session a CLI command created, without any additional coordination layer.
- Stopping the daemon discards all in-memory state for that endpoint.

The daemon is bound to a single local socket (Unix socket on Linux/macOS, Windows Named Pipe on Windows). That socket is the only channel through which clients communicate with the daemon — there is no shared memory, no database, and no REST API.

---

## The Binary Is Also the Daemon

The `rmux` binary has two entry points, separated by a hidden CLI flag:

```
argv[1] == "--__internal-daemon"  →  run hidden daemon mode
anything else                     →  run public CLI
```

The constant `INTERNAL_DAEMON_FLAG = "--__internal-daemon"` is defined in `rmux-client` and shared with `src/main.rs` so both sides stay in sync.

```rust
// src/main.rs (simplified)
match args.get(1) {
    Some(argument) if argument == INTERNAL_DAEMON_FLAG => {
        run_hidden_daemon(internal)
    }
    _ => cli::run(args),
}
```

When run as a daemon, `main.rs` calls `ServerDaemon::new(config).bind().await` and then `server.wait().await`, which blocks until the daemon receives a shutdown signal. The Tokio runtime used is single-threaded on Unix (`new_current_thread`) and multi-threaded on Windows (minimum 4 worker threads) because Windows Named Pipe I/O benefits from concurrency.

Sources: [src/main.rs:53-77](), [src/main.rs:155-177]()

---

## Auto-Start: One Daemon Per Endpoint

The CLI never requires the user to manually start the daemon. On first command, `ensure_server_running` in `rmux-client` transparently spawns it. On every subsequent command, the same function connects to the already-running daemon without any negotiation.

### The Auto-Start Protocol (Unix)

```
Client process
    │
    ├── Try connect to socket
    │     └── Connected? → return existing connection (fast path)
    │
    ├── Acquire per-endpoint flock (startup_lock_path)
    │     └── Race lost? → poll until daemon appears, then connect
    │
    ├── Validate socket directory ownership and permissions
    │
    ├── Spawn: rmux --__internal-daemon <socket_path> [config flags]
    │         stdin/stdout/stderr = null  (daemon is detached)
    │
    └── Poll until socket becomes connectable (25 ms interval, 5 s deadline)
```

The spawned child is immediately `drop`ped without `.wait()`, so the daemon process outlives the short-lived client that started it.

```rust
// crates/rmux-client/src/auto_start.rs
fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
    let child = command.spawn()?;
    // Intentionally drop without `wait()`: the daemon must outlive the
    // short-lived client process that launched it.
    drop(child);
    Ok(())
}
```

Sources: [crates/rmux-client/src/auto_start.rs:200-208](), [crates/rmux-client/src/auto_start.rs:155-183]()

### The Startup Race Lock

On Unix, a per-endpoint `flock`-based lock file prevents two simultaneous callers from both trying to spawn a daemon. The lock path is derived from the socket path. The winner spawns the daemon; losers poll the socket until it appears. This means "if the daemon is already running, every subsequent client reuses the same socket without negotiation" is an invariant enforced by the filesystem-level lock, not by protocol handshake.

Sources: [crates/rmux-sdk/src/bootstrap/startup_unix.rs:1-55]()

### Windows Startup

On Windows, a named mutex (per pipe name) serializes the race instead of flock. The pipe-responds probe (`windows_protocol_probe`) checks whether an existing pipe is a live RMUX daemon before reporting `AddrInUse`.

Sources: [crates/rmux-server/src/daemon.rs:210-260]()

---

## Endpoint Naming

Every daemon is identified by a local endpoint address. The resolution priority is:

| Source | Wins over |
|---|---|
| `-S <socket_path>` flag | everything |
| `-L <label>` flag | `$RMUX` env, default |
| `$RMUX` environment variable | default only |
| Default per-user path | — |

**Default paths:**

| Platform | Default endpoint |
|---|---|
| Linux/macOS | `/tmp/rmux-{uid}/default` |
| Windows | `\\.\pipe\rmux-{SID}-il-{integrity}-default` |

The Unix path uses the real UID, not the effective UID, so `sudo` invocations do not accidentally share state with the root daemon. The Windows path encodes the user SID and integrity level, making per-user isolation a property of the pipe name itself.

```rust
// crates/rmux-ipc/src/endpoint.rs
#[cfg(unix)]
fn endpoint_for_label_impl(label: &OsStr) -> io::Result<LocalEndpoint> {
    let user_id = rmux_os::identity::real_user_id();
    endpoint_from_parts(std::env::var_os(RMUX_TMPDIR_ENV).as_deref(), user_id, label)
}
```

Sources: [crates/rmux-ipc/src/endpoint.rs:71-140]()

---

## The Wire Protocol

The daemon and all clients share a typed frame protocol defined in `rmux-proto`. Each exchange is exactly one request frame → one response frame per connection (or an upgrade to an attach/control stream for interactive use).

### Frame Structure

```
┌───────────┬──────────────┬─────────┬──────────────────┐
│ magic 0x52│ wire version │  LEB128 │  bincode payload  │
│  (1 byte) │  (LEB128 u32)│  length │  (Request/Response│
└───────────┴──────────────┴─────────┴──────────────────┘
```

- Magic byte `0x52` ('R') identifies RMUX frames and prevents accidental cross-protocol communication with a tmux server.
- The wire version is currently `1` (constant `RMUX_WIRE_VERSION`).
- Payload length is unsigned LEB128 — compact for small frames, no upper-size header field needed.
- The payload is bincode-encoded `Request` or `Response` enum values from `rmux-proto`.

```rust
// crates/rmux-proto/src/envelope.rs
pub const RMUX_FRAME_MAGIC: u8 = 0x52;
pub const RMUX_WIRE_VERSION: u32 = 1;
```

Sources: [crates/rmux-proto/src/envelope.rs:7-11](), [crates/rmux-proto/src/lib.rs:1-55]()

### Surfaces and the Protocol

```text
┌──────────────────────────────────────────────────────────────────┐
│                      CLIENT SURFACES                             │
│                                                                  │
│   rmux CLI          rmux-sdk          ratatui-rmux widget        │
│  (blocking,        (async Tokio,      (SDK facade,               │
│   short-lived)      long-lived)        snapshot reads)           │
└──────────┬──────────────────┬──────────────────────┬─────────────┘
           │  rmux-proto      │  Request/Response     │
           │  frames          │  frames               │
           ▼                  ▼                       ▼
┌──────────────────────────────────────────────────────────────────┐
│              LOCAL TRANSPORT (rmux-ipc)                          │
│     Unix socket (Linux/macOS)  │  Named Pipe (Windows)           │
└─────────────────────────────────────────────────────────────────┬┘
                                                                  │
                                                                  ▼
┌──────────────────────────────────────────────────────────────────┐
│                    rmux-server (daemon)                          │
│                                                                  │
│   listener::serve() accept loop                                  │
│   → per-connection Tokio task                                    │
│   → RequestHandler (session/window/pane state)                   │
└──────────────────────────────────────────────────────────────────┘
```

The `rmux-client` crate uses a **blocking** local stream (read timeout: 15 s, write timeout: 5 s) because the CLI is synchronous. The `rmux-sdk` uses an **async** Tokio transport for SDK programs that manage sessions programmatically. Both send the same `rmux-proto` frame format to the same daemon.

Sources: [crates/rmux-client/src/connection.rs:1-35](), [crates/rmux-client/src/lib.rs:1-20]()

---

## The Daemon Accept Loop

Inside the daemon, `listener::serve` runs an async accept loop that spawns a Tokio task per incoming connection:

```rust
// crates/rmux-server/src/listener.rs
pub(crate) async fn serve(
    mut listener: LocalListener,
    socket_path: PathBuf,
    shutdown_handle: ShutdownHandle,
    mut shutdown: oneshot::Receiver<()>,
    options: ServeOptions,
) -> io::Result<()> {
    // ...
    loop {
        tokio::select! {
            result = listener.accept() => { /* spawn per-connection task */ }
            _ = &mut shutdown => { /* begin graceful shutdown */ }
        }
    }
}
```

All session, window, and pane state is owned by the `RequestHandler` behind an `Arc`. Each connection task gets a clone of that `Arc` and can read or mutate shared state without copying it. There is no inter-daemon communication — this single `Arc` is the only state store.

Sources: [crates/rmux-server/src/listener.rs:22-60](), [crates/rmux-server/src/daemon.rs:170-200]()

---

## Daemon Lifecycle State Machine

```text
                  ┌───────────────┐
     first CLI    │               │
     command      │   not running │
  ───────────────►│  (no socket)  │
                  └──────┬────────┘
                         │ auto_start spawns
                         │ rmux --__internal-daemon
                         ▼
                  ┌───────────────┐
                  │   starting    │──── flock held by startup winner
                  │   (polling)   │     losers poll socket
                  └──────┬────────┘
                         │ socket becomes connectable
                         ▼
                  ┌───────────────┐
                  │    running    │◄──── all subsequent clients
                  │  (accepting)  │      connect directly
                  └──────┬────────┘
                         │ kill-server / SIGTERM / last client exits
                         ▼
                  ┌───────────────┐
                  │  shutting     │──── socket cleanup, pane teardown
                  │   down        │
                  └──────┬────────┘
                         │
                         ▼
                  ┌───────────────┐
                  │  not running  │
                  │  (no socket)  │
                  └───────────────┘
```

Sources: [crates/rmux-client/src/auto_start.rs:130-183](), [crates/rmux-server/src/daemon.rs:260-290]()

---

## Crate Dependency Direction

The crate graph enforces the "surfaces talk to daemon" contract at compile time:

```text
rmux (binary) ─── rmux-client ─── rmux-ipc ─── rmux-proto ─── rmux-types
     │                                                │
     └── rmux-server ─── rmux-core                   └── rmux-os
              └── rmux-pty

rmux-sdk ──────── rmux-ipc ──── rmux-proto
     │
     └── ratatui-rmux
```

Key boundary: `rmux-sdk` must **not** depend on `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies. The SDK is a peer of the client crate, not a layer above it. This means third-party code using `rmux-sdk` gets only the public typed protocol, never internals.

Sources: [crates/rmux-sdk/src/lib.rs:1-25](), [Cargo.toml:26-48]()

---

## Failure Modes

| Failure | Observable symptom | Root cause |
|---|---|---|
| Daemon won't start | `failed to launch hidden rmux daemon '...': ...` | Binary path resolution failed, or spawn syscall failed |
| Socket never appears | `timed out after 5s waiting for rmux server socket '...'` | Daemon crashed immediately after spawn (check stderr if possible) |
| Stale socket (daemon died) | `AutoStartError::Client(Io(...))` on connect | Previous daemon left socket file; auto-start cleans it before spawning |
| Two daemons on Unix | Impossible | flock on startup lock file serializes all launchers |
| Two daemons on Windows | `AddrInUse` with helpful message | Named mutex + pipe-responds probe detects live incumbent |
| Wrong session visible | Client connected to wrong endpoint | `-L`/`-S`/`$RMUX` mismatch; endpoint resolution is deterministic but caller-controlled |

Sources: [crates/rmux-client/src/auto_start.rs:215-270](), [crates/rmux-server/src/daemon.rs:215-260]()

---

## What Would Break If the Design Changed

- **Multiple daemons per user:** Session state would be silently partitioned. CLI commands and SDK calls would see different sessions depending on which daemon they happened to connect to.
- **Daemon embedded in the CLI binary:** The re-exec path in `src/main.rs` already achieves zero-install daemon startup. Splitting into a separate binary would require users to install two artifacts and keep versions in sync.
- **State in the client:** Clients are intentionally stateless so they can crash or be killed without losing session state. Moving state to the client would couple session lifetime to client process lifetime — the original problem RMUX solves.
- **Changing `INTERNAL_DAEMON_FLAG`:** Both `src/main.rs` and `crates/rmux-client/src/auto_start.rs` share the constant. If they diverged, auto-start would spawn daemons that immediately returned to CLI mode, producing a spin-and-timeout failure on every first command.

The daemon architecture is the load-bearing invariant: `crates/rmux-server/src/daemon.rs` owns the `ServerDaemon` and `ServerHandle` types that make the single-socket, single-state contract possible, and `crates/rmux-client/src/auto_start.rs` ensures clients never need to know whether they are the first caller or the thousandth.
