Skip to content

Instantly share code, notes, and snippets.

@possibilities
Created August 22, 2026 16:58
Show Gist options
  • Select an option

  • Save possibilities/da4e710a1ff259af68e44607bb52229b to your computer and use it in GitHub Desktop.

Select an option

Save possibilities/da4e710a1ff259af68e44607bb52229b to your computer and use it in GitHub Desktop.
Design sketch: let zmx own fmx PTYs through a supported direct-client contract

Let zmx own fmx's PTYs: a direct-client design sketch

This is a concrete use case for libzmx discussion #127. It is a design sketch, not a request that zmx adopt every name or command shown here. We are happy to reshape it around zmx's preferred architecture and contribute the work incrementally.

What we are building

fmx is an OpenTUI interface for fx. It runs several fx processes and renders each terminal with OpenTUI's libghostty-backed terminal widget. In fmx terminology, one fx process plus its embedded terminal is an Instance.

Today fmx creates and owns a PTY for every Instance:

OpenTUI / libghostty renderer
            ↕ raw VT bytes, input, resize
         Bun PTY
            ↕
            fx

We would like zmx to own the PTY and fx process instead. fmx would become a specialized zmx client and visible renderer:

OpenTUI / libghostty renderer
            ↕ raw VT bytes, input, resize
       Bun Unix socket
            ↕
        zmx daemon
            ↕
       zmx-owned PTY
            ↕
            fx

Each fmx Instance would map to one ordinary zmx session. We are not asking zmx to grow windows, panes, layouts, or other multiplexer concepts. fmx owns its interface and grouping; zmx owns process and terminal durability.

The user-visible benefit is that closing or crashing fmx would detach the UI without killing the fx processes. Starting fmx again would discover its sessions, reattach, restore their terminals, and continue.

What we have already tried

The existing CLI is enough to prove the ownership model:

OpenTUI / libghostty
        ↕
     Bun PTY
        ↕
   zmx attach
        ↕
   Unix socket
        ↕
   zmx daemon → zmx PTY → fx

We tested this topology. Killing the client left the zmx session and fx process alive; reattaching restored output and accepted input correctly.

That may be a useful compatibility path, but it leaves an adapter PTY and zmx attach process in every Instance. It also makes creation, restoration boundaries, child exit, and early exec failures indirect or ambiguous. A supported direct client would remove that layer and give embedding applications an explicit lifecycle.

The smallest useful public boundary

For fmx, the useful product is not necessarily a C ABI or a generic multiplexer builder. A deliberately supported Unix-socket client contract would be enough:

  • zmx continues to use its opinionated one-daemon-per-session model.
  • zmx continues to own its PTY, daemonization, client leadership, and shadow terminal.
  • clients send input and resize events and receive raw VT output.
  • the contract explicitly describes restoration, readiness, and exit.
  • zmx's own CLI uses the same protocol/client implementation.
  • other-language clients can implement the documented wire contract directly.

A reusable Zig module would still be valuable for zmx and other Zig programs. fmx itself can connect with Bun's Unix-socket API, so it does not need C, N-API, or another FFI layer.

Proposed planks

1. Portable, versioned framing

The current IPC works well as an internal implementation, but a public boundary should not depend on Zig-native struct layout.

We have in mind:

  • explicitly assigned message tags;
  • fixed-width integer fields and a specified byte order;
  • length-delimited frames;
  • no padding or native usize on the wire;
  • a Hello / Welcome exchange carrying protocol versions and capabilities;
  • deterministic rejection of an incompatible client, rather than a hang or silently ignored operation.

Capability negotiation lets the public contract evolve without requiring every daemon and client to update together. Existing CLI behavior could remain unchanged while this is introduced, and compatibility tests should cover old/new client and daemon combinations.

The 0.6/0.7 behavior in #211 is a good example of the class of failure this would prevent.

2. An explicit attach and terminal lifecycle

Conceptually, a direct client needs an ordered stream like:

Attach
RestoreBegin
Output...
Ready
Output...
Exit { code, signal, reason }

The exact tags are open for discussion. The important semantics are:

  • output before Ready is restoration;
  • after Ready, output is live PTY output;
  • no PTY output can race across that boundary;
  • an attached client receives a definitive child/session exit event;
  • input and resize behavior remain consistent with zmx's current leader policy;
  • disconnecting the client only detaches it.

This also gives the CLI a way to preserve or report the underlying child exit reason where appropriate.

A small public Zig client module could own framing, negotiation, attach, input, resize, detach, and lifecycle decoding. zmx's terminal client would use it too, preventing the library and CLI implementations from drifting apart.

3. Atomic, headless creation

An embedding application needs to create a session without attaching a terminal client and without racing discovery against daemon startup.

One possible CLI shape is:

zmx create --json \
  --labels "owner=fmx instance=<opaque-id>" \
  <session-name> -- /absolute/path/to/fx <args...>

The exact CLI/API shape is negotiable. The required contract is:

  • create only; do not attach;
  • return only after the daemon is listening;
  • report whether the child successfully crossed exec;
  • return a structured error for name collision, daemon startup failure, or exec failure;
  • never leave an apparently successful but unusable session behind.

