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
17 changes: 17 additions & 0 deletions src/cortex-cli/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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).");
}
Expand Down
29 changes: 25 additions & 4 deletions src/cortex-cli/src/lock_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
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<()> {
Expand Down
97 changes: 97 additions & 0 deletions src/cortex-cli/tests/delete_locks.rs
Original file line number Diff line number Diff line change
@@ -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);
}