diff --git a/apps/backend/ideation/generator.py b/apps/backend/ideation/generator.py index 3068a68e..b03a097e 100644 --- a/apps/backend/ideation/generator.py +++ b/apps/backend/ideation/generator.py @@ -91,11 +91,14 @@ class IdeationGenerator: prompt += f"\n{additional_context}\n" # Create client with thinking budget + # Use agent_type="ideation" to avoid loading unnecessary MCP servers + # which can cause 60-second timeout delays client = create_client( self.project_dir, self.output_dir, resolve_model_id(self.model), max_thinking_tokens=self.thinking_budget, + agent_type="ideation", ) try: @@ -184,11 +187,13 @@ Common fixes: Write the fixed JSON to the file now. """ + # Use agent_type="ideation" for recovery agent as well client = create_client( self.project_dir, self.output_dir, resolve_model_id(self.model), max_thinking_tokens=self.thinking_budget, + agent_type="ideation", ) try: diff --git a/apps/backend/ideation/runner.py b/apps/backend/ideation/runner.py index dfe1e81e..70fb2fbc 100644 --- a/apps/backend/ideation/runner.py +++ b/apps/backend/ideation/runner.py @@ -28,6 +28,7 @@ from .types import IdeationPhaseResult # Configuration MAX_RETRIES = 3 +IDEATION_TIMEOUT_SECONDS = 5 * 60 # 5 minutes max for all ideation types class IdeationOrchestrator: @@ -173,16 +174,45 @@ class IdeationOrchestrator: "progress", ) - # Create tasks for all enabled types - ideation_tasks = [ - self.output_streamer.stream_ideation_result( - ideation_type, self.phase_executor, MAX_RETRIES + # Create tasks explicitly so we can cancel them on timeout + ideation_task_objs = [ + asyncio.create_task( + self.output_streamer.stream_ideation_result( + ideation_type, self.phase_executor, MAX_RETRIES + ) ) for ideation_type in self.enabled_types ] - # Run all ideation types concurrently - ideation_results = await asyncio.gather(*ideation_tasks, return_exceptions=True) + # Run all ideation types concurrently with timeout protection + # 5 minute timeout prevents infinite hangs if one type stalls + try: + ideation_results = await asyncio.wait_for( + asyncio.gather(*ideation_task_objs, return_exceptions=True), + timeout=IDEATION_TIMEOUT_SECONDS, + ) + except asyncio.TimeoutError: + print_status( + "Ideation generation timed out after 5 minutes", + "error", + ) + # Cancel all pending tasks to prevent resource leaks + for task in ideation_task_objs: + if not task.done(): + task.cancel() + # Wait for cancellation to complete and preserve results from completed tasks + # Tasks that finished before timeout will return their results; + # cancelled tasks will return CancelledError + results_after_cancel = await asyncio.gather( + *ideation_task_objs, return_exceptions=True + ) + # Convert CancelledError to timeout exception, preserve completed results + ideation_results = [ + Exception("Ideation timed out") + if isinstance(res, asyncio.CancelledError) + else res + for res in results_after_cancel + ] # Process results for i, result in enumerate(ideation_results): diff --git a/apps/frontend/src/main/agent/agent-queue.ts b/apps/frontend/src/main/agent/agent-queue.ts index 0dcad18d..fc9233ca 100644 --- a/apps/frontend/src/main/agent/agent-queue.ts +++ b/apps/frontend/src/main/agent/agent-queue.ts @@ -417,7 +417,12 @@ export class AgentQueueManager { // Track completed types for progress calculation const completedTypes = new Set(); - const totalTypes = 7; // Default all types + // Derive totalTypes from --types argument instead of hardcoding + const typesArgIndex = args.findIndex((arg) => arg === '--types'); + const totalTypes = + typesArgIndex > -1 && args[typesArgIndex + 1] + ? args[typesArgIndex + 1].split(',').length + : 6; // Default to 6 if not specified // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support childProcess.stdout?.on('data', (data: Buffer) => { diff --git a/apps/frontend/src/renderer/stores/ideation-store.ts b/apps/frontend/src/renderer/stores/ideation-store.ts index f29232e1..dadefb7f 100644 --- a/apps/frontend/src/renderer/stores/ideation-store.ts +++ b/apps/frontend/src/renderer/stores/ideation-store.ts @@ -11,6 +11,8 @@ import type { import { DEFAULT_IDEATION_CONFIG } from '../../shared/constants'; const GENERATION_TIMEOUT_MS = 5 * 60 * 1000; +/** Maximum number of log entries to retain in memory for debugging */ +const MAX_LOG_ENTRIES = 500; const generationTimeoutIds = new Map>(); @@ -266,7 +268,7 @@ export const useIdeationStore = create((set) => ({ addLog: (log) => set((state) => ({ - logs: [...state.logs, log].slice(-100) // Keep last 100 logs + logs: [...state.logs, log].slice(-MAX_LOG_ENTRIES) })), clearLogs: () => set({ logs: [] }),