Skip to content

Commit 3c33018

Browse files
author
Greyforge Admin
committed
Clear session locks on forced delete
1 parent 7954d02 commit 3c33018

3 files changed

Lines changed: 139 additions & 4 deletions

File tree

src/cortex-cli/src/cli/handlers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,15 @@ pub async fn run_delete(delete_cli: DeleteCommand) -> Result<()> {
694694
// Resolve session ID
695695
let conversation_id = resolve_session_id(&delete_cli.session_id, &config.cortex_home)
696696
.map_err(|e| anyhow::anyhow!("{}", e))?;
697+
let conversation_id_str = conversation_id.to_string();
698+
699+
if crate::lock_cmd::is_session_locked(&conversation_id_str) && !delete_cli.force {
700+
bail!(
701+
"Session {} is locked. Use `cortex lock remove {}` first, or rerun delete with --force.",
702+
conversation_id,
703+
conversation_id
704+
);
705+
}
697706

698707
// Confirm deletion
699708
if !delete_cli.yes {
@@ -717,7 +726,15 @@ pub async fn run_delete(delete_cli: DeleteCommand) -> Result<()> {
717726
cortex_engine::rollout::get_rollout_path(&config.cortex_home, &conversation_id);
718727
if rollout_path.exists() {
719728
std::fs::remove_file(&rollout_path)?;
729+
let removed_locks = crate::lock_cmd::remove_session_locks(&conversation_id_str)?;
720730
print_success(&format!("Deleted session: {}", conversation_id));
731+
if removed_locks > 0 {
732+
print_info(&format!(
733+
"Removed {} lock entr{} for deleted session.",
734+
removed_locks,
735+
if removed_locks == 1 { "y" } else { "ies" }
736+
));
737+
}
721738
} else {
722739
print_warning("Session file not found (may have been already deleted).");
723740
}

src/cortex-cli/src/lock_cmd.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,35 @@ fn save_lock_file(lock_file: &LockFile) -> Result<()> {
154154
/// Check if a session is locked.
155155
pub fn is_session_locked(session_id: &str) -> bool {
156156
match load_lock_file() {
157-
Ok(lock_file) => lock_file.locked_sessions.iter().any(|entry| {
158-
entry.session_id == session_id
159-
|| session_id.starts_with(&entry.session_id[..8.min(entry.session_id.len())])
160-
}),
157+
Ok(lock_file) => lock_file
158+
.locked_sessions
159+
.iter()
160+
.any(|entry| lock_matches_session(&entry.session_id, session_id)),
161161
Err(_) => false,
162162
}
163163
}
164164

165+
/// Remove lock entries matching a session ID.
166+
pub fn remove_session_locks(session_id: &str) -> Result<usize> {
167+
let mut lock_file = load_lock_file()?;
168+
let original_len = lock_file.locked_sessions.len();
169+
lock_file
170+
.locked_sessions
171+
.retain(|entry| !lock_matches_session(&entry.session_id, session_id));
172+
173+
let removed = original_len - lock_file.locked_sessions.len();
174+
if removed > 0 {
175+
save_lock_file(&lock_file)?;
176+
}
177+
178+
Ok(removed)
179+
}
180+
181+
fn lock_matches_session(locked_session_id: &str, session_id: &str) -> bool {
182+
locked_session_id == session_id
183+
|| session_id.starts_with(&locked_session_id[..8.min(locked_session_id.len())])
184+
}
185+
165186
impl LockCli {
166187
/// Run the lock command.
167188
pub async fn run(self) -> Result<()> {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use std::fs;
2+
use std::process::Command;
3+
4+
use serde_json::Value;
5+
use tempfile::tempdir;
6+
7+
fn cortex(home_dir: &std::path::Path) -> Command {
8+
let mut command = Command::new(env!("CARGO_BIN_EXE_Cortex"));
9+
command.env("HOME", home_dir).env_remove("CORTEX_HOME");
10+
command
11+
}
12+
13+
fn imported_session_id(stdout: &[u8], stderr: &[u8]) -> String {
14+
let stdout = String::from_utf8_lossy(stdout);
15+
let stderr = String::from_utf8_lossy(stderr);
16+
let combined = format!("{stdout}{stderr}");
17+
let marker = "Imported session as:";
18+
let marker_index = combined
19+
.find(marker)
20+
.unwrap_or_else(|| panic!("import output should include session id:\n{combined}"));
21+
combined[marker_index + marker.len()..]
22+
.split_whitespace()
23+
.next()
24+
.expect("import output should include session id after marker")
25+
.to_string()
26+
}
27+
28+
#[test]
29+
fn delete_force_removes_matching_lock_entry() {
30+
let home_dir = tempdir().unwrap();
31+
let export_file = home_dir.path().join("session.json");
32+
fs::write(
33+
&export_file,
34+
r#"{
35+
"version": 1,
36+
"session": {
37+
"id": "550e8400-e29b-41d4-a716-446655440000",
38+
"title": "Locked delete",
39+
"created_at": "2024-01-01T00:00:00Z",
40+
"cwd": "/tmp",
41+
"model": "test-model"
42+
},
43+
"messages": [
44+
{ "role": "user", "content": "hello" }
45+
]
46+
}
47+
"#,
48+
)
49+
.unwrap();
50+
51+
let import_output = cortex(home_dir.path())
52+
.args(["import", export_file.to_str().unwrap()])
53+
.output()
54+
.unwrap();
55+
assert!(
56+
import_output.status.success(),
57+
"import failed\nstdout:\n{}\nstderr:\n{}",
58+
String::from_utf8_lossy(&import_output.stdout),
59+
String::from_utf8_lossy(&import_output.stderr)
60+
);
61+
let session_id = imported_session_id(&import_output.stdout, &import_output.stderr);
62+
63+
let lock_output = cortex(home_dir.path())
64+
.args(["lock", "add", &session_id])
65+
.output()
66+
.unwrap();
67+
assert!(
68+
lock_output.status.success(),
69+
"lock add failed\nstdout:\n{}\nstderr:\n{}",
70+
String::from_utf8_lossy(&lock_output.stdout),
71+
String::from_utf8_lossy(&lock_output.stderr)
72+
);
73+
74+
let delete_output = cortex(home_dir.path())
75+
.args(["delete", &session_id, "--yes", "--force"])
76+
.output()
77+
.unwrap();
78+
assert!(
79+
delete_output.status.success(),
80+
"delete failed\nstdout:\n{}\nstderr:\n{}",
81+
String::from_utf8_lossy(&delete_output.stdout),
82+
String::from_utf8_lossy(&delete_output.stderr)
83+
);
84+
85+
let lock_list_output = cortex(home_dir.path())
86+
.args(["lock", "list", "--json"])
87+
.output()
88+
.unwrap();
89+
assert!(
90+
lock_list_output.status.success(),
91+
"lock list failed\nstdout:\n{}\nstderr:\n{}",
92+
String::from_utf8_lossy(&lock_list_output.stdout),
93+
String::from_utf8_lossy(&lock_list_output.stderr)
94+
);
95+
let locks: Value = serde_json::from_slice(&lock_list_output.stdout).unwrap();
96+
assert_eq!(locks.as_array().unwrap().len(), 0);
97+
}

0 commit comments

Comments
 (0)