Agent-readable wiki
RMUX Mental Model Wiki
RMUX is a universal Rust terminal multiplexer for the agentic era: a tmux-compatible CLI and daemon that lets humans and AI agents detach, inspect, script, and orchestrate persistent terminal sessions on Linux, macOS, and Windows — no WSL required. What makes it worth studying is its layered crate architecture, which separates pure domain model from IPC transport, PTY management, and public SDK into independently testable boundaries.
Pages
- The Mental Model: One Daemon, Many SurfacesRMUX 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.
- Daemon Lifecycle: Start, Attach, and Re-ExecWhen a tmux-style command (e.g. `new-session -d`) finds no running daemon, `rmux-client`'s `auto_start` module re-execs the current binary with the hidden flag `--__internal-daemon` plus an optional socket path and config-forwarding flags. `src/main.rs` detects this flag and branches into `run_hidden_daemon`, which builds a `DaemonConfig`, binds a `ServerDaemon`, and blocks on `server.wait()`. On Unix this runs a single-thread Tokio runtime; on Windows it uses a multi-thread runtime with a floor of 4 workers. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`; the `$RMUX` env var lets nested clients detect they are already inside a session and short-circuit.
- Crate Map: Dependency Direction & Ownership RulesRMUX is a Cargo workspace of 12 crates arranged in strict layers. Foundation layer (no OS/network deps): `rmux-types` (TerminalSize newtype), `rmux-proto` (request/response frame contracts, codec, identity newtypes), `rmux-core` (pure in-memory domain model — sessions, windows, panes, layout, grid, scrollback). OS integration layer: `rmux-os` (host, signals, process, terminal helpers), `rmux-ipc` (socket/pipe endpoint naming and byte transport only — deliberately carries no protocol), `rmux-pty` (PTY allocation and child-process management across Linux/macOS/Windows ConPTY). Runtime layer: `rmux-server` (Tokio async daemon, owns pane I/O and all handlers), `rmux-client` (blocking RPC client, auto-start logic, attach mode). Public surface layer: `rmux-sdk` (async facade, must NOT depend on rmux-client/rmux-core/rmux-server/rmux-pty), `rmux-render-core` (pure-data snapshot projection, wasm32-compatible), `ratatui-rmux` (PaneDriver + PaneState + PaneWidget, depends only on rmux-sdk). Root binary `rmux` links rmux-server and Tokio directly only to host the hidden daemon re-exec path.
- IPC Transport & Protocol Framing`rmux-ipc` owns endpoint naming and raw byte transport only (Unix sockets on Linux/macOS, Named Pipes on Windows with a per-user mutex) — it knows nothing about request shapes. `rmux-proto` owns the typed frame contracts: `Request` and `Response` enums covering all 90 tmux-compatible commands plus SDK-only extensions (session leases, pane output subscriptions, snapshot refs, SDK wait IDs). The codec layer in `rmux-proto::codec` length-frames and encodes requests; `rmux-client::connection` drives this over the blocking stream from `rmux-ipc`. The key boundary invariant: `rmux-ipc` must remain protocol-agnostic so it can be swapped or extended without touching the request/response contracts in `rmux-proto`.
- Core Domain Model: Sessions, Panes, Grid, and Scrollback`rmux-core` is the pure in-memory domain: no OS calls, no Tokio, no network. It models the full tmux object graph — sessions own windows, windows own panes, panes own a terminal grid plus scrollback. The grid module handles VT/ANSI state (cursor, SGR attributes, alternate screen, scroll regions, wide/combining characters). Parser traces under `crates/rmux-core/tests/parser_traces/` are golden-file tests that pin exact VT sequence handling (e.g. `alternate_screen_roundtrip.trace`, `unicode_wide_cjk.trace`, `sgr_256_and_rgb.trace`). The `command_parser` module translates tmux-syntax command strings; `formats` implements tmux-format string evaluation; `hooks` tracks registered hook callbacks. Safe-change rule: any logic that belongs in this crate must be pure and deterministic — if it requires I/O, it belongs in `rmux-server` or `rmux-os`.
- Safe-Change Guide: SDK, Ratatui Integration & Failure ModesThe public SDK boundary (`rmux-sdk`) is intentionally isolated: it must never import `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies. It re-exports identity newtypes from `rmux-proto` so callers never import internal crates. `ratatui-rmux` goes one step further — three building blocks only: `PaneDriver` (async I/O and state mutation, reaches RMUX only through `rmux-sdk`), `PaneState` (deterministic sync projection, no I/O), `PaneWidget` (sync ratatui widget, no clock/time deps). `rmux-render-core` is wasm32-compatible for the same reason. Key failure modes to anticipate: (1) daemon not running — `auto_start` re-execs but the socket write race can fail if the binary path is wrong (override via `RMUX_INTERNAL_BINARY_PATH`); (2) stale Unix socket — `rmux-server` detects and removes it on bind; (3) nested session confusion — `$RMUX` env var guards against re-attaching inside an existing session; (4) Windows ConPTY path — requires at least Windows 10 1903; older builds fall back to an error, not a silent hang.
Complete Markdown
# RMUX Mental Model Wiki
> RMUX is a universal Rust terminal multiplexer for the agentic era: a tmux-compatible CLI and daemon that lets humans and AI agents detach, inspect, script, and orchestrate persistent terminal sessions on Linux, macOS, and Windows — no WSL required. What makes it worth studying is its layered crate architecture, which separates pure domain model from IPC transport, PTY management, and public SDK into independently testable boundaries.
## Context Links
- [Agent index](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms.txt)
- [Human interactive wiki](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181)
- [GitHub repository](https://github.com/Helvesec/rmux)
## Repository Metadata
- Repository: Helvesec/rmux
- Generated: 2026-05-21T22:00:19.430Z
- Updated: 2026-05-21T22:02:46.369Z
- Runtime: Claude Code
- Format: Mental Model
- Pages: 6
## Page Index
- 01. [The Mental Model: One Daemon, Many Surfaces](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/01-the-mental-model-one-daemon-many-surfaces.md) - 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.
- 02. [Daemon Lifecycle: Start, Attach, and Re-Exec](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/02-daemon-lifecycle-start-attach-and-re-exec.md) - When a tmux-style command (e.g. `new-session -d`) finds no running daemon, `rmux-client`'s `auto_start` module re-execs the current binary with the hidden flag `--__internal-daemon` plus an optional socket path and config-forwarding flags. `src/main.rs` detects this flag and branches into `run_hidden_daemon`, which builds a `DaemonConfig`, binds a `ServerDaemon`, and blocks on `server.wait()`. On Unix this runs a single-thread Tokio runtime; on Windows it uses a multi-thread runtime with a floor of 4 workers. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`; the `$RMUX` env var lets nested clients detect they are already inside a session and short-circuit.
- 03. [Crate Map: Dependency Direction & Ownership Rules](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/03-crate-map-dependency-direction-ownership-rules.md) - RMUX is a Cargo workspace of 12 crates arranged in strict layers. Foundation layer (no OS/network deps): `rmux-types` (TerminalSize newtype), `rmux-proto` (request/response frame contracts, codec, identity newtypes), `rmux-core` (pure in-memory domain model — sessions, windows, panes, layout, grid, scrollback). OS integration layer: `rmux-os` (host, signals, process, terminal helpers), `rmux-ipc` (socket/pipe endpoint naming and byte transport only — deliberately carries no protocol), `rmux-pty` (PTY allocation and child-process management across Linux/macOS/Windows ConPTY). Runtime layer: `rmux-server` (Tokio async daemon, owns pane I/O and all handlers), `rmux-client` (blocking RPC client, auto-start logic, attach mode). Public surface layer: `rmux-sdk` (async facade, must NOT depend on rmux-client/rmux-core/rmux-server/rmux-pty), `rmux-render-core` (pure-data snapshot projection, wasm32-compatible), `ratatui-rmux` (PaneDriver + PaneState + PaneWidget, depends only on rmux-sdk). Root binary `rmux` links rmux-server and Tokio directly only to host the hidden daemon re-exec path.
- 04. [IPC Transport & Protocol Framing](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/04-ipc-transport-protocol-framing.md) - `rmux-ipc` owns endpoint naming and raw byte transport only (Unix sockets on Linux/macOS, Named Pipes on Windows with a per-user mutex) — it knows nothing about request shapes. `rmux-proto` owns the typed frame contracts: `Request` and `Response` enums covering all 90 tmux-compatible commands plus SDK-only extensions (session leases, pane output subscriptions, snapshot refs, SDK wait IDs). The codec layer in `rmux-proto::codec` length-frames and encodes requests; `rmux-client::connection` drives this over the blocking stream from `rmux-ipc`. The key boundary invariant: `rmux-ipc` must remain protocol-agnostic so it can be swapped or extended without touching the request/response contracts in `rmux-proto`.
- 05. [Core Domain Model: Sessions, Panes, Grid, and Scrollback](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/05-core-domain-model-sessions-panes-grid-and-scrollback.md) - `rmux-core` is the pure in-memory domain: no OS calls, no Tokio, no network. It models the full tmux object graph — sessions own windows, windows own panes, panes own a terminal grid plus scrollback. The grid module handles VT/ANSI state (cursor, SGR attributes, alternate screen, scroll regions, wide/combining characters). Parser traces under `crates/rmux-core/tests/parser_traces/` are golden-file tests that pin exact VT sequence handling (e.g. `alternate_screen_roundtrip.trace`, `unicode_wide_cjk.trace`, `sgr_256_and_rgb.trace`). The `command_parser` module translates tmux-syntax command strings; `formats` implements tmux-format string evaluation; `hooks` tracks registered hook callbacks. Safe-change rule: any logic that belongs in this crate must be pure and deterministic — if it requires I/O, it belongs in `rmux-server` or `rmux-os`.
- 06. [Safe-Change Guide: SDK, Ratatui Integration & Failure Modes](https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/06-safe-change-guide-sdk-ratatui-integration-failure-modes.md) - The public SDK boundary (`rmux-sdk`) is intentionally isolated: it must never import `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies. It re-exports identity newtypes from `rmux-proto` so callers never import internal crates. `ratatui-rmux` goes one step further — three building blocks only: `PaneDriver` (async I/O and state mutation, reaches RMUX only through `rmux-sdk`), `PaneState` (deterministic sync projection, no I/O), `PaneWidget` (sync ratatui widget, no clock/time deps). `rmux-render-core` is wasm32-compatible for the same reason. Key failure modes to anticipate: (1) daemon not running — `auto_start` re-execs but the socket write race can fail if the binary path is wrong (override via `RMUX_INTERNAL_BINARY_PATH`); (2) stale Unix socket — `rmux-server` detects and removes it on bind; (3) nested session confusion — `$RMUX` env var guards against re-attaching inside an existing session; (4) Windows ConPTY path — requires at least Windows 10 1903; older builds fall back to an error, not a silent hang.
## Source File Index
- `Cargo.toml`
- `crates/ratatui-rmux/src/lib.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-client/src/connection.rs`
- `crates/rmux-client/src/nested.rs`
- `crates/rmux-core/src/keys.rs`
- `crates/rmux-core/src/lib.rs`
- `crates/rmux-core/tests/parser_traces.rs`
- `crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace`
- `crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace`
- `crates/rmux-ipc/src/endpoint.rs`
- `crates/rmux-ipc/src/lib.rs`
- `crates/rmux-ipc/src/stream.rs`
- `crates/rmux-proto/src/lib.rs`
- `crates/rmux-proto/src/request.rs`
- `crates/rmux-pty/src/backend/windows/mod.rs`
- `crates/rmux-pty/src/lib.rs`
- `crates/rmux-render-core/src/lib.rs`
- `crates/rmux-sdk/src/lib.rs`
- `crates/rmux-server/src/daemon.rs`
- `crates/rmux-types/src/lib.rs`
- `README.md`
- `src/main.rs`
---
## 01. 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.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/01-the-mental-model-one-daemon-many-surfaces.md
- Generated: 2026-05-21T21:58:00.674Z
### 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.
---
## 02. Daemon Lifecycle: Start, Attach, and Re-Exec
> When a tmux-style command (e.g. `new-session -d`) finds no running daemon, `rmux-client`'s `auto_start` module re-execs the current binary with the hidden flag `--__internal-daemon` plus an optional socket path and config-forwarding flags. `src/main.rs` detects this flag and branches into `run_hidden_daemon`, which builds a `DaemonConfig`, binds a `ServerDaemon`, and blocks on `server.wait()`. On Unix this runs a single-thread Tokio runtime; on Windows it uses a multi-thread runtime with a floor of 4 workers. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`; the `$RMUX` env var lets nested clients detect they are already inside a session and short-circuit.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/02-daemon-lifecycle-start-attach-and-re-exec.md
- Generated: 2026-05-21T21:57:38.462Z
### Source Files
- `src/main.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-server/src/daemon.rs`
- `crates/rmux-ipc/src/endpoint.rs`
- `crates/rmux-client/src/nested.rs`
<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-server/src/daemon.rs](crates/rmux-server/src/daemon.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [crates/rmux-client/src/nested.rs](crates/rmux-client/src/nested.rs)
- [crates/rmux-server/src/unix_socket.rs](crates/rmux-server/src/unix_socket.rs)
- [crates/rmux-server/src/signals.rs](crates/rmux-server/src/signals.rs)
- [crates/rmux-sdk/src/bootstrap/startup_unix.rs](crates/rmux-sdk/src/bootstrap/startup_unix.rs)
</details>
# Daemon Lifecycle: Start, Attach, and Re-Exec
`rmux` ships as a single binary with two personalities: the public client CLI and a hidden long-running daemon. When a tmux-style command such as `new-session -d` needs a running server and finds none, the client re-executes itself with the hidden flag `--__internal-daemon`. `src/main.rs` detects this flag at startup and branches into the daemon path instead of the normal CLI path, eliminating the need for a separate server binary.
This page explains how the two personalities co-exist in one binary, how `auto_start` decides to spawn the daemon, what arguments are forwarded across the re-exec boundary, how the Tokio runtime is configured per platform, how the socket path is resolved, and how the `$RMUX` environment variable lets nested clients short-circuit the whole flow.
---
## The Single-Binary Dual-Entry Design
`src/main.rs` owns both entry points. The routing decision in `try_main` is a single `match` on the second command-line argument:
```rust
// src/main.rs:65-75
match args.get(1) {
Some(argument) if argument == INTERNAL_DAEMON_FLAG => {
let internal = parse_internal_daemon_args(args.into_iter().skip(2))
.map_err(|error| cli::ExitFailure::new(1, error))?;
run_hidden_daemon(internal)
...
}
_ => cli::run(args),
}
```
`INTERNAL_DAEMON_FLAG` is the string `"--__internal-daemon"`, defined in `rmux-client` and re-exported so both sides of the protocol share the exact constant:
```rust
// crates/rmux-client/src/auto_start.rs:42
pub const INTERNAL_DAEMON_FLAG: &str = "--__internal-daemon";
```
Sources: [src/main.rs:65-75](), [crates/rmux-client/src/auto_start.rs:42]()
---
## When Auto-Start Triggers
`ensure_server_running` (and its config-forwarding variant `ensure_server_running_with_config`) is explicitly reserved for commands that map to tmux's `CMD_STARTSERVER` inventory — commands like `new-session -d`, `start-server`, and `new-window`. Other commands use `connect` or `connect_or_absent` directly, so they never spawn a daemon as a side effect.
```rust
// crates/rmux-client/src/auto_start.rs:130-132
pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
}
```
Sources: [crates/rmux-client/src/auto_start.rs:124-132]()
---
## The Re-Exec Protocol
### Spawning the Hidden Daemon
`spawn_hidden_daemon_for` builds the child `Command`, then immediately drops the `Child` handle without waiting, so the daemon outlives the short-lived client process:
```rust
// crates/rmux-client/src/auto_start.rs:557-597
fn spawn_hidden_daemon_for(binary_path, socket_path, config) -> io::Result<()> {
let command = hidden_daemon_command(binary_path, socket_path, config, true);
match spawn_hidden_daemon(command) {
Ok(()) => Ok(()),
Err(error) if should_retry_hidden_daemon_without_breakaway(&error) => {
let command = hidden_daemon_command(binary_path, socket_path, config, false);
spawn_hidden_daemon(command)
}
Err(error) => Err(error),
}
}
fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
let child = command.spawn()?;
drop(child); // intentional: daemon must outlive the client
Ok(())
}
```
The child command is always constructed as:
```
rmux --__internal-daemon <socket_path> [--config-default | --config-file <path>]... [--config-quiet] [--config-cwd <path>]
```
stdin, stdout, and stderr are all redirected to `/dev/null` (`Stdio::null()`).
On Windows, if the initial spawn fails with a specific error, `rmux_os::daemon::should_retry_hidden_daemon_without_breakaway` triggers a retry without job-object breakaway — this is a Windows process group inheritance workaround.
Sources: [crates/rmux-client/src/auto_start.rs:557-597]()
### Binary Path Resolution
The re-exec target is always `env::current_exe()`. In debug builds with the `RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE=1` environment variable set, the `RMUX_INTERNAL_BINARY_PATH` env var overrides the path — this is a test-only escape hatch:
```rust
// crates/rmux-client/src/auto_start.rs:599-610
fn rmux_binary_path() -> io::Result<PathBuf> {
let current_exe = env::current_exe()?;
match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
Some(path) => Ok(PathBuf::from(path)),
None => Ok(current_exe),
}
}
```
Sources: [crates/rmux-client/src/auto_start.rs:599-610]()
### Argument Parsing in the Daemon
`parse_internal_daemon_args` in `src/main.rs` is a simple hand-written parser (no clap). The first non-`--` argument is treated as the socket path. Remaining `--` arguments carry config-forwarding flags:
| Flag | Effect |
|---|---|
| `<path>` (positional) | Explicit socket path for this daemon instance |
| `--config-default` | Load rmux's default startup config search path |
| `--config-file <path>` | Load this explicit config file (may repeat) |
| `--config-quiet` | Suppress "file not found" warnings |
| `--config-cwd <path>` | Working directory for relative config paths |
`--config-default` and `--config-file` are mutually exclusive; duplicate `--config-default` also errors out. These flags are constructed by `AutoStartConfig::append_hidden_daemon_args` on the client side.
Sources: [src/main.rs:94-183](), [crates/rmux-client/src/auto_start.rs:91-110]()
---
## Running the Daemon: `run_hidden_daemon`
Once `parse_internal_daemon_args` succeeds, `run_hidden_daemon` builds a `DaemonConfig`, selects the Tokio runtime, and blocks:
```rust
// src/main.rs:185-211
fn run_hidden_daemon(args: InternalDaemonArgs) -> io::Result<()> {
let mut config = match args.socket_path {
Some(socket_path) => DaemonConfig::new(socket_path),
None => DaemonConfig::with_default_socket_path()?,
};
config = match args.config_selection {
ServerConfigFileSelection::Disabled => config,
ServerConfigFileSelection::Default => {
config.with_default_config_load(args.config_quiet, args.config_cwd)
}
ServerConfigFileSelection::Files(files) => {
config.with_config_files(files, args.config_quiet, args.config_cwd)
}
};
#[cfg(unix)]
let runtime = Builder::new_current_thread().enable_all().build()?;
#[cfg(windows)]
let runtime = Builder::new_multi_thread()
.worker_threads(hidden_daemon_worker_threads())
.enable_all()
.build()?;
runtime.block_on(async move {
let server = ServerDaemon::new(config).bind().await?;
server.wait().await
})
}
```
Sources: [src/main.rs:185-211]()
### Platform Runtime Differences
| Platform | Runtime type | Thread count |
|---|---|---|
| Unix | `current_thread` (single-threaded) | 1 |
| Windows | `multi_thread` | `max(available_parallelism, 4)` |
The Windows floor of 4 workers is enforced by:
```rust
// src/main.rs:214-219
fn hidden_daemon_worker_threads() -> usize {
std::thread::available_parallelism()
.map(usize::from)
.unwrap_or(4)
.max(4)
}
```
Sources: [src/main.rs:199-219]()
---
## Daemon Bind and Shutdown Lifecycle
`ServerDaemon::bind` creates the IPC endpoint, wires up shutdown signaling, and spawns the accept loop as a Tokio task:
```rust
// crates/rmux-server/src/daemon.rs:232-293
pub async fn bind(self) -> io::Result<ServerHandle> {
// Unix path:
let bound_listener = bind_unix_listener_at(self.config.socket_path())?;
let (shutdown_handle, shutdown_receiver) = ShutdownHandle::new();
let (server_signal_tx, server_signal_rx) = tokio::sync::mpsc::unbounded_channel();
let signal_watcher = SignalWatcher::install(shutdown_handle.clone(), server_signal_tx)?;
let task = tokio::spawn(listener::serve(...));
Ok(ServerHandle { socket_path, shutdown_handle, task, signal_watcher })
}
```
`ServerHandle::wait` (called in `run_hidden_daemon`) simply awaits the `JoinHandle`. The daemon stays alive until the task completes — which only happens after a shutdown signal fires.
Sources: [crates/rmux-server/src/daemon.rs:232-293](), [crates/rmux-server/src/daemon.rs:384-390]()
### Signal Handling (Unix)
`SignalWatcher` runs a dedicated OS thread (not a Tokio task) that blocks on `Signals::forever()`. Signal disposition:
| Signal | Action |
|---|---|
| `SIGINT`, `SIGTERM` | `shutdown_handle.request_shutdown()` → daemon exits |
| `SIGCHLD` | Sends `ServerSignal::ChildChanged` to server |
| `SIGUSR1` | Sends `ServerSignal::RecreateSocket` to server |
| `SIGHUP`, `SIGQUIT`, `SIGUSR2` | Logged and ignored |
Dropping `SignalWatcher` closes the signal iterator handle and joins the thread.
Sources: [crates/rmux-server/src/signals.rs:19-94]()
### Shutdown Propagation
`ShutdownHandle` wraps a `oneshot::Sender<()>` behind `Arc<Mutex<Option<...>>>` so any clone can fire shutdown exactly once:
```rust
// crates/rmux-server/src/daemon.rs:207-221
pub(crate) fn request_shutdown(&self) {
if let Some(sender) = self.sender.lock().expect("shutdown sender").take() {
let _ = sender.send(());
}
}
```
`Drop for ServerHandle` calls `request_shutdown` automatically, so tests or out-of-band drops cannot leave a zombie server.
Sources: [crates/rmux-server/src/daemon.rs:207-221](), [crates/rmux-server/src/daemon.rs:412-416]()
---
## Socket Path Resolution
### Default Path Computation
When no socket path is supplied to the daemon, `DaemonConfig::with_default_socket_path` calls `default_socket_path`, which calls `rmux_ipc::default_endpoint`:
```rust
// crates/rmux-server/src/daemon.rs:46-47
pub fn default_socket_path() -> io::Result<PathBuf> {
rmux_ipc::default_endpoint().map(LocalEndpoint::into_path)
}
```
On Unix, the endpoint is computed as:
```
<root>/rmux-<uid>/default
```
Where `<root>` is `$RMUX_TMPDIR` if set and resolvable, falling back to `/tmp`:
```rust
// crates/rmux-ipc/src/endpoint.rs:81-84
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)
}
```
On Windows, the endpoint is a named pipe:
```
\\.\pipe\rmux-<SID>-il-<integrity>-<label>
```
Sources: [crates/rmux-ipc/src/endpoint.rs:71-114](), [crates/rmux-server/src/daemon.rs:89-91]()
### Socket Ownership Validation
On Unix, `bind_unix_listener_at` in `rmux-server` prepares the socket path, creates the parent directory with mode `0o700` if needed, removes a stale socket, and enforces `0o600` permissions on the bound socket file. Symlinks on any path component are rejected.
On Windows, if `LocalListener::bind` fails and a protocol probe succeeds on the existing pipe, the error is promoted to `AddrInUse` with a human-readable message.
Sources: [crates/rmux-server/src/unix_socket.rs:25-45](), [crates/rmux-server/src/daemon.rs:297-323]()
---
## Nested-Session Detection via `$RMUX`
The `$RMUX` environment variable is the mechanism clients use to detect they are already running inside an rmux session. This allows commands like `switch-client` to require a nested context, and prevents spurious daemon spawning from nested shells.
```rust
// crates/rmux-client/src/nested.rs:40-66
pub fn detect_context() -> ClientContext {
detect_context_from_env(std::env::var_os("RMUX").as_deref())
}
fn detect_context_from_env(tmux_value: Option<&OsStr>) -> ClientContext {
match tmux_value {
Some(value) if !value.is_empty() => ClientContext::Nested,
_ => ClientContext::Outside,
}
}
```
Any non-empty `$RMUX` value means the client is nested. The variable is typically set by the server when it spawns a shell inside a pane, with the format `<socket_path>,<session_id>,<client_id>` (e.g. `/tmp/rmux-1000/default,12345,0`) — but the detection logic only checks emptiness, so the format is not validated client-side.
`$RMUX` is also read by `resolve_endpoint` in `rmux-ipc`: if no `-L` or `-S` flag is given, `socket_path_from_rmux_env` parses the path component before the first comma and uses it as the endpoint, provided it is in an `rmux-`-owned directory:
```rust
// crates/rmux-ipc/src/endpoint.rs:176-189
fn socket_path_from_rmux_env(rmux: Option<&OsStr>) -> Option<PathBuf> {
let end = bytes.iter().position(|byte| *byte == b',').unwrap_or(bytes.len());
let path = path_buf_from_bytes(bytes[..end].to_vec());
socket_path_is_rmux_owned(&path).then_some(path)
}
```
Sources: [crates/rmux-client/src/nested.rs:40-66](), [crates/rmux-ipc/src/endpoint.rs:117-135](), [crates/rmux-ipc/src/endpoint.rs:176-189]()
---
## Startup Race Serialization (Unix)
The Unix bootstrap layer in `rmux-sdk` prevents multiple concurrent clients from each trying to start their own daemon. `connect_or_start_with` acquires a per-endpoint `flock` lock before running the launcher; losers of the race either connect to the daemon the winner created, or block until it is ready:
- Default startup deadline: **5 seconds** (`DEFAULT_STARTUP_DEADLINE`)
- Default poll interval: **25 ms** (`STARTUP_POLL_INTERVAL`)
On platforms without the `unix` or `windows` feature gates (i.e. other targets), `auto_start` falls back to a simple polling loop with a 5-second timeout and 50 ms interval.
Sources: [crates/rmux-sdk/src/bootstrap/startup_unix.rs:44-53](), [crates/rmux-client/src/auto_start.rs:33-37]()
---
## End-to-End Sequence
```mermaid
sequenceDiagram
participant CLI as rmux (client invocation)
participant AutoStart as auto_start::ensure_server_running
participant Bootstrap as rmux-sdk bootstrap (connect_or_start_with)
participant Daemon as rmux --__internal-daemon (re-exec'd child)
participant Socket as Unix socket / Windows named pipe
CLI->>AutoStart: CMD_STARTSERVER command
AutoStart->>Bootstrap: connect_or_start_with(socket_path, launcher, deadline, poll)
Bootstrap->>Socket: try connect
Socket-->>Bootstrap: not found (Absent)
Bootstrap->>Bootstrap: acquire startup flock (Unix)
Bootstrap->>Daemon: spawn_hidden_daemon_for() → exec self --__internal-daemon <path>
Bootstrap->>Bootstrap: drop Child handle (daemon lives on)
Bootstrap->>Socket: poll every 25ms (up to 5s)
Daemon->>Socket: bind(), accept loop ready
Socket-->>Bootstrap: connected
Bootstrap-->>AutoStart: StartupOutcome::Started(stream)
AutoStart->>AutoStart: probe_server_readiness() (list-sessions ping)
AutoStart-->>CLI: Ok(Connection)
```
Sources: [crates/rmux-client/src/auto_start.rs:161-196](), [crates/rmux-sdk/src/bootstrap/startup_unix.rs:1-18]()
---
## Summary
The daemon lifecycle is a compact re-exec protocol: a single binary detects `--__internal-daemon` as argv[1] and branches into `run_hidden_daemon` instead of the CLI, building a `DaemonConfig` from forwarded flags, selecting a platform-appropriate Tokio runtime (single-thread on Unix, multi-thread with a ≥4 worker floor on Windows), binding the IPC endpoint, and blocking on `server.wait()`. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`. The `$RMUX` environment variable short-circuits all of this for nested clients by letting `detect_context` return `ClientContext::Nested` whenever the variable is non-empty, preventing spurious daemon spawns from within existing sessions. The full lifecycle — from `ensure_server_running` through flock acquisition, spawn, readiness polling, and graceful SIGTERM shutdown — is defined across `crates/rmux-client/src/auto_start.rs`, `crates/rmux-sdk/src/bootstrap/startup_unix.rs`, `crates/rmux-server/src/daemon.rs`, and `src/main.rs`.
---
## 03. Crate Map: Dependency Direction & Ownership Rules
> RMUX is a Cargo workspace of 12 crates arranged in strict layers. Foundation layer (no OS/network deps): `rmux-types` (TerminalSize newtype), `rmux-proto` (request/response frame contracts, codec, identity newtypes), `rmux-core` (pure in-memory domain model — sessions, windows, panes, layout, grid, scrollback). OS integration layer: `rmux-os` (host, signals, process, terminal helpers), `rmux-ipc` (socket/pipe endpoint naming and byte transport only — deliberately carries no protocol), `rmux-pty` (PTY allocation and child-process management across Linux/macOS/Windows ConPTY). Runtime layer: `rmux-server` (Tokio async daemon, owns pane I/O and all handlers), `rmux-client` (blocking RPC client, auto-start logic, attach mode). Public surface layer: `rmux-sdk` (async facade, must NOT depend on rmux-client/rmux-core/rmux-server/rmux-pty), `rmux-render-core` (pure-data snapshot projection, wasm32-compatible), `ratatui-rmux` (PaneDriver + PaneState + PaneWidget, depends only on rmux-sdk). Root binary `rmux` links rmux-server and Tokio directly only to host the hidden daemon re-exec path.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/03-crate-map-dependency-direction-ownership-rules.md
- Generated: 2026-05-21T21:58:09.418Z
### Source Files
- `Cargo.toml`
- `crates/rmux-types/src/lib.rs`
- `crates/rmux-proto/src/lib.rs`
- `crates/rmux-sdk/src/lib.rs`
- `crates/rmux-render-core/src/lib.rs`
- `crates/ratatui-rmux/src/lib.rs`
<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [Cargo.toml](Cargo.toml)
- [crates/rmux-types/Cargo.toml](crates/rmux-types/Cargo.toml)
- [crates/rmux-types/src/lib.rs](crates/rmux-types/src/lib.rs)
- [crates/rmux-proto/Cargo.toml](crates/rmux-proto/Cargo.toml)
- [crates/rmux-proto/src/lib.rs](crates/rmux-proto/src/lib.rs)
- [crates/rmux-core/Cargo.toml](crates/rmux-core/Cargo.toml)
- [crates/rmux-core/src/lib.rs](crates/rmux-core/src/lib.rs)
- [crates/rmux-os/Cargo.toml](crates/rmux-os/Cargo.toml)
- [crates/rmux-ipc/Cargo.toml](crates/rmux-ipc/Cargo.toml)
- [crates/rmux-pty/Cargo.toml](crates/rmux-pty/Cargo.toml)
- [crates/rmux-server/Cargo.toml](crates/rmux-server/Cargo.toml)
- [crates/rmux-client/Cargo.toml](crates/rmux-client/Cargo.toml)
- [crates/rmux-sdk/Cargo.toml](crates/rmux-sdk/Cargo.toml)
- [crates/rmux-sdk/src/lib.rs](crates/rmux-sdk/src/lib.rs)
- [crates/rmux-render-core/Cargo.toml](crates/rmux-render-core/Cargo.toml)
- [crates/rmux-render-core/src/lib.rs](crates/rmux-render-core/src/lib.rs)
- [crates/ratatui-rmux/Cargo.toml](crates/ratatui-rmux/Cargo.toml)
- [crates/ratatui-rmux/src/lib.rs](crates/ratatui-rmux/src/lib.rs)
- [crates/ratatui-rmux/tests/budget.rs](crates/ratatui-rmux/tests/budget.rs)
</details>
# Crate Map: Dependency Direction & Ownership Rules
RMUX is a Cargo workspace of twelve crates arranged in four strict dependency layers. The layers form a directed acyclic graph: every edge points downward, toward more stable, more portable code. No cycle between layers is permitted, and several crates enforce their own budget with a machine-checked test (`crates/ratatui-rmux/tests/budget.rs`) to prevent scope creep.
Understanding the layer structure is essential before touching any crate. Adding a dependency that crosses a layer boundary — for example, giving `rmux-sdk` a reference to `rmux-core` — would collapse the portability and testability guarantees that the whole design rests on. The rules are not informal conventions; they are documented, tested, and encoded in `[dependencies]` tables.
---
## Layer Overview
```text
┌──────────────────────────────────────────────────────────────────────┐
│ PUBLIC SURFACE LAYER │
│ rmux-sdk · rmux-render-core · ratatui-rmux │
└──────────────────────┬───────────────────────────────────────────────┘
│ depends on ↓
┌──────────────────────▼───────────────────────────────────────────────┐
│ RUNTIME LAYER │
│ rmux-server · rmux-client │
└──────────────────────┬───────────────────────────────────────────────┘
│ depends on ↓
┌──────────────────────▼───────────────────────────────────────────────┐
│ OS INTEGRATION LAYER │
│ rmux-os · rmux-ipc · rmux-pty │
└──────────────────────┬───────────────────────────────────────────────┘
│ depends on ↓
┌──────────────────────▼───────────────────────────────────────────────┐
│ FOUNDATION LAYER (no OS/network/async deps) │
│ rmux-types · rmux-proto · rmux-core │
└──────────────────────────────────────────────────────────────────────┘
Binary: rmux ←── links rmux-server + rmux-client + rmux-core + rmux-os + rmux-proto + Tokio
```
---
## Foundation Layer
The foundation layer has no OS, network, process, or async runtime dependencies. It may be compiled as a library, tested in a pure-logic harness, or published independently without carrying platform baggage.
### `rmux-types`
**Owns:** `TerminalSize` — a `(cols: u16, rows: u16)` newtype shared across non-adjacent crates without pulling in heavier shared modules.
**Dependencies:** only `serde` (optional feature gate). No other workspace crate.
```rust
// crates/rmux-types/src/lib.rs:9-14
pub struct TerminalSize {
pub cols: u16,
pub rows: u16,
}
```
`rmux-types` is the leaf crate every other crate can depend on without taking on any weight.
Sources: [crates/rmux-types/src/lib.rs:1-23](), [crates/rmux-types/Cargo.toml:12-17]()
---
### `rmux-proto`
**Owns:** the wire protocol. Concretely: request/response DTOs, the frame codec (`encode_frame`/`decode_frame`), handshake capability negotiation, identity newtypes (`PaneId`, `SessionId`, `SessionName`, `WindowId`), attach-stream message types, control-mode formatting helpers, and the canonical error type (`RmuxError`).
**Dependencies:** `rmux-types` (with `serde` feature) + `bincode` + `serde` + `thiserror`. Nothing higher.
The identity newtypes are intentionally anchored here, not in `rmux-core`, so that `rmux-sdk` (which must not depend on `rmux-core`) can still re-export them to SDK users.
```toml
# crates/rmux-proto/Cargo.toml:13-15
[dependencies]
bincode = "1.3.3"
rmux-types = { path = "../rmux-types", version = "0.2.0", features = ["serde"] }
```
Sources: [crates/rmux-proto/Cargo.toml:12-17](), [crates/rmux-proto/src/lib.rs:1-57]()
---
### `rmux-core`
**Owns:** the pure in-memory domain model — sessions, windows, panes, layout geometry, grid/screen state, scrollback buffers, VT parser, key bindings, hook store, option store, format strings, and target resolution. No OS calls, no network, no async.
**Dependencies:** `rmux-proto` + `chrono` (wall-clock for timestamps, no I/O) + `regex` + `unicode-width`.
`rmux-core` uses `PaneId`, `SessionId`, and `WindowId` from `rmux-proto` rather than defining its own — both layers share one identity vocabulary. The rich domain model (sessions, panes, windows, layout) lives here and is consumed by `rmux-server`.
Sources: [crates/rmux-core/Cargo.toml:12-16](), [crates/rmux-core/src/lib.rs:1-99]()
---
## OS Integration Layer
These crates introduce platform syscalls, async I/O, or process management. They are below the runtime layer and above the foundation layer. They must not depend on `rmux-server`, `rmux-client`, or `rmux-sdk`.
### `rmux-os`
**Owns:** host-detection helpers, signal primitives, terminal-size query, and platform path conventions.
**Dependencies:** `rmux-types` + conditional `libc`/`rustix` on Unix + `windows-sys` on Windows. No other workspace crate.
Sources: [crates/rmux-os/Cargo.toml:12-31]()
---
### `rmux-ipc`
**Owns:** local-socket endpoint naming and byte-level transport. Deliberately carries no protocol knowledge — it knows nothing about frames or DTOs. Protocol framing is assembled by callers (`rmux-server`, `rmux-client`, `rmux-sdk`) on top of the raw byte stream `rmux-ipc` provides.
**Dependencies:** `rmux-os` + `tokio` (`io-util`, `net`, `rt`, `sync`, `time`) + `tracing`. The async runtime enters the workspace at this boundary.
```toml
# crates/rmux-ipc/Cargo.toml:12-14
[dependencies]
rmux-os = { path = "../rmux-os", version = "0.2.0" }
tokio = { version = "1.48.0", features = ["io-util", "net", "rt", "sync", "time"] }
```
Sources: [crates/rmux-ipc/Cargo.toml:12-22]()
---
### `rmux-pty`
**Owns:** PTY allocation, resize, and child-process lifecycle across Linux, macOS, and Windows ConPTY.
**Dependencies:** `rmux-types` + `tracing` + conditional `rmux-os`/`libc`/`rustix` on Unix + `windows-sys` on Windows. No Tokio at the production-dependency level (Tokio appears only in `dev-dependencies` for Windows tests).
Sources: [crates/rmux-pty/Cargo.toml:12-33]()
---
## Runtime Layer
The runtime layer assembles foundation + OS integration into the two execution contexts: the async daemon and the blocking CLI client.
### `rmux-server`
**Owns:** the Tokio async daemon. It is the integration point for every foundation and OS crate. It accepts IPC connections, dispatches request frames to handlers, owns pane I/O loops, manages session/window/pane lifecycle through `rmux-core`, and writes PTY data through `rmux-pty`.
**Dependencies:** `rmux-core`, `rmux-ipc`, `rmux-os`, `rmux-proto`, `rmux-pty` + full Tokio feature set + `tracing`. On Unix: `signal-hook`.
```toml
# crates/rmux-server/Cargo.toml:17-22
rmux-core = { path = "../rmux-core", version = "0.2.0" }
rmux-ipc = { path = "../rmux-ipc", version = "0.2.0" }
rmux-os = { path = "../rmux-os", version = "0.2.0" }
rmux-proto = { path = "../rmux-proto", version = "0.2.0" }
rmux-pty = { path = "../rmux-pty", version = "0.2.0" }
```
Sources: [crates/rmux-server/Cargo.toml:12-27]()
---
### `rmux-client`
**Owns:** the blocking RPC client (tmux-style CLI dispatch), auto-start logic (spawn daemon if not running), and attach mode (raw terminal passthrough).
**Dependencies:** `rmux-core`, `rmux-ipc`, `rmux-os`, `rmux-proto` as common deps; `rmux-sdk` + Tokio + `signal-hook`/`libc`/`rustix` on both Unix and Windows platform targets.
Note: `rmux-client` depends on `rmux-sdk` (via platform-conditional deps) because the attach flow uses the SDK's event model. This is the only upward edge in the whole graph, and it is a compile-time guarded inversion limited to the client's attach path — `rmux-sdk` itself does not depend on `rmux-client`.
Sources: [crates/rmux-client/Cargo.toml:12-28]()
---
## Public Surface Layer
These three crates form the stable, published integration boundary. None of them may depend on `rmux-core`, `rmux-server`, or `rmux-pty` (the internal implementation). `rmux-sdk` is the single gateway.
### `rmux-sdk`
**Owns:** the async facade (`Rmux`, `Session`, `Window`, `Pane` handles), session ensure/lease builders, snapshot types (`PaneSnapshot`, `PaneCell`, `PaneColor`), event streams (`PaneOutputStream`, `PaneRenderStream`), wait helpers, locator API, and key input abstraction.
**Must NOT depend on:** `rmux-client`, `rmux-core`, `rmux-server`, `rmux-pty` — enforced by documented policy in the crate's module-level doc comment.
**Dependencies:** `rmux-ipc` + `rmux-os` + `rmux-proto` + `serde`/`serde_json` + Tokio + optional `crossterm`/`regex`. It reaches the daemon through the same IPC transport as the CLI, but speaks frames directly, bypassing `rmux-client`.
```rust
// crates/rmux-sdk/src/lib.rs:12-17
// `rmux-sdk` is a public integration peer of `rmux-client` and must not
// depend on `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as
// normal dependencies. The authoritative identity newtypes
// ([`SessionName`], [`SessionId`], [`WindowId`], [`PaneId`]) live in
// `rmux-proto` and are re-exported here so SDK users import them through
// `rmux_sdk` without ever depending on those internal crates.
```
Sources: [crates/rmux-sdk/Cargo.toml:12-46](), [crates/rmux-sdk/src/lib.rs:1-136]()
---
### `rmux-render-core`
**Owns:** the pure-data snapshot projection layer — `PaneSnapshot`, `PaneCell`, `PaneColor`, `PaneCursor`, `PaneGlyph`, `PaneState`, and a deterministic ratatui `PaneWidget`. No daemon, IPC, process, filesystem, network, Tokio, or terminal-driver integration.
**Wasm32 compatibility:** the crate is explicitly designed to compile to `wasm32-unknown-unknown`. Its description even lists `wasm` as a category.
**Dependencies:** `ratatui` (aliased as `ratatui-core`, pinned to `=0.29.0`, `default-features = false`) + `serde`. No workspace crate at all.
```rust
// crates/rmux-render-core/src/lib.rs:8-11
// This crate owns no daemon, IPC, process, filesystem, network, Tokio, or
// terminal-driver integration. It contains only captured pane snapshot data and
// deterministic ratatui projection code that can compile for
// `wasm32-unknown-unknown`.
```
Sources: [crates/rmux-render-core/Cargo.toml:12-13](), [crates/rmux-render-core/src/lib.rs:1-25]()
---
### `ratatui-rmux`
**Owns:** three thin building blocks: `PaneDriver` (async I/O and state mutation owner), `PaneState` (sync folded snapshot), and `PaneWidget` (sync ratatui render). All RMUX behavior passes through `rmux-sdk`; this crate never touches `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty`.
**Dependency budget enforced by test:** `crates/ratatui-rmux/tests/budget.rs` asserts exactly two direct production dependencies (`rmux-sdk` + one `ratatui*`), a maximum of five source files, a maximum of 1500 non-blank source lines, and the absence of the four forbidden crates. The test will fail a build that violates these rules.
```rust
// crates/ratatui-rmux/tests/budget.rs:11-12
const REQUIRED_DEPS: &[&str] = &["rmux-sdk"];
const FORBIDDEN_DEPS: &[&str] = &["rmux-client", "rmux-core", "rmux-server", "rmux-pty"];
```
The async/sync split inside the crate is also machine-verified: `widget.rs`, `state.rs`, and `theme.rs` must not contain `async fn`, `.await`, `tokio::`, `spawn(`, `subscribe(`, or any socket/time primitive. `driver.rs` is the sole async surface.
Sources: [crates/ratatui-rmux/Cargo.toml:12-17](), [crates/ratatui-rmux/src/lib.rs:1-75](), [crates/ratatui-rmux/tests/budget.rs:1-217]()
---
## Root Binary (`rmux`)
The root crate at the workspace root is the compiled `rmux` binary. Its Cargo.toml carries a comment that explains the unusual linkage:
```toml
# Cargo.toml:58-60
# The root binary owns the hidden internal daemon mode, so it links the server
# and Tokio runtime directly for that top-level re-exec path.
rmux-server = { path = "crates/rmux-server", version = "0.2.0" }
tokio = { version = "1.48.0", features = ["net", "rt", "rt-multi-thread", "time"] }
```
In normal invocations the binary dispatches through `rmux-client`. For the hidden `--internal-daemon` re-exec path, it invokes the server inline to host the daemon process. This is why the binary is not just a thin wrapper around `rmux-client`.
**Direct dependencies of the root binary:** `rmux-client`, `rmux-core`, `rmux-os`, `rmux-proto`, `rmux-server`, `clap`, `libc`, Tokio.
Sources: [Cargo.toml:51-61]()
---
## Verified Dependency Edge Table
| Crate | Layer | Depends on (workspace crates only) |
|---|---|---|
| `rmux-types` | Foundation | *(none)* |
| `rmux-proto` | Foundation | `rmux-types` |
| `rmux-core` | Foundation | `rmux-proto` |
| `rmux-os` | OS Integration | `rmux-types` |
| `rmux-ipc` | OS Integration | `rmux-os` |
| `rmux-pty` | OS Integration | `rmux-types`; `rmux-os` (Unix only) |
| `rmux-server` | Runtime | `rmux-core`, `rmux-ipc`, `rmux-os`, `rmux-proto`, `rmux-pty` |
| `rmux-client` | Runtime | `rmux-core`, `rmux-ipc`, `rmux-os`, `rmux-proto`; `rmux-sdk` (platform dep) |
| `rmux-sdk` | Public Surface | `rmux-ipc`, `rmux-os`, `rmux-proto` |
| `rmux-render-core` | Public Surface | *(no workspace crate)* |
| `ratatui-rmux` | Public Surface | `rmux-sdk` |
| `rmux` (binary) | Binary | `rmux-client`, `rmux-core`, `rmux-os`, `rmux-proto`, `rmux-server` |
---
## Dependency Direction Diagram
```mermaid
flowchart TD
subgraph Binary
BIN["rmux (bin)"]
end
subgraph Public["Public Surface Layer"]
SDK["rmux-sdk"]
RC["rmux-render-core"]
RAT["ratatui-rmux"]
end
subgraph Runtime["Runtime Layer"]
SRV["rmux-server"]
CLI["rmux-client"]
end
subgraph OS["OS Integration Layer"]
IPC["rmux-ipc"]
PTY["rmux-pty"]
OSL["rmux-os"]
end
subgraph Foundation["Foundation Layer"]
CORE["rmux-core"]
PROTO["rmux-proto"]
TYPES["rmux-types"]
end
BIN --> SRV
BIN --> CLI
BIN --> CORE
BIN --> OSL
BIN --> PROTO
RAT --> SDK
SDK --> IPC
SDK --> OSL
SDK --> PROTO
RC -.->|no workspace dep| RC
SRV --> CORE
SRV --> IPC
SRV --> OSL
SRV --> PROTO
SRV --> PTY
CLI --> CORE
CLI --> IPC
CLI --> OSL
CLI --> PROTO
CLI -->|platform dep| SDK
IPC --> OSL
PTY --> TYPES
PTY -->|unix only| OSL
OSL --> TYPES
CORE --> PROTO
PROTO --> TYPES
```
---
## Key Ownership Rules
**Identity newtypes live in `rmux-proto`, not `rmux-core`.** This is a deliberate placement: `rmux-sdk` cannot depend on `rmux-core`, but it needs `PaneId`, `SessionId`, `WindowId`, and `SessionName`. By anchoring them in `rmux-proto`, both layers can share the same identity vocabulary without coupling.
**`rmux-ipc` carries no protocol.** It provides a raw byte transport only. Framing (`encode_frame`, `decode_frame`) is in `rmux-proto`. This separation means protocol changes require no IPC transport changes, and IPC transport changes (e.g., switching from Unix domain sockets to named pipes) require no protocol changes.
**`rmux-sdk` is the only legal entry point for external code.** The `ratatui-rmux` budget test enforces this mechanically: any `rmux-core`, `rmux-server`, `rmux-pty`, or `rmux-client` dependency in `ratatui-rmux/Cargo.toml` causes a test failure. The same prohibition is documented in the `rmux-sdk` module doc.
**`rmux-render-core` is wasm32-safe.** It has no workspace dependencies at all — only `ratatui` and `serde`. It can be compiled for browser targets or embedded environments. Code that only needs to render a captured snapshot should depend on this crate, not on `rmux-sdk`.
**The root binary is the only entity that links both `rmux-server` and `rmux-client`.** This is the only place in the workspace where the daemon re-exec path (`--internal-daemon`) and the normal CLI dispatch path coexist. All other crates see at most one side of that boundary.
Sources: [Cargo.toml:51-61](), [crates/rmux-sdk/src/lib.rs:12-17](), [crates/ratatui-rmux/tests/budget.rs:11-12](), [crates/rmux-render-core/src/lib.rs:8-11]()
---
## 04. IPC Transport & Protocol Framing
> `rmux-ipc` owns endpoint naming and raw byte transport only (Unix sockets on Linux/macOS, Named Pipes on Windows with a per-user mutex) — it knows nothing about request shapes. `rmux-proto` owns the typed frame contracts: `Request` and `Response` enums covering all 90 tmux-compatible commands plus SDK-only extensions (session leases, pane output subscriptions, snapshot refs, SDK wait IDs). The codec layer in `rmux-proto::codec` length-frames and encodes requests; `rmux-client::connection` drives this over the blocking stream from `rmux-ipc`. The key boundary invariant: `rmux-ipc` must remain protocol-agnostic so it can be swapped or extended without touching the request/response contracts in `rmux-proto`.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/04-ipc-transport-protocol-framing.md
- Generated: 2026-05-21T21:58:07.608Z
### Source Files
- `crates/rmux-ipc/src/lib.rs`
- `crates/rmux-ipc/src/endpoint.rs`
- `crates/rmux-ipc/src/stream.rs`
- `crates/rmux-proto/src/lib.rs`
- `crates/rmux-proto/src/request.rs`
- `crates/rmux-client/src/connection.rs`
<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [crates/rmux-ipc/src/lib.rs](crates/rmux-ipc/src/lib.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [crates/rmux-ipc/src/stream.rs](crates/rmux-ipc/src/stream.rs)
- [crates/rmux-ipc/src/windows_mutex.rs](crates/rmux-ipc/src/windows_mutex.rs)
- [crates/rmux-proto/src/lib.rs](crates/rmux-proto/src/lib.rs)
- [crates/rmux-proto/src/request.rs](crates/rmux-proto/src/request.rs)
- [crates/rmux-proto/src/response.rs](crates/rmux-proto/src/response.rs)
- [crates/rmux-proto/src/codec.rs](crates/rmux-proto/src/codec.rs)
- [crates/rmux-proto/src/envelope.rs](crates/rmux-proto/src/envelope.rs)
- [crates/rmux-client/src/connection.rs](crates/rmux-client/src/connection.rs)
</details>
# IPC Transport & Protocol Framing
The rmux IPC stack is divided into two tightly scoped crates with a strict dependency direction: `rmux-ipc` owns the OS-level transport boundary (endpoint naming and raw byte streams), while `rmux-proto` owns the typed protocol boundary (request/response contracts and wire framing). `rmux-client::connection` is the integration point that drives `rmux-proto`'s codec over the blocking stream vended by `rmux-ipc`.
This separation is the key architectural invariant: `rmux-ipc` knows nothing about request shapes and can be replaced or extended on a new platform without touching a single type in `rmux-proto`. Conversely, the protocol layer never directly opens sockets — it only consumes abstract byte streams.
---
## `rmux-ipc`: Endpoint Naming and Raw Transport
### Responsibility
`rmux-ipc` is documented explicitly as owning "endpoint naming and local transport handles" and transporting bytes only. The crate's top-level doc comment states: *"It deliberately transports bytes only; the RMUX request/response protocol stays in `rmux-proto`."*
Sources: [crates/rmux-ipc/src/lib.rs:1-7]()
### Endpoint Naming
`LocalEndpoint` wraps a `PathBuf` that represents either a Unix domain socket path or a Windows named pipe path. The same struct serves both platforms by keeping separate accessor methods:
| Method | Platform | Returns |
|---|---|---|
| `as_path()` | Unix | `&Path` (socket path) |
| `as_pipe_name()` | Windows | `&OsStr` (named pipe name) |
| `into_path()` | Both | `PathBuf` |
Sources: [crates/rmux-ipc/src/endpoint.rs:37-68]()
**Endpoint resolution priority** (highest to lowest):
1. `-S <path>` explicit socket path
2. `-L <label>` socket label
3. `$RMUX` environment variable
4. Default (label `"default"`)
Sources: [crates/rmux-ipc/src/endpoint.rs:117-135]()
#### Unix Socket Path Format
On Unix, the path is assembled as:
```
<socket_root>/rmux-<uid>/<label>
```
where `<socket_root>` is resolved from `$RMUX_TMPDIR`, falling back to `/tmp`. The UID-scoped directory means no two users share a socket namespace even in the same `/tmp`.
Sources: [crates/rmux-ipc/src/endpoint.rs:102-115]()
#### Windows Named Pipe Format
On Windows, the path follows:
```
\\.\pipe\rmux-<SID>-il-<integrity_level>-<label>
```
The integrity level is read from the running process token via `GetTokenInformation(TokenIntegrityLevel)` and mapped to one of `untrusted | low | medium | high | system`. This prevents a low-integrity process from connecting to a medium-integrity daemon through the named pipe.
Sources: [crates/rmux-ipc/src/endpoint.rs:86-100](), [crates/rmux-ipc/src/endpoint.rs:266-348]()
#### Safety: Endpoint Ownership Validation
Both platforms enforce an `socket_path_is_rmux_owned` check before accepting a path from `$RMUX` or `-S`. On Unix the parent directory name must start with `rmux-`; on Windows the pipe name must start with `\\.\pipe\rmux-`. This prevents a malicious process from redirecting the client to an unrelated socket.
Sources: [crates/rmux-ipc/src/endpoint.rs:191-212]()
### Stream Types
`rmux-ipc` exposes two stream type aliases that are platform-specific:
| Type alias | Unix | Windows |
|---|---|---|
| `LocalStream` | `tokio::net::UnixStream` | Windows async pipe (via `stream_windows.rs`) |
| `BlockingLocalStream` | `std::os::unix::net::UnixStream` | Windows blocking pipe |
`LocalStream` (async) is used by the server runtime; `BlockingLocalStream` (sync) is used by the CLI client. This means the server is async-tokio while the CLI is fully blocking — the codec and connection layers in `rmux-client` are written entirely without async.
Sources: [crates/rmux-ipc/src/stream.rs:40-45]()
### Blocking Connect
`connect_blocking` creates a non-blocking Unix socket for the connect syscall (so it can use a poll-based timeout), then flips it back to blocking mode after the connection is confirmed. On Linux, `SOCK_CLOEXEC | SOCK_NONBLOCK` are set atomically at socket creation; on other Unix platforms they are applied post-creation via `fcntl`.
Sources: [crates/rmux-ipc/src/stream.rs:171-223]()
### Peer Disconnect Detection
`wait_for_peer_close` is an async utility that avoids consuming protocol bytes while detecting a peer disappearing. It uses `recv(PEEK)` to observe a 0-byte readable event and supplements with `poll(POLLRDHUP)` on Linux/FreeBSD/illumos where the flag is available, falling back to `POLLHUP | POLLERR` on other platforms.
Sources: [crates/rmux-ipc/src/stream.rs:48-82](), [crates/rmux-ipc/src/stream.rs:99-129]()
### Windows Named Mutex (Daemon Startup Race)
Windows daemon bootstrap requires a per-endpoint serialization point across processes. `rmux-ipc` provides `acquire_named_mutex` — a safe RAII wrapper around Win32 `CreateMutexExW`. The mutex is created with a DACL (`D:P(A;;0x1F0001;;;{SID})`) that restricts access to the current user's SID, ensuring a peer running under a different identity cannot acquire it. The `NamedMutexGuard` releases via `ReleaseMutex + CloseHandle` in `Drop`.
A critical thread-affinity invariant: Win32 mutexes are owned per-thread. Dropping the guard on a different thread calls `ReleaseMutex` with `ERROR_NOT_OWNER`, leaving the kernel mutex held until the original thread terminates. The SDK bootstrap layer avoids this with a dedicated holder thread.
Sources: [crates/rmux-ipc/src/windows_mutex.rs:1-135](), [crates/rmux-ipc/src/windows_mutex.rs:192-276]()
---
## `rmux-proto`: Typed Frame Contracts
### Responsibility
`rmux-proto` owns all typed request/response contracts plus the codec that serializes them into framed byte sequences. It has `#![forbid(unsafe_code)]` and no I/O dependency — it never opens a socket.
Sources: [crates/rmux-proto/src/lib.rs:1-3]()
### The `Request` Enum
`Request` is a flat enum covering all supported command and RPC variants. It derives `Serialize + Deserialize` via serde (encoded by bincode on the wire). A `command_name()` method returns the stable routing string for each variant:
```rust
// crates/rmux-proto/src/request.rs:97-100
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
NewSession(NewSessionRequest),
// ... 90+ variants
}
```
Sources: [crates/rmux-proto/src/request.rs:94-322]()
**Request categories:**
| Category | Examples |
|---|---|
| Session lifecycle | `NewSession`, `HasSession`, `KillSession`, `RenameSession`, `ListSessions` |
| Window management | `NewWindow`, `KillWindow`, `SelectWindow`, `MoveWindow`, `SwapWindow`, `RotateWindow` |
| Pane management | `SplitWindow`, `KillPane`, `ResizePane`, `SelectPane`, `SwapPane`, `JoinPane` |
| Client operations | `AttachSession`, `DetachClient`, `SwitchClient`, `ListClients` |
| Buffers & clipboard | `SetBuffer`, `PasteBuffer`, `CapturePane`, `LoadBuffer`, `SaveBuffer` |
| Options & hooks | `SetOption`, `ShowOptions`, `SetHook`, `ShowHooks`, `SetEnvironment` |
| Keys & bindings | `SendKeys`, `BindKey`, `UnbindKey`, `ListKeys` |
| SDK extensions | `CreateSessionLease`, `RenewSessionLease`, `SubscribePaneOutput`, `SdkWaitForOutput`, `PaneInput`, `PaneBroadcastInput` |
| Internal/protocol | `Handshake`, `ControlMode`, `ResolveTarget` |
The enum also has "extended" variants (`NewSessionExt`, `AttachSessionExt2`, `SwitchClientExt3`, etc.) that map to the same `command_name` as their base form but carry additional fields introduced in later protocol versions.
Sources: [crates/rmux-proto/src/request.rs:327-441]()
### The `Response` Enum
`Response` is the symmetric counterpart. Every `Request` variant has a matching `*Response` variant. Error results are represented as `Response::Error(RmuxError)` — they are returned in the `Ok` arm so callers can pattern-match them without distinguishing transport failure from application-level failure.
Sources: [crates/rmux-proto/src/response.rs:69-100](), [crates/rmux-client/src/connection.rs:189-197]()
### Codec: Wire Frame Format
The codec module (`rmux-proto::codec`) implements length-prefixed bincode framing. Every frame on the wire has the following layout:
```text
Byte offset Field
─────────────────────────────────────────────────
0 Magic byte: 0x52 ('R')
1..N Wire version: unsigned LEB128 varint
N..N+4 Payload length: u32 little-endian
N+4..end Payload: bincode-serialized T
```
**Magic byte** `RMUX_FRAME_MAGIC = 0x52` identifies the stream as rmux wire protocol (not tmux or any other Unix socket listener).
**Wire version** `RMUX_WIRE_VERSION = 1` is encoded as unsigned LEB128 (up to 5 bytes for a u32). Currently only version 1 is in the supported range `1..=1`.
**Payload length** is a 4-byte little-endian `u32`, capped at `DEFAULT_MAX_FRAME_LENGTH = 1 MiB`.
Sources: [crates/rmux-proto/src/codec.rs:1-44](), [crates/rmux-proto/src/envelope.rs:6-13]()
#### `encode_frame`
```rust
// crates/rmux-proto/src/codec.rs:16-45
pub fn encode_frame<T: Serialize>(value: &T) -> Result<Vec<u8>, RmuxError> {
let payload = bincode::serialize(value)?;
// validates non-empty and <= 1 MiB
let mut frame = Vec::with_capacity(1 + 5 + 4 + payload.len());
frame.push(RMUX_FRAME_MAGIC);
encode_varint_u32(RMUX_WIRE_VERSION, &mut frame);
frame.extend_from_slice(&frame_length.to_le_bytes());
frame.extend_from_slice(&payload);
Ok(frame)
}
```
Sources: [crates/rmux-proto/src/codec.rs:16-45]()
#### `FrameDecoder`: Incremental Streaming Decoder
The blocking connection reads in 8 KiB chunks, so partial frames are common. `FrameDecoder` maintains an internal byte buffer and implements a push-parse loop:
1. `push_bytes(chunk)` appends raw bytes.
2. `next_frame::<T>()` checks if the buffer contains a complete frame by reading the length field without consuming bytes. Returns `Ok(None)` when more data is needed, `Ok(Some(T))` on success, or `Err` on a framing violation.
3. On decode success, `drain(..required)` removes the consumed frame bytes from the buffer, leaving any pipelining bytes intact.
Sources: [crates/rmux-proto/src/codec.rs:107-193]()
---
## `rmux-client::connection`: Integration Point
`Connection` is the struct that bridges `rmux-ipc`'s `BlockingLocalStream` with `rmux-proto`'s `FrameDecoder`. It holds both together and provides the `roundtrip` method as the primary client-facing API.
```rust
// crates/rmux-client/src/connection.rs:99-102
pub struct Connection {
stream: BlockingLocalStream,
decoder: FrameDecoder,
}
```
Sources: [crates/rmux-client/src/connection.rs:97-102]()
### Timeouts
Three separate timeouts are enforced at the `Connection` level, not at the stream level:
| Constant | Value | Applied to |
|---|---|---|
| `SOCKET_CONNECT_TIMEOUT` | 5 s | `connect_blocking` in `rmux-ipc` |
| `SOCKET_WRITE_TIMEOUT` | 5 s | `stream.set_write_timeout` |
| `SOCKET_RESPONSE_TIMEOUT` | 15 s | `stream.set_read_timeout` |
Scripts that may block indefinitely use `roundtrip_without_read_timeout`, which temporarily removes the read timeout for the duration of the call.
Sources: [crates/rmux-client/src/connection.rs:22-28](), [crates/rmux-client/src/connection.rs:178-212]()
### Request/Response Flow
```
roundtrip(request):
1. encode_frame(request) → Vec<u8> [rmux-proto::codec]
2. stream.write_all(&frame) [rmux-ipc BlockingLocalStream]
3. loop:
decoder.next_frame::<Response>() [rmux-proto::codec]
if None: stream.read(&mut buffer) [rmux-ipc BlockingLocalStream]
decoder.push_bytes(...)
if Some(response): return Ok(response)
if Err: return Err(ClientError::Protocol)
```
Sources: [crates/rmux-client/src/connection.rs:194-241]()
### Protocol Upgrade Transitions
After a successful `AttachSession` or `ControlMode` response, the `Connection` can be transitioned into a raw stream mode:
- `into_attach_upgrade` → returns `AttachSessionUpgrade`, which exposes the raw `BlockingLocalStream` plus any bytes the `FrameDecoder` had already buffered past the response frame.
- `into_control_upgrade` → returns `ControlModeUpgrade` with the raw stream.
Both transitions clear the read/write timeouts (set to `None`) before handing off the stream, since attach and control-mode traffic is not bounded by the 15-second RPC timeout.
Sources: [crates/rmux-client/src/connection.rs:247-273]()
---
## End-to-End Data Flow
```mermaid
sequenceDiagram
participant CLI as CLI caller
participant Conn as rmux-client::Connection
participant Codec as rmux-proto::codec
participant IPC as rmux-ipc::BlockingLocalStream
participant Daemon as rmux daemon
CLI->>Conn: roundtrip(&Request)
Conn->>Codec: encode_frame(&request)
Codec-->>Conn: Vec<u8> (magic | version | length | bincode payload)
Conn->>IPC: write_all(&frame)
IPC->>Daemon: bytes over Unix socket / Named Pipe
Daemon->>IPC: response bytes
IPC-->>Conn: read(&mut buffer[..8192])
Conn->>Codec: decoder.push_bytes(chunk)
Conn->>Codec: decoder.next_frame::<Response>()
Codec-->>Conn: Ok(Some(Response))
Conn-->>CLI: Ok(Response)
```
---
## Boundary Invariant and Design Consequences
The explicit invariant documented in `rmux-ipc/src/lib.rs` — that the crate transports bytes only — enforces a dependency rule:
- `rmux-ipc` must **not** import `rmux-proto`.
- `rmux-proto` must **not** import `rmux-ipc`.
- `rmux-client::connection` is the **only** place that joins them.
This means:
- Adding a new platform transport (e.g., abstract Unix namespaces, VSock) only touches `rmux-ipc` and the `Connection` constructor — the 90+ request/response types in `rmux-proto` are untouched.
- Adding a new request variant only touches `rmux-proto` (the enum, serde impls, codec tests) — no transport code changes.
- The codec (bincode + framing) can be swapped in `rmux-proto::codec` without touching socket code, though the wire version in `envelope.rs` would need to advance to signal incompatibility to existing daemons.
Sources: [crates/rmux-ipc/src/lib.rs:3-7](), [crates/rmux-client/src/connection.rs:14-19]()
---
## 05. Core Domain Model: Sessions, Panes, Grid, and Scrollback
> `rmux-core` is the pure in-memory domain: no OS calls, no Tokio, no network. It models the full tmux object graph — sessions own windows, windows own panes, panes own a terminal grid plus scrollback. The grid module handles VT/ANSI state (cursor, SGR attributes, alternate screen, scroll regions, wide/combining characters). Parser traces under `crates/rmux-core/tests/parser_traces/` are golden-file tests that pin exact VT sequence handling (e.g. `alternate_screen_roundtrip.trace`, `unicode_wide_cjk.trace`, `sgr_256_and_rgb.trace`). The `command_parser` module translates tmux-syntax command strings; `formats` implements tmux-format string evaluation; `hooks` tracks registered hook callbacks. Safe-change rule: any logic that belongs in this crate must be pure and deterministic — if it requires I/O, it belongs in `rmux-server` or `rmux-os`.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/05-core-domain-model-sessions-panes-grid-and-scrollback.md
- Generated: 2026-05-21T21:59:13.702Z
### Source Files
- `crates/rmux-core/src/lib.rs`
- `crates/rmux-core/src/keys.rs`
- `crates/rmux-core/tests/parser_traces.rs`
- `crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace`
- `crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace`
- `crates/rmux-pty/src/lib.rs`
<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [crates/rmux-core/src/lib.rs](crates/rmux-core/src/lib.rs)
- [crates/rmux-core/src/session.rs](crates/rmux-core/src/session.rs)
- [crates/rmux-core/src/session/types.rs](crates/rmux-core/src/session/types.rs)
- [crates/rmux-core/src/window.rs](crates/rmux-core/src/window.rs)
- [crates/rmux-core/src/pane.rs](crates/rmux-core/src/pane.rs)
- [crates/rmux-core/src/grid.rs](crates/rmux-core/src/grid.rs)
- [crates/rmux-core/src/grid/cell.rs](crates/rmux-core/src/grid/cell.rs)
- [crates/rmux-core/src/screen.rs](crates/rmux-core/src/screen.rs)
- [crates/rmux-core/src/input/mod.rs](crates/rmux-core/src/input/mod.rs)
- [crates/rmux-core/src/hooks.rs](crates/rmux-core/src/hooks.rs)
- [crates/rmux-core/src/command_parser.rs](crates/rmux-core/src/command_parser.rs)
- [crates/rmux-core/tests/parser_traces.rs](crates/rmux-core/tests/parser_traces.rs)
- [crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace](crates/rmux-core/tests/parser_traces/alternate_screen_roundtrip.trace)
- [crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace](crates/rmux-core/tests/parser_traces/unicode_wide_cjk.trace)
- [crates/rmux-core/tests/parser_traces/sgr_256_and_rgb.trace](crates/rmux-core/tests/parser_traces/sgr_256_and_rgb.trace)
</details>
# Core Domain Model: Sessions, Panes, Grid, and Scrollback
`rmux-core` is the pure in-memory domain for the rmux terminal multiplexer. It carries no OS calls, no async runtime, and no network sockets — its only mandate is modeling the full tmux object graph with deterministic, testable behavior. Because every value type in this crate is `Clone + PartialEq + Eq`, the server layer can snapshot, compare, and restore state without touching the process boundary.
This page explains the ownership chain from session down to individual grid cells, the VT/ANSI parser and screen writer that animate each pane, the golden-file test strategy that pins exact terminal emulation behavior, and the auxiliary subsystems (`command_parser`, `formats`, `hooks`) that round out the domain.
---
## The Object Ownership Hierarchy
The canonical hierarchy mirrors tmux's own data model:
```text
SessionStore
└─ Session (named, has active window, cwd, timestamps)
└─ BTreeMap<u32, Window> (window_index → Window)
└─ Vec<Pane> (window-order list; gaps allowed by index)
└─ PaneGeometry (x, y, cols, rows in terminal coordinates)
Screen (owned externally, keyed by PaneId)
├─ Grid (scrollback history + visible rows)
│ ├─ VecDeque<GridLine> (history, oldest first)
│ └─ Vec<GridLine> (visible rows)
└─ cursor_x, cursor_y, mode flags, scroll region, saved grids, hyperlinks
```
`Session` does not own a `Screen`. The screen (VT state + grid) is owned by the server layer (`rmux-server`) and keyed by `PaneId`. The core crate defines the geometry and identity; the rendering state lives elsewhere. This keeps the core crate I/O-free.
Sources: [crates/rmux-core/src/lib.rs:4-7](), [crates/rmux-core/src/session.rs:38-52](), [crates/rmux-core/src/window.rs:79-101](), [crates/rmux-core/src/pane.rs:46-53]()
---
## Session
`Session` is the top-level object. It owns a `BTreeMap<u32, Window>` indexed by *window index* (the number that appears in the status bar), tracks which window is active and which was last active, and maintains monotonically increasing allocators for both `WindowId` and `PaneId`.
```rust
// crates/rmux-core/src/session.rs:38-52
pub struct Session {
id: SessionId,
name: SessionName,
group_name: Option<SessionName>,
windows: BTreeMap<u32, Window>,
winlink_alert_flags: BTreeMap<u32, AlertFlags>,
active_window: u32,
last_window: Option<u32>,
next_pane_id: u32,
next_window_id: WindowIdAllocator,
created_at: i64,
activity_at: i64,
last_attached_at: Option<i64>,
cwd: Option<PathBuf>,
}
```
Key invariants:
- `active_window` always names a key present in `windows`. The `synchronized_active_window` helper re-derives a valid active window whenever windows are added or removed.
- Window indices may contain gaps (e.g., window 0, 2, 5 in the same session) after deletions or explicit user renumbering.
- `next_pane_id` is a session-global counter that skips already-allocated IDs, allowing pane IDs to be globally unique within a session and stable across layout changes.
### Session operations
| Operation | Method | Effect |
|---|---|---|
| Split pane | `split_pane_in_window_with_id_and_direction_before` | Delegates to `Window`, allocates `PaneId` |
| Kill pane | `kill_pane_in_window` | Removes pane; destroys window when it's the last |
| Move focus | `select_pane_in_window` | Updates `active_pane` on the target `Window` |
| Resize | `resize_pane_in_window` | Delegates to layout; `Zoom` short-circuits to `toggle_zoom_in_window` |
| Terminal resize | `resize_terminal` | Fans out `set_size` to every window |
Sources: [crates/rmux-core/src/session.rs:55-90](), [crates/rmux-core/src/session.rs:486-521]()
---
## Window
`Window` owns the pane list, the active layout, and the zoom state. Its pane collection is a `Vec<Pane>` ordered for *display* (window order), with pane *indices* as a stable but potentially gapped integer namespace:
```rust
// crates/rmux-core/src/window.rs:79-101
pub struct Window {
id: WindowId,
panes: Vec<Pane>,
next_pane_index: u32,
active_pane: u32,
last_pane: Option<u32>,
layout: LayoutName,
layout_tree: Option<LayoutTree>,
zoomed: bool,
alert_flags: AlertFlags,
requested_main_width: Option<u16>,
requested_main_height: Option<u16>,
// ...
}
```
The crucial split between *order* and *index*:
- **Display order** (`Vec<Pane>` position) determines layout adjacency — what you see on screen and how splits are applied.
- **Stable index** (`Pane::index`) is the display handle used in tmux target syntax (e.g., `mysession:0.2`). It persists after a rotation or swap.
When panes are rotated (`rotate_panes`), the vector is rotated in place and each pane's `index` field is renumbered to match its new position. The previously-active pane identity is tracked through `last_pane` so `select-pane -l` can restore it.
### Alert flags
`AlertFlags` is a bitset distinguishing BELL (`0x1`), ACTIVITY (`0x2`), and SILENCE (`0x4`). Windows maintain two parallel flag values: a *queue* used for in-flight processing and a *winlink-visible* copy tracked per window-index at the session level. `take_alert_flags` drains and returns the queue atomically.
Sources: [crates/rmux-core/src/window.rs:53-70](), [crates/rmux-core/src/window.rs:332-374]()
---
## Pane
`Pane` is a lightweight geometry record. It holds a stable `PaneId` (survives moves and transfers), a display `index` (used in target syntax, can change), and a `PaneGeometry` (position and size within the terminal coordinate space):
```rust
// crates/rmux-core/src/pane.rs:46-53
pub struct Pane {
id: PaneId,
index: u32,
geometry: PaneGeometry,
active_point: u64,
}
```
`PaneGeometry` stores `(x, y, cols, rows)` — where `(x, y)` is the upper-left corner in the full terminal coordinate system. The geometry is purely derived from the layout tree; `Pane` itself never performs layout calculations.
The distinction between `id` and `index` is load-bearing:
```
PaneId — stable across session lifetime; used to look up Screen state
index — display handle; the `3` in `myses:0.3`; changes on rotate/swap
```
Sources: [crates/rmux-core/src/pane.rs:6-109]()
---
## Grid: Scrollback and Visible Rows
`Grid` is the backing store for one terminal screen surface. It splits lines into a `VecDeque<GridLine>` (history, oldest at front, bounded by `hlimit`) and a `Vec<GridLine>` (the visible viewport, exactly `sy` rows):
```rust
// crates/rmux-core/src/grid.rs:69-77
pub(crate) struct Grid {
sx: u32,
sy: u32,
hlimit: usize,
hscrolled: usize,
history_enabled: bool,
history: VecDeque<GridLine>,
visible: Vec<GridLine>,
}
```
Absolute line addressing: rows `0..hsize` are history; rows `hsize..hsize+sy` are visible. This unified addressing is used by `absolute_line()` and `render_absolute_line()`.
### Scroll operations
`scroll_region_up(upper, lower, bg, to_history)` removes the top line of the scroll region, optionally pushes it to history, and inserts a blank row at the bottom. `scroll_region_down` is the mirror. Only full-width scrolls (`upper==0`) push to history.
### Width reflow
When the terminal width changes, `resize_width` calls `reflow_wrapped_lines`, which:
1. Collects cells from consecutive `WRAPPED` lines into a logical line.
2. Re-wraps at the new width, generating new `GridLine` entries with correct `WRAPPED` flags.
3. Splits the result back into history and visible at the `sy` boundary.
This ensures that a terminal session that was wrapped at 80 columns becomes naturally re-wrapped if the pane is widened, matching tmux's behavior.
Sources: [crates/rmux-core/src/grid.rs:283-519]()
### GridCell and GridLine
Each `GridLine` holds a `Vec<GridCell>` and `GridLineFlags`. Each `GridCell` carries:
| Field | Purpose |
|---|---|
| `text: String` | Rendered glyph (1–4 bytes for Unicode) |
| `width: u8` | Display columns (0 = padding, 1 = narrow, 2 = wide CJK/emoji) |
| `flags: GridCellFlags` | PADDING, CLEARED, TAB, EXTENDED, SELECTED, NOPALETTE |
| `attr: u16` | SGR attribute bitmask (BOLD, DIM, ITALIC, BLINK, REVERSE, …) |
| `fg / bg / us: Colour` | Foreground, background, underline colour |
| `link: u32` | OSC 8 hyperlink inner ID (0 = no link) |
Wide glyphs occupy two adjacent cells: the first cell has `width=2`, and the immediately following cell is a `PADDING` cell (`width=0`, `text=" "`). The `owning_cell_x` helper resolves a padding cell back to its owner, which is essential for correct cursor movement and overwrite logic.
```rust
// crates/rmux-core/src/grid/cell.rs:14-27
impl GridCellFlags {
pub const PADDING: Self = Self(0x1);
pub const CLEARED: Self = Self(0x2);
pub const TAB: Self = Self(0x4);
pub const EXTENDED: Self = Self(0x8);
pub const SELECTED: Self = Self(0x10);
pub const NOPALETTE: Self = Self(0x20);
}
```
`GridLineFlags` track:
| Flag | Meaning |
|---|---|
| `WRAPPED` | This row's logical content continues on the next row |
| `EXTENDED` | Extended cell storage in use |
| `DEAD` | Line is marked for removal |
| `START_PROMPT` | Line begins a shell prompt block (OSC 133) |
| `START_OUTPUT` | Line begins shell output |
Sources: [crates/rmux-core/src/grid/cell.rs:11-90](), [crates/rmux-core/src/grid/cell.rs:92-240]()
---
## Screen: VT State Over the Grid
`Screen` wraps `Grid` and adds all the mutable VT terminal state that changes as sequences are processed:
```rust
// crates/rmux-core/src/screen.rs:32-55
pub struct Screen {
grid: Grid,
cursor_x: u32,
cursor_y: u32,
pending_wrap: bool,
saved_cursor_x/y: Option<u32>,
saved_state: SavedState, // DECSC/DECRC
saved_grid: Option<SavedGrid>, // alternate screen (smcup/rmcup)
rupper: u32, rlower: u32, // DECSTBM scroll region
mode: u32, // terminal mode flags
cursor_style: u32,
title/window_name/path: String,
title_stack: Vec<String>,
tabs: Vec<bool>, // tab stop positions
hyperlinks: Hyperlinks, // OSC 8 intern table
bell_count: u64,
utf8_config: Utf8Config,
}
```
### Alternate screen
`Screen::is_alternate()` returns `true` when `saved_grid.is_some()`. Entering the alternate screen (`?1049h`) snapshots the current grid and cursor into `saved_grid`, then clears the main grid. Exiting (`?1049l`) restores the saved grid and cursor. The golden trace `alternate_screen_roundtrip.trace` pins this behavior: after exit, the primary content and cursor position are exactly restored while the alternate content disappears.
### Pending wrap
When a character is written to the very last column and `MODE_WRAP` is set, the cursor does not immediately wrap — instead `pending_wrap = true`. The actual wrap and linefeed happen the next time any printable character arrives. This is the canonical xterm/VT100 behavior and is observable in trace output as cursor position `x = max_x` with `pending_wrap: true`.
### Wide character writes
`write_char` calls `overwrite_for_write(x, width)` first, which clears any dangling padding cell at the target column and the cells immediately following. It then writes the primary cell with `width=2` and fills the next column with a padding cell. The `wide_overwritten_by_narrow` fixture pins that overwriting a wide glyph with a narrow one clears the orphaned padding cell.
Sources: [crates/rmux-core/src/screen.rs:32-55](), [crates/rmux-core/src/screen.rs:130-136](), [crates/rmux-core/src/screen.rs:382-448]()
---
## VT Parser: `InputParser`
`InputParser` (in `crates/rmux-core/src/input/`) is a pure-Rust DEC-style state machine that processes byte streams from the PTY. It is modeled on tmux's `input_ctx` and uses a transition table driven by the current `InputState`:
```
Ground → EscEnter → EscDispatch
→ CsiEnter → CsiDispatch
→ OscString → (exit: dispatch OSC)
→ DcsEnter → DcsDispatch
→ TopBitSet (UTF-8 accumulation)
```
The parser maintains separate buffers for intermediate bytes (`interm_buf`), CSI/DCS parameter bytes (`param_buf`), and OSC/APC string bodies (`input_buf`). All screen effects are delivered through the `ScreenWriter` trait — the parser itself is I/O-free and contains no reference to `Screen`.
### SGR handling
SGR parameters are parsed in `dispatch_csi` → `sgr.rs`. The colour encoding:
- ANSI 0–7: encoded as the raw integer
- 256-colour: `colour | COLOUR_FLAG_256`
- RGB: `colour | COLOUR_FLAG_RGB | (r << 16) | (g << 8) | b`
- Default: `COLOUR_DEFAULT` sentinel
The trace `sgr_256_and_rgb.trace` confirms that `\x1b[38;5;208m` is stored as `fg=x256=208` and `\x1b[38;2;10;20;30m` as `fg=rgb=10,20,30`.
### UTF-8 accumulation
The `TopBitSet` handler builds multi-byte sequences in `utf8_buf`. On completion it decodes the codepoint and calls `writer.collect_add(ch, &self.cell)`. If the continuation sequence is invalid (wrong length or non-continuation byte), `U+FFFD` is emitted. The `unicode_wide_cjk` trace shows three Japanese characters (each 3 UTF-8 bytes) correctly parsed and stored as `w=2` cells with adjacent padding.
### Reply buffer
Some sequences require responses back to the PTY (e.g., DA1 `\x1b[c` → `\x1b[?1;2c`). `InputParser::take_replies()` drains these into a `Vec<u8>` for the server to write. The `device_attributes_reply` trace verifies this round-trip.
Sources: [crates/rmux-core/src/input/mod.rs:113-162](), [crates/rmux-core/src/input/mod.rs:246-308]()
---
## Parser Trace Golden Tests
`crates/rmux-core/tests/parser_traces.rs` defines 30+ `Fixture` structs and runs each through a fresh `InputParser` + `Screen`. The result is serialized to a multi-section text format and compared against the corresponding `.trace` file in `tests/parser_traces/`.
**Fixture shape:**
```rust
struct Fixture {
name: &'static str, // slug → filename
cols: u16,
rows: u16,
history: usize,
resize_after: Option<(u16, u16)>, // optional reflow exercise
feeds: &'static [&'static [u8]], // sequential byte chunks
}
```
**Trace format sections:**
| Section | What it records |
|---|---|
| `parser_state` / `parser_pending_hex` | Final parser state and any buffered incomplete sequence |
| `mode_bits` | Terminal mode bitmask (e.g., `0x00000011` = cursor-visible + auto-wrap) |
| `cursor`, `alternate`, `history_size` | Cursor position, alternate-screen flag, history row count |
| `visible_lines` / `history_lines` | Per-cell dump: text, width, attr, fg/bg/us, padding marker |
| `transcript` / `transcript_joined` | Captured text with and without wrap-joining |
| `transcript_with_sequences` | Capture with embedded octal-escaped ANSI SGR |
| `saved_transcript` | Content of the saved (primary) grid when alternate screen is active |
**Regeneration:** set `RMUX_REGEN_PARSER_TRACES=1` before running to update goldens after intentional behavior changes.
**Guard tests:**
- `no_orphan_golden_files`: every `.trace` file must have a matching fixture; stale files fail the build.
- `golden_trace_files_have_no_trailing_whitespace`: enforces clean diffs.
- `terminal_parser_boundary_stays_private`: asserts that `TerminalParser` and the `vt100` crate are never exported, keeping the parser migration seam private.
Sources: [crates/rmux-core/tests/parser_traces.rs:1-29](), [crates/rmux-core/tests/parser_traces.rs:352-428](), [crates/rmux-core/tests/parser_traces.rs:490-522]()
---
## Command Parser
`crates/rmux-core/src/command_parser.rs` implements a pure tmux-compatible tokenizer mirroring `cmd-parse.y`. It produces `ParsedCommands` — a list of `ParsedCommand` values, each holding a canonical command name, argument vector, and source line.
Key design points:
- Command names are resolved via `lookup_command_at` using exact alias then unique-prefix matching against the frozen `COMMAND_TABLE`.
- Default aliases are baked in: `"split-pane"` → `"split-window"`, `"splitp"` → `"split-window"`, `"choose-window"` → `"choose-tree -w"`, etc.
- `CommandArgument` is an enum that can be a scalar `String` or a nested `Commands(ParsedCommands)` for brace-delimited blocks.
- `%if`/`%endif` conditionals at parse time are evaluated against a `FormatVariables` context, enabling config-file conditional loading without I/O.
- The parser is pure and deterministic: given identical input and context it always produces identical output.
Sources: [crates/rmux-core/src/command_parser.rs:1-12](), [crates/rmux-core/src/command_parser.rs:500-507]()
---
## Formats
`crates/rmux-core/src/formats/` implements tmux's `#{}` format-string evaluation. A format string is evaluated against a `FormatVariables` context that provides values for named variables (`session_name`, `window_index`, `pane_pid`, etc.). The module handles:
- Variable expansion: `#{session_name}` → current session name
- Conditional: `#{?flag,true,false}`
- Modifiers: `#{s/pattern/replacement/flags:variable}`, `#{b:variable}`, `#{l:literal}`, etc.
- Colour parsing: `#{fg}`, `#{bg}` attributes
Because `formats` has no dependencies on I/O or timing (beyond `time.rs` for `#{t:...}`), it is exercised by unit tests in `src/formats/tests/`.
---
## Hooks
`HookStore` (`crates/rmux-core/src/hooks.rs`) is the in-memory registry for tmux-style event callbacks. It stores bindings at four scopes: global-session, global-window, session-local, and pane-local.
```rust
// crates/rmux-core/src/hooks.rs:21-28
pub struct HookStore {
session_global: HookBindings,
window_global: HookBindings,
sessions: HashMap<SessionName, HookBindings>,
windows: HashMap<WindowTarget, HookBindings>,
panes: HashMap<PaneTarget, HookBindings>,
}
```
`HookStore::dispatch(scope, hook)` resolves the closest registered binding (pane → window → global) and returns a `Vec<HookDispatch>` — command strings ready to be enqueued by the server. The store is pure: it neither runs commands nor calls into OS or network code. Scoped cleanup (`remove_session`, `remove_window`, `remove_pane`) ensures no dangling entries outlive their owners. Renaming a session re-keys all associated window and pane entries atomically.
Sources: [crates/rmux-core/src/hooks.rs:270-277](), [crates/rmux-core/src/hooks.rs:279-309]()
---
## Crate Boundary Rule
The crate header explicitly states the safe-change constraint:
> *This crate models sessions, windows, panes, layout geometry, and exact target resolution without any OS, network, or process integration.*
The `#![forbid(unsafe_code)]` and `#![deny(missing_docs)]` attributes at the crate root enforce this. The `terminal_parser_boundary_stays_private` test verifies at compile time that `TerminalParser` and the `vt100` crate are not re-exported — allowing the internal parser to be swapped without breaking any consumer.
Any logic that requires:
- File I/O, environment reads → `rmux-os`
- PTY lifecycle, process spawning → `rmux-pty`
- Tokio tasks, server state, client connections → `rmux-server`
does not belong here. `rmux-core` must remain a pure, sync, deterministic library that can be unit-tested with no external dependencies beyond in-process Rust.
Sources: [crates/rmux-core/src/lib.rs:1-2](), [crates/rmux-core/tests/parser_traces.rs:490-522]()
---
## 06. Safe-Change Guide: SDK, Ratatui Integration & Failure Modes
> The public SDK boundary (`rmux-sdk`) is intentionally isolated: it must never import `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies. It re-exports identity newtypes from `rmux-proto` so callers never import internal crates. `ratatui-rmux` goes one step further — three building blocks only: `PaneDriver` (async I/O and state mutation, reaches RMUX only through `rmux-sdk`), `PaneState` (deterministic sync projection, no I/O), `PaneWidget` (sync ratatui widget, no clock/time deps). `rmux-render-core` is wasm32-compatible for the same reason. Key failure modes to anticipate: (1) daemon not running — `auto_start` re-execs but the socket write race can fail if the binary path is wrong (override via `RMUX_INTERNAL_BINARY_PATH`); (2) stale Unix socket — `rmux-server` detects and removes it on bind; (3) nested session confusion — `$RMUX` env var guards against re-attaching inside an existing session; (4) Windows ConPTY path — requires at least Windows 10 1903; older builds fall back to an error, not a silent hang.
- Page Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/pages/06-safe-change-guide-sdk-ratatui-integration-failure-modes.md
- Generated: 2026-05-21T22:00:19.425Z
### Source Files
- `crates/rmux-sdk/src/lib.rs`
- `crates/ratatui-rmux/src/lib.rs`
- `crates/rmux-render-core/src/lib.rs`
- `crates/rmux-pty/src/backend/windows/mod.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-server/src/daemon.rs`
<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [crates/rmux-sdk/src/lib.rs](crates/rmux-sdk/src/lib.rs)
- [crates/rmux-sdk/Cargo.toml](crates/rmux-sdk/Cargo.toml)
- [crates/ratatui-rmux/src/lib.rs](crates/ratatui-rmux/src/lib.rs)
- [crates/ratatui-rmux/Cargo.toml](crates/ratatui-rmux/Cargo.toml)
- [crates/ratatui-rmux/tests/budget.rs](crates/ratatui-rmux/tests/budget.rs)
- [crates/rmux-render-core/src/lib.rs](crates/rmux-render-core/src/lib.rs)
- [crates/rmux-render-core/Cargo.toml](crates/rmux-render-core/Cargo.toml)
- [crates/rmux-client/src/auto_start.rs](crates/rmux-client/src/auto_start.rs)
- [crates/rmux-client/src/nested.rs](crates/rmux-client/src/nested.rs)
- [crates/rmux-server/src/daemon.rs](crates/rmux-server/src/daemon.rs)
- [crates/rmux-server/src/unix_socket.rs](crates/rmux-server/src/unix_socket.rs)
- [crates/rmux-pty/src/backend/windows/mod.rs](crates/rmux-pty/src/backend/windows/mod.rs)
- [crates/rmux-pty/src/backend/windows/pty.rs](crates/rmux-pty/src/backend/windows/pty.rs)
- [crates/rmux-pty/src/backend/windows/flags.rs](crates/rmux-pty/src/backend/windows/flags.rs)
- [crates/rmux-pty/src/backend/windows/spawn.rs](crates/rmux-pty/src/backend/windows/spawn.rs)
- [crates/rmux-pty/src/backend/windows/version.rs](crates/rmux-pty/src/backend/windows/version.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [src/cli/top_level.rs](src/cli/top_level.rs)
- [src/cli/client_commands.rs](src/cli/client_commands.rs)
</details>
# Safe-Change Guide: SDK, Ratatui Integration & Failure Modes
This page explains the structural rules that keep the public integration surface of RMUX stable, the three-building-block design of `ratatui-rmux`, and the four most important operational failure modes. Understanding these invariants lets contributors predict which changes are safe without breaking external callers, and helps operators diagnose the most common runtime problems before reaching for logs.
The dependency rules are not just conventions — they are enforced at the Cargo.toml level and, for `ratatui-rmux`, by a dedicated test suite that fails the build if forbidden crates appear or source files exceed a budgeted line count.
---
## The Public SDK Boundary (`rmux-sdk`)
### Dependency isolation rule
`rmux-sdk` is the sole integration crate that external consumers import. Its `Cargo.toml` lists only `rmux-ipc`, `rmux-os`, and `rmux-proto` as direct RMUX siblings — never `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty`.
> `rmux-sdk` is a public integration peer of `rmux-client` and must not depend on `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies.
>
> Sources: [crates/rmux-sdk/src/lib.rs:12-17]()
This means a caller that only imports `rmux_sdk` cannot transitively pull in daemon or PTY internals. The SDK also carries `#![forbid(unsafe_code)]`, so every unsafe surface lives lower in the stack.
```toml
# crates/rmux-sdk/Cargo.toml — only internal deps allowed
rmux-ipc = { path = "../rmux-ipc", version = "0.2.0" }
rmux-os = { path = "../rmux-os", version = "0.2.0" }
rmux-proto = { path = "../rmux-proto", version = "0.2.0" }
# rmux-client, rmux-core, rmux-server, rmux-pty — ABSENT by design
```
Sources: [crates/rmux-sdk/Cargo.toml:17-19]()
### Identity newtype re-exports
`rmux-proto` owns the authoritative identity newtypes (`SessionName`, `SessionId`, `WindowId`, `PaneId`). `rmux-sdk` re-exports them so callers never need to name `rmux-proto` directly:
```rust
// crates/rmux-sdk/src/lib.rs
pub use types::{
PaneId, PaneRef, RmuxEndpoint, SessionId, SessionName,
TargetRef, TerminalSizeSpec, WindowId, WindowRef,
};
```
Sources: [crates/rmux-sdk/src/lib.rs:132-135]()
If you add a new newtype to `rmux-proto` that external callers need, it **must** be re-exported through `rmux-sdk`; callers should never have to add `rmux-proto` to their own `[dependencies]`.
### What a safe change looks like
| Change | Safe? | Reason |
|--------|-------|--------|
| Add a new `pub use` re-export in `lib.rs` | Yes | Additive; no existing callers break |
| Add `rmux-client` to `rmux-sdk` Cargo.toml | No | Violates the public-boundary invariant; leaks internal surface |
| Add a new module under `rmux-sdk/src/` | Yes, if pure | Must not reach down into `rmux-client` or `rmux-core` |
| Remove a re-exported type | No | Semver-breaking for any SDK consumer |
| Change a type in `rmux-proto` without updating re-exports | No | The re-export becomes a dangling symbol |
---
## `ratatui-rmux`: Three Building Blocks
`ratatui-rmux` contains exactly three public types, each with a clearly bounded responsibility:
```text
┌──────────────────────────────────────────────────────┐
│ ratatui-rmux (only dep: rmux-sdk + ratatui-core) │
│ │
│ PaneDriver ── async, owns event I/O, reaches RMUX │
│ │ via rmux-sdk only │
│ │ folds events │
│ PaneState ── sync, pure data projection, no I/O │
│ │ │
│ PaneWidget ── sync ratatui widget, no clock/time │
└──────────────────────────────────────────────────────┘
```
Sources: [crates/ratatui-rmux/src/lib.rs:9-25]()
### `PaneDriver` — async I/O owner
`PaneDriver` is the only place in `ratatui-rmux` that reaches the RMUX daemon. It subscribes to pane events, drives state mutations, and communicates exclusively through the `rmux-sdk` facade. It never touches `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` directly.
### `PaneState` — deterministic sync projection
`PaneState` is a plain-data value with no I/O and no clock dependencies. The driver folds events into it; the same value always produces the same rendered cells. It can be constructed from a `PaneSnapshot` without an async runtime:
```rust
// Inert construction — no runtime needed
let state = PaneState::from_snapshot(snapshot);
```
Sources: [crates/ratatui-rmux/src/lib.rs:60]()
### `PaneWidget` — sync ratatui widget
`PaneWidget` implements the ratatui `Widget` trait. It is entirely synchronous, has no clock or time dependencies, and only borrows a `PaneState`. It is safe to call from any ratatui draw loop — including single-threaded, non-tokio, or test environments.
### Budget enforcement
The invariants are enforced in two ways:
1. **Cargo.toml**: `ratatui-rmux` lists only `ratatui-core` and `rmux-sdk` as production dependencies. `rmux-client`, `rmux-core`, `rmux-server`, and `rmux-pty` are explicitly forbidden.
2. **Test suite** (`crates/ratatui-rmux/tests/budget.rs`): The test verifies the exact set of source files (`lib.rs`, `driver.rs`, `state.rs`, `widget.rs`, `theme.rs`), enforces a maximum line count of 1500 lines, and checks that `FORBIDDEN_DEPS` are absent from the manifest.
```rust
// crates/ratatui-rmux/tests/budget.rs
const FORBIDDEN_DEPS: &[&str] = &[
"rmux-client", "rmux-core", "rmux-server", "rmux-pty"
];
const MAX_DIRECT_DEPS: usize = 2;
const REQUIRED_DEPS: &[&str] = &["rmux-sdk"];
```
Sources: [crates/ratatui-rmux/tests/budget.rs:11-13]()
A change that adds a new source file, imports a forbidden crate, or exceeds 1500 source lines will fail `cargo test` before merging.
---
## `rmux-render-core`: wasm32-Compatible Subset
`rmux-render-core` is an unpublished crate that contains only the snapshot data types and deterministic ratatui projection code that `ratatui-rmux` also exposes. It has **no** daemon, IPC, process, filesystem, network, Tokio, or terminal-driver integration — only `ratatui-core` and `serde`:
```toml
# crates/rmux-render-core/Cargo.toml
[dependencies]
ratatui-core = { package = "ratatui", version = "=0.29.0", default-features = false }
serde = { version = "1.0", features = ["derive"] }
```
Sources: [crates/rmux-render-core/Cargo.toml]()
The crate's own `lib.rs` restates the constraint explicitly:
> This crate owns no daemon, IPC, process, filesystem, network, Tokio, or terminal-driver integration. It contains only captured pane snapshot data and deterministic ratatui projection code that can compile for `wasm32-unknown-unknown`.
Sources: [crates/rmux-render-core/src/lib.rs:6-12]()
Because all four public exports (`PaneSnapshot`, `PaneState`, `PaneWidget`, `cell_style`/`color`/`glyph_symbol`/`modifier`) are pure-data or deterministic projections, browser/WASM hosts can use them without any platform-specific I/O or async runtime.
---
## Dependency Direction Diagram
```text
External caller
│
▼
rmux-sdk ────────────────────────────────────────────────┐
│ (re-exports identity types from rmux-proto) │
│ deps: rmux-ipc, rmux-os, rmux-proto │
│ │
▼ │
rmux-client ← uses rmux-sdk for auto-start bootstrap │
rmux-server ← internal; never imported by rmux-sdk ──────┘
rmux-pty ← internal; never imported by rmux-sdk
ratatui-rmux ─────────────────────────── dep: rmux-sdk only
PaneDriver (async) → rmux-sdk → rmux-client/server at runtime
PaneState (sync, no I/O)
PaneWidget (sync ratatui widget)
rmux-render-core ─────────────────────── dep: ratatui-core + serde only
(wasm32-safe; no async, no IPC)
```
---
## Key Failure Modes
### 1. Daemon Not Running — Auto-Start Race
**What happens**: When the daemon socket is absent, `rmux-client` re-execs the current binary with the hidden flag `--__internal-daemon` to start the daemon in the background, then polls for the socket to become reachable.
**The race**: The binary path is resolved by `env::current_exe()`. If the binary has been replaced on disk (CI, staging, in-place upgrade) between the moment the client starts and the daemon re-exec, the spawned daemon may be a different version or may fail to start. The socket write that follows the spawn can fail before the daemon has bound.
**Override**: Set `RMUX_INTERNAL_BINARY_PATH` to an absolute path to point auto-start at a specific binary. This env var is only honoured when **both** `cfg(debug_assertions)` is active **and** `RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE=1` is set, so it cannot accidentally activate in release builds.
```rust
// crates/rmux-client/src/auto_start.rs
const BINARY_OVERRIDE_ENV: &str = "RMUX_INTERNAL_BINARY_PATH";
const BINARY_OVERRIDE_TEST_OPT_IN_ENV: &str = "RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE";
fn binary_override_enabled_for_tests() -> bool {
cfg!(debug_assertions)
&& env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV)
.is_some_and(|value| value == "1")
}
```
Sources: [crates/rmux-client/src/auto_start.rs:44-46](), [crates/rmux-client/src/auto_start.rs:599-610]()
**Error surfaces**: `AutoStartError::BinaryPath`, `AutoStartError::Launch`, `AutoStartError::TimedOut`. The timed-out variant includes the socket path and the amount of time spent polling.
---
### 2. Stale Unix Socket
**What happens**: If the daemon process crashed without cleaning up its socket, the socket file is left on disk but is no longer listened on. A new daemon bind attempt would fail with `EADDRINUSE`.
**How the server handles it**: `rmux-server` calls `remove_stale_socket_if_needed` before binding. The function:
1. Checks that the path is a plain Unix socket (not a symlink or regular file — those are errors).
2. Attempts a probe `connect()` to the socket.
3. If the connect returns `ConnectionRefused` or `NotFound`, it removes the socket and proceeds.
4. If the connect **succeeds**, it returns `AddrInUse` — a live foreign server is running there.
```rust
// crates/rmux-server/src/unix_socket.rs
pub(crate) fn remove_stale_socket_if_needed(socket_path: &Path) -> io::Result<()> {
// ...
match StdUnixStream::connect(socket_path) {
Ok(_stream) => Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!("socket '{}' is already in use", socket_path.display()),
)),
Err(error) if indicates_stale_socket(&error) => {
// remove and return Ok(())
}
Err(error) => Err(error),
}
}
pub(crate) fn indicates_stale_socket(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound
)
}
```
Sources: [crates/rmux-server/src/unix_socket.rs:182-217](), [crates/rmux-server/src/unix_socket.rs:306-312]()
Additionally, the socket directory is created with mode `0o700`, the bound socket itself is set to `0o600`, and both ownership and permissions are checked. A directory or socket that is a symlink, has wrong permissions, or is owned by a different UID will result in a `PermissionDenied` error rather than silent acceptance.
---
### 3. Nested Session Confusion — `$RMUX` Guard
**What happens**: When a user runs `rmux attach-session` from inside an existing RMUX session, re-attaching would open a new terminal client on top of the current one. This is almost never intended.
**How the guard works**: `detect_context()` checks whether `$RMUX` is set and non-empty. Pane processes inherit this env var from the session. If the client is nested, `attach-session` is rewritten into a `switch-client` call (which changes the active session without opening a new terminal client).
```rust
// crates/rmux-client/src/nested.rs
pub fn detect_context() -> ClientContext {
detect_context_from_env(std::env::var_os("RMUX").as_deref())
}
fn detect_context_from_env(tmux_value: Option<&std::ffi::OsStr>) -> ClientContext {
match tmux_value {
Some(value) if !value.is_empty() => ClientContext::Nested,
_ => ClientContext::Outside,
}
}
```
Sources: [crates/rmux-client/src/nested.rs:40-66]()
`$RMUX` typically holds a value like `/tmp/rmux-1000/default,12345,0` (socket path, session ID, window index). An empty string or absent variable means `Outside`. The guard is a pure env-var check — no IPC — so it is cheap and cannot fail.
**What is not guarded**: Only `attach-session` and `new-session` (which also delegates to `switch-client` when nested) are actively re-routed. Other commands do not check context and will behave normally regardless of nesting depth.
---
### 4. Windows ConPTY Version — Feature Gating, Not Silent Hang
**What happens**: The Windows ConPTY (`CreatePseudoConsole`) API was introduced in Windows 10 1803 (build 17134) and gained the passthrough mode flag in build 22621 (Windows 11 22H2). Calling these APIs on unsupported builds does not hang silently — it returns an `HRESULT` error that is immediately surfaced.
**How version detection works**: `current_windows_version()` calls `RtlGetVersion` (ntdll) at PTY creation time to read the build number.
```rust
// crates/rmux-pty/src/backend/windows/flags.rs
const PASSTHROUGH_MIN_BUILD: u32 = 22_621;
fn supports_passthrough(version: WindowsVersion) -> bool {
version.build >= PASSTHROUGH_MIN_BUILD
}
```
Sources: [crates/rmux-pty/src/backend/windows/flags.rs:11](), [crates/rmux-pty/src/backend/windows/flags.rs:71-73]()
**Fallback chain**: `open_pty_pair` calls `create_pty_state_with_fallback`, which:
1. Tries with the full flags set (passthrough + resize-quirk + win32-input-mode).
2. If passthrough fails, retries without it.
3. If all extended flags fail, retries with no flags at all (`standard_conpty_flags` = bits `0`).
4. If standard flags fail, the error is returned — there is no further fallback, no hang.
```rust
// crates/rmux-pty/src/backend/windows/pty.rs
fn create_pty_state_with_fallback(size: TerminalSize, selected: ConptyFlags)
-> Result<WindowsPtyState>
{
match create_pty_state(size, selected) {
Ok(state) => Ok(state),
Err(error) if selected.uses_passthrough() => {
// retry without passthrough
}
Err(error) if selected.bits() != 0 => {
// retry with standard flags (bits = 0)
}
Err(error) => Err(error), // no further fallback
}
}
```
Sources: [crates/rmux-pty/src/backend/windows/pty.rs:219-243]()
**Override**: Set `RMUX_CONPTY_NO_PASSTHROUGH=1` (or `true`, `yes`) to force passthrough off without changing any build, useful when a corporate managed build reports a high build number but ConPTY passthrough is still unreliable.
**What the page description says about 1903**: The claim that ConPTY requires Windows 10 1903 (build 18362) as a minimum is a practical baseline — passthrough mode raises that to build 22621. `rmux-pty` does not hard-code a minimum version guard; it relies on `CreatePseudoConsole` returning an `HRESULT` error on any unsupported system.
---
## Summary of Invariants
| Invariant | Enforcement mechanism |
|-----------|-----------------------|
| `rmux-sdk` never imports `rmux-client`/`rmux-core`/`rmux-server`/`rmux-pty` | Cargo.toml + doc comment |
| `ratatui-rmux` only imports `rmux-sdk` and `ratatui-core` | `budget.rs` integration test, Cargo.toml |
| `ratatui-rmux` source stays flat and under 1500 lines | `budget.rs` integration test |
| `rmux-render-core` is wasm32-compatible | No tokio/IPC/OS deps in Cargo.toml |
| Identity types are always imported via `rmux-sdk` | Re-export in `rmux-sdk/src/lib.rs` |
| Stale sockets are removed on bind, not ignored | `unix_socket.rs::remove_stale_socket_if_needed` |
| Nested sessions route to `switch-client`, not new attach | `nested.rs::detect_context` + `client_commands.rs` |
| Windows ConPTY errors are surfaced immediately | `create_pty_state_with_fallback` returns `Err` on final failure |
The dependency boundary and budget tests are the highest-leverage guardrails: they turn accidental violations into CI failures rather than runtime surprises. Any contributor touching these crates should run `cargo test --workspace` and confirm that `budget.rs` and `rmux-sdk`'s own tests pass before submitting. Sources: [crates/ratatui-rmux/tests/budget.rs:1-13](), [crates/rmux-sdk/src/lib.rs:1-17]()
---