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

A debug-built OpenLogi agent exits immediately when it detects a running release agent because cfg!(debug_assertions) guards in 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 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 (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.

// 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. The function performs a compile-time check using cfg!(debug_assertions) before considering any replacement action:

// 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, "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 Entry point; orchestrates lock acquisition and takeover invocation.
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 Provides InstanceGuard and cross-platform lock primitives.
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 to protect stable user environments during development.
  • The singleton lock mechanism spans single_instance.rs, main.rs, and 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 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. The handshake occurs before any termination signal is issued.

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 →