Skip to content

A multiplexer backend is the surface that hosts an agent session -- the terminal multiplexer that owns the pane an AI tool runs in. By default Open Orchestrator uses tmux. You can opt in to herdr as an alternative backend, in which case owt becomes the orchestration brain and herdr becomes the rendering surface.

herdr support is purely additive: tmux remains the default, and nothing changes for users who never opt in.

tmux vs herdr ​

Aspecttmux (default)herdr backend
Default UXUnchangedUnchanged for everyone else
Multiplexer UItmux conventionsherdr's window manager + sidebar
Mouse-nativeNoYes
Agent state in UIStatus DB onlyStatus DB + sidebar via pane.report_agent
Remote attachtmux attachherdr --remote
Install footprinttmux onlytmux or herdr (still optional)

When to use each ​

  • Use tmux for the default experience, and for CI and headless runs.
  • Use herdr when you want a mouse-native window manager with a live agent sidebar, or a richer remote-attach experience for owt new worktrees.

Installing herdr ​

herdr is not bundled with Open Orchestrator -- install it yourself, then opt in. tmux remains the default until you do.

bash
# Install herdr
curl https://herdr.dev/install.sh | sh

# Verify the daemon is running and the socket exists
herdr status
ls $XDG_CONFIG_HOME/herdr/herdr.sock

When [backend] mode = "auto", owt probes for herdr with which herdr plus a socket ping and silently falls back to tmux if it is not reachable -- so installing herdr is the only step needed to "turn it on" in auto mode.

Opting In ​

One-off, per invocation:

bash
owt new "Refactor billing" --herdr
owt attach my-feature --herdr

Force tmux on a single command (overrides config):

bash
owt attach my-feature --tmux

Project-wide via .worktreerc.toml:

toml
[backend]
mode          = "auto"      # tmux | herdr | auto
herdr_session = "default"   # named herdr session (selects which socket)
# herdr_socket = "/custom/path/to/herdr.sock"  # only if you've moved the socket

The named session resolves to a socket path:

  • default -> $XDG_CONFIG_HOME/herdr/herdr.sock
  • any other name -> $XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock

Selection Precedence ​

owt resolves the backend in this order:

  1. --herdr / --tmux on the command line (per invocation)
  2. [backend] mode in .worktreerc.toml
  3. tmux as the safe default

mode = "auto" reaches for herdr first (via which herdr plus HerdrClient.ping()) and falls back to tmux when herdr is not installed or its socket is not reachable.

You can also set OWT_BACKEND_MODE=tmux in the environment to force tmux for a single shell.

What owt Sends to herdr ​

Each owt worktree maps to one herdr workspace; the agent runs in that workspace's root pane.

