Hmbown/CodeWhale · critical

Clock lock failed

Error message

Clock lock failed

What it means

The pet owner loop keeps a JS interrupt deadline under a `Mutex<Instant>`; before each tick it locks that mutex and resets the deadline five seconds out so the QuickJS interrupt handler sees fresh work. This error is raised only when `Mutex::lock` itself fails — i.e. the mutex was poisoned because another thread panicked while holding the lock. The deadline mutex is shared with the interrupt-handler closure installed at runtime creation, so a poisoned lock means the interrupt machinery is no longer trustworthy and the loop aborts rather than ticking with an unbounded JS budget.

Solutions

  1. Look earlier in the stderr/stdout of `pet serve` for the original panic message — poisoning is a downstream symptom; fix or report that panic.
  2. Restart `pet serve`; the mutex poisoning does not persist across processes, so a fresh start clears it.
  3. If reproducible, capture the panic backtrace (RUST_BACKTRACE=1 pet serve ...) and file a bug against the code path that panicked while holding the deadline lock.
  4. As a defensive change, wrap deadline updates in `lock().unwrap_or_else(|p| p.into_inner())` if a stale deadline is acceptable — though upstream intentionally treats poisoning as fatal here.

Example fix

// before: poisoning propagates as an opaque error
*deadline.lock().map_err(|_| anyhow::anyhow!("Clock lock failed"))? = Instant::now() + Duration::from_secs(5);
// after: recover from poisoning (deadline is a plain Instant, safe to reuse)
*deadline.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = Instant::now() + Duration::from_secs(5);
Defensive patterns

Strategy: try-catch

Try / catch

// Poisoning means some holder panicked; recover only when the guarded data is trivially valid:
match deadline.lock() {
    Ok(mut d) => *d = Instant::now() + Duration::from_secs(5),
    Err(poisoned) => {
        let mut d = poisoned.into_inner(); // Instant is always valid
        *d = Instant::now() + Duration::from_secs(5);
        log::warn!("deadline mutex was poisoned; recovered");
    }
}

Prevention

When it happens

Trigger: Any thread or closure that panics while holding the `deadline` mutex — in practice the interrupt-handler closure (`limit.lock().map_or(true, |d| Instant::now() > *d)`) panicking inside QuickJS, or any other holder panicking between lock and release — after which every subsequent `deadline.lock()` in the tick loop returns `PoisonError` and maps to this error.

Common situations: A panic inside the QuickJS interrupt handler during script evaluation (e.g. an unwinding bug in a native callback) poisoning the mutex on a long-running pet owner; this is essentially never a user-config problem — it indicates an internal panic earlier in the same `pet serve` process, whose message would have appeared first.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

        .map_err(|_| anyhow::anyhow!("Shared habitat could not restore; its file was kept"))?;
    let mut producer: Option<(String, u64, Instant, String)> = None;
    let mut audio: Option<(String, Instant)> = None;
    let mut playback: Option<(super::audio::Output, super::audio_cursor::AudioCursor)> = None;
    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;

View on GitHub (pinned to 433685b202)