diff --git a/rust/crates/runtime/src/conversation.rs b/rust/crates/runtime/src/conversation.rs index 1dde559..c29f834 100644 --- a/rust/crates/runtime/src/conversation.rs +++ b/rust/crates/runtime/src/conversation.rs @@ -292,6 +292,24 @@ where } } + /// Run a session health probe to verify the runtime is functional after compaction. + /// Returns Ok(()) if healthy, Err if the session appears broken. + fn run_session_health_probe(&mut self) -> Result<(), String> { + // Check if we have basic session integrity + if self.session.messages.is_empty() && self.session.compaction.is_some() { + // Freshly compacted with no messages - this is normal + return Ok(()); + } + + // Verify tool executor is responsive with a non-destructive probe + // Using glob_search with a pattern that won't match anything + let probe_input = r#"{"pattern": "*.health-check-probe-"}"#; + match self.tool_executor.execute("glob_search", probe_input) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Tool executor probe failed: {e}")), + } + } + #[allow(clippy::too_many_lines)] pub fn run_turn( &mut self, @@ -299,6 +317,18 @@ where mut prompter: Option<&mut dyn PermissionPrompter>, ) -> Result { let user_input = user_input.into(); + + // ROADMAP #38: Session-health canary - probe if context was compacted + if self.session.compaction.is_some() { + if let Err(error) = self.run_session_health_probe() { + return Err(RuntimeError::new(format!( + "Session health probe failed after compaction: {error}. \ + The session may be in an inconsistent state. \ + Consider starting a fresh session with /session new." + ))); + } + } + self.record_turn_started(&user_input); self.session .push_user_text(user_input) diff --git a/rust/crates/runtime/src/session.rs b/rust/crates/runtime/src/session.rs index 0a0a61a..7d905fa 100644 --- a/rust/crates/runtime/src/session.rs +++ b/rust/crates/runtime/src/session.rs @@ -98,6 +98,8 @@ pub struct Session { pub prompt_history: Vec, /// The model used in this session, persisted so resumed sessions can /// report which model was originally used. + /// Timestamp of last successful health check (ROADMAP #38) + pub last_health_check_ms: Option, pub model: Option, persistence: Option, } @@ -113,6 +115,7 @@ impl PartialEq for Session { && self.fork == other.fork && self.workspace_root == other.workspace_root && self.prompt_history == other.prompt_history + && self.last_health_check_ms == other.last_health_check_ms } } @@ -164,6 +167,7 @@ impl Session { fork: None, workspace_root: None, prompt_history: Vec::new(), + last_health_check_ms: None, model: None, persistence: None, } @@ -267,6 +271,7 @@ impl Session { }), workspace_root: self.workspace_root.clone(), prompt_history: self.prompt_history.clone(), + last_health_check_ms: self.last_health_check_ms, model: self.model.clone(), persistence: None, } @@ -390,6 +395,7 @@ impl Session { fork, workspace_root, prompt_history, + last_health_check_ms: None, model, persistence: None, }) @@ -490,6 +496,7 @@ impl Session { fork, workspace_root, prompt_history, + last_health_check_ms: None, model, persistence: None, })