From 3c330180ebf3d5e23b88257afdeebe6a126496df Mon Sep 17 00:00:00 2001 From: Greyforge Admin Date: Tue, 19 May 2026 23:35:04 -0400 Subject: [PATCH] Clear session locks on forced delete --- src/cortex-cli/src/cli/handlers.rs | 17 +++++ src/cortex-cli/src/lock_cmd.rs | 29 +++++++-- src/cortex-cli/tests/delete_locks.rs | 97 ++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/cortex-cli/tests/delete_locks.rs diff --git a/src/cortex-cli/src/cli/handlers.rs b/src/cortex-cli/src/cli/handlers.rs index e92fa8632..98d64c16f 100644 --- a/src/cortex-cli/src/cli/handlers.rs +++ b/src/cortex-cli/src/cli/handlers.rs @@ -694,6 +694,15 @@ pub async fn run_delete(delete_cli: DeleteCommand) -> Result<()> { // Resolve session ID let conversation_id = resolve_session_id(&delete_cli.session_id, &config.cortex_home) .map_err(|e| anyhow::anyhow!("{}", e))?; + let conversation_id_str = conversation_id.to_string(); + + if crate::lock_cmd::is_session_locked(&conversation_id_str) && !delete_cli.force { + bail!( + "Session {} is locked. Use `cortex lock remove {}` first, or rerun delete with --force.", + conversation_id, + conversation_id + ); + } // Confirm deletion if !delete_cli.yes { @@ -717,7 +726,15 @@ pub async fn run_delete(delete_cli: DeleteCommand) -> Result<()> { cortex_engine::rollout::get_rollout_path(&config.cortex_home, &conversation_id); if rollout_path.exists() { std::fs::remove_file(&rollout_path)?; + let removed_locks = crate::lock_cmd::remove_session_locks(&conversation_id_str)?; print_success(&format!("Deleted session: {}", conversation_id)); + if removed_locks > 0 { + print_info(&format!( + "Removed {} lock entr{} for deleted session.", + removed_locks, + if removed_locks == 1 { "y" } else { "ies" } + )); + } } else { print_warning("Session file not found (may have been already deleted)."); } diff --git a/src/cortex-cli/src/lock_cmd.rs b/src/cortex-cli/src/lock_cmd.rs index dc652cad7..ff079d4f1 100644 --- a/src/cortex-cli/src/lock_cmd.rs +++ b/src/cortex-cli/src/lock_cmd.rs @@ -154,14 +154,35 @@ fn save_lock_file(lock_file: &LockFile) -> Result<()> { /// Check if a session is locked. pub fn is_session_locked(session_id: &str) -> bool { match load_lock_file() { - Ok(lock_file) => lock_file.locked_sessions.iter().any(|entry| { - entry.session_id == session_id - || session_id.starts_with(&entry.session_id[..8.min(entry.session_id.len())]) - }), + Ok(lock_file) => lock_file + .locked_sessions + .iter() + .any(|entry| lock_matches_session(&entry.session_id, session_id)), Err(_) => false, } } +/// Remove lock entries matching a session ID. +pub fn remove_session_locks(session_id: &str) -> Result { + let mut lock_file = load_lock_file()?; + let original_len = lock_file.locked_sessions.len(); + lock_file + .locked_sessions + .retain(|entry| !lock_matches_session(&entry.session_id, session_id)); + + let removed = original_len - lock_file.locked_sessions.len(); + if removed > 0 { + save_lock_file(&lock_file)?; + } + + Ok(removed) +} + +fn lock_matches_session(locked_session_id: &str, session_id: &str) -> bool { + locked_session_id == session_id + || session_id.starts_with(&locked_session_id[..8.min(locked_session_id.len())]) +} + impl LockCli { /// Run the lock command. pub async fn run(self) -> Result<()> { diff --git a/src/cortex-cli/tests/delete_locks.rs b/src/cortex-cli/tests/delete_locks.rs new file mode 100644 index 000000000..a40577245 --- /dev/null +++ b/src/cortex-cli/tests/delete_locks.rs @@ -0,0 +1,97 @@ +use std::fs; +use std::process::Command; + +use serde_json::Value; +use tempfile::tempdir; + +fn cortex(home_dir: &std::path::Path) -> Command { + let mut command = Command::new(env!("CARGO_BIN_EXE_Cortex")); + command.env("HOME", home_dir).env_remove("CORTEX_HOME"); + command +} + +fn imported_session_id(stdout: &[u8], stderr: &[u8]) -> String { + let stdout = String::from_utf8_lossy(stdout); + let stderr = String::from_utf8_lossy(stderr); + let combined = format!("{stdout}{stderr}"); + let marker = "Imported session as:"; + let marker_index = combined + .find(marker) + .unwrap_or_else(|| panic!("import output should include session id:\n{combined}")); + combined[marker_index + marker.len()..] + .split_whitespace() + .next() + .expect("import output should include session id after marker") + .to_string() +} + +#[test] +fn delete_force_removes_matching_lock_entry() { + let home_dir = tempdir().unwrap(); + let export_file = home_dir.path().join("session.json"); + fs::write( + &export_file, + r#"{ + "version": 1, + "session": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "title": "Locked delete", + "created_at": "2024-01-01T00:00:00Z", + "cwd": "/tmp", + "model": "test-model" + }, + "messages": [ + { "role": "user", "content": "hello" } + ] +} +"#, + ) + .unwrap(); + + let import_output = cortex(home_dir.path()) + .args(["import", export_file.to_str().unwrap()]) + .output() + .unwrap(); + assert!( + import_output.status.success(), + "import failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&import_output.stdout), + String::from_utf8_lossy(&import_output.stderr) + ); + let session_id = imported_session_id(&import_output.stdout, &import_output.stderr); + + let lock_output = cortex(home_dir.path()) + .args(["lock", "add", &session_id]) + .output() + .unwrap(); + assert!( + lock_output.status.success(), + "lock add failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&lock_output.stdout), + String::from_utf8_lossy(&lock_output.stderr) + ); + + let delete_output = cortex(home_dir.path()) + .args(["delete", &session_id, "--yes", "--force"]) + .output() + .unwrap(); + assert!( + delete_output.status.success(), + "delete failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&delete_output.stdout), + String::from_utf8_lossy(&delete_output.stderr) + ); + + let lock_list_output = cortex(home_dir.path()) + .args(["lock", "list", "--json"]) + .output() + .unwrap(); + assert!( + lock_list_output.status.success(), + "lock list failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&lock_list_output.stdout), + String::from_utf8_lossy(&lock_list_output.stderr) + ); + let locks: Value = serde_json::from_slice(&lock_list_output.stdout).unwrap(); + assert_eq!(locks.as_array().unwrap().len(), 0); +}