# Why a Debug Build OpenLogi Agent Never Takes Over a Running Release Agent

> Discover why a debug OpenLogi agent exits on startup. Learn how `cfg!(debug_assertions)` protects your production environment from unintended takeovers.

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

---

**A debug-built OpenLogi agent exits immediately when it detects a running release agent because `cfg!(debug_assertions)` guards in [`takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/takeover.rs) intentionally prevent displacement to protect the user's stable production environment.**

The OpenLogi **agent** is a singleton background process that exclusively owns HID device I/O, system event taps, and inter-process communication (IPC) sockets. Because only one agent instance may hold the system-wide lock (`agent.lock`) at any time, the codebase implements a deliberate safeguard in [`crates/openlogi-agent/src/takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/takeover.rs) that distinguishes between developer builds and production binaries.

## How Agent Lock Acquisition Works

When an agent starts, it attempts to acquire the singleton lock in [`crates/openlogi-agent/src/main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/main.rs) (line 63). If another agent already holds `agent.lock`, the new process invokes `takeover::try_replace_stale()` to determine whether it should displace the incumbent.

```rust
// crates/openlogi-agent/src/main.rs – lock acquisition
let _guard = match openlogi_core::single_instance::acquire("agent.lock") {
    Ok(g) => g,
    Err(openlogi_core::single_instance::InstanceError::AlreadyRunning { path }) => {
        // If another agent holds the lock, try to replace a stale one.
        let Some(g) = takeover::try_replace_stale() else {
            info!(path = %path.display(), "another openlogi-agent is already running — exiting");
            return;
        };
        info!("replaced a stale agent — continuing as the new one");
        g
    }
    Err(e) => { /* … */ }
};

```

The `takeover::try_replace_stale()` function returns `Some(InstanceGuard)` when replacement succeeds, or `None` when the new agent must defer to the existing one.

## The Debug Build Guard in try_replace_stale()

The decisive logic resides in [`crates/openlogi-agent/src/takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/takeover.rs). The function performs a compile-time check using `cfg!(debug_assertions)` before considering any replacement action:

```rust
// crates/openlogi-agent/src/takeover.rs – debug-build guard
pub fn try_replace_stale() -> Option<InstanceGuard> {
    if cfg!(debug_assertions) {
        // Debug builds intentionally never displace a running release agent.
        info!("debug build — leaving the running agent in place");
        return None;
    }
    replace_stale()
}

```

**Key implications:**

- **`cfg!(debug_assertions)`** evaluates to `true` for builds compiled with the `debug` profile (e.g., `cargo build`).
- When true, the function returns `None` immediately, signaling that the current process **will not** acquire the lock or terminate the older agent.
- The debug-built agent exits cleanly as a duplicate, leaving the release agent's HID and IPC resources undisturbed.

## How Release Builds Handle Stale Agent Replacement

When `cfg!(debug_assertions)` is false (release builds), `try_replace_stale()` delegates to `replace_stale()`, which implements a full protocol-version handshake:

1. Connects to the incumbent agent's IPC socket and queries its `protocol_version`.
2. If the holder's version is older than the current binary's `PROTOCOL_VERSION`, sends `SIGTERM` to terminate the stale process.
3. Retries lock acquisition (`LOCK_RETRY`) upon successful termination.
4. Returns the new `InstanceGuard` to allow the updated agent to start (see `replace_stale` lines 89-99).

This version-gated replacement ensures that newer agent binaries can supersede outdated ones, but only in production builds where intentional agent updates are expected.

## Developer Workflow Rationale

The debug-build guard exists to support the **expected developer workflow**: a developer running `cargo build` and testing changes locally should never accidentally overwrite a user's stable production agent. According to the source code comment in [`takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/takeover.rs), "a dev agent losing the lock to the user's production agent is the *expected* dev workflow."

This safeguard prevents:
- Accidental disruption of HID input for active users.
- IPC socket conflicts between incompatible protocol versions.
- Resource leaks from forcibly terminated release agents during local testing.

## Key Source Files

| File | Purpose |
|------|---------|
| [`crates/openlogi-agent/src/main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/main.rs) | Entry point; orchestrates lock acquisition and takeover invocation. |
| [`crates/openlogi-agent/src/takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/takeover.rs) | Implements `try_replace_stale()` with the `cfg!(debug_assertions)` guard and `replace_stale()` with version-based termination logic. |
| [`crates/openlogi-core/src/single_instance.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/single_instance.rs) | Provides `InstanceGuard` and cross-platform lock primitives. |
| [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs) | Defines IPC socket protocol for version handshake between agents. |

## Summary

- **Debug builds** (`cfg!(debug_assertions)` = true) immediately exit when `agent.lock` is held, never displacing a running release agent.
- **Release builds** perform a `PROTOCOL_VERSION` check through IPC and send `SIGTERM` to outdated agents before acquiring the lock.
- This behavior is hardcoded in `try_replace_stale()` within [`crates/openlogi-agent/src/takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/takeover.rs) to protect stable user environments during development.
- The singleton lock mechanism spans [`single_instance.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/single_instance.rs), [`main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/main.rs), and [`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs) to coordinate exclusive resource ownership.

## Frequently Asked Questions

### How can I force a debug build to replace a running agent?

You cannot override this behavior without modifying the source. The `cfg!(debug_assertions)` check is a compile-time constant; to replace a running agent, build in release mode (`cargo build --release`) or temporarily remove the guard in [`takeover.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/takeover.rs) for local testing.

### Does a release agent replace a debug agent?

Yes. The guard is unidirectional: debug builds defer to any running agent, but release builds follow the standard `replace_stale()` logic, which terminates outdated agents regardless of their build profile based on `PROTOCOL_VERSION` comparison.

### What happens if two release agents with different versions compete for the lock?

The newer agent (higher `PROTOCOL_VERSION`) sends `SIGTERM` to the older holder, retries acquisition up to `LOCK_RETRY` times, and assumes control. The older agent terminates and does not restart automatically.

### Where is the protocol version defined?

`PROTOCOL_VERSION` is defined in the OpenLogi agent's build configuration and serialized through the IPC socket defined in [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs). The handshake occurs before any termination signal is issued.