How OpenLogi Architecture Works: GUI and Agent Process Communication

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. 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, 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.

// 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, 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.
// 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 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.

// 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.

// 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →