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

  1. Stop the second Codewhale TUI/process that took over the lock, then restart this one so it becomes the fresh owner
  2. Do not delete or rewrite the habitat lock file while a TUI session is running
  3. 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

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


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)