diff --git a/rust/crates/runtime/src/session_control.rs b/rust/crates/runtime/src/session_control.rs index e6c3f6c06f..896ad91580 100644 --- a/rust/crates/runtime/src/session_control.rs +++ b/rust/crates/runtime/src/session_control.rs @@ -93,8 +93,19 @@ impl SessionStore { } pub fn resolve_reference(&self, reference: &str) -> Result { + self.resolve_reference_excluding(reference, None) + } + + /// Resolve a session reference, optionally excluding a session by ID. + /// When the reference is an alias, the excluded session is skipped + /// so /resume latest returns the previous session, not the current one. + pub fn resolve_reference_excluding( + &self, + reference: &str, + exclude_id: Option<&str>, + ) -> Result { if is_session_reference_alias(reference) { - let latest = self.latest_session()?; + let latest = self.latest_session_excluding(exclude_id)?; return Ok(SessionHandle { id: latest.id, path: latest.path, @@ -158,10 +169,34 @@ impl SessionStore { } pub fn latest_session(&self) -> Result { - if let Some(latest) = self.list_sessions()?.into_iter().next() { + self.latest_session_excluding(None) + } + + /// Find the most recent session, optionally excluding a session by ID + /// and skipping sessions with 0 messages. Used by /resume latest to skip + /// the current empty session and find the previous session with actual + /// conversation history. + pub fn latest_session_excluding( + &self, + exclude_id: Option<&str>, + ) -> Result { + let exclude = exclude_id.unwrap_or(""); + // First: look in the current workspace's session namespace + if let Some(latest) = self + .list_sessions()? + .into_iter() + .find(|s| s.id != exclude && s.message_count > 0) + { return Ok(latest); } - if let Some(latest) = self.scan_global_sessions()?.into_iter().next() { + // Fallback: scan all workspace namespaces under ~/.claw/sessions/ + // and project-local .claw/sessions/ so /resume latest finds sessions + // from other workspaces. + if let Some(latest) = self + .scan_global_sessions()? + .into_iter() + .find(|s| s.id != exclude && s.message_count > 0) + { return Ok(latest); } Err(SessionControlError::Format(format_no_managed_sessions( @@ -204,28 +239,41 @@ impl SessionStore { &self, reference: &str, ) -> Result { - match self.load_session(reference) { - Ok(loaded) => Ok(loaded), - Err(SessionControlError::WorkspaceMismatch { expected, actual }) - if is_session_reference_alias(reference) => + self.load_session_excluding(reference, None) + } + + /// Like `load_session_loose` but also excludes a session by ID. + /// Used by /resume latest to skip the current empty session and find + /// the previous session with actual conversation history. + pub fn load_session_excluding( + &self, + reference: &str, + exclude_id: Option<&str>, + ) -> Result { + let handle = self.resolve_reference_excluding(reference, exclude_id)?; + let session = Session::load_from_path(&handle.path)?; + // For alias references, allow cross-workspace resume + if is_session_reference_alias(reference) { + if let Err(SessionControlError::WorkspaceMismatch { + expected: _, + actual, + }) = self.validate_loaded_session(&handle.path, &session) { - let handle = self.resolve_reference(reference)?; - let session = Session::load_from_path(&handle.path)?; eprintln!( " Note: resuming session from a different workspace (origin: {})", actual.display() ); - let _ = expected; // suppress unused warning - Ok(LoadedManagedSession { - handle: SessionHandle { - id: session.session_id.clone(), - path: handle.path, - }, - session, - }) } - Err(other) => Err(other), + } else { + self.validate_loaded_session(&handle.path, &session)?; } + Ok(LoadedManagedSession { + handle: SessionHandle { + id: session.session_id.clone(), + path: handle.path, + }, + session, + }) } pub fn fork_session( diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 5febf8417a..7ee36c8898 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -6364,7 +6364,8 @@ impl LiveCli { return Ok(false); }; - let (handle, session) = load_session_reference(&session_ref)?; + let (handle, session) = + load_session_reference_excluding(&session_ref, Some(&self.session.id))?; let message_count = session.messages.len(); let session_id = session.session_id.clone(); let runtime = build_runtime( @@ -7059,17 +7060,18 @@ fn latest_managed_session() -> Result Result<(SessionHandle, Session), Box> { + load_session_reference_excluding(reference, None) +} + +fn load_session_reference_excluding( + reference: &str, + exclude_id: Option<&str>, ) -> Result<(SessionHandle, Session), Box> { let store = current_session_store()?; - // For alias references ("latest", "last", "recent"), allow cross-workspace - // resume so /resume latest finds the most recent session globally. - // For explicit references, workspace validation is enforced. - let result = if runtime::session_control::is_session_reference_alias(reference) { - store.load_session_loose(reference) - } else { - store.load_session(reference) - }; - let loaded = result.map_err(|e| Box::new(e) as Box)?; + let loaded = store + .load_session_excluding(reference, exclude_id) + .map_err(|e| Box::new(e) as Box)?; Ok(( SessionHandle { id: loaded.handle.id,