Hmbown/CodeWhale · error

Another pet owner is running

Error message

Another pet owner is running

What it means

`pet serve` enforces single ownership of a shared pet habitat via an advisory file lock (`fd_lock::RwLock::try_write`) on `owner.lock` inside the pet directory. `try_write` fails immediately if another process already holds the write lock, and the code converts that into "Another pet owner is running" rather than blocking. It exists so two `pet serve` processes (two HTTP servers, two tick loops, two checkpoint writers) never mutate the same habitat state file concurrently.

Solutions

  1. Find the existing owner: `fuser -v <root>/owner.lock` or `lsof <root>/owner.lock`, then stop that `pet serve` process (Ctrl-C or kill).
  2. If you need a second habitat, point the new serve at a different pet root directory instead of sharing one.
  3. Only after confirming no live process holds the lock, remove the stale `owner.lock` file and retry `pet serve`.
  4. Do not bypass by force-deleting while an owner runs — the running owner checks the lock file identity each tick and will abort with "Owner lock was replaced".

Example fix

// before: second serve on the same root fails
$ pet serve ~/.local/share/codewhale/pet
Error: Another pet owner is running
// after
$ fuser -v ~/.local/share/codewhale/pet/owner.lock   # find PID
$ kill <pid>                                          # stop the old owner
$ pet serve ~/.local/share/codewhale/pet
Defensive patterns

Strategy: try-catch

Validate before calling

// Check for a live owner before starting serve:
let lock_alive = std::fs::File::open(root.join("owner.lock"))
    .ok()
    .and_then(|f| fd_lock::RwLock::new(f).try_write().ok())
    .is_none();
if lock_alive {
    eprintln!("A pet owner is already running for this root; stop it first.");
    std::process::exit(1);
}

Try / catch

match serve(root, port) {
    Err(e) if e.to_string() == "Another pet owner is running" => {
        eprintln!("Pet already served; attach to the existing instance instead.");
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: Running `pet serve` (or attaching a view that starts an owner) while another `pet serve` process holding the `owner.lock` write lock is still alive — even if that process is idle or hung. Any successful `lifetime.try_write()` holder (i.e. the first serve) causes all later serves against the same root to fail with this error.

Common situations: A previous `pet serve` left running in another terminal, tmux pane, or detached session; a crashed serve whose lock file persists — note on most Unix filesystems the flock is released on process death, so this usually means a live process, but a stale `owner.lock` file plus an inherited/shared file descriptor (child process, another session in the same container) can also hold it; two TUI instances attaching to the same pet root simultaneously.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/05958e757782b3c4. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/pet_watch/owner.rs:301

// startup line and stop reason are the operator's only output.
#[allow(clippy::print_stdout, clippy::print_stderr)]
pub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> {
    std::fs::create_dir_all(&root)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::{MetadataExt, PermissionsExt};
        let metadata = std::fs::symlink_metadata(&root)?;
        if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {
            anyhow::bail!("Pet directory must belong to this user and cannot be a link");
        }
        std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?;
    }
    let lock_path = WorkspaceFile::open(&root, Path::new("owner.lock"), true)?;
    let original = lock_path.open_update(true, false)?;
    let mut lifetime = fd_lock::RwLock::new(original.try_clone()?);
    let _guard = lifetime
        .try_write()
        .map_err(|_| anyhow::anyhow!("Another pet owner is running"))?;
    let mut store = Store::at(&root)?;
    let previous = store.load()?;
    let mut saved = if let Some(text) = previous {
        let value: Saved = serde_json::from_str(&text)?;
        if value.version != 1
            || uuid::Uuid::parse_str(&value.identity).is_err()
            || value.token.len() != 64
            || !value.token.bytes().all(|b| b.is_ascii_hexdigit())
            || !valid_source(&value.source)
            || value.clients.len() > MAX_CLIENTS
            || !value.appearance.valid()
        {
            anyhow::bail!("Invalid shared habitat; the existing file was kept");
        }
        value
    } else {
        Saved {
            version: 1,

View on GitHub (pinned to 433685b202)