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

> Discover how OpenLogi implements IPC using local sockets, bincode, and tarpc within its three-layer architecture for efficient cross-process communication. Learn the details.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/client.rs):

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent/src/server.rs) and the mock agent in [`crates/openlogi-agent/src/bin/mock_agent.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs) must internally synchronize access to shared hardware resources (HID devices) to prevent conflicts between multiple controlling processes.