How OpenLogi's IPC Architecture Separates GUI, Agent, and Overlay Processes

OpenLogi uses a single tarpc-based IPC channel with client kind declarations and role-specific observable streams to cleanly isolate the GUI, Agent, and Overlay processes while avoiding shared mutable state.

OpenLogi is an open-source input device configuration tool that splits functionality across three distinct processes. Understanding its IPC architecture reveals how modern Rust applications achieve clean separation of concerns without sacrificing performance. The design centers on the Agent process as the single source of truth, with the GUI (openlogi-desktop) and Overlay (openlogi-overlay) acting as specialized consumers connected through crates/openlogi-ipc/src/ipc.rs.

The Three Process Roles

Each process in OpenLogi's architecture has a narrowly defined responsibility:

Process Binary Core Purpose Connection Model
Agent openlogi-agent HID input hooks, device enumeration, DPI/lighting writes, canonical state storage Listens on Unix/Windows socket via openlogi-ipc::transport
GUI openlogi-desktop Configuration UI, full state polling, user-visible settings tarpc client calling Agent::protocol_version, Agent::status, Agent::snapshot
Overlay openlogi-overlay Actions Ring display, hover/activate/cancel event reporting tarpc client with ClientKind::Overlay using observe_action_ring and OverlayCommand

Client Kind Declaration: The Role Verification Mechanism

After version handshake, every client must explicitly declare its role through declare_client(kind). This declaration in crates/openlogi-ipc/src/ipc.rs (lines 102-106) enables the agent to enforce role-specific policies:

// Overlay declaration
client.declare_client(context::current(), ClientKind::Overlay).await.ok()?;

// GUI declaration  
client.declare_client(context::current(), ClientKind::Gui).await.ok()?;

The ClientKind enum distinguishes between Gui and Overlay variants, allowing the agent to apply different behaviors—for example, the macOS dormancy gate only arms for ClientKind::Gui.

Stream Separation: Right-Sized Data for Each Client

OpenLogi's IPC architecture prevents unnecessary data transfer through dedicated observable streams defined in crates/openlogi-ipc/src/ipc.rs:

Full State Stream for GUI

The GUI receives complete system state via observe(since) (lines 550-557), a level-triggered channel returning the full AgentSnapshot:

// GUI side polling loop
let mut gen = 0;
loop {
    let obs = client.observe(context::current(), gen).await?;
    gen = obs.generation;
    // obs.snapshot contains status, inventory, pairing, etc.
    update_ui(obs.snapshot);
}

Minimal Ring Stream for Overlay

The overlay uses observe_action_ring(since) (lines 554-558), receiving only an optional ActionRingInvocation:

// Overlay side observation loop
let mut gen = 0;
loop {
    let ring_obs = client.observe_action_ring(context::current(), gen).await?;
    gen = ring_obs.generation;
    match ring_obs.invocation {
        Some(inv) => draw_ring(inv),
        None => hide_ring(),
    }
}

This payload isolation keeps the overlay lightweight—critical for a helper that must render UI overlays with minimal latency.

Bidirectional Overlay Communication

The overlay sends user interactions back to the agent through OverlayCommand, defined in crates/openlogi-overlay/src/agent.rs (lines 30-41):

pub enum OverlayCommand {
    Hover { session_id: Uuid, slot: u8 },
    Activate { session_id: Uuid, slot: u8 },
    Cancel { session_id: Uuid },
}

The agent's spawn_ipc routine handles this with two concurrent async tasks:

  1. Pushing observe_action_ring updates to the overlay
  2. Receiving OverlayCommand messages from the overlay
// Sending actions from overlay to agent
commands.send(OverlayCommand::Hover { session_id, slot }).ok();
commands.send(OverlayCommand::Activate { session_id, slot }).ok();
commands.send(OverlayCommand::Cancel { session_id }).ok();

Process Supervision and Lifecycle Management

The agent enforces single-overlay semantics through crates/openlogi-agent/src/overlay.rs (lines 23-34). A run token (OPENLOGI_RUN) passed via environment variable identifies the current agent session:

  • New agent starts → generates fresh run token
  • Overlay supervisor launches overlay with token
  • Stale overlays from previous runs self-terminate when token mismatch detected

This enables independent restarts—the overlay can crash and restart without disturbing the GUI or agent state.

Transport Layer Implementation

The low-level socket handling resides in crates/openlogi-ipc/src/transport.rs, abstracting platform differences:

  • Unix: UnixListener on filesystem socket
  • Windows: NamedPipe for inter-process communication

Both transport types present identical tarpc service contracts to higher layers.

Complete Connection Flow

// === Agent initialization ===
openlogi_agent::overlay::spawn(); // Start supervisor thread

// === GUI connection sequence ===
let client = openlogi_ipc::client::connect().await?;
client.declare_client(context::current(), ClientKind::Gui).await?;
// Begin polling observe(since)

// === Overlay connection sequence ===
let client = openlogi_ipc::client::connect().await?;
client.declare_client(context::current(), ClientKind::Overlay).await?;
// Begin polling observe_action_ring(since) + sending OverlayCommands

Key Architectural Benefits

  • No shared mutable state between processes—only message passing via tarpc
  • Independent failure domains—overlay restart doesn't affect GUI or agent
  • Bandwidth optimization—each client receives precisely the data it needs
  • Policy enforcement—agent can apply role-specific rules based on ClientKind

Summary

OpenLogi's IPC architecture achieves clean process separation through:

  • A single tarpc socket multiplexing all client connections
  • Mandatory declare_client(kind) calls that establish role identity
  • Separate observable streams (observe vs observe_action_ring) right-sizing data transfer
  • OverlayCommand enum for lightweight bidirectional overlay communication
  • Run token supervision ensuring overlay lifecycle alignment with agent sessions

The implementation in AprilNEA/OpenLogi demonstrates how Rust's type system and async runtimes enable sophisticated multi-process architectures without runtime overhead.

Frequently Asked Questions

What IPC library does OpenLogi use for process communication?

OpenLogi uses tarpc, a Rust RPC framework built on Tokio. The service contract is defined as a trait in crates/openlogi-ipc/src/ipc.rs, with implementations for both client and server sides. Transport uses platform-native async sockets via crates/openlogi-ipc/src/transport.rs.

Why does the overlay use a separate stream instead of the full state?

The observe_action_ring stream minimizes payload size and parsing overhead. The overlay only needs ring visibility state, not the full device inventory, DPI settings, and pairing information contained in AgentSnapshot. This keeps the overlay responsive as a UI helper.

Can multiple GUI instances connect to the same agent simultaneously?

According to the source code, the architecture supports multiple concurrent connections. However, role-specific policies may apply—some operations gated by ClientKind::Gui could create contention. The design prioritizes single-GUI operation with overlay as secondary helper.

How does the agent prevent overlay processes from accumulating across restarts?

The overlay supervisor in crates/openlogi-agent/src/overlay.rs passes an OPENLOGI_RUN environment variable to each spawned overlay. When a new agent starts with a fresh token, existing overlays detect the mismatch and self-terminate, ensuring at most one overlay runs per agent session.

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 →