Skip to content

Commit 97290e5

Browse files
committed
feat(core): implement session isolation for agent and claude executions
- Add run_id/session_id based event isolation for concurrent executions - Enhance process registry with graceful shutdown and fallback kill methods - Implement session-specific event listeners in React components - Add proper process cleanup with timeout handling - Support both isolated and backward-compatible event emissions - Improve error handling and logging for process management This change prevents event crosstalk between multiple concurrent agent/claude sessions running simultaneously, ensuring proper isolation and user experience.
1 parent f73d21e commit 97290e5

8 files changed

Lines changed: 352 additions & 148 deletions

File tree

src-tauri/src/commands/agents.rs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,7 +1251,9 @@ pub async fn execute_agent(
12511251
}
12521252
}
12531253

1254-
// Emit the line to the frontend
1254+
// Emit the line to the frontend with run_id for isolation
1255+
let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line);
1256+
// Also emit to the generic event for backward compatibility
12551257
let _ = app_handle.emit("agent-output", &line);
12561258
}
12571259

@@ -1277,7 +1279,9 @@ pub async fn execute_agent(
12771279
}
12781280

12791281
error!("stderr[{}]: {}", error_count, line);
1280-
// Emit error lines to the frontend
1282+
// Emit error lines to the frontend with run_id for isolation
1283+
let _ = app_handle_stderr.emit(&format!("agent-error:{}", run_id), &line);
1284+
// Also emit to the generic event for backward compatibility
12811285
let _ = app_handle_stderr.emit("agent-error", &line);
12821286
}
12831287

@@ -1366,6 +1370,7 @@ pub async fn execute_agent(
13661370
}
13671371

13681372
let _ = app.emit("agent-complete", false);
1373+
let _ = app.emit(&format!("agent-complete:{}", run_id), false);
13691374
return;
13701375
}
13711376

@@ -1398,6 +1403,7 @@ pub async fn execute_agent(
13981403
// Cleanup will be handled by the cleanup_finished_processes function
13991404

14001405
let _ = app.emit("agent-complete", true);
1406+
let _ = app.emit(&format!("agent-complete:{}", run_id), true);
14011407
});
14021408

