# How OpenLogi Architecture Works: GUI and Agent Process Communication

> Explore the OpenLogi architecture Discover how the GUI client communicates with the agent process using tarpc for seamless hardware access and state management. Learn more today.

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

---

**OpenLogi implements a strict client-server architecture where the GUI runs as a pure IPC client that communicates with a background agent process via tarpc, while the agent exclusively owns hardware access and maintains observable state through long-polling RPC calls.**

OpenLogi is an open-source device management application that separates user interface concerns from hardware operations through a well-defined inter-process communication (IPC) layer. Understanding how the OpenLogi architecture coordinates its GUI and agent processes reveals a robust design pattern for desktop applications requiring privileged hardware access. The system distributes work across three distinct tiers—`openlogi-desktop`, `openlogi-agent`, and shared core libraries—to ensure security and stability, as documented in the repository's **AGENTS.md** file.

## OpenLogi Architecture Overview

The codebase ships three processes bundled together but operating with distinct responsibilities:

- **GUI Process** (`openlogi-desktop` + `openlogi-overlay`): Acts as a pure IPC client that renders the UI, receives user input, and forwards device commands. It never touches hardware directly.
- **Agent Process** (`openlogi-agent`): Runs as a background server that owns the input hook, performs all HID++ device I/O, and maintains the observable state.
- **Shared Orchestration** (`openlogi-agent-core`, `openlogi-ipc`): Provides the IPC contract and runtime utilities used by both sides, ensuring type safety across process boundaries.

## IPC Contract and Wire Protocol

The GUI and Agent communicate through a strict request/response RPC defined by the **`Agent`** trait in [`openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-ipc/src/ipc.rs). This contract uses **tarpc** to generate client and server glue, with **bincode** handling serialization.

The wire format stores the *variant index* of enum methods, making **method order part of the binary protocol**. As noted in lines 41-45 of [`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs), methods must only be appended to maintain backward compatibility. The first method, **`protocol_version`**, performs handshake validation against `PROTOCOL_VERSION` (currently 29) to ensure GUI and Agent compatibility.

```rust
// Excerpt from openlogi-ipc/src/ipc.rs
#[tarpc::service]
pub trait Agent {
    async fn protocol_version() -> u16;
    async fn set_dpi(route: DeviceRoute, dpi: Dpi) -> Result<(), WriteError>;
    async fn observe(since: Generation) -> Observation;
    // ... additional methods
}

```

## Agent Process Implementation

The agent implements the `Agent` trait in [`openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent/src/server.rs), serving as the single source of truth for device state. It holds the input hook, HID++ backends, and device enumeration caches.

Key responsibilities include:

- **Observable State Management**: Exposes `AgentStatus`, `AgentSnapshot`, and `Observation` types that represent the complete system state.
- **Long-Polling RPC**: Provides the **`observe(since: Generation)`** method that blocks until state changes or the `OBSERVE_HOLD` timeout (20 seconds) expires. This level-triggered approach allows the GUI to reconnect without missing updates.
- **Hardware Commands**: Handles mutations like `set_dpi`, `set_lighting`, and `start_pairing` through the HID++ protocol layer.

```rust
// Agent-side implementation excerpt from server.rs
async fn set_dpi(
    ctx: Context,
    route: DeviceRoute,
    dpi: Dpi,
) -> Result<(), WriteError> {
    // The agent owns the HID backend; perform the write here.
    agent_core::write::set_dpi(route, dpi).await
}

```

## GUI Client Implementation

The desktop GUI creates an **`IpcClient`** in [`openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-desktop/src/services/ipc.rs) that manages connection lifecycle and state synchronization.

### Connection and Handshake

The **`ensure()`** function (lines 54-87) opens the local socket, checks `PROTOCOL_VERSION`, and declares the client kind as `ClientKind::Gui`. If the agent socket is absent, the **`SpawnReflex`** utility decides when to launch the agent based on `SPAWN_AFTER_LOSS` and `SPAWN_RETRY_PERIOD` timers, preventing rapid respawn loops while ensuring recovery from crashes.

### Observation Loop

The **`observe_loop()`** maintains exactly one in-flight `observe` request at all times (lines 78-84). When a new `Observation` arrives containing a higher generation number, the GUI receives a `GuiUpdate::Snapshot` with the full `AgentSnapshot`. If the hold expires without state changes, the same snapshot serves as a heartbeat.

