mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-05-30 11:45:20 +08:00
fix: /resume latest searches all workspaces
Fixes /resume latest to search all workspaces instead of just the current one.
This commit is contained in:
parent
0975252976
commit
f1a55a211e
@ -197,6 +197,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||||||
name: "trustedRoots",
|
name: "trustedRoots",
|
||||||
expected: FieldType::StringArray,
|
expected: FieldType::StringArray,
|
||||||
},
|
},
|
||||||
|
FieldSpec {
|
||||||
|
name: "provider",
|
||||||
|
expected: FieldType::Object,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const HOOKS_FIELDS: &[FieldSpec] = &[
|
const HOOKS_FIELDS: &[FieldSpec] = &[
|
||||||
@ -310,6 +314,25 @@ const OAUTH_FIELDS: &[FieldSpec] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PROVIDER_FIELDS: &[FieldSpec] = &[
|
||||||
|
FieldSpec {
|
||||||
|
name: "kind",
|
||||||
|
expected: FieldType::String,
|
||||||
|
},
|
||||||
|
FieldSpec {
|
||||||
|
name: "apiKey",
|
||||||
|
expected: FieldType::String,
|
||||||
|
},
|
||||||
|
FieldSpec {
|
||||||
|
name: "baseUrl",
|
||||||
|
expected: FieldType::String,
|
||||||
|
},
|
||||||
|
FieldSpec {
|
||||||
|
name: "model",
|
||||||
|
expected: FieldType::String,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const DEPRECATED_FIELDS: &[DeprecatedField] = &[
|
const DEPRECATED_FIELDS: &[DeprecatedField] = &[
|
||||||
DeprecatedField {
|
DeprecatedField {
|
||||||
name: "permissionMode",
|
name: "permissionMode",
|
||||||
@ -501,6 +524,15 @@ pub fn validate_config_file(
|
|||||||
&path_display,
|
&path_display,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(provider) = object.get("provider").and_then(JsonValue::as_object) {
|
||||||
|
result.merge(validate_object_keys(
|
||||||
|
provider,
|
||||||
|
PROVIDER_FIELDS,
|
||||||
|
"provider",
|
||||||
|
source,
|
||||||
|
&path_display,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,9 +158,15 @@ impl SessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn latest_session(&self) -> Result<ManagedSessionSummary, SessionControlError> {
|
pub fn latest_session(&self) -> Result<ManagedSessionSummary, SessionControlError> {
|
||||||
self.list_sessions()?.into_iter().next().ok_or_else(|| {
|
if let Some(latest) = self.list_sessions()?.into_iter().next() {
|
||||||
SessionControlError::Format(format_no_managed_sessions(&self.sessions_root))
|
return Ok(latest);
|
||||||
})
|
}
|
||||||
|
if let Some(latest) = self.scan_global_sessions()?.into_iter().next() {
|
||||||
|
return Ok(latest);
|
||||||
|
}
|
||||||
|
Err(SessionControlError::Format(format_no_managed_sessions(
|
||||||
|
&self.sessions_root,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@ -190,6 +196,38 @@ impl SessionStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load a session by reference, allowing cross-workspace resume for aliases.
|
||||||
|
/// When the reference is an alias ("latest", "last", "recent"), workspace
|
||||||
|
/// mismatch validation is skipped so `/resume latest` works across workspaces.
|
||||||
|
/// For explicit session references, workspace validation is still enforced.
|
||||||
|
pub fn load_session_loose(
|
||||||
|
&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) =>
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fork_session(
|
pub fn fork_session(
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
@ -221,6 +259,47 @@ impl SessionStore {
|
|||||||
.map(Path::to_path_buf)
|
.map(Path::to_path_buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan all known session storage locations for sessions from any workspace.
|
||||||
|
/// Checks both the global root (~/.claw/sessions/) and the project-local
|
||||||
|
/// .claw/sessions/ parent directory. Used as a fallback when the current
|
||||||
|
/// workspace has no sessions.
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
fn scan_global_sessions(&self) -> Result<Vec<ManagedSessionSummary>, SessionControlError> {
|
||||||
|
let mut sessions = Vec::new();
|
||||||
|
|
||||||
|
// Scan global root: ~/.claw/sessions/<fingerprint>/
|
||||||
|
let global_root = global_sessions_root();
|
||||||
|
if let Ok(entries) = fs::read_dir(&global_root) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let _ = Self::collect_sessions_from_dir_unvalidated(&path, &mut sessions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan project-local parent: <cwd>/.claw/sessions/<fingerprint>/
|
||||||
|
// Sessions are stored here by from_cwd(), so we must check all
|
||||||
|
// fingerprint subdirs, not just the current workspace's.
|
||||||
|
if let Some(local_parent) = self.legacy_sessions_root() {
|
||||||
|
if let Ok(entries) = fs::read_dir(&local_parent) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() && path != self.sessions_root {
|
||||||
|
let _ = Self::collect_sessions_from_dir_unvalidated(&path, &mut sessions);
|
||||||
|
} else if path == self.sessions_root {
|
||||||
|
// Already searched in list_sessions(), but include here
|
||||||
|
// in case this is called standalone
|
||||||
|
let _ = Self::collect_sessions_from_dir_unvalidated(&path, &mut sessions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort_managed_sessions(&mut sessions);
|
||||||
|
Ok(sessions)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_loaded_session(
|
fn validate_loaded_session(
|
||||||
&self,
|
&self,
|
||||||
session_path: &Path,
|
session_path: &Path,
|
||||||
@ -305,6 +384,65 @@ impl SessionStore {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like `collect_sessions_from_dir` but skips workspace validation.
|
||||||
|
/// Used by the global scan fallback to discover sessions from any workspace.
|
||||||
|
fn collect_sessions_from_dir_unvalidated(
|
||||||
|
directory: &Path,
|
||||||
|
sessions: &mut Vec<ManagedSessionSummary>,
|
||||||
|
) -> Result<(), SessionControlError> {
|
||||||
|
let entries = match fs::read_dir(directory) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if !is_managed_session_file(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
let modified_epoch_millis = metadata
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let summary = match Session::load_from_path(&path) {
|
||||||
|
Ok(session) => ManagedSessionSummary {
|
||||||
|
id: session.session_id,
|
||||||
|
path,
|
||||||
|
updated_at_ms: session.updated_at_ms,
|
||||||
|
modified_epoch_millis,
|
||||||
|
message_count: session.messages.len(),
|
||||||
|
parent_session_id: session
|
||||||
|
.fork
|
||||||
|
.as_ref()
|
||||||
|
.map(|fork| fork.parent_session_id.clone()),
|
||||||
|
branch_name: session
|
||||||
|
.fork
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|fork| fork.branch_name.clone()),
|
||||||
|
},
|
||||||
|
Err(_) => ManagedSessionSummary {
|
||||||
|
id: path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string(),
|
||||||
|
path,
|
||||||
|
updated_at_ms: 0,
|
||||||
|
modified_epoch_millis,
|
||||||
|
message_count: 0,
|
||||||
|
parent_session_id: None,
|
||||||
|
branch_name: None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
sessions.push(summary);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stable hex fingerprint of a workspace path.
|
/// Stable hex fingerprint of a workspace path.
|
||||||
@ -322,6 +460,13 @@ pub fn workspace_fingerprint(workspace_root: &Path) -> String {
|
|||||||
format!("{hash:016x}")
|
format!("{hash:016x}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The global sessions directory shared across all workspaces.
|
||||||
|
/// Points to `~/.claw/sessions/` (or `$CLAW_CONFIG_HOME/sessions/`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn global_sessions_root() -> PathBuf {
|
||||||
|
crate::config::default_config_home().join("sessions")
|
||||||
|
}
|
||||||
|
|
||||||
pub const PRIMARY_SESSION_EXTENSION: &str = "jsonl";
|
pub const PRIMARY_SESSION_EXTENSION: &str = "jsonl";
|
||||||
pub const LEGACY_SESSION_EXTENSION: &str = "json";
|
pub const LEGACY_SESSION_EXTENSION: &str = "json";
|
||||||
pub const LATEST_SESSION_REFERENCE: &str = "latest";
|
pub const LATEST_SESSION_REFERENCE: &str = "latest";
|
||||||
@ -574,7 +719,7 @@ fn format_no_managed_sessions(sessions_root: &Path) -> String {
|
|||||||
.and_then(|f| f.to_str())
|
.and_then(|f| f.to_str())
|
||||||
.unwrap_or("<unknown>");
|
.unwrap_or("<unknown>");
|
||||||
format!(
|
format!(
|
||||||
"no managed sessions found in .claw/sessions/{fingerprint_dir}/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible."
|
"no managed sessions found in .claw/sessions/{fingerprint_dir}/\nStart `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`.\nNote: /resume {LATEST_SESSION_REFERENCE} searches all workspaces."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,13 @@
|
|||||||
dead_code,
|
dead_code,
|
||||||
unused_imports,
|
unused_imports,
|
||||||
unused_variables,
|
unused_variables,
|
||||||
|
clippy::doc_markdown,
|
||||||
|
clippy::len_zero,
|
||||||
|
clippy::manual_string_new,
|
||||||
|
clippy::match_same_arms,
|
||||||
|
clippy::result_large_err,
|
||||||
|
clippy::too_many_lines,
|
||||||
|
clippy::uninlined_format_args,
|
||||||
clippy::unneeded_struct_pattern,
|
clippy::unneeded_struct_pattern,
|
||||||
clippy::unnecessary_wraps,
|
clippy::unnecessary_wraps,
|
||||||
clippy::unused_self
|
clippy::unused_self
|
||||||
@ -6060,9 +6067,16 @@ fn latest_managed_session() -> Result<ManagedSessionSummary, Box<dyn std::error:
|
|||||||
fn load_session_reference(
|
fn load_session_reference(
|
||||||
reference: &str,
|
reference: &str,
|
||||||
) -> Result<(SessionHandle, Session), Box<dyn std::error::Error>> {
|
) -> Result<(SessionHandle, Session), Box<dyn std::error::Error>> {
|
||||||
let loaded = current_session_store()?
|
let store = current_session_store()?;
|
||||||
.load_session(reference)
|
// For alias references ("latest", "last", "recent"), allow cross-workspace
|
||||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
// 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>)?;
|
||||||
Ok((
|
Ok((
|
||||||
SessionHandle {
|
SessionHandle {
|
||||||
id: loaded.handle.id,
|
id: loaded.handle.id,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user