# How OpenLogi's IPC Layer Connects the Agent, GUI, and Overlay Processes

> Discover how OpenLogi's IPC layer connects agent, GUI, and overlay processes using tarpc for secure, type-safe communication over Unix sockets or named pipes. Learn more today.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-13

---

**OpenLogi uses a tarpc-based binary-coded RPC protocol over local Unix sockets (or Windows named pipes) to enable secure, type-safe communication between the hardware-owning Agent, the Desktop GUI, and the Overlay helper process.**

OpenLogi's architecture isolates hardware I/O within a dedicated Agent process while delegating user interaction to separate GUI and Overlay components. The **OpenLogi IPC layer** bridges these processes using a Rust tarpc service definition that exposes device state and control commands through a local socket transport. This design ensures that only the privileged Agent accesses HID devices, while the unprivileged GUI and Overlay clients observe state and issue commands through a version-checked, multiplexed RPC channel.

## Transport Layer: Local Sockets and tarpc

The foundation of the OpenLogi IPC layer relies on platform-specific local socket transports wrapped with tarpc's typed RPC framework. In [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs), the `connect()` function establishes either a Unix domain socket at `/tmp/openlogi.sock` (macOS/Linux) or a Windows named pipe, then wraps the stream using `transport::wrap` to create a framed tarpc transport.

Clients receive a generated `AgentClient` from this transport, which implements the `Agent` trait defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs). This trait serves as the exclusive contract for all cross-process communication, ensuring type safety through bincode serialization.

## Connection Handshake and Client Declaration

Before issuing commands, every client must complete a mandatory three-step handshake enforced by the protocol:

1. **Protocol Version Check** — The first RPC call is always `protocol_version()` (method 0). Because tarpc encodes method order, this check guarantees both sides share the same bincode schema. A mismatch results in the GUI displaying `OutdatedGui` or the overlay exiting immediately.

2. **Client Kind Declaration** — After a successful handshake, the client calls `declare_client(kind)`, passing either `ClientKind::Gui` or `ClientKind::Overlay`. The agent only arms a dormant component after this call, preventing stray processes from unintentionally waking a sleeping agent.

3. **Identity Token (Overlay Only)** — The overlay additionally calls `identity()` to obtain a run-token and watches for supersession via the `succession` crate, ensuring only one overlay instance controls the action ring.

## Agent Process: The Hardware Authority

The **Agent** is the sole owner of all HID and device I/O within the OpenLogi architecture. It runs a tarpc server that implements the `Agent` trait from [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs), started by the `openlogi-agent` binary. The Agent maintains the canonical state of connected devices and exposes this state exclusively through RPC methods, never allowing direct hardware access from client processes.

## GUI Client: Observing State and Sending Commands

The Desktop GUI, built on the GPUI framework, manages its IPC connection in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs). The `spawn()` function creates a dedicated background thread running a Tokio runtime that maintains a long-lived connection to the Agent.

```rust
// crates/openlogi-desktop/src/services/ipc.rs
pub fn spawn() -> IpcClient {
    let (update_tx, updates) = mpsc::unbounded_channel();
    let (commands, mut cmd_rx) = mpsc::unbounded_channel::<Command>();

    std::thread::Builder::new()
        .name("openlogi-ipc-client".into())
        .spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("tokio runtime init failed");
            rt.block_on(async { observe_loop(&update_tx, &mut cmd_rx).await });
        })
        .expect("failed to spawn IPC client thread");

    IpcClient { updates, commands }
}

```

The GUI client implements a generation-driven observation loop. The `observe_loop` function maintains a continuous `Agent::observe` request that carries the last seen generation ID. The agent replies immediately when the `AgentSnapshot` changes, or after `OBSERVE_HOLD` (20 seconds) as a heartbeat. This eliminates polling overhead while ensuring the GUI receives state updates within milliseconds of hardware changes.

Device control commands—such as `set_dpi`, `set_light`, or `start_pairing`—travel from the GUI to the Agent via the same multiplexed connection, interleaved with the ongoing observation request.

## Overlay Client: Action Ring Coordination

