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 persistedActionRingConfigfor the active devicecurrent_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 —
ActionRingInvocationandRingObservationregenerate on every change - Minimal surface area — Tiny, typed messages across process boundaries with no shared mutable state
Summary
DISPLAY_LIFETIME(15 seconds) inopenlogi-coresynchronizes overlay and agent timers- Desktop configuration in
openlogi-desktopprovides TOML-backed edit operations with immediate persistence ActionRingManagerinopenlogi-agent-coreowns 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
RingObservationviawatchchannel 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →