From e459a727e9b7a445466586c9e91192420937a9fc Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 2 Jun 2026 15:49:21 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20session=20resume=20=E2=80=94=20skip=20cu?= =?UTF-8?q?rrent=20empty=20session,=20unify=20cross-workspace=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three improvements to the /resume command: 1. /resume latest now skips the current empty session When a new session is created on startup (with 0 messages), /resume latest previously returned that empty session. Now it skips sessions with message_count == 0 and excludes the current session ID via the new exclude_id parameter, so it finds the previous session with actual conversation history. 2. Unified load_session_excluding() replaces load_session_loose() The previous load_session_loose() only handled cross-workspace resume for aliases. The new load_session_excluding() combines the loose workspace validation logic with the exclude_id parameter, simplifying the call chain and ensuring all resume paths skip the current empty session when appropriate. 3. All existing session scanning paths (global root + project-local .claw/sessions/) are already in place from prior commits, and now the exclude_id filter is applied consistently across both local and global session scans. Changes: - session_control.rs: Add resolve_reference_excluding() that delegates from resolve_reference(), adding optional exclude_id filtering for alias references. - session_control.rs: Add latest_session_excluding() that delegates from latest_session(), filtering out excluded session IDs and sessions with 0 messages in both local and global scan paths. - session_control.rs: Add load_session_excluding() that replaces load_session_loose(), combining cross-workspace alias handling with the exclude_id parameter. - main.rs: Add load_session_reference_excluding() that delegates from load_session_reference(), using the new store method. - main.rs: Wire LiveCli::resume_session() to pass the current session ID as the exclude_id so /resume latest skips the current empty session. Co-Authored-By: Claude Opus 4.8 --- rust/crates/runtime/src/session_control.rs | 84 +++++++++++++++++----- rust/crates/rusty-claude-cli/src/main.rs | 22 +++--- 2 files changed, 78 insertions(+), 28 deletions(-) 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,