diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 5b21d7f5..7504192d 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -505,6 +505,38 @@ enum SandboxCommand { #[tokio::main] async fn main() -> Result<()> { + // Set up process panic hook before anything else — writes crash dumps + // to ~/.deepseek/crashes/ even if the panic happens before tokio is up. + let orig_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = panic_info.payload().downcast_ref::() { + s.clone() + } else { + format!("{:?}", panic_info.payload()) + }; + let location = panic_info + .location() + .map(|loc| loc.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + tracing::error!(target: "panic", "Process panicked at {location}: {msg}"); + // Write crash dump best-effort + if let Some(home) = dirs::home_dir() { + let crash_dir = home.join(".deepseek").join("crashes"); + let _ = std::fs::create_dir_all(&crash_dir); + use chrono::Utc; + let ts = Utc::now().format("%Y%m%dT%H%M%S%.3fZ"); + let path = crash_dir.join(format!("{ts}-process-panic.log")); + let contents = format!( + "Process panicked\nLocation: {location}\nTimestamp: {ts}\nPanic: {msg}\n", + ); + let _ = std::fs::write(&path, contents); + } + // Invoke the original hook (prints to stderr, etc.) + orig_hook(panic_info); + })); + dotenv().ok(); let cli = Cli::parse(); logging::set_verbose(cli.verbose || logging::env_requests_verbose_logging()); @@ -2638,7 +2670,7 @@ fn save_mcp_config(path: &Path, cfg: &McpConfig) -> Result<()> { } let rendered = serde_json::to_string_pretty(cfg) .map_err(|e| anyhow!("Failed to serialize MCP config: {e}"))?; - std::fs::write(path, rendered) + crate::utils::write_atomic(path, rendered.as_bytes()) .map_err(|e| anyhow!("Failed to write MCP config {}: {}", path.display(), e))?; Ok(()) } diff --git a/crates/tui/src/tui/persistence_actor.rs b/crates/tui/src/tui/persistence_actor.rs index c21fb9f8..50d80757 100644 --- a/crates/tui/src/tui/persistence_actor.rs +++ b/crates/tui/src/tui/persistence_actor.rs @@ -29,6 +29,7 @@ use std::sync::OnceLock; use tokio::sync::mpsc; use crate::session_manager::{SavedSession, SessionManager}; +use crate::utils::spawn_supervised; // --------------------------------------------------------------------------- // Request type @@ -99,7 +100,7 @@ pub fn spawn_persistence_actor(manager: SessionManager) -> PersistActorHandle { let (tx, mut rx) = mpsc::unbounded_channel::(); let handle = PersistActorHandle { tx }; - tokio::spawn(async move { + spawn_supervised("persistence-actor", std::panic::Location::caller(), async move { let mut latest_checkpoint: Option = None; let mut latest_session: Option = None; let mut should_clear: bool = false; diff --git a/crates/tui/src/utils.rs b/crates/tui/src/utils.rs index 99da8018..d8fb60e0 100644 --- a/crates/tui/src/utils.rs +++ b/crates/tui/src/utils.rs @@ -202,6 +202,72 @@ pub fn flush_and_sync(writer: &mut std::io::BufWriter) -> std::io writer.get_ref().sync_all() } +/// Spawn a tokio task with panic supervision. +/// +/// Wraps the future in `AssertUnwindSafe` + `catch_unwind`. On panic: +/// 1. Logs the panic with the task name and caller location via `tracing::error!`. +/// 2. Writes a crash dump to `~/.deepseek/crashes/-.log`. +/// +/// The returned `JoinHandle` resolves to `()` — the panic is caught and +/// handled internally so the parent process stays alive. +pub fn spawn_supervised( + name: &'static str, + location: &'static std::panic::Location<'static>, + future: F, +) -> tokio::task::JoinHandle<()> +where + F: std::future::Future + Send + 'static, +{ + tokio::spawn(async move { + use futures_util::FutureExt; + let result = std::panic::AssertUnwindSafe(future) + .catch_unwind() + .await; + if let Err(panic_info) = result { + let msg = if let Some(s) = panic_info.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = panic_info.downcast_ref::() { + s.clone() + } else { + "unknown panic".to_string() + }; + tracing::error!( + target: "panic", + "Task '{name}' panicked at {}: {msg}", + location, + ); + // Write crash dump (best-effort) + let _ = write_panic_dump(name, location, &msg); + } + }) +} + +/// Write a panic dump file to `~/.deepseek/crashes/`. +/// +/// Creates the directory if needed and writes a timestamped log +/// with the task name, caller location, and panic message. +/// Best-effort — failures are silently ignored. +fn write_panic_dump( + name: &str, + location: &std::panic::Location<'_>, + message: &str, +) -> std::io::Result<()> { + use chrono::Utc; + let home = dirs::home_dir().ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found") + })?; + let crash_dir = home.join(".deepseek").join("crashes"); + std::fs::create_dir_all(&crash_dir)?; + let timestamp = Utc::now().format("%Y%m%dT%H%M%S%.3fZ"); + let filename = format!("{timestamp}-{name}.log"); + let path = crash_dir.join(&filename); + let contents = format!( + "Task: {name}\nLocation: {location}\nTimestamp: {timestamp}\nPanic: {message}\n" + ); + std::fs::write(&path, contents)?; + Ok(()) +} + #[allow(dead_code)] pub fn ensure_dir(path: &Path) -> Result<()> { fs::create_dir_all(path)