fix(agents): improve process termination with multi-layer kill strategy

Resolves #87 and #9 by implementing a robust three-tier process
termination approach:

1. ProcessRegistry kill - primary method using run_id tracking
2. ClaudeProcessState kill - fallback via stored process handle
3. System kill command - last resort using PID and OS commands

Key improvements:
- Enhanced logging throughout termination flow for better debugging
- Graceful fallback between termination methods
- Proper UI state management even when backend termination fails
- Track run_id in AgentExecution component for targeted process killing
- Comprehensive error handling with user-friendly feedback
- Consistent event emission for UI synchronization

This ensures agents can be properly stopped without requiring
application restart, addressing the core issue where STOP
requests were ignored and processes continued running.
This commit is contained in:
Mufeed VH
2025-07-02 18:17:05 +05:30
parent e8c54d7fad
commit a7e17f16ec
4 changed files with 174 additions and 46 deletions

View File

@@ -92,6 +92,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
const fullscreenMessagesEndRef = useRef<HTMLDivElement>(null);
const unlistenRefs = useRef<UnlistenFn[]>([]);
const elapsedTimeIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [runId, setRunId] = useState<number | null>(null);
// Filter out messages that shouldn't be displayed
const displayableMessages = React.useMemo(() => {
@@ -266,24 +267,24 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
};
const handleExecute = async () => {
if (!projectPath || !task.trim()) return;
let runId: number | null = null;
try {
setIsRunning(true);
setError(null);
setExecutionStartTime(Date.now());
setMessages([]);
setRawJsonlOutput([]);
setExecutionStartTime(Date.now());
setElapsedTime(0);
setTotalTokens(0);
// Execute the agent with model override and get run ID
runId = await api.executeAgent(agent.id!, projectPath, task, model);
setRunId(null);
// Clear any existing listeners
unlistenRefs.current.forEach(unlisten => unlisten());
unlistenRefs.current = [];
// Execute the agent and get the run ID
const executionRunId = await api.executeAgent(agent.id!, projectPath, task, model);
console.log("Agent execution started with run ID:", executionRunId);
setRunId(executionRunId);
// Set up event listeners with run ID isolation
const outputUnlisten = await listen<string>(`agent-output:${runId}`, (event) => {
const outputUnlisten = await listen<string>(`agent-output:${executionRunId}`, (event) => {
try {
// Store raw JSONL
setRawJsonlOutput(prev => [...prev, event.payload]);
@@ -296,12 +297,12 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
}
});
const errorUnlisten = await listen<string>(`agent-error:${runId}`, (event) => {
const errorUnlisten = await listen<string>(`agent-error:${executionRunId}`, (event) => {
console.error("Agent error:", event.payload);
setError(event.payload);
});
const completeUnlisten = await listen<boolean>(`agent-complete:${runId}`, (event) => {
const completeUnlisten = await listen<boolean>(`agent-complete:${executionRunId}`, (event) => {
setIsRunning(false);
setExecutionStartTime(null);
if (!event.payload) {
@@ -309,7 +310,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
}
});
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${runId}`, () => {
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${executionRunId}`, () => {
setIsRunning(false);
setExecutionStartTime(null);
setError("Agent execution was cancelled");
@@ -318,16 +319,41 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
} catch (err) {
console.error("Failed to execute agent:", err);
setError("Failed to execute agent");
setIsRunning(false);
setExecutionStartTime(null);
setRunId(null);
// Show error in messages
setMessages(prev => [...prev, {
type: "result",
subtype: "error",
is_error: true,
result: `Failed to execute agent: ${err instanceof Error ? err.message : 'Unknown error'}`,
duration_ms: 0,
usage: {
input_tokens: 0,
output_tokens: 0
}
}]);
}
};
const handleStop = async () => {
try {
// TODO: Implement actual stop functionality via API
// For now, just update the UI state
if (!runId) {
console.error("No run ID available to stop");
return;
}
// Call the API to kill the agent session
const success = await api.killAgentSession(runId);
if (success) {
console.log(`Successfully stopped agent session ${runId}`);
} else {
console.warn(`Failed to stop agent session ${runId} - it may have already finished`);
}
// Update UI state
setIsRunning(false);
setExecutionStartTime(null);
@@ -349,6 +375,22 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
}]);
} catch (err) {
console.error("Failed to stop agent:", err);
// Still update UI state even if the backend call failed
setIsRunning(false);
setExecutionStartTime(null);
// Show error message
setMessages(prev => [...prev, {
type: "result",
subtype: "error",
is_error: true,
result: `Failed to stop execution: ${err instanceof Error ? err.message : 'Unknown error'}`,
duration_ms: elapsedTime * 1000,
usage: {
input_tokens: totalTokens,
output_tokens: 0
}
}]);
}
};

View File

@@ -606,7 +606,25 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
setError(null);
} catch (err) {
console.error("Failed to cancel execution:", err);
setError("Failed to cancel execution");
// Even if backend fails, we should update UI to reflect stopped state
// Add error message but still stop the UI loading state
const errorMessage: ClaudeStreamMessage = {
type: "system",
subtype: "error",
result: `Failed to cancel execution: ${err instanceof Error ? err.message : 'Unknown error'}. The process may still be running in the background.`,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, errorMessage]);
// Clean up listeners anyway
unlistenRefs.current.forEach(unlisten => unlisten());
unlistenRefs.current = [];
// Reset states to allow user to continue
setIsLoading(false);
hasActiveSessionRef.current = false;
setError(null);
} finally {
setIsCancelling(false);
}