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

> Understand the OpenLogi IPC contract. Discover how the GUI, agent, and overlay communicate seamlessly via a single tarpc service trait.

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

---

**OpenLogi’s IPC contract is defined by a single tarpc service trait named `Agent` located in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) | GUI client implementation that creates `AgentClient` and drives the `observe()` loop |
| [`crates/openlogi-overlay/src/main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/main.rs) | Overlay helper that declares `ClientKind::Overlay` and consumes `observe_action_ring()` |

## Code Examples

**Overlay Connection Pattern**

```rust
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**

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs) lines 26–50.