OpenLogi Actions Ring: IPC State Management & Session Lifecycle Implementation

The OpenLogi Actions Ring uses a three-layer architecture with the agent-core crate holding authoritative session state, ActionRingManager exposing IPC APIs like begin(), hover(), and activate(), and a 15-second DISPLAY_LIFETIME constant governing automatic expiration across all components.

The Actions Ring in OpenLogi is a per-device radial menu that enables users to trigger configurable actions with a mouse button press. This system spans multiple crates in the OpenLogi codebase, with clear separation between configuration state, session management, and UI rendering. Understanding how OpenLogi handles IPC state management and the session lifecycle is essential for developers extending or debugging the ring functionality.

Actions Ring Architecture Overview

The implementation distributes responsibilities across four distinct layers:

Layer Responsibility Primary Source File
Display-duration constant Shared 15s overlay visibility timer crates/openlogi-core/src/action_ring.rs
Desktop state & editor UI-side configuration with TOML persistence crates/openlogi-desktop/src/state/action_ring.rs
Agent-core manager Authoritative session state & IPC API crates/openlogi-agent-core/src/action_ring.rs
Overlay UI Presentation-only rendering crates/openlogi-overlay/src/ring.rs

Core Constant: DISPLAY_LIFETIME

The foundation of session timing lives in crates/openlogi-core/src/action_ring.rs:

pub const DISPLAY_LIFETIME: Duration = Duration::from_secs(15);

This constant synchronizes the overlay timer and agent session expiry. The runtime session exists only in the agent, but both components reference this shared value to maintain consistent behavior.

Desktop Configuration State

The AppState struct in crates/openlogi-desktop/src/state/action_ring.rs provides configuration management:

  • current_action_ring() — Retrieves persisted ActionRingConfig for the active device
  • current_action_ring_layout() — Merges device-wide defaults with per-application overlays
  • Mutators — commit_action_ring_slot, commit_action_ring_icon, commit_action_ring_enabled, commit_action_ring_haptics

Each mutation immediately calls persist_and_reload() to write TOML changes and trigger agent reloads. The edit_action_ring_layout function implements the core edit flow, delegating to ActionRingLayout and RingAction binding structures.

Agent-Core Session Management

crates/openlogi-agent-core/src/action_ring.rs contains the authoritative ring state with these key types:

Type Role
ActionRingSessionSpec Immutable session parameters: device key, HID++ haptic route, layout, language
Session (private) Holds ActionRingInvocation, slot-to-action mapping, hovered slot, opened_at timestamp
State Wraps Option<Session> with expire() and active_session() helpers
ActionRingManager Public IPC façade with monotonic AtomicU64 session IDs, Mutex<State>, and watch::Sender<RingObservation>

IPC API Methods

The ActionRingManager exposes six core operations:

// 1. Begin a new session
pub fn begin(&self, spec: ActionRingSessionSpec) -> ActionRingSession

// 2. Poll for state changes (used by overlay)
pub async fn observe(&self, since: u64) -> RingObservation

// 3. Report hover for haptic feedback
pub fn hover(&self, session_id: u64, slot: ActionRingSlot) -> Option<ActionRingHover>

// 4. Activate a slot and consume the session
pub fn activate(&self, session_id: u64, slot: ActionRingSlot) -> ActionRingActivation

// 5. Dismiss the active ring
pub fn dismiss_active(&self)

// 6. Cancel a stale session
pub fn cancel(&self, session_id: u64)

Session Lifecycle Steps

Opening a ring — begin(spec) generates a fresh session_id, builds ActionRingPresentation from the layout, stores a Session in State, and publishes RingObservation to the overlay.

Observing state — The overlay calls observe(since) and blocks on the watch channel until the generation differs. Initial calls with since = 0 receive the current invocation immediately, enabling overlay restarts to recover live rings.

Hover feedback — hover(session_id, slot) validates the session, checks for action presence, deduplicates repeated reports, and returns ActionRingHover containing the HID++ haptic route.

Activation — activate(session_id, slot) validates, removes the action from the session, publishes an empty observation (dismissing the overlay), and returns ActionRingActivation for execution.

Dismissal — dismiss_active() clears the session and broadcasts invocation = None to hide the ring, triggered when users press the ring button twice.

Cancellation — cancel(session_id) removes stale sessions from delayed input events.

Expiration — Sessions expire after SESSION_LIFETIME = DISPLAY_LIFETIME + SESSION_GRACE (18 seconds total, with 3-second grace for overlay-agent timing gaps). The expire() method runs before every public operation to prevent stale session presentation.

All changes flow through publish(&state), which uses send_if_modified to update the watch sender only when observations actually change—guarantefing precise overlay notifications.

Overlay Rendering

The overlay in crates/openlogi-overlay/src/ring.rs subscribes via Agent::observe_action_ring:

let mut rx = manager.published.subscribe();
let observation = rx.wait_for(|obs| obs.generation != 0).await?;

It receives RingObservation with a generation number and optional ActionRingInvocation. When invocation is Some, the overlay renders slots with labels, icons, and literal flags. When None, the ring hides.

Complete Session Flow Example

// Desktop: Open a ring for the active device
let spec = ActionRingSessionSpec {
    device_key: "mouse-01".into(),
    haptic_route: None,
    layout: default_layout,
    language: Some("en".into()),
};
let session = manager.begin(spec);

// Overlay: Poll for updates
let mut rx = manager.published.subscribe();
let observation = rx.wait_for(|obs| obs.generation != 0).await?;

// User hovers top slot — optional haptic feedback
if let Ok(Some(hover)) = manager.hover(session.session_id, ActionRingSlot::Top) {
    // trigger haptic via hover.haptic_route
}

// User activates — consumes session, returns action
let activation = manager.activate(session.session_id, ActionRingSlot::Top)?;
println!("Activated {} on {}", activation.action, activation.device_key);

Design Principles

The Actions Ring implementation follows three architectural patterns:

  • State ownership separation — Agent holds canonical state; overlay is presentation-only
  • Versioned IPC contract — ActionRingInvocation and RingObservation regenerate on every change
  • Minimal surface area — Tiny, typed messages across process boundaries with no shared mutable state

Summary

  • DISPLAY_LIFETIME (15 seconds) in openlogi-core synchronizes overlay and agent timers
  • Desktop configuration in openlogi-desktop provides TOML-backed edit operations with immediate persistence
  • ActionRingManager in openlogi-agent-core owns session state and exposes six IPC methods: begin, observe, hover, activate, dismiss_active, cancel
  • Sessions expire automatically after 18 seconds with built-in grace period handling
  • Overlay renders pure snapshots from RingObservation via watch channel subscription

Frequently Asked Questions

How does the overlay recover a ring after restarting?

The overlay calls observe(0) on startup, which immediately returns the current RingObservation if a session is active. This design lets restarted overlays pick up live rings without special recovery logic.

What triggers automatic session expiration?

The expire() method runs before every public ActionRingManager operation, comparing opened_at against SESSION_LIFETIME (18 seconds). Stale sessions are silently cleared, ensuring no expired state reaches the overlay.

Why is there a 3-second grace period added to DISPLAY_LIFETIME?

SESSION_GRACE accounts for timing gaps between the overlay starting its display timer and the agent creating the session. The 18-second total prevents premature expiration when these events are not perfectly synchronized.

Can multiple rings be active simultaneously for different devices?

No—ActionRingManager holds a single Mutex<State> with Option<Session>. Each new begin() call generates a fresh session ID and replaces any existing session. The design prioritizes single-device interaction clarity over concurrent multi-device rings.

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 →