Internally, a close-on-exec error pipe between the daemon and child is one conventional way to distinguish successful exec from an early failure.

This operation could be a public client function with a thin CLI wrapper, or a CLI operation first if that better fits zmx.

4. Structured discovery and inspection

fmx must distinguish sessions it owns from unrelated human-created zmx sessions and reconcile them after a restart.

A possible surface is:

zmx list --json --where owner=fmx
zmx inspect --json <session-name>

The useful fields include a stable session name/identity, labels, command, cwd, pid, start time, client count, and supported protocol version/capabilities. Labels provide an excellent ownership boundary:

owner=fmx
instance=<opaque-id>

Human-oriented tabular output can remain exactly as it is. The structured form would be a separate compatibility contract for supervisors and embedding applications.

5. Protocol documentation and compatibility fixtures

A public client contract needs more than exported Zig declarations. We would like to help provide:

  • a language-neutral protocol document;
  • golden encoded frames and expected decodings;
  • malformed/truncated-frame tests;
  • old/new version and capability-negotiation tests;
  • lifecycle ordering tests;
  • end-to-end tests for create, attach, restore, detach, reattach, and exit.

This would make non-Zig clients possible without committing zmx to maintaining bindings for every language.

Terminal restoration and snapshots

zmx should retain its libghostty terminal as the authoritative shadow used for restoration. fmx should retain its own libghostty instance as the visible renderer. We do not need to share a live terminal grid or internal libghostty object between processes.

Raw VT bytes are the simplest baseline boundary because they are already what terminal clients consume and are language-neutral.

The snapshot work in PR #243 may provide a stronger optional restoration capability for clients that can consume libghostty snapshots. We would be happy to align with it. One possible model is capability negotiation:

  • every direct client can request raw-VT restoration;
  • snapshot-aware clients may negotiate a snapshot restore instead;
  • live output remains raw VT.

fmx can begin with the raw-VT path. If OpenTUI later exposes compatible snapshot loading, it could opt into the snapshot capability without changing process ownership.

fmx restart and lifecycle semantics

Moving PTYs into zmx changes fmx from process owner to durable-session client.

The intended behavior is:

  1. fmx creates an opaque Instance identity and a zmx session bearing ownership labels.
  2. fmx keeps a small manifest mapping its identity and launch metadata to that zmx session.
  3. On startup, fmx reconciles the manifest with structured zmx discovery.
  4. On fmx shutdown or crash, socket disconnection detaches; zmx and fx keep running.
  5. On restart, fmx reconnects, consumes restoration, then resumes live rendering.
  6. Natural fx exit produces an explicit zmx exit event and removes the corresponding fmx Instance.

fx also reports higher-level agent status to a separate fmx socket. Those reports are transient today. Frames sent while fmx is absent can be lost, so after restoration fmx may initially show the agent's status as unknown until fx reports again. Exact recovery of that higher-level state is a separate broker/heartbeat problem and does not need to expand zmx's scope.

Non-goals

This proposal does not require zmx to provide:

  • windows, panes, tabs, layouts, or a multiplexer UI;
  • fmx-specific state or fx-specific knowledge;
  • a network transport;
  • a shared live libghostty grid;
  • JSON terminal rendering;
  • C, N-API, or language-specific bindings solely for fmx;
  • durable storage of fmx's higher-level agent status.

The supported product can remain an opinionated PTY proxy with terminal restoration.

A possible contribution sequence

We are willing to work through this one plank at a time, with a focused issue or PR for each piece:

  1. Agree on the intended public boundary and compatibility policy.
  2. Introduce explicit portable framing, version/capability negotiation, fixtures, and compatibility tests.
  3. Extract or establish the reusable Zig protocol/client module and migrate zmx's CLI client onto it.
  4. Add ordered restore/ready/exit lifecycle semantics.
  5. Add atomic headless creation with startup and exec acknowledgement.
  6. Add structured list/inspect output and ownership-label discovery.
  7. Implement the fmx transport, exercise it against real workloads, and upstream any protocol or lifecycle fixes uncovered.
  8. Release a tagged zmx version with the supported contract before fmx makes it a required dependency.

These can be reordered or reduced based on what is most useful to zmx. We are also comfortable with Eric doing architectural refactoring while we provide implementation, tests, documentation, QA, or downstream validation.

Questions for zmx

  1. Does a supported Unix-socket client contract fit the direction intended for libzmx, or would an in-process PTY-proxy builder be preferred?
  2. Is raw VT plus explicit restoration boundaries an acceptable baseline, with snapshots as an optional capability?
  3. Should headless creation live in the client module, the CLI, or both?
  4. What backward-compatibility window should a public protocol promise?
  5. Which plank would be the most useful and least disruptive first contribution?

The core outcome we care about is simple: zmx owns and preserves the PTY/process; fmx attaches as a renderer and controller. We are happy to help wherever that overlaps with the direction zmx already wants to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment