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
interprocesscrate, providing Unix-domain sockets on Unix systems and named pipes on Windows - Message Framing: Tarpc's
serde_transport::Transportwrapped 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 theAgenttrait 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:
- Resolves the endpoint name via
endpoint_name(), expanding to~/.config/openlogi/agent.sockon Unix or the named pipe path on Windows - Ensures parent directories exist on Unix systems
- Creates a listener with
ListenerOptions::new().try_overwrite(true).create_tokio(), wheretry_overwritecleans up stale sockets from previous crashes - Yields a
tokio::net::UnixStream(Unix) orinterprocess::local_socket::tokio::Stream(Windows) wrapped viatransport::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 processesserveglue: The server-side dispatcher implemented in the agent
Key RPC methods include:
protocol_version: Returns the current wire protocol version (29) for compatibility checkingobserve(since): Implements long-polling state observation; blocks until the agent's observable state changes or hits theOBSERVE_HOLDtimeoutnext_pairingandnext_action_ring: Stream-like RPCs delivering one-shot updates for specific eventsdeclare_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.sockwithin 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-ipccrate, isolating communication logic from HID and GUI concerns - Transport uses the
interprocesscrate for Unix-domain sockets or named pipes, wrapped intransport.rs - Serialization employs length-delimited bincode through Tarpc's transport wrapper for deterministic binary encoding
- RPC contract is defined by the
Agenttrait inipc.rs, generatingAgentClientfor callers andserveimplementations 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
observemethod 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →