diff --git a/server/src/chat/transport/matrix/bot/messages/handle_message.rs b/server/src/chat/transport/matrix/bot/messages/handle_message.rs index 21f4c265..3b93ce46 100644 --- a/server/src/chat/transport/matrix/bot/messages/handle_message.rs +++ b/server/src/chat/transport/matrix/bot/messages/handle_message.rs @@ -210,6 +210,17 @@ pub(in crate::chat::transport::matrix::bot) async fn handle_message( // Persist to disk so history survives server restarts. save_history(&ctx.services.project_root, &guard); + } else { + // Clear session_id on error so a poisoned session (e.g. pending + // tool call with no response) doesn't cause a crash loop on resume. + let mut guard = ctx.history.lock().await; + if let Some(conv) = guard.get_mut(&room_id) + && conv.session_id.is_some() + { + slog!("[matrix-bot] clearing session_id after error to prevent crash loop"); + conv.session_id = None; + save_history(&ctx.services.project_root, &guard); + } } } diff --git a/server/src/llm/providers/claude_code/mod.rs b/server/src/llm/providers/claude_code/mod.rs index fc9376d9..047923b9 100644 --- a/server/src/llm/providers/claude_code/mod.rs +++ b/server/src/llm/providers/claude_code/mod.rs @@ -371,23 +371,38 @@ fn run_pty_session( // Wait briefly for Claude Code to flush its session transcript to disk. // The `result` event means the API response is done, but the process // still needs to write the conversation to the JSONL session file. - match child.try_wait() { - Ok(Some(_)) => {} // Already exited + let exit_status = match child.try_wait() { + Ok(Some(status)) => Some(status), _ => { - // Give it up to 2 seconds to exit cleanly + let mut status = None; for _ in 0..20 { std::thread::sleep(std::time::Duration::from_millis(100)); - if let Ok(Some(_)) = child.try_wait() { + if let Ok(Some(s)) = child.try_wait() { + status = Some(s); break; } } - // If still running after 2s, kill it - let _ = child.kill(); - let _ = child.wait(); + if status.is_none() { + let _ = child.kill(); + status = child.wait().ok(); + } + status } - } + }; // Wait for the reader thread to release the cloned PTY master fd. let _ = reader_handle.join(); + + // Non-zero exit without a result event means Claude Code crashed + // (e.g. MCP server not yet connected). Propagate as an error so + // the caller can clear the session_id instead of persisting it. + if !got_result + && let Some(ref status) = exit_status + && !status.success() + { + slog!("[pty-debug] Claude Code exited with non-zero status: {status}"); + return Err(format!("Claude Code crashed (exit status: {status})")); + } + Ok(()) }