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
- Find the existing owner: `fuser -v <root>/owner.lock` or `lsof <root>/owner.lock`, then stop that `pet serve` process (Ctrl-C or kill).
- If you need a second habitat, point the new serve at a different pet root directory instead of sharing one.
- Only after confirming no live process holds the lock, remove the stale `owner.lock` file and retry `pet serve`.
- 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
- Use a process supervisor or pidfile check so only one `pet serve` per root runs.
- Find the lock holder with `fuser`/`lsof` before assuming the lock file is stale.
- Never delete `owner.lock` while an owner lives — it triggers the replacement check and aborts the running owner.
- Point concurrent sessions at separate pet roots.
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
- Cannot open session : its queued input is already open in…
- Fleet ledger lock was replaced; reopen the workspace before…
- Task store is busy; state is unavailable
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
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)