Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 66 additions & 18 deletions rust/crates/runtime/src/session_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,19 @@ impl SessionStore {
}

pub fn resolve_reference(&self, reference: &str) -> Result<SessionHandle, SessionControlError> {
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<SessionHandle, SessionControlError> {
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,
Expand Down Expand Up @@ -158,10 +169,34 @@ impl SessionStore {
}

pub fn latest_session(&self) -> Result<ManagedSessionSummary, SessionControlError> {
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<ManagedSessionSummary, SessionControlError> {
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(
Expand Down Expand Up @@ -204,28 +239,41 @@ impl SessionStore {
&self,
reference: &str,
) -> Result<LoadedManagedSession, SessionControlError> {
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<LoadedManagedSession, SessionControlError> {
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(
Expand Down
22 changes: 12 additions & 10 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -7059,17 +7060,18 @@ fn latest_managed_session() -> Result<ManagedSessionSummary, Box<dyn std::error:

fn load_session_reference(
reference: &str,
) -> Result<(SessionHandle, Session), Box<dyn std::error::Error>> {
load_session_reference_excluding(reference, None)
}

fn load_session_reference_excluding(
reference: &str,
exclude_id: Option<&str>,
) -> Result<(SessionHandle, Session), Box<dyn std::error::Error>> {
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<dyn std::error::Error>)?;
let loaded = store
.load_session_excluding(reference, exclude_id)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
Ok((
SessionHandle {
id: loaded.handle.id,
Expand Down
Loading