diff --git a/parallel_orchestrator.py b/parallel_orchestrator.py index 09f4e22c..98778c11 100644 --- a/parallel_orchestrator.py +++ b/parallel_orchestrator.py @@ -439,14 +439,20 @@ def _spawn_coding_agent(self, feature_id: int) -> tuple[bool, str]: cmd.append("--yolo") try: - proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - cwd=str(AUTOCODER_ROOT), - env={**os.environ, "PYTHONUNBUFFERED": "1"}, - ) + # CREATE_NO_WINDOW on Windows prevents console window pop-ups + # stdin=DEVNULL prevents blocking on stdin reads + popen_kwargs = { + "stdin": subprocess.DEVNULL, + "stdout": subprocess.PIPE, + "stderr": subprocess.STDOUT, + "text": True, + "cwd": str(AUTOCODER_ROOT), # Run from autocoder root for proper imports + "env": {**os.environ, "PYTHONUNBUFFERED": "1"}, + } + if sys.platform == "win32": + popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + + proc = subprocess.Popen(cmd, **popen_kwargs) except Exception as e: # Reset in_progress on failure session = self.get_session() @@ -626,6 +632,12 @@ def _read_output( print(f"[Feature #{feature_id}] {line}", flush=True) proc.wait() finally: + # CRITICAL: Kill the process tree to clean up any child processes (e.g., Claude CLI) + # This prevents zombie processes from accumulating + try: + _kill_process_tree(proc, timeout=2.0) + except Exception as e: + debug_log.log("CLEANUP", f"Error killing process tree for {agent_type} agent", error=str(e)) self._on_agent_complete(feature_id, proc.returncode, agent_type, proc) def _on_agent_complete( diff --git a/server/services/process_manager.py b/server/services/process_manager.py index 0c50fd3d..cd685ea1 100644 --- a/server/services/process_manager.py +++ b/server/services/process_manager.py @@ -354,14 +354,20 @@ async def start( try: # Start subprocess with piped stdout/stderr # Use project_dir as cwd so Claude SDK sandbox allows access to project files - # IMPORTANT: Set PYTHONUNBUFFERED to ensure output isn't delayed - self.process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=str(self.project_dir), - env={**os.environ, "PYTHONUNBUFFERED": "1"}, - ) + # stdin=DEVNULL prevents blocking if Claude CLI or child process tries to read stdin + # CREATE_NO_WINDOW on Windows prevents console window pop-ups + # PYTHONUNBUFFERED ensures output isn't delayed + popen_kwargs = { + "stdin": subprocess.DEVNULL, + "stdout": subprocess.PIPE, + "stderr": subprocess.STDOUT, + "cwd": str(self.project_dir), + "env": {**os.environ, "PYTHONUNBUFFERED": "1"}, + } + if sys.platform == "win32": + popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + + self.process = subprocess.Popen(cmd, **popen_kwargs) # Atomic lock creation - if it fails, another process beat us if not self._create_lock(): diff --git a/start_ui.bat b/start_ui.bat index 2c597539..c8ad646a 100644 --- a/start_ui.bat +++ b/start_ui.bat @@ -39,5 +39,3 @@ pip install -r requirements.txt --quiet REM Run the Python launcher python "%~dp0start_ui.py" %* - -pause