zeroclaw-labs/zeroclaw · error · std::io::Error

local IPC endpoint is already serving at {}

Error message

local IPC endpoint is already serving at {}

What it means

Raised by remove_stale (crates/zeroclaw-runtime/src/rpc/local.rs:474, called from bind_locked before UnixListener::bind) when the socket path exists and a probe UnixStream::connect(path) succeeds. A successful connect proves a live server is accepting connections on that endpoint, so zeroclaw refuses to treat the socket as stale debris and reports ErrorKind::AddrInUse rather than unlinking a working endpoint out from under its owner.

Source

Thrown at crates/zeroclaw-runtime/src/rpc/local.rs:474

        if let Some(parent) = path.parent() {
            tokio::fs::create_dir_all(parent).await?;
            use std::os::unix::fs::PermissionsExt;
            tokio::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
                .await
                .ok();
        }
        Ok(())
    }

    pub async fn remove_stale(path: &Path) -> Result<()> {
        let observed = match tokio::fs::symlink_metadata(path).await {
            Ok(metadata) => SocketIdentity::from_metadata(&metadata),
            Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
            Err(error) => return Err(error).context("inspecting local IPC endpoint"),
        };

        match UnixStream::connect(path).await {
            Ok(_) => Err(std::io::Error::new(
                ErrorKind::AddrInUse,
                format!(
                    "local IPC endpoint is already serving at {}",
                    path.display()
                ),
            )
            .into()),
            Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
            Err(error) if error.kind() != ErrorKind::ConnectionRefused => {
                Err(error).context("probing existing local IPC endpoint")
            }
            Err(_) => {
                let current = match tokio::fs::symlink_metadata(path).await {
                    Ok(metadata) => SocketIdentity::from_metadata(&metadata),
                    Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
                    Err(error) => {
                        return Err(error).context("rechecking stale local IPC endpoint");
                    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Identify and stop the existing listener: `ss -xlp | grep <path>` or `lsof <path>` shows the PID; stop that process (or reuse it as your endpoint).
  2. If both services are legitimately needed, move one to a different socket path (own data dir or unique ZEROCLAW_SOCKET).
  3. Prefer the default per-data-dir socket (data_dir/daemon.sock) whose lifecycle lock already gives single-ownership, instead of hand-picked paths in shared directories.
  4. If the other listener is orphaned, kill its PID; once it exits, connect probes fail with ECONNREFUSED and remove_stale will clean the path automatically.

Example fix

// before: bind onto a path another live server is using
let (listener, guard) = rpc::local::bind(Path::new("/tmp/shared.sock")).await?; // AddrInUse: already serving

// after: probe first and fail with a clear diagnosis (or pick another path)
let path = Path::new("/tmp/shared.sock");
if tokio::net::UnixStream::connect(path).await.is_ok() {
    anyhow::bail!("another server is already serving at {}; stop it or choose a different ZEROCLAW_SOCKET", path.display());
}
let (listener, guard) = rpc::local::bind(path).await?;
Defensive patterns

Strategy: validation

Validate before calling

use tokio::net::UnixStream;

async fn socket_has_live_server(path: &std::path::Path) -> bool {
    // remove_stale uses exactly this probe: connect() success == live listener.
    UnixStream::connect(path).await.is_ok()
}

// if socket_has_live_server(&path).await { /* reuse it or bail before bind() */ }

Type guard

fn is_addr_in_use(err: &anyhow::Error) -> bool {
    err.chain()
        .filter_map(|c| c.downcast_ref::<std::io::Error>())
        .any(|e| e.kind() == std::io::ErrorKind::AddrInUse)
}

Try / catch

match rpc::local::bind(&path).await {
    Ok(bound) => { /* serve */ }
    Err(e) if is_addr_in_use(&e) => {
        eprintln!("a live server is already accepting at {}; reuse it or pick another path", path.display());
        std::process::exit(1);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling bind(path) (via bind_locked -> remove_stale) when some process is actively listening on the path and accepting connections. Typical when the socket lives in a shared/external directory (e.g. ZEROCLAW_SOCKET pointing into /tmp) where the lifecycle lock does not confer exclusive ownership, or when a non-cooperating process (another service, a leftover agent of a different version) bound the same path.

Common situations: Pointing two different daemon deployments at the same ZEROCLAW_SOCKET in a world-writable dir like /tmp; a socket path colliding with another application's Unix socket; running against a socket served by a container/host process the caller does not manage; leftover socket from a daemon started outside systemd while configuring a new unit.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/2de90102356ba715. Report an issue: GitHub.