The Overlay process, responsible for rendering the action ring interface, uses a specialized subset of the OpenLogi IPC layer defined in [`crates/openlogi-overlay/src/agent.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/agent.rs). Unlike the GUI, the overlay does not receive full device snapshots; instead, it polls `observe_action_ring` to receive `ActionRingInvocation` state.

```rust
// crates/openlogi-overlay/src/agent.rs
async fn poll_invocations(tx: mpsc::UnboundedSender<Option<ActionRingInvocation>>) {
    let mut state = InvocationPollState::default();

    loop {
        if matches!(&state, InvocationPollState::Reconnecting { .. }) {
            if let Some(client) = connect().await {
                state.connected(client);
            } else {
                if state.connection_failed(Instant::now()) {
                    stand_down("no agent answered for 1 min");
                }
                tokio::time::sleep(RETRY_PERIOD).await;
                continue;
            }
        }

        let Some((client, seen)) = state.observation() else { continue };
        let mut ctx = context::current();
        ctx.deadline = Instant::now() + OBSERVE_HOLD + Duration::from_secs(5);
        match client.observe_action_ring(ctx, seen).await {
            Ok(observed) if observed.generation != seen => {
                state.observed(observed.generation);
                let _ = tx.send(observed.invocation);
            }
            Ok(_) => continue,
            Err(_) => state.disconnected(),
        }
    }
}

```

When the user interacts with the ring, the overlay sends discrete commands back to the Agent:

```rust
async fn send_command(client: &AgentClient, command: OverlayCommand) -> bool {
    let ctx = context::current();
    match command {
        OverlayCommand::Hover { session_id, slot } => {
            client.action_ring_hover(ctx, session_id, slot).await.is_ok()
        }
        OverlayCommand::Activate { session_id, slot } => {
            client.action_ring_activate(ctx, session_id, slot).await.is_ok()
        }
        OverlayCommand::Cancel { session_id } => {
            client.action_ring_cancel(ctx, session_id).await.is_ok()
        }
    }
}

```

## Error Recovery and Transport Resilience

Both the GUI and Overlay treat transport failures as disconnect events. When the tarpc stream fails, the client drops the current `LiveConnection`, fires a reconnection attempt using `openlogi_ipc::client::connect`, and propagates status to the user interface. The GUI displays `GuiUpdate::Unreachable`, while the overlay silently retries commands until the Agent acknowledges them or a timeout expires (typically one minute of total silence).

This design ensures that temporary agent restarts or socket interruptions do not crash the GUI or Overlay; instead, they enter a graceful reconnection loop that recovers automatically when the Agent socket becomes available again.

## Summary

- **OpenLogi's IPC layer** implements a tarpc-based RPC protocol over local Unix sockets or Windows named pipes, defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs).
- The **Agent** process exclusively owns HID hardware and exposes state through the `Agent` trait, while the **GUI** and **Overlay** act as unprivileged clients.
- All connections begin with a mandatory `protocol_version()` handshake followed by `declare_client()` to identify as either `ClientKind::Gui` or `ClientKind::Overlay`.
- The GUI synchronizes state via a generation-driven `observe()` long-poll that returns immediately on changes or after a 20-second hold timeout.
- The Overlay uses `observe_action_ring()` to receive ring-specific state and sends user interactions via `action_ring_hover`, `action_ring_activate`, and `action_ring_cancel`.
- Transport failures trigger automatic reconnection logic, ensuring resilient communication without process restarts.

## Frequently Asked Questions

### What transport protocol does OpenLogi use for inter-process communication?

OpenLogi uses **tarpc over local sockets** for IPC. On macOS and Linux, it creates a Unix domain socket at `/tmp/openlogi.sock`; on Windows, it uses a named pipe. The transport is wrapped with tarpc's framing to provide type-safe RPC over bincode serialization.

### How does the GUI application stay synchronized with the Agent's device state?

The GUI maintains a **long-polling observation loop** via the `Agent::observe` RPC method. It sends the last known generation ID; the Agent responds immediately when the `AgentSnapshot` changes, or after 20 seconds (`OBSERVE_HOLD`) as a heartbeat. This push-style mechanism eliminates polling overhead while ensuring sub-second synchronization.

### What happens if the GUI and Agent have incompatible protocol versions?

During the initial handshake, the client calls `protocol_version()` as the first RPC. If the returned version does not match the client's expected schema, the GUI displays an `OutdatedGui` error message, and the overlay exits cleanly. This prevents serialization errors and undefined behavior from mismatched bincode definitions.

### Can multiple Overlay instances connect to the same Agent simultaneously?

While the protocol supports multiple connections, the Overlay calls `identity()` to obtain a unique run-token and monitors for supersession via the `succession` crate. This mechanism ensures that only one Overlay instance controls the action ring at any given time, with older instances gracefully standing down when a new connection claims authority.