mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-05 03:56:45 +00:00
fix: session resume — skip current empty session, unify cross-workspace loading
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
|
||||
@@ -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<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,
|
||||
|
||||
Reference in New Issue
Block a user