diff --git a/PluginCore/backend_registry.py b/PluginCore/backend_registry.py index 6d16931..e5e1a5d 100644 --- a/PluginCore/backend_registry.py +++ b/PluginCore/backend_registry.py @@ -6,6 +6,7 @@ Backends are discovered from their owning plugins and instantiated from config. from __future__ import annotations import importlib +import importlib.util import inspect import re from typing import Any, Dict, Optional, Type @@ -119,6 +120,16 @@ def _discover_plugin_backend_class(backend_type: str) -> Optional[Type[BackendBa return resolved +def _plugin_module_exists(plugin_name: str) -> bool: + normalized = _normalize_backend_type(plugin_name) + if not normalized: + return False + try: + return importlib.util.find_spec(f"plugins.{normalized}") is not None + except Exception: + return False + + def _resolve_backend_class(backend_type: str) -> Optional[Type[BackendBase]]: normalized = _normalize_backend_type(backend_type) if not normalized: @@ -200,7 +211,11 @@ class BackendRegistry: continue backend_cls = _resolve_backend_class(backend_type) if backend_cls is None: - if backend_type not in _PROVIDER_ONLY_BACKEND_NAMES and not self._suppress_debug: + if ( + backend_type not in _PROVIDER_ONLY_BACKEND_NAMES + and not self._suppress_debug + and not _plugin_module_exists(backend_type) + ): debug(f"[BackendRegistry] Unknown backend type '{raw_backend_type}'") continue diff --git a/SYS/worker.py b/SYS/worker.py index 96ef960..3bd9e08 100644 --- a/SYS/worker.py +++ b/SYS/worker.py @@ -185,7 +185,7 @@ class WorkerManagerRegistry: except Exception: from SYS.logger import logger logger.exception("Failed to close existing WorkerManager during registry ensure") - cls._manager = WorkerManager(resolved_root, auto_refresh_interval=0.5) + cls._manager = WorkerManager(auto_refresh_interval=0.5) cls._manager_root = resolved_root manager = cls._manager diff --git a/SYS/worker_manager.py b/SYS/worker_manager.py index 3bcbb98..e4ee8e0 100644 --- a/SYS/worker_manager.py +++ b/SYS/worker_manager.py @@ -277,6 +277,7 @@ class WorkerManager: self._lock = Lock() self.worker_handlers: Dict[str, WorkerLoggingHandler] = {} self._worker_last_step: Dict[str, str] = {} + self._db_lock = Lock() # Buffered stdout/log batching to reduce DB lock contention. self._stdout_buffers: Dict[Tuple[str, str], List[str]] = {} @@ -596,8 +597,30 @@ class WorkerManager: limit: int = 500) -> List[Dict[str, Any]]: """Fetch recorded worker timeline events.""" - with self._db_lock: - return self.db.get_worker_events(worker_id, limit) + try: + with self._db_lock: + rows = db.fetchall( + "SELECT content, channel, timestamp FROM worker_stdout WHERE worker_id = ? ORDER BY timestamp ASC LIMIT ?", + (worker_id, int(limit or 500)), + ) or [] + events: List[Dict[str, Any]] = [] + for row in rows: + payload = dict(row) + events.append( + { + "message": payload.get("content"), + "channel": payload.get("channel") or "stdout", + "created_at": payload.get("timestamp"), + "step": self._worker_last_step.get(worker_id), + } + ) + return events + except Exception as e: + logger.error( + f"[WorkerManager] Error getting worker events for {worker_id}: {e}", + exc_info=True, + ) + return [] def log_step(self, worker_id: str, step_text: str) -> bool: """Log a step to a worker's step history. @@ -610,10 +633,14 @@ class WorkerManager: True if successful """ try: - with self._db_lock: - success = self.db.append_worker_steps(worker_id, step_text) + step_value = str(step_text or "").strip() + if not step_value: + return True + + success = update_worker(worker_id, details=step_value) + append_worker_stdout(worker_id, step_value, channel="step") if success: - self._worker_last_step[worker_id] = step_text + self._worker_last_step[worker_id] = step_value return success except Exception as e: logger.error( @@ -637,7 +664,18 @@ class WorkerManager: """ try: with self._db_lock: - return self.db.get_worker_steps(worker_id) + rows = db.fetchall( + "SELECT content FROM worker_stdout WHERE worker_id = ? AND channel = 'step' ORDER BY timestamp ASC", + (worker_id,), + ) or [] + parts = [str(dict(row).get("content") or "").strip() for row in rows] + parts = [part for part in parts if part] + if parts: + return "\n".join(parts) + worker = db_get_worker(worker_id) + if isinstance(worker, dict): + return str(worker.get("details") or "") + return "" except Exception as e: logger.error( f"[WorkerManager] Error getting steps for worker {worker_id}: {e}", @@ -725,7 +763,17 @@ class WorkerManager: """ try: with self._db_lock: - count = self.db.cleanup_old_workers(days) + cutoff_days = max(0, int(days or 0)) + cutoff_expr = f"-{cutoff_days} days" + db.execute( + "DELETE FROM worker_stdout WHERE worker_id IN (SELECT id FROM workers WHERE status != 'running' AND updated_at < datetime('now', ?))", + (cutoff_expr,), + ) + cur = db.execute( + "DELETE FROM workers WHERE status != 'running' AND updated_at < datetime('now', ?)", + (cutoff_expr,), + ) + count = int(getattr(cur, "rowcount", 0) or 0) if count > 0: logger.info(f"[WorkerManager] Cleaned up {count} old workers") return count diff --git a/cmdlet/file/add.py b/cmdlet/file/add.py index 38aea01..e68e6de 100644 --- a/cmdlet/file/add.py +++ b/cmdlet/file/add.py @@ -42,6 +42,10 @@ from SYS.utils import sha256_file, unique_path, sanitize_filename # Canonical supported filetypes for all stores/cmdlets SUPPORTED_MEDIA_EXTENSIONS = ALL_SUPPORTED_EXTENSIONS +_SCREENSHOT_TIME_SUFFIX_RE = re.compile( + r"^(?P