owt actionherdr RPC
owt new --herdrworkspace.create + pane.send_text
owt sendpane.send_text (per worktree's recorded backend)
owt attach --herdrherdr agent attach <pane_id> (exec)
status update (hooks)pane.report_agent (non-fatal)
owt deletepane.close + workspace.close

Status DB Is the Source of Truth ​

owt writes the canonical agent state into SQLite (~/.open-orchestrator/status.db). When herdr is enabled, the tracker also forwards each write to the backend via report_agent_state(session, state, message) so herdr's sidebar reflects the same picture.

Status forwarding is best-effort. If the herdr call fails, the SQLite write is unaffected and the control plane (owt) keeps working -- the sidebar just falls behind.

Per-Worktree Backend Records ​

Each status row carries four fields, written at create-time, so later commands route correctly without re-passing flags:

FieldMeaning
session_type"worktree" (default) or "branch" (in-place branch session)
backend_kind"tmux" or "herdr" -- picked by the launcher
backend_session_idtmux session name OR herdr pane id
backend_metaJSON with workspace_id, socket, and herdr_session (herdr)

Once a worktree is created with --herdr, follow-up commands (owt attach, owt send, owt switch, owt delete) read this row and dispatch to the right adapter -- including the exact herdr socket path for custom deployments. You only need --herdr / --tmux again when you want to override what was recorded.

Forcing a Backend on owt attach ​

When the forced backend (--tmux / --herdr) differs from the recorded one, owt re-resolves the session via backend.session_for(name) on the forced backend rather than coercing the recorded id. tmux session names and herdr pane ids are different shapes, so coercing them would silently misroute. If the forced backend has no session for that worktree, owt errors clearly:

No herdr session for 'my-feature'. Recorded as tmux.

Headless Behavior ​

Headless launches (owt new --headless) skip backend resolution entirely -- the detached subprocess never touches a multiplexer. This means a CI configuration with [backend] mode = "herdr" keeps working even when herdr is not installed:

bash
# Works even with no herdr installed.
owt new "Run my batch job" --headless

Note that --herdr is incompatible with --headless (there is no terminal to host); use tmux (the default) for CI.

TUI Prompt Submission ​

herdr's pane.send_text types text into a pane as text -- it does not synthesize a real Enter key event. TUI agents (pi, claude in TUI mode, droid) read raw stdin and treat a literal \n as "newline in input", not "submit". So owt routes every agent-facing message through a single chokepoint (HerdrBackend._send_line()) that delivers Enter as a carriage return (\r by default).

If your herdr build needs a different terminator, override it per shell with OWT_HERDR_SUBMIT:

bash
# CRLF terminator embedded in pane.send_text.
export OWT_HERDR_SUBMIT='text:\r\n'

# Body via pane.send_text, then a real key event via pane.send_keys.
export OWT_HERDR_SUBMIT='keys:Enter'      # most TUIs
export OWT_HERDR_SUBMIT='keys:Return'     # some keymaps name it Return
export OWT_HERDR_SUBMIT='keys:C-m'        # the literal Enter byte

Unknown values (for example OWT_HERDR_SUBMIT=foo:bar) log a warning and fall back to the default. \r / \n escapes are expanded so the variable stays readable in a shell.

Architecture ​

Call sites depend only on the MultiplexerBackend protocol. AgentLauncher, commands/agent.py (send), commands/worktree.py (new, switch, attach, delete, list), and commands/doctor.py resolve a backend through core/backend_factory.py and never touch TmuxManager directly.

                   ┌─────────────────────────────┐
                   │   commands/ (owt CLI)       │
                   └──────────────┬──────────────┘
                                  │
                  ┌───────────────▼─────────────────┐
                  │ MultiplexerBackend  (protocol)  │
                  └───────────────┬─────────────────┘
                                  │
              ┌───────────────────┴─────────────────────┐
              │                                          │
   ┌──────────▼──────────┐                  ┌────────────▼───────────┐
   │ TmuxBackend         │                  │ HerdrBackend           │
   │  → TmuxManager      │                  │  → HerdrClient (RPC)   │
   └─────────────────────┘                  └────────────────────────┘

The concrete adapters live behind core/tmux_backend.py (wraps TmuxManager) and core/herdr_backend.py (wraps HerdrClient, JSON-RPC over a Unix socket). The factory at core/backend_factory.py is the single resolution point.

Known Limitations ​

  • Plan mode and automated mode are honored only by tmux. They are propagated via the agent command and the OWT_AUTOMATED=1 env var, which herdr's pane.send_text does not currently set on the pane shell.

The two flows that drive agent sessions are the standard owt new worktree (fully herdr-aware) and the Control Plane (owt); both respect the configured backend.

Troubleshooting ​

herdr is not installed or its socket is not reachable

  • Install it: curl https://herdr.dev/install.sh | sh
  • Verify the daemon is running: herdr status
  • Confirm the socket exists: ls $XDG_CONFIG_HOME/herdr/herdr.sock
  • If you use a named session, point [backend] herdr_session at it

--herdr is incompatible with --headless

  • Headless mode has no terminal to host. Use --tmux (the default) for CI.

Multiple herdr daemons (for example, work and personal)

  • [backend] herdr_session = "work" selects ~/.config/herdr/sessions/work/herdr.sock.

Falling back to tmux temporarily

  • Pass --tmux on any command to override config for that invocation.
  • Or set OWT_BACKEND_MODE=tmux in the environment for a single shell.

See Also ​