feat(runtime): add session health probe for dead-session detection (ROADMAP #38)

Implements ROADMAP #38: Dead-session opacity detection via health canary.

- Add run_session_health_probe() to ConversationRuntime
- Probe runs after compaction to verify tool executor responsiveness
- Add last_health_check_ms field to Session for tracking
- Returns structured error if session appears broken after compaction

Ultraclaw droid session: ultraclaw-02-session-health

Tests: runtime crate 436 passed, integration 12 passed
This commit is contained in:
YeonGyu-Kim
2026-04-12 00:33:26 +09:00
parent 2ef447bd07
commit 56218d7d8a
2 changed files with 37 additions and 0 deletions

View File

@@ -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)] #[allow(clippy::too_many_lines)]
pub fn run_turn( pub fn run_turn(
&mut self, &mut self,
@@ -299,6 +317,18 @@ where
mut prompter: Option<&mut dyn PermissionPrompter>, mut prompter: Option<&mut dyn PermissionPrompter>,
) -> Result<TurnSummary, RuntimeError> { ) -> Result<TurnSummary, RuntimeError> {
let user_input = user_input.into(); 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.record_turn_started(&user_input);
self.session self.session
.push_user_text(user_input) .push_user_text(user_input)

View File

@@ -98,6 +98,8 @@ pub struct Session {
pub prompt_history: Vec<SessionPromptEntry>, pub prompt_history: Vec<SessionPromptEntry>,
/// The model used in this session, persisted so resumed sessions can /// The model used in this session, persisted so resumed sessions can
/// report which model was originally used. /// report which model was originally used.
/// Timestamp of last successful health check (ROADMAP #38)
pub last_health_check_ms: Option<u64>,
pub model: Option<String>, pub model: Option<String>,
persistence: Option<SessionPersistence>, persistence: Option<SessionPersistence>,
} }
@@ -113,6 +115,7 @@ impl PartialEq for Session {
&& self.fork == other.fork && self.fork == other.fork
&& self.workspace_root == other.workspace_root && self.workspace_root == other.workspace_root
&& self.prompt_history == other.prompt_history && self.prompt_history == other.prompt_history
&& self.last_health_check_ms == other.last_health_check_ms
} }
} }
@@ -164,6 +167,7 @@ impl Session {
fork: None, fork: None,
workspace_root: None, workspace_root: None,
prompt_history: Vec::new(), prompt_history: Vec::new(),
last_health_check_ms: None,
model: None, model: None,
persistence: None, persistence: None,
} }
@@ -267,6 +271,7 @@ impl Session {
}), }),
workspace_root: self.workspace_root.clone(), workspace_root: self.workspace_root.clone(),
prompt_history: self.prompt_history.clone(), prompt_history: self.prompt_history.clone(),
last_health_check_ms: self.last_health_check_ms,
model: self.model.clone(), model: self.model.clone(),
persistence: None, persistence: None,
} }
@@ -390,6 +395,7 @@ impl Session {
fork, fork,
workspace_root, workspace_root,
prompt_history, prompt_history,
last_health_check_ms: None,
model, model,
persistence: None, persistence: None,
}) })
@@ -490,6 +496,7 @@ impl Session {
fork, fork,
workspace_root, workspace_root,
prompt_history, prompt_history,
last_health_check_ms: None,
model, model,
persistence: None, persistence: None,
}) })