{"record":{"id":"bdee008ddca16dbe","repo":"Hmbown/CodeWhale","slug":"frame-lock-failed","errorCode":null,"errorMessage":"Frame lock failed","messagePattern":"Frame lock failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/owner.rs","lineNumber":603,"sourceCode":"                }\n                pose[\"style\"][\"alpha\"] = json!(\n                    (pose[\"style\"][\"alpha\"].as_f64().unwrap_or(0.5) * saved.appearance.brightness)\n                        .clamp(0.0, 1.0)\n                );\n            }\n            frame[\"appearance\"] = json!(saved.appearance);\n            frame[\"producerConnected\"] = json!(producer.is_some());\n            frame[\"storageAvailable\"] = json!(!storage_error);\n            frame[\"audioOwner\"] = json!(audio.as_ref().map(|(id, _)| id));\n            frame[\"audioUnavailable\"] = json!(audio_error);\n            measurements.push_back(started.elapsed().as_secs_f64() * 1000.0);\n            if measurements.len() > 300 {\n                measurements.pop_front();\n            }\n            frame[\"performance\"] = json!({\"worldHz\":30,\"frames\":ticks,\"uptimeSeconds\":origin.elapsed().as_secs_f64(),\"workMs\":measurements.back()});\n            let mut output = frames\n                .lock()\n                .map_err(|_| anyhow::anyhow!(\"Frame lock failed\"))?;\n            output.push_back(frame);\n            if output.len() > 16 {\n                output.pop_front();\n            }\n        }\n        if last_save.elapsed() >= Duration::from_secs(1) {\n            storage_error = save(&context, &mut saved, &mut store).is_err();\n            last_save = Instant::now();\n        }\n        let work = match rx.recv_timeout(Duration::from_millis(2)) {\n            Ok(work) => work,\n            Err(mpsc::RecvTimeoutError::Timeout) => continue,\n            Err(mpsc::RecvTimeoutError::Disconnected) => {\n                save(&context, &mut saved, &mut store)?;\n                return Ok(());\n            }\n        };\n        match work {","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/tui/pet_watch/owner.rs#L585-L621","documentation":"run_world, the pet-render world simulation loop, keeps a bounded ring of the last 16 rendered frames behind a std::sync::Mutex. This error is thrown when locking that mutex returns a poisoning error, meaning another thread panicked while holding the frame-lock and left the shared state inconsistent. The world loop aborts rather than continue rendering from possibly-corrupt frame data.","triggerScenarios":"Calling run_world (from serve) while another thread holding the frames Mutex panics; the subsequent frames.lock() call in the tick loop returns Err(PoisonError), which is mapped to this anyhow error.","commonSituations":"A panic inside a rendering/consumer thread that also touches the frame buffer (e.g. a bug in frame serialization or a JSON build panic); most common during development with panicking frame consumers, or under a genuinely concurrent world server with a buggy frame reader.","solutions":["Find and fix the panic in the thread that poisoned the frames mutex (the original panic message is printed before this error appears).","Restart the world/serve process to clear the poisoned lock.","If intentional recovery is desired, use frames.lock().unwrap_or_else(|p| p.into_inner()) instead of failing, accepting the possibly-partial state.","Guard the frame-producing code against panics (catch_unwind or fix indexing/bounds errors) so the lock never poisons."],"exampleFix":"// before\nlet mut output = frames\n    .lock()\n    .map_err(|_| anyhow::anyhow!(\"Frame lock failed\"))?;\n// after — recover from a poisoned lock instead of aborting the world loop\nlet mut output = frames.lock().unwrap_or_else(|poisoned| poisoned.into_inner());","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn frames_healthy(frames: &Mutex<VecDeque<Value>>) -> bool { !frames.is_poisoned() }","tryCatchPattern":"match frames.lock() {\n    Ok(guard) => { /* push frame */ }\n    Err(poisoned) => {\n        let guard = poisoned.into_inner(); // or log + abort world loop\n    }\n}","preventionTips":["Never panic while holding a shared mutex; return Result from frame producers instead.","Run world consumers under catch_unwind so a consumer panic does not poison the render loop's lock.","Treat any earlier panic in serve logs as the root cause and fix it before adding lock recovery."],"tags":["concurrency","mutex-poisoned","rust","rendering"],"backgroundTag":"internal-invariant-violation","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}