# jackin❯ Capsule: Multiplexer Rewrite Design History (https://jackin.tailrocks.com/reference/research/agent-orchestration/jackin-capsule-multiplexer-rewrite/)



**Status**: Historical design record. The rewrite this page documents has shipped; the canonical description of Capsule's current architecture lives in [jackin❯ Capsule](/reference/capsule/) and [Multiplexer design rules](/reference/capsule/multiplexer-design-rules/). This page exists so the *why* behind the shipped design — the defects that forced the rewrite, the prior-art borrows, and the compatibility rationale — is not lost once the [jackin-capsule roadmap item](/roadmap/jackin-capsule/) is trimmed to current/remaining work only.

## Why the first attempt was rewritten [#why-the-first-attempt-was-rewritten]

The first `jackin-capsule` attempt produced a working PID 1 binary that opened PTYs, drew a status bar, managed a binary pane tree, and framed messages over a Unix socket. The shape was right; the execution was not. Five categories of defect made the result unusable for modern TUI agents and forced a ground-up rewrite rather than incremental fixes:

1. **Hand-rolled VT emulator without alt-screen support.** The original `crates/jackin-capsule/src/terminal.rs` implemented the `vte::Perform` trait directly, but its `csi_dispatch` ignored the `h`/`l` actions and never inspected the `?` intermediate, so every DEC private mode was silently dropped — alternate screen (`\x1b[?1049h`), cursor visibility (`\x1b[?25h/l`), application cursor keys (`\x1b[?1`), bracketed paste (`\x1b[?2004`), and mouse modes (`\x1b[?1000-1006`). Modern TUIs enter alt-screen and expect these modes; without them the visible pane was garbled or blank. This module is long gone — the [Capsule Terminal Model](/reference/capsule/terminal-model/) page records the two later stages (an intermediate `vt100` fork, then the current `jackin-term` `DamageGrid`) that replaced it.
2. **`Ctrl+J = 0x0A = line feed` as the palette-open key.** That byte is identical to a literal newline; any LF in pasted text or from a TUI emitting `\n` opened the command palette and consumed it, so operators effectively could not send Enter to the agent on terminals that don't strictly send `\r` for the return key. The fix was a proper prefix-key state machine, not a single hardcoded byte — see the shipped input model in [jackin❯ Capsule](/reference/capsule/#input-routing).
3. **Daemon exits when all sessions die.** The original daemon called `std::process::exit(0)` once every tracked session was no longer alive. Because the daemon is PID 1, that call brought down the whole container. An agent that crashed during its first second of launch — common while iterating on roles — looked to the operator like "jackin❯ keeps exiting on me." The daemon now persists until `SIGTERM`.
4. **Mouse passthrough was a literal no-op.** The `MousePress` branch outside row 0 discarded the event (`let _ = session; None`) instead of forwarding it. The current daemon (<RepoFile path="crates/jackin-capsule/src/daemon/mouse_input.rs">crates/jackin-capsule/src/daemon/mouse\_input.rs</RepoFile>) implements full mouse routing: pane-relative re-encoding, hover/pointer-shape feedback, scrollbar drag, and text selection.
5. **Hot-path framing was full-screen redraw, base64, and JSON.** Every PTY chunk rebuilt the entire cell grid as ANSI, base64-encoded it, wrapped it in a JSON `ServerMsg::Output`, and length-prefixed the result — megabytes per second of pointless re-encoding between two processes sharing a kernel for an 80×24 pane redrawing at 60 Hz. The shipped attach channel sends raw PTY bytes through a tag-plus-length binary frame instead (see the wire protocol in [jackin❯ Capsule](/reference/capsule/#unix-socket-and-wire-protocol)).

Smaller defects rode along with these five: single-tab borders always drawn, status-bar tabs appending their state glyph after click-region width was computed (so click targets drifted), the reattach path dropping the outbound channel sender, and the client never propagating `SIGWINCH`. All are fixed in the shipped implementation.

## Architectural reference: Zellij [#architectural-reference-zellij]

[Zellij](https://github.com/zellij-org/zellij) was the closest architectural reference for the rewrite. It is Apache-2.0 (freely studied and adapted) and solves the same class of problem at larger scope. The pieces of its design jackin❯ adopted:

* **Client-server split over a Unix domain socket.** The server owns all PTYs, all VT state, all tabs/panes; the client owns user input and rendering. Detach and reattach leave PTYs running.
* **Typed instruction bus.** Zellij's server threads talk over MPSC channels carrying typed enums. jackin❯'s server is single-threaded enough that a full bus was overkill, but the *typed-enum-over-channel* shape is what the daemon's `tokio::select!` loop converged on.
* **Per-pane VT state, replayed on switch.** The server keeps a grid for every pane, including non-visible ones; switching tabs replays the target pane from its saved screen instead of asking the program to redraw.
* **Binary protocol on the hot path.** Zellij uses protobuf; jackin❯ uses simpler tag-plus-length-prefix binary framing. The point of the borrow was never protobuf specifically — it was "don't put base64-inside-JSON on the hot path."

Zellij carries scope jackin❯ explicitly does not need — multi-client collaboration, plugins via WASM, scrollback search, copy-mode regex. The rewrite cherry-picked the structural pieces and left the rest.

## Prior art: Herdr [#prior-art-herdr]

[Herdr](https://github.com/ogulcancelik/herdr) is the closest public reference for the multiplexer-server concept: a single Rust binary managing multiple AI coding agents with per-project workspace grouping, four-state status tracking, a Unix socket API, and session persistence across client detach. The comparative table against other tools (CCManager, Agent Session Manager, WezTerm Agent Deck, etc.) lives in the [Agent Orchestration Program](/reference/research/agent-orchestration/program-research/) research page; this section records the specific design borrows Capsule took from Herdr's UI shape (not its code — Herdr is AGPL-3.0, so any reimplementation had to be written from scratch).

Herdr wraps **bare host processes**; jackin❯ wraps **Docker containers**. When Herdr sees `docker attach` rather than the agent itself, its foreground-process and screen heuristics degrade — it is watching the wrong process. `jackin-capsule` runs *inside* the container and reads the agent's PTY output directly, which is why the same heuristic approach is reliable for jackin❯ in a way it structurally cannot be for Herdr wrapping a container.

**Structural UI borrows** (from Herdr's UI shape):

* **Top-of-screen chrome** — a brand pill on the left followed by one tab per active session, active tab visually distinct, tab labels carrying the rolled-up state glyph.
* **Empty initial state when no agent is preselected** — brand header, zero tabs, a centred hint listing configured agents plus `Shell`, matching `jackin console`'s no-preselection launch path.
* **Per-tab "most urgent" state roll-up** — a tab containing any `blocked` pane is `blocked`; otherwise `done`; otherwise `working`; otherwise `idle`. This priority order (`blocked > done > working > idle > unknown`) drives the in-container tab-strip glyph (`tab_label` in <RepoFile path="crates/jackin-capsule/src/tui/components/status_bar.rs">crates/jackin-capsule/src/tui/components/status\_bar.rs</RepoFile>, computed inline). <RepoFile path="crates/jackin-agent-status/src/arbitrate.rs">crates/jackin-agent-status/src/arbitrate.rs</RepoFile> also exposes a reusable `roll_up_states` helper with the same priority order and unit tests, but it has no production call site yet — it is the natural building block for the still-open host-side per-instance roll-up (see the jackin-capsule roadmap item's remaining work).

**Concepts implemented independently** (validated by Herdr's live usage, reimplemented from scratch because of the license):

* **Two-stage done state.** `done` (work finished, not yet reviewed) vs. `idle` (reviewed or empty) so the autonomous task queue does not refill a slot before the operator acknowledges it. Shipped as `SessionStatus::acknowledge()` in <RepoFile path="crates/jackin-agent-status/src/lib.rs">crates/jackin-agent-status/src/lib.rs</RepoFile>.
* **Notification suppression when already looking** and **sound escalation as opt-in** — validated design inputs for [agent attention prompts](/reference/research/agent-orchestration/agent-attention-prompts-design/), which is still an open roadmap item.
* **Blocking `wait` semantics on the socket** — Herdr's `herdr wait agent-status 1-1 --status done` is a better interface than polling for automation scripts. This is validated design input, not yet shipped: the control channel in <RepoFile path="crates/jackin-protocol/src/control.rs">crates/jackin-protocol/src/control.rs</RepoFile> is request/reply only today (`Status`, `Snapshot`, `Agents`, `ReportRuntimeEvent`, `StatusCapture`, the `Usage*` family, `TokenUsage`), with no blocking-wait or streaming-subscribe method. See the jackin-capsule roadmap item's remaining work.
* **Layered state authority** — foreground process state, visible-screen signals, and semantic integration reports combined into one arbitration result. Fully specified and shipped as the [agent runtime status authority](/roadmap/agent-runtime-status/).

**What Capsule deliberately did not borrow:** Herdr as the session substrate (bare-host PTY management is the wrong shape for a container-internal, statically-linked binary), Herdr's theme system (jackin❯ TUI uses a fixed palette), Herdr's SSH remote tunneling (different threat model than the explicit operator-controlled tunneling planned for [jackin-remote](/roadmap/jackin-remote/)), and Herdr's wire protocol verbatim (the socket vocabulary was designed for jackin❯'s own data model).

## Terminal compatibility rationale: tmux setting parity [#terminal-compatibility-rationale-tmux-setting-parity]

Before the rewrite, <RepoFile path="docker/runtime/entrypoint.sh" /> carried five non-default tmux option choices the operator had pinned over time to keep agent TUIs working. The rewrite had to reproduce the same observable behaviour without tmux, or real workflows would regress. Each option is now a canonical rule in [Multiplexer design rules](/reference/capsule/multiplexer-design-rules/#reference-tmux-options-we-replicate); the rationale for each substitution:

| tmux option            | Why it existed                                                                          | jackin-capsule's substitute                                                                                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `extended-keys always` | Agent TUIs don't emit the per-app activation escape that `on` waits for                 | The attach channel forwards raw bytes both directions, so there is nothing to negotiate — kitty keyboard and CSI-u sequences round-trip unchanged                                                         |
| `focus-events on`      | Claude Code / Codex pause animations and polling on focus-out                           | The daemon tracks a per-pane "outer-terminal-focused" flag and synthesises focus events only for the focused pane in the active tab                                                                       |
| `allow-passthrough on` | Desktop notifications, progress, clipboard, and titles were silently dropped without it | OSC sequences forward from the focused pane only. OSC 52 clipboard writes are default-deny and require `JACKIN_OSC52=allow`; other families have per-family operator opt-outs (`JACKIN_OSC_NOTIFY`, etc.) |
| `escape-time 0`        | tmux's default `ESC` disambiguation delay misfired vi-mode navigation in agent TUIs     | A configurable `JACKIN_ESCAPE_TIME` deadline for a bare `ESC`, while preserving complete sequences split across chunk boundaries                                                                          |
| `mouse on`             | Mouse clicks/drags/scroll needed to reach agent TUIs that opt into reporting            | Any-event SGR mouse tracking on the outer terminal, re-encoded per pane's negotiated protocol/encoding                                                                                                    |

## Ghostty compatibility notes [#ghostty-compatibility-notes]

The operator runs jackin❯ inside [Ghostty](https://ghostty.org/), whose feature set is wider than xterm's. Most of the features the rewrite targeted are shipped and now live in [Multiplexer design rules](/reference/capsule/multiplexer-design-rules/): kitty keyboard protocol pass-through, true-color/OSC 10-11 answered from the pane grid's stored palette, bracketed paste, OSC 52 clipboard, OSC 8 hyperlinks (scheme-filtered), and synchronized output (`?2026`) — absorbed by the grid rather than forwarded, because the capsule's own `ClientWriter` brackets every composed frame and forwarding the agent's markers verbatim could let a dropped ESU freeze the outer terminal.

Two Ghostty features from the original design remain **unimplemented rather than shipped**: the kitty graphics protocol (inline images via APC `\x1b_G…\x1b\\`) and Sixel graphics. Both were designed to forward unchanged for the focused pane and stay silent for backgrounded panes, matching the OSC passthrough model, but no `PassthroughEvent` variant or session-level handling for APC/Sixel exists in `jackin-term` today — unhandled APC sequences fall under the grid's default-deny-unless-allowlisted CSI/APC policy and are dropped. An agent that emits inline images or Sixel output today loses that output rather than having it rendered. This is tracked as remaining work on the [jackin-capsule roadmap item](/roadmap/jackin-capsule/).

## Historical implementation arc [#historical-implementation-arc]

**Phase 1 — cleanup gap closed.** Before Capsule, containers used tmux as the session layer, wrapped by a bash wait loop (`docker/runtime/supervisor.sh`, since deleted) that polled `tmux list-sessions` every second rather than watching the socket file disappear — `tmux list-sessions` was robust to stale socket files left behind by a crashed tmux server, where a plain file-existence check would loop forever. A 60-second startup grace period waited for the socket file to appear before entering the monitor loop. Two companion host-side fixes made the cleanup path actually fire: `finalize_foreground_session` (<RepoFile path="crates/jackin-isolation/src/finalize.rs">crates/jackin-isolation/src/finalize.rs</RepoFile>) started calling `has_tmux_sessions` when the container was still `Running` after `docker exec` returned, and <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs">crates/jackin-runtime/src/runtime/launch.rs</RepoFile> started tearing down the DinD sidecar and Docker network in every branch where the container had exited, including crashes.

**Phase 2 — Rust binary skeleton.** Created the `crates/jackin-capsule/` workspace member; implemented PID 1 bootstrap (zombie reaping, signal handling), a tmux-socket `inotify` watch (replacing the bash polling loop), and a Unix socket listener with a `status` command; added the cross-compile CI job and the socket mount to `docker run`.

**Phase 3 — the rewrite.** Landed as one cohesive change because the five defects above were interlocked — fixing one without the others left the binary unusable for testing — but the work was reviewed in sub-phases, each required to pass `cargo nextest run` and `cargo clippy` independently: replacing the VT emulator (later superseded again by the `vt100` → `jackin-term` transition documented in [Capsule Terminal Model](/reference/capsule/terminal-model/)); the prefix-key input state machine; the persistent server and binary attach channel; and resize/mouse/status-bar polish. All four sub-phases are shipped. The one piece of that plan explicitly **not** carried forward: an intermediate design considered keeping exited tabs around for replay until `SIGTERM` — that was rejected because it weakened the "daemon exits when the last session ends" cleanup contract the host relies on to tear down the container, DinD sidecar, cert volume, and network.

## Original (superseded) control-channel API sketch [#original-superseded-control-channel-api-sketch]

The pre-rewrite target API sketched a phased vocabulary of `status` (Phase 2), `session.create` / `session.kill` / `session.title` / `session.attach` (Phase 3), and `events` (Phase 3, streaming). The shipped control channel took a different shape: request/reply methods (`Status`, `Snapshot`, `Agents`, `ReportRuntimeEvent`, `StatusCapture`, the `Usage*` family, `TokenUsage`, `ExecCommand`) defined in <RepoFile path="crates/jackin-protocol/src/control.rs">crates/jackin-protocol/src/control.rs</RepoFile>, with session creation folded into the attach channel's `Hello { spawn }` frame rather than a standalone control message, and session kill/title handled by daemon-side operator actions (palette close, tab-strip glyph) rather than exposed as host-callable control methods. Structured `session.kill` / `session.title` control methods and a streaming event channel remain open — see the jackin-capsule roadmap item.
