# How OpenLogi GUI and Overlay Communicate with the Agent: IPC Architecture Explained

> Understand how the OpenLogi GUI and overlay communicate with the agent using tarpc RPCs over local sockets. Learn about the IPC architecture and handshake protocol.

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

---

**Both the OpenLogi desktop GUI and the Actions-Ring overlay communicate with the hardware-isolated agent via a cross-platform IPC transport using tarpc RPCs over local sockets, with each client declaring its role through a versioned handshake protocol.**

The OpenLogi project (AprilNEA/OpenLogi) isolates all hardware access within the `openlogi-agent` process. Both the desktop application (`openlogi-desktop`) and the overlay interface (`openlogi-overlay`) reside in separate processes and rely on a shared IPC crate (`openlogi-ipc`) to exchange structured messages with the agent. This architecture ensures consistent device management regardless of which frontend initiates the request.

## The IPC Transport Layer

All communication traverses a **cross-platform local socket** implemented in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs). On macOS and Linux, this uses a Unix-domain socket; on Windows, it uses a named pipe. The transport layer wraps this stream with **length-delimited framing** and **bincode serialization** to carry tarpc RPC messages between clients and the agent.

The `openlogi-ipc` crate abstracts platform differences, presenting a uniform `client::connect()` API that both the GUI and overlay consume. This design ensures that connection establishment and stream management remain consistent across operating systems.

## The Connection Handshake

Before issuing device commands, every client must complete a two-step handshake defined in the tarpc service contract. According to [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs), the client first invokes `protocol_version()` to verify wire compatibility. The current `PROTOCOL_VERSION` constant is set to **29**; a mismatch causes the GUI to restart the agent or the overlay to exit immediately.

After version validation, the client declares its identity by calling `declare_client()` with a `ClientKind` enum:

- **`ClientKind::Gui`** – Indicates the desktop settings application.
- **`ClientKind::Overlay`** – Indicates the Actions-Ring heads-up display.

This declaration occurs in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) for the GUI and in [`crates/openlogi-overlay/src/agent.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/agent.rs) for the overlay. The agent uses this information to arm specific subsystems or remain dormant when only the overlay is active.

## GUI-to-Agent Communication

The GUI spawns a dedicated thread running a Tokio runtime to manage its `AgentClient` lifecycle. In [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs), the `observe_loop` function establishes the connection via `openlogi_ipc::client::connect()`, then enters a blocking loop calling `observe()`.

This **long-polling** approach allows the agent to push state changes (`GuiUpdate::Snapshot`) only when hardware state mutates, rather than wasting CPU on constant polling. When the GUI issues device commands—such as setting DPI, adjusting lighting, or triggering pairing—it sends fire-and-forget RPCs through the same `AgentClient` instance. Results return asynchronously via an internal `mpsc` channel back to the GPUI main loop.

```rust
// GUI: Spawn the IPC client thread
let ipc = openlogi_desktop::services::ipc::spawn();

// Inside the client thread: connect and declare the GUI role
let connection = openlogi_ipc::client::connect().await?;
connection.client
    .declare_client(context::current(), ClientKind::Gui)
    .await?;

// GUI: Request a state update (blocking until generation changes)
let ctx = context::current();
let obs = client.observe(ctx, last_generation).await?;

```

## Overlay-to-Agent Communication

The overlay follows a similar connection pattern but optimizes for low-latency input handling. In [`crates/openlogi-overlay/src/agent.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/agent.rs), the `spawn_ipc()` function initiates the connection and declares `ClientKind::Overlay`. It then splits execution into two concurrent tasks:

1. **`poll_invocations`** – Calls `observe_action_ring()` to block until the Actions-Ring state changes, pushing updates onto an `mpsc::UnboundedReceiver` for the UI thread.
2. **`send_commands`** – Listens for UI events (`Hover`, `Activate`, `Cancel`) and forwards them via `action_ring_hover()`, `action_ring_activate()`, and `action_ring_cancel()`. This task handles command coalescing and retry logic to ensure that rapid user inputs do not overwhelm the agent.

Unlike the GUI's general-purpose `observe()` method, the overlay uses `observe_action_ring()`, which returns a slimmer payload focused exclusively on ring invocation state.

```rust
// Overlay: Start the IPC runtime and declare the overlay role
let client = openlogi_ipc::client::connect().await?;
client.client
    .declare_client(context::current(), ClientKind::Overlay)
    .await?;

// Overlay: Watch the ring invocation
let obs = client.observe_action_ring(ctx, last_generation).await?;
if let Some(invocation) = obs.invocation {
    // Forward invocation to the overlay UI
}

```

## The RPC Protocol Contract

All method signatures reside in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) within the `Agent` tarpc service definition. This centralized contract ensures that the GUI, overlay, and agent share a strict type-safe boundary. The protocol supports:

- **State observation** (`observe`, `observe_action_ring`)
- **Device configuration** (DPI, lighting, pairing)
- **Ring interaction** (hover, activate, cancel)

Because tarpc generates client and server stubs from the same definition, compile-time checks prevent drift between the agent's implementation and the frontends' expectations.

## Summary

- **Transport**: Cross-platform local sockets (Unix domain/named pipes) with length-delimited bincode framing, implemented in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs).
- **Handshake**: Clients verify `protocol_version()` (currently 29) then declare their role via `ClientKind` enum.
- **GUI Pattern**: Long-polling via `observe()` in a dedicated Tokio thread, with commands and results flowing through `AgentClient` and `mpsc` channels.
- **Overlay Pattern**: Concurrent `poll_invocations` (blocking on `observe_action_ring`) and `send_commands` tasks for real-time input handling.
- **Safety**: Protocol version mismatches trigger immediate client termination or agent restart, preventing undefined behavior across API boundaries.

## Frequently Asked Questions

### What transport protocol does OpenLogi use for IPC?

OpenLogi uses **tarpc** (a Tokio-based RPC library) over a raw byte stream provided by local sockets. The transport layer adds length-delimited framing and bincode serialization on top of Unix-domain sockets (macOS/Linux) or named pipes (Windows), as defined in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs).

### How does the agent distinguish between GUI and overlay clients?

During the initial handshake, clients call `declare_client()` with a `ClientKind` variant—either `Gui` or `Overlay`—defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs). The agent uses this declaration to selectively enable features; for example, it may keep power-hungry subsystems dormant when only the overlay is connected.

### What happens if the protocol version mismatches between client and agent?

The client checks `PROTOCOL_VERSION` (currently 29) immediately after connecting. If the agent reports a different version, the GUI triggers an agent restart to align binaries, while the overlay process exits cleanly. This prevents serialization errors from corrupting device state.

### Why does the GUI use a blocking observe call instead of polling?

The `observe()` method implements **long-polling**: it blocks until the agent's observable state generation counter increments or a timeout expires. This reduces CPU usage and latency compared to traditional polling loops, ensuring the GUI reflects hardware changes within milliseconds without consuming resources during idle periods.