Hmbown/CodeWhale · warning
Owner lock was replaced
Error message
Owner lock was replaced
What it means
During the pet-watch owner's periodic world update (`run_world`), the code re-opens the lock file and compares it against the original lock contents. If the file no longer matches, another process has replaced the owner lock, so this holder stops rather than acting as a stale owner. It is an ownership-continuity invariant, not an I/O failure.
Solutions
- Stop the second Codewhale TUI/process that took over the lock, then restart this one so it becomes the fresh owner
- Do not delete or rewrite the habitat lock file while a TUI session is running
- If stale state persists after crashes, fully exit all instances and remove both the habitat file and lock before restarting
Example fix
null
Defensive patterns
Strategy: try-catch
Validate before calling
// before relying on ownership, confirm the lock still matches
if !same_file(&lock_path.open_update(false, false)?, &original)? {
eprintln!("lock replaced; restarting as fresh owner");
} Try / catch
match run_world(...).await {
Err(e) if e.to_string().contains("Owner lock was replaced") => {
// yield ownership and re-enter serve loop as a new owner
restart_as_fresh_owner().await
}
r => r,
} Prevention
- Don't delete or rewrite lock files while a TUI instance is running
- Run only one owner TUI per shared habitat
- After a crash, exit all instances and clean both habitat and lock files before restarting
When it happens
Trigger: Calling `run_world` (via `serve`) after another process deleted and recreated the habitat lock file, or wrote different contents into it, so `same_file` fails on the reopened lock.
Common situations: Two TUI instances racing for ownership; a user deleting/cleaning temp or state directories while the TUI runs; a crash of a competing owner followed by manual cleanup.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Agent continuation target is outside the active session
- Cannot open session : its queued input is already open in…
- Cannot open session : its queued input is already open in…
- catalog cache unavailable
- child wall-time budget exhausted
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/95bbcc3a4cb59f52.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tui/pet_watch/owner.rs:467
let mut audio_error = false;
let mut waiting = false;
let mut last_save = Instant::now();
let mut storage_error = false;
let origin = Instant::now();
let initial_time: f64 = context.with(|ctx| ctx.eval("JSON.parse(pet.snapshot()).timeMs"))?;
let mut last = origin;
let mut ticks = 0u64;
let mut measurements = VecDeque::<f64>::new();
save(&context, &mut saved, &mut store)?;
let _ = ready.send(Ok(()));
loop {
*deadline
.lock()
.map_err(|_| anyhow::anyhow!("Clock lock failed"))? =
Instant::now() + Duration::from_secs(5);
let now = Instant::now();
if !same_file(&lock_path.open_update(false, false)?, original)? {
anyhow::bail!("Owner lock was replaced");
}
if producer
.as_ref()
.is_some_and(|(_, _, seen, _)| now.duration_since(*seen) > LEASE)
{
producer = None;
waiting = false;
context.with(|ctx| ctx.eval::<(), _>("pet.disconnectEngine()"))?;
}
if audio
.as_ref()
.is_some_and(|(_, seen)| now.duration_since(*seen) > LEASE)
{
audio = None;
}
let elapsed = now.duration_since(last).as_secs_f64();
if elapsed >= 1.0 / 30.0 {
// A suspended machine advances a bounded amount and marks a gap;View on GitHub (pinned to 73e0f67d83)