OpenLogi IPC Contract: How the GUI, Agent, and Overlay Communicate

OpenLogi’s IPC contract is defined by a single tarpc service trait named Agent located in crates/openlogi-ipc/src/ipc.rs, serving as the exclusive wire format for all communication between the GUI desktop client, background agent, and Actions-Ring overlay.

The AprilNEA/OpenLogi repository centralizes its inter-process communication in the openlogi-ipc crate. This contract establishes a strict, versioned boundary that allows the GUI, agent daemon, and overlay helper to operate as decoupled binaries while maintaining synchronized state through method calls rather than ad-hoc message passing.

Core Architecture and the tarpc Service Trait

The entire contract is implemented as a tarpc service trait called Agent in crates/openlogi-ipc/src/ipc.rs. This trait generates the client and server stubs that handle serialization, transport, and method dispatch across the Unix domain socket.

Because tarpc encodes method calls as enum variants, the order of methods in the Agent trait constitutes the wire format. New methods must be appended to the end of the trait definition to maintain backward compatibility (see comments around lines 40–46). Reordering or inserting methods would break existing clients because the enum index changes.

Protocol Versioning and Handshake

On connection, the GUI verifies binary compatibility through a strict versioning protocol to prevent serialization errors.

  • protocol_version() returns the constant PROTOCOL_VERSION (currently set to 30 at line 65)
  • The GUI aborts immediately if the returned version does not match its compile-time expectation
  • PROTOCOL_VERSION is only incremented when breaking changes occur to the types or method signatures (see version history comments at lines 26–50)

This handshake ensures that mismatched GUI and agent binaries cannot connect, preventing undefined behavior from schema mismatches.

State Observation Pattern (Long-Polling)

Instead of using push-based subscriptions or WebSocket streams, the agent implements level-triggered long-polling. Clients initiate requests that block until state changes or a timeout occurs.

  • observe(since: Generation) -> Observation blocks until any observable state changes, returning a full AgentSnapshot containing AgentStatus, device inventory, PairingPhase, and foreground applications. The GUI relies on this as its primary synchronization mechanism.
  • observe_action_ring(since: Generation) -> RingObservation provides the overlay with ActionRingInvocation data (slots, labels, icons SVG) when the user triggers the ring or when the state expires.
  • Both methods respect the OBSERVE_HOLD timeout of approximately 20 seconds, after which they return the current state anyway, allowing clients to detect liveness without persistent connections.

This design eliminates the need for subscription management, replay buffers, or complex backpressure logic on the agent side.

Client Declaration and Overlay Specialization

Clients must declare their type to receive appropriate treatment and security permissions.

  • declare_client(kind: ClientKind) registers the connection as either a GUI, CLI, or Overlay client
  • The overlay sends ClientKind::Overlay to identify itself as a non-arming client that should not trigger security-sensitive actions
  • identity() -> Identity returns a stable token that the overlay uses to detect agent restarts and reset its local Generation counter to zero

The overlay’s required method set is minimal: declare_client(ClientKind::Overlay), identity(), and observe_action_ring(). It never performs direct device I/O; all actions are executed by the agent on its behalf.

Key Service Methods and Data Types

The Agent trait exposes methods organized by functional domain:

Status and Inventory

  • status() -> AgentStatus returns the current agent state
  • inventory() -> Vec<DeviceInventory> lists connected peripherals
  • snapshot() -> AgentSnapshot combines status, inventory, pairing progress, and foreground applications into a single struct for atomic UI updates

Configuration

  • reload_config(), set_dpi(), read_dpi(), set_smartshift(), read_smartshift(), set_lighting(), set_light(), set_light_manual_power()
  • Returns ConfigReloadError or WriteError on failure

Pairing

  • start_pairing(), pair_device(), cancel_pairing(), next_pairing() (legacy stream)
  • PairingCommandError maps to PairingFailure for error handling
  • States tracked via PairingPhase and PairingUpdate

Event Monitoring

  • poll_event_monitor() streams MonitorEvent structs containing live mouse button and scroll events for debugging or macro recording

Actions-Ring Control

  • next_action_ring() (legacy), action_ring_hover(), action_ring_activate(), action_ring_cancel()
  • Returns ActionRingCommandError on invalid operations

Implementation Reference

File Role
crates/openlogi-ipc/src/ipc.rs Defines the Agent trait, all request/response types, ClientKind enum, PROTOCOL_VERSION, and versioning comments
crates/openlogi-desktop/src/services/ipc.rs GUI client implementation that creates AgentClient and drives the observe() loop
crates/openlogi-overlay/src/main.rs Overlay helper that declares ClientKind::Overlay and consumes observe_action_ring()

Code Examples

Overlay Connection Pattern

use openlogi_ipc::AgentClient;
use openlogi_ipc::{ClientKind, Generation};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = AgentClient::connect("/tmp/openlogi.sock").await?;
    
    // Declare overlay client type
    client.declare_client(ClientKind::Overlay).await?;
    
    // Detect agent restarts via stable identity
    let _id = client.identity().await?;
    
    // Poll for ring invocations using generation counters
    let mut gen: Generation = 0;
    loop {
        let ring_obs = client.observe_action_ring(gen).await?;
        gen = ring_obs.generation;
        if let Some(inv) = ring_obs.invocation {
            // Render ring using inv.slots, inv.language, etc.
        }
    }
}

GUI State Synchronization Loop

let client = AgentClient::connect("/tmp/openlogi.sock").await?;
let mut gen: Generation = 0;
loop {
    // Block until any state changes (20s max timeout)
    let obs = client.observe(gen).await?;
    gen = obs.generation;
    update_ui(obs.snapshot); // Contains full AgentSnapshot
}

Summary

  • The OpenLogi IPC contract is a single tarpc Agent trait in crates/openlogi-ipc/src/ipc.rs shared by all components.
  • Protocol version 30 enforces binary compatibility; the GUI aborts immediately on mismatched agents.
  • Long-polling via observe() and observe_action_ring() replaces push notifications, using Generation counters to track state changes without subscription management.
  • The overlay declares ClientKind::Overlay and uses identity() to handle restarts, while the GUI relies on comprehensive AgentSnapshot updates for its interface.

Frequently Asked Questions

What transport layer does OpenLogi use for IPC?

The agent binds to a Unix domain socket at a path such as /tmp/openlogi.sock. The tarpc library handles framing and serialization over this socket, providing typed async methods for the GUI and overlay clients according to the contract defined in crates/openlogi-ipc/src/ipc.rs.

How does the overlay detect if the agent has restarted?

The overlay calls identity() immediately after connection to receive a stable Identity token. If a subsequent observe_action_ring() call returns an error or the connection drops, the overlay reconnects and compares the new identity. A changed token indicates an agent restart, prompting the overlay to reset its Generation counter to zero and resynchronize state.

Why does OpenLogi use long-polling instead of push notifications?

The observe() and observe_action_ring() methods implement long-polling with a 20-second timeout (OBSERVE_HOLD) rather than server-sent events or WebSocket pushes. This eliminates the need for the agent to maintain subscription state or replay buffers for disconnected clients, simplifying the implementation while still providing near real-time updates through the AgentSnapshot and RingObservation types.

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

When the GUI connects, it calls protocol_version() and compares the result against its compile-time PROTOCOL_VERSION constant (currently 30). If they differ, the GUI aborts the connection immediately. This prevents serialization errors or logic bugs that could arise from mismatched message formats according to the strict contract versioning policy documented in ipc.rs lines 26–50.

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 →