diff --git a/src/app_controller.py b/src/app_controller.py index 0d2d4663..5433dceb 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -2002,8 +2002,7 @@ class AppController: self.ai_status = f"Manual prune error: {e}" print(f"Error during manual log pruning: {e}") - thread = threading.Thread(target=run_manual_prune, daemon=True) - thread.start() + self.submit_io(run_manual_prune) def _load_active_project(self) -> None: """Loads the active project configuration, with fallbacks.""" @@ -2052,8 +2051,7 @@ class AppController: pruner.prune() except Exception as e: print(f"Error during log pruning: {e}") - thread = threading.Thread(target=run_prune, daemon=True) - thread.start() + self.submit_io(run_prune) def _fetch_models(self, provider: str) -> None: """ @@ -2152,6 +2150,25 @@ class AppController: """ self._warmup.on_complete(callback) + def submit_io(self, fn: Callable, *args: Any, **kwargs: Any) -> Any: + """ + Submit a background job to the shared _io_pool. Use this for any + fire-and-forget background work; avoids the per-spawn cost of a new + threading.Thread. + + Returns a concurrent.futures.Future that can be used to track + completion, raise exceptions, or cancel the job. The pool is capped + at 4 workers (see src/io_pool.py) so the job may queue briefly if + the pool is saturated. + + Domain-specific threads (HookServer, WebSocketServer, MMA WorkerPool, + asyncio loop) are NOT submitted here - they have their own lifecycle + management. + [SDM: src/app_controller.py:submit_io] + """ + import concurrent.futures + return self._io_pool.submit(fn, *args, **kwargs) + def shutdown(self) -> None: """