```rust
// GUI-side IPC client creation
use openlogi_desktop::services::ipc::IpcClient;

let ipc = IpcClient::spawn();

std::thread::spawn(move || {
    while let Some(update) = ipc.updates.recv() {
        match update {
            GuiUpdate::Snapshot(snapshot) => {
                ui::update_from_snapshot(snapshot);
            }
            GuiUpdate::Unreachable => ui::show_error("Agent not reachable"),
            GuiUpdate::OutdatedGui => ui::show_error("Please restart OpenLogi"),
            _ => {}
        }
    }
});

```

### Command Handling

UI actions (DPI adjustments, lighting changes, pairing initiation) are sent through an `mpsc` channel to the IPC thread as **`Command`** enum variants. The thread re-uses the existing tarpc connection, ensuring that pending `observe` requests remain undisturbed. Errors translate into UI-visible `GuiUpdate` variants like `LightCommandResult` or `PairingUndeliverable`.

```rust
// Sending a DPI command from the UI
use openlogi_core::hid::{DeviceRoute, Dpi};

let route = DeviceRoute::new(/* receiver, device id */);
let dpi = Dpi::new(800);

let _ = ipc.commands.send(Command::SetDpi(route, dpi));

```

## State Flow and Lifecycle

The OpenLogi architecture follows a specific lifecycle for coordination:

1. **Startup**: The GUI spawns the IPC client thread, which attempts connection to the agent's local socket. If absent, `SpawnReflex` may trigger `launch::spawn_agent`.

2. **Handshake**: `ensure()` validates protocol compatibility and declares the client as `Gui`. Mismatches return `ConnectFailure::NewerAgent`, triggering an "Outdated GUI" banner.

3. **Initial Observation**: The client calls `observe(0)` to receive the current `AgentSnapshot` immediately, storing the returned generation number.

4. **Continuous Updates**: The agent completes pending `observe` calls whenever internal watchers (device enumeration, hook state, pairing sessions) detect changes, sending new generations to the GUI.

5. **Command Execution**: UI actions become RPC calls multiplexed over the same connection. The tarpc transport handles concurrent request/response pairs without canceling the long-poll.

## Resilience Mechanisms

The architecture includes specific safeguards for production reliability:

- **Protocol Version Enforcement**: If the agent reports a higher version than the GUI supports, the client returns `ConnectFailure::NewerAgent`, prompting the user to restart the application.
- **Connection Regeneration**: The generation counter lives with the connection struct (`LiveConnection.seen`). When the agent restarts, the connection drops, the socket recreates, and the first `observe` call returns the full state snapshot again.
- **Heartbeat Timeouts**: `OBSERVE_HOLD` (20 seconds) plus `OBSERVE_DEADLINE` ensure the GUI can detect agent unavailability even during periods of no state changes.

## Summary

- The **OpenLogi architecture** strictly separates hardware access (agent) from user interface (GUI) through a tarpc-based IPC layer.
- **Bincode serialization** over local sockets requires immutable method ordering in the `Agent` trait to maintain wire compatibility.
- The **agent process** exclusively owns HID++ I/O and exposes state through generation-tracked `Observation` snapshots.
- The **GUI client** uses a single long-polling `observe` request with 20-second hold timeouts to receive real-time updates without polling overhead.
- Automatic agent spawning and **protocol version 29** handshake ensure the system recovers gracefully from crashes and version mismatches.

## Frequently Asked Questions

### What is the role of the agent process in OpenLogi?

The agent process (`openlogi-agent`) serves as the sole hardware gateway, owning all HID++ device I/O, input hooks, and device enumeration. It runs as a background server that responds to RPC requests from the GUI, maintaining observable state through the `observe` method while executing commands like `set_dpi` or `start_pairing` against physical devices.

### How does the GUI receive real-time device state updates?

The GUI maintains a single in-flight `observe` RPC call that blocks until the agent detects state changes or the 20-second `OBSERVE_HOLD` timeout expires. When the agent returns an `Observation` with a higher generation number than previously seen, the GUI receives a `GuiUpdate::Snapshot` containing the complete `AgentSnapshot`, ensuring the UI always reflects the current system state.

### What happens if the GUI and agent have different protocol versions?

During the handshake phase, the `ensure()` function calls `protocol_version` and compares the result against the GUI's compiled `PROTOCOL_VERSION` (currently 29). If the agent reports a higher version, the client returns `ConnectFailure::NewerAgent`, causing the GUI to display an "Outdated GUI" banner that requires an application restart to resolve.

### Why does OpenLogi use a separate agent process instead of direct hardware access?

Separating hardware access into a distinct agent process improves security and stability by isolating privileged operations (input hooks and HID++ communication) from the GUI process. This architecture allows the GUI to run with standard user permissions while the agent handles device-level I/O, and enables automatic agent recovery and version checking without crashing the user interface.