14031409
Ok(run_id)
@@ -1442,43 +1448,45 @@ pub async fn list_running_sessions(
14421448
/// Kill a running agent session
14431449
#[tauri::command]
14441450
pub async fn kill_agent_session(
1451+
app: AppHandle,
14451452
db: State<'_, AgentDb>,
1453+
registry: State<'_, crate::process::ProcessRegistryState>,
14461454
run_id: i64,
14471455
) -> Result<bool, String> {
1448-
// First try to kill the process using system kill
1449-
let pid_result = {
1450-
let conn = db.0.lock().map_err(|e| e.to_string())?;
1451-
conn.query_row(
1452-
"SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'",
1453-
params![run_id],
1454-
|row| row.get::<_, Option<i64>>(0)
1455-
)
1456-
.map_err(|e| e.to_string())?
1456+
info!("Attempting to kill agent session {}", run_id);
1457+
1458+
// First try to kill using the process registry
1459+
let killed_via_registry = match registry.0.kill_process(run_id).await {
1460+
Ok(success) => {
1461+
if success {
1462+
info!("Successfully killed process {} via registry", run_id);
1463+
true
1464+
} else {
1465+
warn!("Process {} not found in registry", run_id);
1466+
false
1467+
}
1468+
}
1469+
Err(e) => {
1470+
warn!("Failed to kill process {} via registry: {}", run_id, e);
1471+
false
1472+
}
14571473
};
14581474

1459-
if let Some(pid) = pid_result {
1460-
// Try to kill the process
1461-
let kill_result = if cfg!(target_os = "windows") {
1462-
std::process::Command::new("taskkill")
1463-
.args(["/F", "/PID", &pid.to_string()])
1464-
.output()
1465-
} else {
1466-
std::process::Command::new("kill")
1467-
.args(["-TERM", &pid.to_string()])
1468-
.output()
1475+
// If registry kill didn't work, try fallback with PID from database
1476+
if !killed_via_registry {
1477+
let pid_result = {
1478+
let conn = db.0.lock().map_err(|e| e.to_string())?;
1479+
conn.query_row(
1480+
"SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'",
1481+
params![run_id],
1482+
|row| row.get::<_, Option<i64>>(0)
1483+
)
1484+
.map_err(|e| e.to_string())?
14691485
};
14701486

1471-
match kill_result {
1472-
Ok(output) => {
1473-
if output.status.success() {
1474-
info!("Successfully killed process {}", pid);
1475-
} else {
1476-
warn!("Kill command failed for PID {}: {}", pid, String::from_utf8_lossy(&output.stderr));
1477-
}
1478-
}
1479-
Err(e) => {
1480-
warn!("Failed to execute kill command for PID {}: {}", pid, e);
1481-
}
1487+
if let Some(pid) = pid_result {
1488+
info!("Attempting fallback kill for PID {} from database", pid);
1489+
let _ = registry.0.kill_process_by_pid(run_id, pid as u32)?;
14821490
}
14831491
}
14841492

@@ -1489,7 +1497,10 @@ pub async fn kill_agent_session(
14891497
params![run_id],
14901498
).map_err(|e| e.to_string())?;
14911499

1492-
Ok(updated > 0)
1500+
// Emit cancellation event with run_id for proper isolation
1501+
let _ = app.emit(&format!("agent-cancelled:{}", run_id), true);
1502+
1503+
Ok(updated > 0 || killed_via_registry)
14931504
}
14941505

14951506
/// Get the status of a specific agent session

src-tauri/src/commands/claude.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use tauri::{AppHandle, Emitter, Manager};
99
use tokio::process::{Command, Child};
1010
use tokio::sync::Mutex;
1111
use std::sync::Arc;
12+
use uuid;
1213

1314
/// Global state to track current Claude process
1415
pub struct ClaudeProcessState {
@@ -857,8 +858,8 @@ pub async fn resume_claude_code(
857858

858859
/// Cancel the currently running Claude Code execution
859860
#[tauri::command]
860-
pub async fn cancel_claude_execution(app: AppHandle) -> Result<(), String> {
861-
log::info!("Cancelling Claude Code execution");
861+
pub async fn cancel_claude_execution(app: AppHandle, session_id: Option<String>) -> Result<(), String> {
862+
log::info!("Cancelling Claude Code execution for session: {:?}", session_id);
862863

863864
let claude_state = app.state::<ClaudeProcessState>();
864865
let mut current_process = claude_state.current_process.lock().await;
@@ -872,9 +873,16 @@ pub async fn cancel_claude_execution(app: AppHandle) -> Result<(), String> {
872873
match child.kill().await {
873874
Ok(_) => {
874875
log::info!("Successfully killed Claude process");
875-
// Emit cancellation event
876+
877+
// If we have a session ID, emit session-specific events
878+
if let Some(sid) = session_id {
879+
let _ = app.emit(&format!("claude-cancelled:{}", sid), true);
880+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
881+
let _ = app.emit(&format!("claude-complete:{}", sid), false);
882+
}
883+
884+
// Also emit generic events for backward compatibility
876885
let _ = app.emit("claude-cancelled", true);
877-
// Also emit complete with false to indicate failure
878886
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
879887
let _ = app.emit("claude-complete", false);
880888
Ok(())
@@ -1055,6 +1063,15 @@ fn get_claude_settings_sync(_app: &AppHandle) -> Result<ClaudeSettings, String>
10551063
async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> {
10561064
use tokio::io::{AsyncBufReadExt, BufReader};
10571065

1066+
// Generate a unique session ID for this Claude Code session
1067+
let session_id = format!("claude-{}-{}",
1068+
std::time::SystemTime::now()
1069+
.duration_since(std::time::UNIX_EPOCH)
1070+
.unwrap_or_default()
1071+
.as_millis(),
1072+
uuid::Uuid::new_v4().to_string()
1073+
);
1074+
10581075
// Spawn the process
10591076
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?;
10601077

@@ -1064,43 +1081,55 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
10641081

10651082
// Get the child PID for logging
10661083
let pid = child.id();
1067-
log::info!("Spawned Claude process with PID: {:?}", pid);
1084+
log::info!("Spawned Claude process with PID: {:?} and session ID: {}", pid, session_id);
10681085

10691086
// Create readers
10701087
let stdout_reader = BufReader::new(stdout);
10711088
let stderr_reader = BufReader::new(stderr);
10721089

1073-
// Store the child process in the global state
1090+
// Store the child process in the global state (for backward compatibility)
10741091
let claude_state = app.state::<ClaudeProcessState>();
10751092
{
10761093
let mut current_process = claude_state.current_process.lock().await;
1094+
// If there's already a process running, kill it first
1095+
if let Some(mut existing_child) = current_process.take() {
1096+
log::warn!("Killing existing Claude process before starting new one");
1097+
let _ = existing_child.kill().await;
1098+
}
10771099
*current_process = Some(child);
10781100
}
10791101

10801102
// Spawn tasks to read stdout and stderr
10811103
let app_handle = app.clone();
1104+
let session_id_clone = session_id.clone();
10821105
let stdout_task = tokio::spawn(async move {
10831106
let mut lines = stdout_reader.lines();
10841107
while let Ok(Some(line)) = lines.next_line().await {
10851108
log::debug!("Claude stdout: {}", line);
1086-
// Emit the line to the frontend
1109+
// Emit the line to the frontend with session isolation
1110+
let _ = app_handle.emit(&format!("claude-output:{}", session_id_clone), &line);
1111+
// Also emit to the generic event for backward compatibility
10871112
let _ = app_handle.emit("claude-output", &line);
10881113
}
10891114
});
10901115

10911116
let app_handle_stderr = app.clone();
1117+
let session_id_clone2 = session_id.clone();
10921118
let stderr_task = tokio::spawn(async move {
10931119
let mut lines = stderr_reader.lines();
10941120
while let Ok(Some(line)) = lines.next_line().await {
10951121
log::error!("Claude stderr: {}", line);
1096-
// Emit error lines to the frontend
1122+
// Emit error lines to the frontend with session isolation
1123+
let _ = app_handle_stderr.emit(&format!("claude-error:{}", session_id_clone2), &line);
1124+
// Also emit to the generic event for backward compatibility
10971125
let _ = app_handle_stderr.emit("claude-error", &line);
10981126
}
10991127
});
11001128

11011129
// Wait for the process to complete
11021130
let app_handle_wait = app.clone();
11031131
let claude_state_wait = claude_state.current_process.clone();
1132+
let session_id_clone3 = session_id.clone();
11041133
tokio::spawn(async move {
11051134
let _ = stdout_task.await;
11061135
let _ = stderr_task.await;
@@ -1113,12 +1142,16 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
11131142
log::info!("Claude process exited with status: {}", status);
11141143
// Add a small delay to ensure all messages are processed
11151144
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1145+
let _ = app_handle_wait.emit(&format!("claude-complete:{}", session_id_clone3), status.success());
1146+
// Also emit to the generic event for backward compatibility
11161147
let _ = app_handle_wait.emit("claude-complete", status.success());
11171148
}
11181149
Err(e) => {
11191150
log::error!("Failed to wait for Claude process: {}", e);
11201151
// Add a small delay to ensure all messages are processed
11211152
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1153+
let _ = app_handle_wait.emit(&format!("claude-complete:{}", session_id_clone3), false);
1154+
// Also emit to the generic event for backward compatibility
11221155
let _ = app_handle_wait.emit("claude-complete", false);
11231156
}
11241157
}
@@ -1128,6 +1161,9 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), St
11281161
*current_process = None;
11291162
});
11301163

1164+
// Return the session ID to the frontend
1165+
let _ = app.emit(&format!("claude-session-started:{}", session_id), session_id.clone());
1166+
11311167
Ok(())
11321168
}
11331169

0 commit comments

Comments
 (0)