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
- Look earlier in the stderr/stdout of `pet serve` for the original panic message — poisoning is a downstream symptom; fix or report that panic.
- Restart `pet serve`; the mutex poisoning does not persist across processes, so a fresh start clears it.
- 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.
- 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
- Keep the deadline-mutex critical sections panic-free (no unwrap/indexing inside the lock).
- Set RUST_BACKTRACE=1 when running `pet serve` so the original poisoning panic is diagnosable.
- For trivially-valid data like `Instant`, recover with `into_inner()` instead of failing the tick loop.
- Treat this error as a symptom: hunt the earlier panic, not the lock failure.
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
- connection stdin poisoned by an earlier panic
- finite points
- LSP diagnostics timed out waiting for another document
- shell manager lock poisoned
- A saved trace identifier collided. Try saving again.
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)