How Inter-Process Communication (IPC) Works in OpenLogi: Architecture and Implementation

OpenLogi implements IPC through a three-layer architecture in the openlogi-ipc crate, using cross-platform local sockets for transport, length-delimited bincode for serialization, and tarpc for type-safe RPC between the GUI client and background agent.

The AprilNEA/OpenLogi repository uses a dedicated IPC crate to coordinate between its graphical interface and system-level agent process. This design ensures deterministic binary communication across Unix and Windows platforms without requiring heavy dependencies in the GUI binary.

IPC Architecture Overview

The implementation rests on three distinct foundations that isolate transport concerns from application logic:

  • Transport Layer: Cross-platform local-socket abstraction from the interprocess crate, providing Unix-domain sockets on Unix systems and named pipes on Windows
  • Message Framing: Tarpc's serde_transport::Transport wrapped with length-delimited bincode encoding for deterministic binary serialization
  • Service Definition: A pure-RPC contract defined with tarpc in crates/openlogi-ipc/src/ipc.rs, exposing the Agent trait that enumerates all callable methods

This architecture routes all communication through a single endpoint—openlogi-agent.sock on Unix or \\.\pipe\openlogi-agent.sock on Windows—accessible to both the GUI client and the background agent.

Transport Layer Implementation

The transport layer in crates/openlogi-ipc/src/transport.rs handles platform-specific socket creation and stream management while presenting a unified async interface.

Server-Side Binding

The agent initiates the IPC server by calling transport::bind() in crates/openlogi-agent/src/server.rs. This function:

  1. Resolves the endpoint name via endpoint_name(), expanding to ~/.config/openlogi/agent.sock on Unix or the named pipe path on Windows
  2. Ensures parent directories exist on Unix systems
  3. Creates a listener with ListenerOptions::new().try_overwrite(true).create_tokio(), where try_overwrite cleans up stale sockets from previous crashes
  4. Yields a tokio::net::UnixStream (Unix) or interprocess::local_socket::tokio::Stream (Windows) wrapped via transport::wrap() before handing to Tarpc

Client-Side Connection

GUI clients establish connections through client::connect() in crates/openlogi-ipc/src/client.rs:

let stream = transport::connect().await?;
let client = AgentClient::new(client::Config::default(), transport::wrap(stream)).spawn();
let version = client.protocol_version(context::current()).await?;

The transport::connect() function creates a stream to the shared endpoint, applies the same bincode wrapping, and spawns a Tarpc client. Immediately after connection, the GUI performs a version handshake by calling protocol_version, comparing the returned constant (currently 29) against its own supported protocol version to verify compatibility.

RPC Protocol and Message Framing

Bincode Serialization

The wrap function in transport.rs applies length-delimited bincode encoding to all socket streams. This guarantees that every RPC payload receives deterministic, version-stable binary encoding where each field serializes exactly once, eliminating ambiguity in the wire format across different releases.

The Agent Service Contract

The Agent trait defined in crates/openlogi-ipc/src/ipc.rs serves as the canonical API contract. Tarpc generates two critical components from this definition:

  • AgentClient: The client stub used by GUI and overlay processes
  • serve glue: The server-side dispatcher implemented in the agent

Key RPC methods include:

  • protocol_version: Returns the current wire protocol version (29) for compatibility checking
  • observe(since): Implements long-polling state observation; blocks until the agent's observable state changes or hits the OBSERVE_HOLD timeout
  • next_pairing and next_action_ring: Stream-like RPCs delivering one-shot updates for specific events
  • declare_client(kind): Registers the client type (GUI vs. overlay) for platform-specific feature gating

All methods follow an append-only evolution strategy documented in the version-history comments of ipc.rs. New methods extend the trait without altering existing signatures, ensuring forward compatibility where newer agents can serve older GUI clients.

State Observation Pattern

Rather than implementing push-based notifications, OpenLogi uses a pull-based observation model that simplifies state synchronization and eliminates subscription bookkeeping.

The observe method accepts a Generation parameter representing the last seen state version:

use openlogi_ipc::{client, Observation, Generation};

async fn monitor_changes() -> Result<(), Box<dyn std::error::Error>> {
    let mut conn = client::connect().await?;
    let mut gen: Generation = 0;
    loop {
        // Blocks until state changes or timeout
        let Observation { generation, snapshot } = conn.client.observe(gen).await?;
        gen = generation;
        println!("New snapshot: {:?}", snapshot);
    }
}

When called, the server blocks until the current generation exceeds the provided since value, then returns the complete state snapshot. This pattern ensures the client always receives full state contexts rather than delta updates, avoiding complex replay logic or missed transition events during reconnections.

Cross-Platform Endpoint Resolution

The IPC layer automatically adapts to host operating systems through the endpoint_name() helper:

  • Unix: Creates sockets under ~/.config/openlogi/agent.sock within the user's config directory
  • Windows: Uses the named pipe \\.\pipe\openlogi-agent.sock

This abstraction allows both openlogi-agent/src/server.rs and the mock agent in crates/openlogi-agent/src/bin/mock_agent.rs to bind identical logical endpoints without platform-conditional compilation at the call site.

Summary

  • OpenLogi IPC resides in the openlogi-ipc crate, isolating communication logic from HID and GUI concerns
  • Transport uses the interprocess crate for Unix-domain sockets or named pipes, wrapped in transport.rs
  • Serialization employs length-delimited bincode through Tarpc's transport wrapper for deterministic binary encoding
  • RPC contract is defined by the Agent trait in ipc.rs, generating AgentClient for callers and serve implementations for the agent
  • Version handshake occurs via protocol_version (currently returning 29) to prevent incompatible client-agent pairings
  • State observation uses generation-tracked long-polling via the observe method rather than server-push mechanisms
  • Append-only wire format ensures forward compatibility as the protocol evolves

Frequently Asked Questions

How does OpenLogi handle stale socket files from crashed agents?

The transport::bind() implementation in openlogi-agent/src/server.rs passes try_overwrite(true) to ListenerOptions. This flag forces removal of existing socket files at the endpoint path before binding, preventing "address already in use" errors when restarting the agent after a crash.

Why does the IPC use bincode instead of JSON or Protocol Buffers?

Bincode provides deterministic binary encoding where field order and serialization are predictable and stable across Rust versions. As implemented in transport.rs, length-delimited bincode frames guarantee that every RPC payload encodes exactly once, creating a compact, version-stable wire format without the schema complexity of Protocol Buffers or the non-deterministic field ordering of JSON.

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

During client::connect(), the GUI calls protocol_version immediately after establishing the transport. If the returned value (currently hardcoded as 29 in the source) differs from the GUI's expected version, the connection fails before any business-logic RPCs execute. This prevents serialization mismatches that could corrupt state or cause undefined behavior.

Can multiple GUI clients connect to the same agent simultaneously?

Yes. The agent's server implementation in openlogi-agent/src/server.rs runs an accept loop that spawns a new Tarpc channel for every incoming connection. Each client receives its own AgentClient instance, though the agent implementation in ipc.rs must internally synchronize access to shared hardware resources (HID devices) to prevent conflicts between multiple controlling processes.

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 →