# OpenLogi Actions Ring: IPC State Management & Session Lifecycle Implementation

> Explore the OpenLogi Actions Ring's IPC state management and session lifecycle. Learn how agent-core crate manages session state and APIs like begin hover and activate control actions with a 15-second display lifetime.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-02

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/action_ring.rs) |
| Desktop state & editor | UI-side configuration with TOML persistence | [`crates/openlogi-desktop/src/state/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/action_ring.rs) |
| Agent-core manager | Authoritative session state & IPC API | [`crates/openlogi-agent-core/src/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/action_ring.rs) |
| Overlay UI | Presentation-only rendering | [`crates/openlogi-overlay/src/ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/ring.rs) |

## Core Constant: DISPLAY_LIFETIME

The foundation of session timing lives in [`crates/openlogi-core/src/action_ring.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/action_ring.rs):

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

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-overlay/src/ring.rs) subscribes via `Agent::observe_action_ring`:

```rust
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

```rust
// 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.