# OpenLogi Three-Process Architecture Communication: IPC Design and Implementation

> Explore the OpenLogi three-process architecture communication. Learn how tarpc/bincode local sockets ensure responsive, push-based state synchronization between GUI, Agent, and Overlay.

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

---

**OpenLogi uses a three-process architecture where the GUI, Agent, and Overlay communicate exclusively via a tarpc/bincode local socket, eliminating hardware access from the UI layer while maintaining responsive, push-based state synchronization.**

The **AprilNEA/OpenLogi** repository implements a local-first replacement for Logitech Options+ by decoupling device I/O from the user interface. This architecture relies on three specialized processes that coordinate through a strictly versioned IPC contract defined in the `openlogi-ipc` crate.

## The Three-Process Architecture

OpenLogi splits functionality across three isolated processes to enforce security boundaries and platform-specific responsibilities.

### GUI Process (openlogi-desktop)

The **GUI process** (`crates/openlogi-desktop`) renders the Settings UI, per-app profile picker, and Actions Ring overlay using the GPUI framework. Crucially, this process **never touches hardware directly**—all device interactions are delegated to the Agent via the IPC socket.

### Agent Process (openlogi-agent)

The **Agent process** (`crates/openlogi-agent`) is a headless background server that owns all mouse and keyboard HID++ device I/O, platform input hooks (macOS/Windows), and the Unix domain socket listener. It acts as the single source of truth for device state and executes all hardware commands.

### Overlay Process (openlogi-overlay)

The **Overlay process** (`crates/openlogi-overlay`) is a lightweight helper that draws the cursor-centered Actions Ring. It links only against `openlogi-ui` (containing presentation data like ring geometry and icons) and communicates with the Agent using the same IPC contract as the GUI, allowing it to operate independently without pulling in heavy GUI dependencies.

## The IPC Contract and Wire Protocol

All inter-process communication flows through a **tarpc/bincode local socket** defined in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs). The contract centers on a `pub trait Agent` service definition that both client and server implement.

### Protocol Versioning and Stability

The IPC protocol maintains strict backward compatibility through two mechanisms:

- **Method ordering**: New RPC methods must be **appended** to the trait to preserve bincode variant indices (see the comment at lines 18-22 in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs)). Reordering existing methods breaks the wire format.
- **Protocol version constant**: The `PROTOCOL_VERSION` constant (line 61) is checked during the initial handshake; a mismatch forces the GUI to relaunch and upgrade.

### Core RPC Methods

The `Agent` trait exposes read-only queries and fire-and-forget writes:

- **State observation**: `protocol_version()`, `status()`, `inventory()`, `snapshot()`, and `observe()` provide poll-free access to the complete Agent state.
- **Device control**: `set_dpi()`, `set_lighting()`, `set_smartshift()`, and `set_light()` return `Result<(), WriteError>` after performing HID++ operations.
- **Pairing workflows**: `start_pairing()`, `pair_device()`, `cancel_pairing()`, and `next_pairing()` manage device discovery.
- **Actions Ring**: `next_action_ring()`, `observe_action_ring()`, `action_ring_hover()`, and `action_ring_activate()` handle the overlay interactions.

## How the GUI Communicates with the Agent

The GUI spawns an IPC client thread in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) (function `spawn()` at lines 31-36) that manages the connection lifecycle.

### Connection Lifecycle

1. **Runtime creation**: The client creates a dedicated Tokio runtime on an OS thread (lines 39-45).
2. **Handshake**: It connects via `openlogi_ipc::client::connect()` and validates the remote `PROTOCOL_VERSION`. If the Agent is newer, the GUI emits `GuiUpdate::OutdatedGui` (lines 68-71).
3. **Long-polling**: The client maintains **one persistent `observe` request** at all times (lines 65-70). The Agent returns a full `AgentSnapshot` only when observable state changes (new devices, DPI adjustments, etc.), eliminating polling timers.
4. **Command channel**: UI actions send commands through an `mpsc::UnboundedSender<Command>` (lines 27-31). The client loop handles these via `handle()` and forwards them as RPC calls.

### Resilience and Auto-Spawning

If the socket disappears, the client enters a retry loop using `RECONNECT_DELAY` and `SPAWN_RETRY_PERIOD`. If the Agent is not running, the GUI can spawn a fresh binary via `spawn_agent()` (lines 75-86).

## Server Implementation in the Agent

The `AgentServer` implementation in [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs) (lines 90-138) holds the runtime state:

- **`orchestrator`**: Core state machine managing device configuration and DPI cycles.
- **`observable`**: An `Arc<ObservableState>` storing the complete GUI-visible state (status, inventory, foreground application).
- **Subsystem helpers**: `pairing`, `event_monitor`, `action_ring`, and `dispatcher` manage specific responsibilities.

Read-only RPC methods simply read from `observable` (e.g., `status()`, `inventory()`). Write methods route through `shared.device(&route).run(...)` to execute HID++ operations and return typed `WriteError` results.

The `observe(since)` method (line 22 of the trait) implements the state channel using a monotonically increasing `generation` counter. When state changes, the Agent immediately responds to pending `observe` calls with a fresh `Observation` containing the new `generation` and `AgentSnapshot`.

## Practical Implementation Examples

### Spawning the IPC Client

```rust
use openlogi_desktop::services::ipc::spawn;

// Initialize inside the GPUI app:
let ipc = spawn();               // Returns IpcClient { updates, commands }
let updates_rx = ipc.updates;    // Receive GuiUpdate events from Agent
let cmds_tx = ipc.commands;      // Send Command::SetDpi, etc.

```

*See the full implementation in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) (lines 31-36).*

### Requesting a One-Off Snapshot

```rust
use openlogi_ipc::client::connect;

// In an async context:
let conn = openlogi_ipc::client::connect().await?;
let snapshot = conn.client.snapshot(context::current()).await?;
println!("Agent version: {}", snapshot.status.agent_version);

```

*The `snapshot` RPC is defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) (line 118).*

### Changing Device DPI

```rust
use openlogi_core::hid::{DeviceRoute, Dpi};

let route = DeviceRoute::new(/*receiver*/ 0, /*device*/ 1, /*endpoint*/ 0);
let new_dpi = Dpi::new(1200);

// Fire-and-forget via the command channel
let _ = cmds_tx.send(Command::SetDpi(route, new_dpi));

```

*Commands are handled in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) (lines 95-97) and forwarded to the Agent's `set_dpi` method in [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs) (lines 34-42).*

### Observing State Changes

```rust
// Inside the client's observe_loop:
let observation = client.observe(context::current(), last_generation).await?;
let generation = observation.generation;
let snapshot = observation.snapshot;

// Update UI, then immediately re-poll
last_generation = generation;

```

*Implemented in the client at [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) (lines 71-78) and the server at [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs) (lines 22-24).*

## Summary

- OpenLogi isolates hardware access in the **Agent process**, UI rendering in the **GUI process**, and overlay drawing in the **Overlay process**.
- All communication uses a **tarpc/bincode local socket** defined by the `Agent` trait in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs).
- The protocol uses **append-only method definitions** and a `PROTOCOL_VERSION` constant to ensure wire compatibility.
- The GUI maintains a **long-poll `observe` connection** for push-based state updates, eliminating polling overhead.
- The system is resilient: the GUI auto-spawns the Agent if missing, and the Agent holds a single-instance lock to prevent duplicates.

## Frequently Asked Questions

### How do the OpenLogi processes communicate with each other?

The three processes communicate exclusively via a **local Unix socket** using the tarpc framework with bincode serialization. The contract is defined by the `pub trait Agent` in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs), which exposes methods for state observation (`observe`, `snapshot`) and device control (`set_dpi`, `set_lighting`). This socket-based approach ensures the GUI never requires hardware permissions, while the Agent runs with elevated privileges to access HID++ devices.

### What happens if the Agent process crashes while the GUI is running?

The GUI's IPC client ([`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs)) detects socket disconnection and enters a retry loop with configurable `RECONNECT_DELAY` and `SPAWN_RETRY_PERIOD`. If the Agent is not found, the GUI automatically spawns a new Agent binary via `spawn_agent()` (lines 75-86). Additionally, the Agent itself holds a single-instance lock (`agent.lock`) to prevent duplicate processes, and implements `takeover::try_replace_stale()` to restart if a stale version is detected.

### Why must new RPC methods be appended to the trait definition?

The IPC uses **bincode** for serialization, which assigns variant indices to enum and trait methods based on their declaration order. Adding methods in the middle or reordering existing ones shifts these indices, breaking wire compatibility with running processes. The comment at lines 18-22 in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs) explicitly warns that new methods must be appended to maintain backward compatibility. Breaking changes require bumping the `PROTOCOL_VERSION` constant (line 61) to force coordinated upgrades.

### How does the Overlay process render the Actions Ring without the GUI?

The Overlay links only against the `openlogi-ui` crate, which contains shared presentation data including ring geometry, icons, and locale strings. It communicates directly with the Agent using the same IPC contract methods (`observe_action_ring`, `action_ring_activate`) as the GUI. Because it inherits its own TCC identity (accessibility permissions) via `launch_agent()` in the IPC client (lines 15-19), it operates independently without sharing the GUI's accessibility grant or heavy rendering code.