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.
This commit is contained in:
@@ -268,6 +268,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
const handleExecute = async () => {
|
||||
if (!projectPath || !task.trim()) return;
|
||||
|
||||
let runId: number | null = null;
|
||||
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
@@ -277,8 +279,11 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
setElapsedTime(0);
|
||||
setTotalTokens(0);
|
||||
|
||||
// Set up event listeners
|
||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
||||
// Execute the agent with model override and get run ID
|
||||
runId = await api.executeAgent(agent.id!, projectPath, task, model);
|
||||
|
||||
// Set up event listeners with run ID isolation
|
||||
const outputUnlisten = await listen<string>(`agent-output:${runId}`, (event) => {
|
||||
try {
|
||||
// Store raw JSONL
|
||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||
@@ -291,12 +296,12 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
||||
const errorUnlisten = await listen<string>(`agent-error:${runId}`, (event) => {
|
||||
console.error("Agent error:", event.payload);
|
||||
setError(event.payload);
|
||||
});
|
||||
|
||||
const completeUnlisten = await listen<boolean>("agent-complete", (event) => {
|
||||
const completeUnlisten = await listen<boolean>(`agent-complete:${runId}`, (event) => {
|
||||
setIsRunning(false);
|
||||
setExecutionStartTime(null);
|
||||
if (!event.payload) {
|
||||
@@ -304,10 +309,13 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${runId}`, () => {
|
||||
setIsRunning(false);
|
||||
setExecutionStartTime(null);
|
||||
setError("Agent execution was cancelled");
|
||||
});
|
||||
|
||||
// Execute the agent with model override
|
||||
await api.executeAgent(agent.id!, projectPath, task, model);
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||
} catch (err) {
|
||||
console.error("Failed to execute agent:", err);
|
||||
setError("Failed to execute agent");
|
||||
|
@@ -176,13 +176,15 @@ export function AgentRunOutputViewer({
|
||||
};
|
||||
|
||||
const setupLiveEventListeners = async () => {
|
||||
if (!run.id) return;
|
||||
|
||||
try {
|
||||
// Clean up existing listeners
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
unlistenRefs.current = [];
|
||||
|
||||
// Set up live event listeners
|
||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
||||
// Set up live event listeners with run ID isolation
|
||||
const outputUnlisten = await listen<string>(`agent-output:${run.id}`, (event) => {
|
||||
try {
|
||||
// Store raw JSONL
|
||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||
@@ -195,16 +197,20 @@ export function AgentRunOutputViewer({
|
||||
}
|
||||
});
|
||||
|
||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
||||
const errorUnlisten = await listen<string>(`agent-error:${run.id}`, (event) => {
|
||||
console.error("Agent error:", event.payload);
|
||||
setToast({ message: event.payload, type: 'error' });
|
||||
});
|
||||
|
||||
const completeUnlisten = await listen<boolean>("agent-complete", () => {
|
||||
const completeUnlisten = await listen<boolean>(`agent-complete:${run.id}`, () => {
|
||||
setToast({ message: 'Agent execution completed', type: 'success' });
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${run.id}`, () => {
|
||||
setToast({ message: 'Agent execution was cancelled', type: 'error' });
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||
} catch (error) {
|
||||
console.error('Failed to set up live event listeners:', error);
|
||||
}
|
||||
|
@@ -71,10 +71,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
|
||||
const [isFirstPrompt, setIsFirstPrompt] = useState(!session);
|
||||
const [totalTokens, setTotalTokens] = useState(0);
|
||||
const [extractedSessionInfo, setExtractedSessionInfo] = useState<{
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
} | null>(null);
|
||||
const [extractedSessionInfo, setExtractedSessionInfo] = useState<{ sessionId: string; projectId: string } | null>(null);
|
||||
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(null);
|
||||
const [showTimeline, setShowTimeline] = useState(false);
|
||||
const [timelineVersion, setTimelineVersion] = useState(0);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
@@ -268,33 +266,39 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
};
|
||||
|
||||
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
|
||||
if (!projectPath || !prompt.trim() || isLoading) return;
|
||||
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath });
|
||||
|
||||
if (!projectPath) {
|
||||
setError("Please select a project directory first");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
hasActiveSessionRef.current = true;
|
||||
|
||||
// Add the user message immediately to the UI
|
||||
const userMessage: ClaudeStreamMessage = {
|
||||
type: "user",
|
||||
message: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
|
||||
// Clean up any existing listeners before creating new ones
|
||||
|
||||
// Clean up previous listeners
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
unlistenRefs.current = [];
|
||||
|
||||
// Set up event listeners
|
||||
const outputUnlisten = await listen<string>("claude-output", async (event) => {
|
||||
|
||||
// Set up event listeners before executing
|
||||
console.log('[ClaudeCodeSession] Setting up event listeners...');
|
||||
|
||||
// Listen for the session started event to get the Claude session ID
|
||||
const sessionStartedUnlisten = await listen<string>(`claude-session-started:*`, (event) => {
|
||||
const eventName = event.event;
|
||||
const sessionId = eventName.split(':')[1];
|
||||
if (sessionId && !claudeSessionId) {
|
||||
console.log('[ClaudeCodeSession] Received Claude session ID:', sessionId);
|
||||
setClaudeSessionId(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// If we already have a Claude session ID, use isolated listeners
|
||||
const eventSuffix = claudeSessionId ? `:${claudeSessionId}` : '';
|
||||
|
||||
const outputUnlisten = await listen<string>(`claude-output${eventSuffix}`, async (event) => {
|
||||
try {
|
||||
console.log('[ClaudeCodeSession] Received claude-output:', event.payload);
|
||||
|
||||
@@ -325,84 +329,69 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const errorUnlisten = await listen<string>("claude-error", (event) => {
|
||||
const errorUnlisten = await listen<string>(`claude-error${eventSuffix}`, (event) => {
|
||||
console.error("Claude error:", event.payload);
|
||||
setError(event.payload);
|
||||
});
|
||||
|
||||
const completeUnlisten = await listen<boolean>("claude-complete", async (event) => {
|
||||
const completeUnlisten = await listen<boolean>(`claude-complete${eventSuffix}`, async (event) => {
|
||||
console.log('[ClaudeCodeSession] Received claude-complete:', event.payload);
|
||||
setIsLoading(false);
|
||||
setIsCancelling(false);
|
||||
hasActiveSessionRef.current = false;
|
||||
if (!event.payload) {
|
||||
setError("Claude execution failed");
|
||||
}
|
||||
|
||||
// Track all messages at once after completion (batch operation)
|
||||
if (effectiveSession && rawJsonlOutput.length > 0) {
|
||||
console.log('[ClaudeCodeSession] Tracking all messages in batch:', rawJsonlOutput.length);
|
||||
api.trackSessionMessages(
|
||||
effectiveSession.id,
|
||||
effectiveSession.project_id,
|
||||
projectPath,
|
||||
rawJsonlOutput
|
||||
).catch(err => {
|
||||
console.error("Failed to track session messages:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we should auto-checkpoint
|
||||
if (effectiveSession && messages.length > 0) {
|
||||
// Check if we should create an auto checkpoint after completion
|
||||
if (effectiveSession && event.payload) {
|
||||
try {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const shouldCheckpoint = await api.checkAutoCheckpoint(
|
||||
const settings = await api.getCheckpointSettings(
|
||||
effectiveSession.id,
|
||||
effectiveSession.project_id,
|
||||
projectPath,
|
||||
JSON.stringify(lastMessage)
|
||||
projectPath
|
||||
);
|
||||
|
||||
if (shouldCheckpoint) {
|
||||
await api.createCheckpoint(
|
||||
if (settings.auto_checkpoint_enabled) {
|
||||
await api.checkAutoCheckpoint(
|
||||
effectiveSession.id,
|
||||
effectiveSession.project_id,
|
||||
projectPath,
|
||||
messages.length - 1,
|
||||
"Auto-checkpoint after tool use"
|
||||
prompt
|
||||
);
|
||||
console.log("Auto-checkpoint created");
|
||||
// Trigger timeline reload if it's currently visible
|
||||
// Reload timeline to show new checkpoint
|
||||
setTimelineVersion((v) => v + 1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to check/create auto-checkpoint:", err);
|
||||
console.error('Failed to check auto checkpoint:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up listeners after completion
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
unlistenRefs.current = [];
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||
unlistenRefs.current = [sessionStartedUnlisten, outputUnlisten, errorUnlisten, completeUnlisten];
|
||||
|
||||
// Add the user message immediately to the UI (after setting up listeners)
|
||||
const userMessage: ClaudeStreamMessage = {
|
||||
type: "user",
|
||||
message: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
|
||||
// Execute the appropriate command
|
||||
if (isFirstPrompt && !session) {
|
||||
// New session
|
||||
await api.executeClaudeCode(projectPath, prompt, model);
|
||||
setIsFirstPrompt(false);
|
||||
} else if (session && isFirstPrompt) {
|
||||
// Resuming a session
|
||||
await api.resumeClaudeCode(projectPath, session.id, prompt, model);
|
||||
setIsFirstPrompt(false);
|
||||
if (effectiveSession && !isFirstPrompt) {
|
||||
console.log('[ClaudeCodeSession] Resuming session:', effectiveSession.id);
|
||||
await api.resumeClaudeCode(projectPath, effectiveSession.id, prompt, model);
|
||||
} else {
|
||||
// Continuing conversation
|
||||
await api.continueClaudeCode(projectPath, prompt, model);
|
||||
console.log('[ClaudeCodeSession] Starting new session');
|
||||
setIsFirstPrompt(false);
|
||||
await api.executeClaudeCode(projectPath, prompt, model);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to send prompt:", err);
|
||||
setError("Failed to execute Claude Code");
|
||||
setError("Failed to send prompt");
|
||||
setIsLoading(false);
|
||||
hasActiveSessionRef.current = false;
|
||||
}
|
||||
@@ -499,8 +488,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
try {
|
||||
setIsCancelling(true);
|
||||
|
||||
// Cancel the Claude execution
|
||||
await api.cancelClaudeExecution();
|
||||
// Cancel the Claude execution with session ID if available
|
||||
await api.cancelClaudeExecution(claudeSessionId || undefined);
|
||||
|
||||
// Clean up listeners
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
|
@@ -153,13 +153,15 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
};
|
||||
|
||||
const setupLiveEventListeners = async () => {
|
||||
if (!session.id) return;
|
||||
|
||||
try {
|
||||
// Clean up existing listeners
|
||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||
unlistenRefs.current = [];
|
||||
|
||||
// Set up live event listeners similar to AgentExecution
|
||||
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
||||
// Set up live event listeners with run ID isolation
|
||||
const outputUnlisten = await listen<string>(`agent-output:${session.id}`, (event) => {
|
||||
try {
|
||||
// Store raw JSONL
|
||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||
@@ -172,17 +174,21 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp
|
||||
}
|
||||
});
|
||||
|
||||
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
||||
const errorUnlisten = await listen<string>(`agent-error:${session.id}`, (event) => {
|
||||
console.error("Agent error:", event.payload);
|
||||
setToast({ message: event.payload, type: 'error' });
|
||||
});
|
||||
|
||||
const completeUnlisten = await listen<boolean>("agent-complete", () => {
|
||||
const completeUnlisten = await listen<boolean>(`agent-complete:${session.id}`, () => {
|
||||
setToast({ message: 'Agent execution completed', type: 'success' });
|
||||
// Don't set status here as the parent component should handle it
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||
const cancelUnlisten = await listen<boolean>(`agent-cancelled:${session.id}`, () => {
|
||||
setToast({ message: 'Agent execution was cancelled', type: 'error' });
|
||||
});
|
||||
|
||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten];
|
||||
} catch (error) {
|
||||
console.error('Failed to set up live event listeners:', error);
|
||||
}
|
||||
|
Reference in New Issue
Block a user