diff --git a/apps/admin_console/database/connection.py b/apps/admin_console/database/connection.py index 2590b35..83eeedd 100644 --- a/apps/admin_console/database/connection.py +++ b/apps/admin_console/database/connection.py @@ -13,11 +13,13 @@ # limitations under the License. from contextlib import contextmanager +import logging from pathlib import Path import sqlite3 from artemis.config import DB_PATH +logger = logging.getLogger(__name__) _initialized_dbs = set() @@ -33,14 +35,17 @@ def get_db(db_path: Path | None = None) -> sqlite3.Connection: StorageManager(db_path=path, base_trace_dir=path.parent) _initialized_dbs.add(path_key) - except Exception: - pass + except (ImportError, OSError, sqlite3.Error) as exc: + # Schema bootstrap is best-effort here; queries against a missing + # schema will surface their own errors, but log the root cause. + logger.warning("Schema bootstrap failed for %s: %s", path, exc) conn = sqlite3.connect(path, timeout=10.0) conn.row_factory = sqlite3.Row try: conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=10000") - except Exception: + except sqlite3.Error: + # Optional performance PRAGMAs; the connection works without them. pass return conn diff --git a/apps/admin_console/database/repositories/session_repository.py b/apps/admin_console/database/repositories/session_repository.py index b51d512..a3937e7 100644 --- a/apps/admin_console/database/repositories/session_repository.py +++ b/apps/admin_console/database/repositories/session_repository.py @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import time from typing import Any from artemis.runtime import trace_store +logger = logging.getLogger(__name__) + try: from admin_console.database.connection import db_session except ImportError: @@ -317,8 +320,15 @@ class SessionRepository: "failed", error="Process terminated prior to server startup.", ) - except Exception: - pass + except OSError as exc: + # read_status itself never raises; this guards the + # lock/write side of update_trace_status. + logger.warning( + "Could not mark trace %s failed during orphan " + "reconciliation: %s", + row["session_id"], + exc, + ) conn.commit() return count except Exception: diff --git a/apps/admin_console/database/repositories/step_repository.py b/apps/admin_console/database/repositories/step_repository.py index 3d18126..515a987 100644 --- a/apps/admin_console/database/repositories/step_repository.py +++ b/apps/admin_console/database/repositories/step_repository.py @@ -106,7 +106,8 @@ class StepRepository: if isinstance(payload_raw, str): try: payload_obj = json.loads(payload_raw) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: the isinstance(dict) guard below bails out. pass if not isinstance(payload_obj, dict): return 0, 0, 0 @@ -175,7 +176,8 @@ class StepRepository: if isinstance(payload_raw, str): try: payload_obj = json.loads(payload_raw) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: the isinstance(dict) guard below bails out. pass if not isinstance(payload_obj, dict): @@ -308,7 +310,8 @@ class StepRepository: if step_dict.get(json_col): try: step_dict[json_col] = json.loads(step_dict[json_col]) - except Exception: + except (ValueError, TypeError): + # Non-JSON column value: keep the raw string. pass # If post_image_name is identical to pre_image_name, normalize to None @@ -471,7 +474,8 @@ class StepRepository: p_raw_th.append(str(th_text).strip()) elif thr["type"] == "thinking" and th_text: p_nat_th.append(str(th_text).strip()) - except Exception: + except (ValueError, TypeError, AttributeError): + # Unparseable or non-dict thinking payload: skip it. pass virtual_step = { diff --git a/apps/admin_console/database/repositories/trace_repository.py b/apps/admin_console/database/repositories/trace_repository.py index 95225c5..7bc0dbc 100644 --- a/apps/admin_console/database/repositories/trace_repository.py +++ b/apps/admin_console/database/repositories/trace_repository.py @@ -52,7 +52,8 @@ class TraceRepository: if row_dict.get("payload"): try: row_dict["payload"] = json.loads(row_dict["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: keep the raw string for display. pass nodes[row_dict["trace_id"]] = {**row_dict, "children": []} @@ -94,7 +95,8 @@ class TraceRepository: if row_dict.get("payload"): try: row_dict["payload"] = json.loads(row_dict["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: keep the raw string for display. pass nodes[row_dict["trace_id"]] = {**row_dict, "children": []} diff --git a/apps/admin_console/replay_manager.py b/apps/admin_console/replay_manager.py index 6202ea8..7174d64 100644 --- a/apps/admin_console/replay_manager.py +++ b/apps/admin_console/replay_manager.py @@ -485,7 +485,8 @@ class ReplayManager: if td.get("payload"): try: td["payload"] = json.loads(td["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: keep the raw string. pass traces.append(td) conn.close() @@ -721,8 +722,11 @@ class ReplayManager: with open(traces_json_path, encoding="utf-8") as f: traces = json.load(f) return {str(t.get("trace_id")).lower() for t in traces if t.get("trace_id")} - except Exception: - pass + except (OSError, ValueError) as e: + print( + f"Warning: Failed to read preloaded trace ids from {traces_json_path}: {e}", + file=sys.stderr, + ) return set() def load_traces_from_db(self, db_path: Path, step_num: int, preloaded_trace_ids: set) -> list: @@ -747,7 +751,8 @@ class ReplayManager: if td.get("payload"): try: td["payload"] = json.loads(td["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: normalized to {} just below. pass if not isinstance(td.get("payload"), dict): td["payload"] = {} @@ -808,7 +813,8 @@ class ReplayManager: session_id = None try: session_id = temp_db_path.parents[1].name.split("_")[0] - except Exception: + except IndexError: + # Path too shallow to carry a session dir: fall back to the DB probe. pass if not session_id: @@ -820,7 +826,8 @@ class ReplayManager: if row: session_id = str(row[0]) conn.close() - except Exception: + except sqlite3.Error: + # Unreadable sandbox DB: fall back to single-step trace loading. pass if not session_id: @@ -890,8 +897,11 @@ class ReplayManager: with open(step_json_path, encoding="utf-8") as f: step_data = json.load(f) current_step_id = step_data.get("step_id") - except Exception: - pass + except (OSError, ValueError) as e: + print( + f"Warning: Failed to read step id from {step_json_path}: {e}", + file=sys.stderr, + ) current_step_id_str = str(current_step_id).lower() if current_step_id else None @@ -995,7 +1005,9 @@ class ReplayManager: sid = step_data.get("step_id") if sid: step_dirs[str(sid).lower()] = subdir - except Exception: + except (OSError, ValueError): + # Unreadable step.json: screenshots for this step + # simply are not linked into the trace payloads. pass for td in all_traces: @@ -1043,7 +1055,8 @@ class ReplayManager: return ( f"file://{abs_path}" if abs_path.startswith("/") else f"file:///{abs_path}" ) - except Exception: + except (OSError, ValueError): + # Not a resolvable filesystem path: return the value verbatim. pass return val @@ -1104,7 +1117,8 @@ class ReplayManager: if r["payload"]: try: payload = json.loads(r["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: treated as an empty tool payload. pass response_val = payload.get("result") @@ -1116,7 +1130,8 @@ class ReplayManager: ): try: response_val = json.loads(response_val) - except Exception: + except ValueError: + # Looks like JSON but is not: keep the raw string. pass args = payload.get("args", {}) @@ -1682,7 +1697,8 @@ class ReplayManager: if row and row[0]: try: device_info = json.loads(row[0]) - except Exception: + except (ValueError, TypeError): + # Malformed device_info: simulate with the defaults below. pass sim_device_id = device_info.get("device_id", "replay-device") @@ -2294,8 +2310,11 @@ class ReplayManager: res_data = json.load(f) replay_outcome = res_data.get("result") replay_id = res_data.get("replay_id") - except Exception: - pass + except (OSError, ValueError) as e: + print( + f"Warning: Failed to read replay result from {result_json_path}: {e}", + file=sys.stderr, + ) # Update virtual tool trace result with the actual formatted outcome if available if replay_outcome is not None: @@ -2464,8 +2483,11 @@ class ReplayManager: llm_calls_log = res_data.get("llm_calls") or [] replay_outcome = res_data.get("result") replay_id = res_data.get("replay_id") - except Exception: - pass + except (OSError, ValueError) as e: + print( + f"Warning: Failed to read replay result from {result_json_path}: {e}", + file=sys.stderr, + ) # Update virtual tool trace result with the actual formatted outcome if available if replay_outcome is not None: diff --git a/apps/admin_console/routers/sessions.py b/apps/admin_console/routers/sessions.py index 3ad3e4b..14ccc06 100644 --- a/apps/admin_console/routers/sessions.py +++ b/apps/admin_console/routers/sessions.py @@ -83,7 +83,8 @@ def _list_sessions_sync(): d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw if isinstance(d_info, dict): device_id = d_info.get("device_id") or d_info.get("device_serial") - except Exception: + except (ValueError, TypeError): + # Malformed device_info JSON: fall back to the recording's device_id. pass if not device_id and recording: device_id = recording.get("device_id") @@ -157,8 +158,10 @@ def _list_sessions_sync(): "failed", error="Process terminated unexpectedly (auto-harvested).", ) - except Exception: - pass + except OSError as e: + # read_status never raises; this guards the lock/write side + # of update_trace_status. + print(f"[list_sessions] Could not mark harvested traces failed: {e}") except Exception as e: print(f"[list_sessions] Failed to update orphaned sessions: {e}") diff --git a/apps/admin_console/routers/steps.py b/apps/admin_console/routers/steps.py index 4e4c31f..d682396 100644 --- a/apps/admin_console/routers/steps.py +++ b/apps/admin_console/routers/steps.py @@ -78,7 +78,8 @@ async def get_trace(trace_id: str, session_id: str = None, step_number: int = No try: payload_obj = json.loads(trace_dict["payload"]) trace_dict["payload"] = media_service.unwrap_payload(payload_obj) - except Exception: + except (ValueError, TypeError, KeyError, AttributeError): + # Non-JSON or unexpectedly shaped payload: serve it raw. pass return trace_dict diff --git a/apps/admin_console/routers/tasks.py b/apps/admin_console/routers/tasks.py index fd18f35..d732958 100644 --- a/apps/admin_console/routers/tasks.py +++ b/apps/admin_console/routers/tasks.py @@ -209,7 +209,8 @@ async def stop_task( target_sid = str(body["session_id"]) if body.get("device_id"): target_dev = str(body["device_id"]) - except Exception: + except ValueError: + # Empty or non-JSON body: fall back to the query parameters. pass stopped = task_queue_service.stop_tasks( diff --git a/apps/admin_console/services/ipc_service.py b/apps/admin_console/services/ipc_service.py index f93accb..712b1d8 100644 --- a/apps/admin_console/services/ipc_service.py +++ b/apps/admin_console/services/ipc_service.py @@ -15,8 +15,11 @@ import asyncio from contextlib import suppress import json +import logging from typing import Any +logger = logging.getLogger(__name__) + try: from admin_console.core.state import state except ImportError: @@ -76,7 +79,8 @@ class IPCService: if isinstance(payload, str): try: payload = json.loads(payload) - except Exception: + except ValueError: + # Non-JSON payload string: cleaned as-is below. pass if tr_type in ("tool", "action") and isinstance(payload, dict): @@ -102,7 +106,8 @@ class IPCService: if isinstance(pl, str): try: pl = json.loads(pl) - except Exception: + except ValueError: + # Non-JSON payload string: cleaned as-is below. pass if tr_dict.get("type") == "llm_call": @@ -190,7 +195,14 @@ class IPCService: try: cb(event_type, data) except Exception: - pass + # One broken subscriber must not block the + # others, but a silent drop hides it entirely. + logger.warning( + "IPC subscriber %r failed for event %s", + cb, + event_type, + exc_info=True, + ) except (ValueError, UnicodeDecodeError, TypeError) as e: print(f"IPC parse error: {e}") except asyncio.CancelledError: diff --git a/apps/admin_console/services/media_service.py b/apps/admin_console/services/media_service.py index 96eb877..7b8940c 100644 --- a/apps/admin_console/services/media_service.py +++ b/apps/admin_console/services/media_service.py @@ -15,6 +15,7 @@ import base64 import hashlib import json +import logging import os from pathlib import Path import subprocess @@ -25,6 +26,8 @@ from fastapi import HTTPException from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT +logger = logging.getLogger(__name__) + class MediaService: """Service handling media indexing, local file security, payload unwrapping and notes/plans.""" @@ -76,7 +79,9 @@ class MediaService: str(result), ) return result - except Exception: + except OSError: + # Filesystem probe failed (missing/locked file): serve the + # original path and let the media endpoint report the error. pass return p @@ -167,8 +172,9 @@ class MediaService: url = cls.path_to_video_url(item) idx[item.stem] = url idx[item.name] = url - except Exception: - pass + except OSError as exc: + # Unreadable directory entry: index the rest of the tree. + logger.warning("Video index scan failed under %s: %s", d, exc) return idx @classmethod @@ -219,7 +225,8 @@ class MediaService: item = cls.ensure_browser_playable_video(item) v_url = cls.path_to_video_url(item) break - except Exception: + except OSError: + # Unreadable session dir: no recording to resolve. pass if not v_url and row_dict.get("initial_goal"): @@ -274,7 +281,8 @@ class MediaService: obj.startswith("[") and obj.endswith("]") ): return MediaService.unwrap_payload(json.loads(obj)) - except Exception: + except ValueError: + # Looks like JSON but is not: treat it as a plain string. pass # Check if the string is a Base64 image @@ -284,7 +292,8 @@ class MediaService: try: _, base64_data = obj.split(";base64,", 1) is_base64_img = True - except Exception: + except ValueError: + # Malformed data URI: fall through to plain-string handling. pass elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"): is_base64_img = True @@ -301,8 +310,10 @@ class MediaService: image_path.write_bytes(image_bytes) return f"image://{image_hash}" - except Exception: - pass + except (ValueError, OSError) as exc: + # Invalid base64 or image cache write failure: fall back + # to the truncated raw string below. + logger.debug("Could not persist inline image: %s", exc) if len(obj) > 2000: preview_start = obj[:100].replace("\n", " ") diff --git a/apps/admin_console/services/model_service.py b/apps/admin_console/services/model_service.py index 4206ca7..0ccc0bc 100644 --- a/apps/admin_console/services/model_service.py +++ b/apps/admin_console/services/model_service.py @@ -13,9 +13,12 @@ # limitations under the License. import json +import logging import time from typing import Any +logger = logging.getLogger(__name__) + class ModelService: """Service handling agent architecture profile detection and metadata formatting.""" @@ -44,8 +47,10 @@ class ModelService: elif llm_cfg and llm_cfg.default: provider = str(llm_cfg.default.provider) model_id = str(llm_cfg.default.model) - except Exception: - pass + except Exception as exc: + # Unreadable/invalid LLM config: fall back to display defaults + # (cached for the TTL, so this logs at most once per window). + logger.warning("Could not resolve LLM config for display: %s", exc) cls._llm_info_cache = (now, provider, model_id) return provider, model_id @@ -94,7 +99,8 @@ class ModelService: return "pro" elif "flash" in p_str: return "flash" - except Exception: + except (ValueError, TypeError): + # Malformed device_info JSON: try the other profile sources. pass if (row_dict.get("status") == "running") and running_profile: @@ -132,7 +138,8 @@ class ModelService: for x in ("planner", "validator", "operator", "checker") ): return "pro" - except Exception: + except (ValueError, TypeError): + # Malformed trace payload JSON: skip this trace. pass return None diff --git a/apps/admin_console/services/task_queue_service.py b/apps/admin_console/services/task_queue_service.py index b10bf08..9881052 100644 --- a/apps/admin_console/services/task_queue_service.py +++ b/apps/admin_console/services/task_queue_service.py @@ -15,6 +15,7 @@ import asyncio import codecs from datetime import datetime +import logging import os import psutil import shutil @@ -48,6 +49,8 @@ from artemis.runtime import ( trace_store, ) +logger = logging.getLogger(__name__) + class TaskQueueService: """Service managing FIFO task execution, background worker, subprocess lifecycle, @@ -76,7 +79,14 @@ class TaskQueueService: try: cb(event_type, data) except Exception: - pass + # One broken subscriber must not block the others, but a + # silent drop hides it entirely. + logger.warning( + "Event subscriber %r failed for event %s", + cb, + event_type, + exc_info=True, + ) @classmethod def _broadcast_startup_progress( @@ -194,7 +204,9 @@ class TaskQueueService: if log_file is not None: try: log_file.write(text) - except Exception: + except (OSError, ValueError): + # Best-effort tee into stdout.log; console output above + # already carried the text. pass try: @@ -214,7 +226,8 @@ class TaskQueueService: if log_file is not None: try: log_file.close() - except Exception: + except OSError: + # Flush-on-close of the best-effort tee failed; nothing to do. pass @staticmethod @@ -265,7 +278,8 @@ class TaskQueueService: return proc.returncode if proc.returncode is not None else -15 except (psutil.NoSuchProcess, ProcessLookupError): return proc.returncode if proc.returncode is not None else -15 - except Exception: + except psutil.Error: + # Transient probe failure (e.g. AccessDenied): keep waiting. pass else: return proc.returncode if proc.returncode is not None else -15 @@ -279,7 +293,9 @@ class TaskQueueService: state.worker_task = loop.create_task(cls.queue_worker()) try: state.wake_event.set() - except Exception: + except AttributeError: + # Partially initialized state (unit tests): the worker + # loop polls anyway. pass except RuntimeError: pass @@ -515,8 +531,11 @@ class TaskQueueService: if st: st["pid"] = proc.pid trace_store.write_status(str(sess_id), st) - except Exception: - pass + except OSError as exc: + # Status probes fall back to DB PID bookkeeping, but note it. + print( + f"[QueueWorker] Could not record worker pid in status.json for {sess_id}: {exc}" + ) cls._broadcast_startup_progress( sess_id, "process_ready", "Execution process started" ) @@ -583,8 +602,12 @@ class TaskQueueService: str(sess_id), canonical_mcp_status, ) - except Exception: - pass + except OSError as exc: + # A stale status.json would leave MCP pollers seeing + # "running" forever, so surface the failure. + print( + f"[QueueWorker] Could not persist terminal MCP status for {sess_id}: {exc}" + ) rec_info = session_repo.get_video_recording_for_session(sess_id) rec_status = (rec_info or {}).get("status") recovered_video_path = None @@ -848,8 +871,9 @@ class TaskQueueService: active_owners: dict[str, Any] = {} try: active_owners = DeviceExecutionLock.get_active_owners() - except Exception: - pass + except OSError as exc: + # Unreadable lock dir: fall back to the single-owner probe below. + print(f"[stop_tasks] Could not enumerate device owners: {exc}") fallback = DeviceExecutionLock.get_active_owner() if fallback and not active_owners: active_owners["default"] = fallback @@ -872,8 +896,10 @@ class TaskQueueService: "cancelled", error="Task stopped from the Artemis frontend.", ) - except Exception: - pass + except OSError as exc: + print( + f"[stop_tasks] Could not persist cancelled MCP status for {sid}: {exc}" + ) cls._broadcast_event( "session_ended", { @@ -895,7 +921,8 @@ class TaskQueueService: if run_proc is not None and run_proc.returncode is None: try: run_proc.kill() - except Exception: + except (ProcessLookupError, OSError): + # Process already exited between the check and the kill. pass # active_runs entries are popped by each run's finalizer once the # process exit is observed; clearing them here would free the device @@ -903,7 +930,8 @@ class TaskQueueService: if state.current_process: try: state.current_process.kill() - except Exception: + except (ProcessLookupError, OSError): + # Process already exited between the check and the kill. pass state.current_process = None @@ -915,7 +943,9 @@ class TaskQueueService: if PAUSE_FILE.exists(): try: PAUSE_FILE.unlink() - except Exception: + except OSError: + # Best-effort cleanup of the pause marker; a leftover file + # only pauses until the next resume request. pass cls.ensure_worker_running() @@ -926,8 +956,9 @@ class TaskQueueService: active_owners = {} try: active_owners = DeviceExecutionLock.get_active_owners() - except Exception: - pass + except OSError as exc: + # Unreadable lock dir: the local-process fallbacks below still apply. + print(f"[stop_tasks] Could not enumerate device owners: {exc}") owner = None if target_sid: @@ -1066,7 +1097,8 @@ class TaskQueueService: try: local_proc.kill() stopped = True - except Exception: + except (ProcessLookupError, OSError): + # Process already exited; terminate_tree above may have got it. pass stopped = True is_local_owner = True @@ -1097,8 +1129,11 @@ class TaskQueueService: if row_pid and session_repo.process_is_alive(row_pid): try: process_supervisor.terminate_tree(int(row_pid)) - except Exception: - pass + except Exception as exc: + # Report it: a surviving worker keeps the device busy. + print( + f"[stop_tasks] Could not terminate worker pid {row_pid}: {exc}" + ) DeviceExecutionLock.cleanup_stale_locks() stopped = True @@ -1176,7 +1211,9 @@ class TaskQueueService: if PAUSE_FILE.exists(): try: PAUSE_FILE.unlink() - except Exception: + except OSError: + # Best-effort cleanup of the pause marker; a leftover file + # only pauses until the next resume request. pass cls.ensure_worker_running() diff --git a/mcp_server/background/task_runner.py b/mcp_server/background/task_runner.py index 13f6665..4d52733 100644 --- a/mcp_server/background/task_runner.py +++ b/mcp_server/background/task_runner.py @@ -93,7 +93,7 @@ def resolve_profile_file() -> str | None: os.path.join(app_dir, "llm-config.json"), ] ) - except Exception: + except (ImportError, OSError): pass candidates.extend( diff --git a/mcp_server/base.py b/mcp_server/base.py index 1d015b7..242761b 100644 --- a/mcp_server/base.py +++ b/mcp_server/base.py @@ -21,6 +21,8 @@ try: FastMCPSettings.model_rebuild() except Exception: + # Version-compat shim: older/newer FastMCP releases may not expose + # Settings or need the rebuild; the server works without it. pass # Define the shared FastMCP instance for external IDE and agent clients. diff --git a/mcp_server/notifiers/agentapi.py b/mcp_server/notifiers/agentapi.py index d0489db..8ca6a15 100644 --- a/mcp_server/notifiers/agentapi.py +++ b/mcp_server/notifiers/agentapi.py @@ -37,6 +37,8 @@ class AgentApiNotifier(BaseNotifier): if addr and token: self._save_shared_env(addr, token) except Exception: + # Environment recovery is opportunistic; notifier construction + # must never fail because of it. pass @property @@ -59,7 +61,8 @@ class AgentApiNotifier(BaseNotifier): for cand in candidates: if os.path.exists(cand) and os.access(cand, os.X_OK): return cand - except Exception: + except OSError: + # Filesystem probe failed: report the binary as absent. pass return None @@ -70,7 +73,8 @@ class AgentApiNotifier(BaseNotifier): return True if "ANTIGRAVITY_LS_ADDRESS" in os.environ: return True - except Exception: + except OSError: + # Filesystem probe failed: report the notifier as unavailable. pass return False @@ -85,7 +89,8 @@ class AgentApiNotifier(BaseNotifier): os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) shared_candidates.append(os.path.join(parent_dir, ".jetski_env")) - except Exception: + except OSError: + # Repo-relative candidate is optional; the home-dir ones remain. pass data = { @@ -128,7 +133,8 @@ class AgentApiNotifier(BaseNotifier): os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) shared_candidates.append(os.path.join(parent_dir, ".jetski_env")) - except Exception: + except OSError: + # Repo-relative candidate is optional; the home-dir ones remain. pass for shared_file in shared_candidates: @@ -190,7 +196,8 @@ class AgentApiNotifier(BaseNotifier): ) if local_addr and local_token: proc_matches.append((mtime, local_addr, local_token)) - except Exception: + except OSError: + # /proc entries vanish or deny access as processes exit. pass # Sort matches descending by creation timestamp so active/newest sessions come first diff --git a/mcp_server/server.py b/mcp_server/server.py index 2c1da04..8353c8b 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -36,7 +36,8 @@ def _rotate_log(path: str, max_bytes: int = _LOG_MAX_BYTES) -> None: if os.path.exists(old_path): os.remove(old_path) os.replace(path, old_path) - except Exception: + except OSError: + # Best-effort rotation; a locked or vanished log file is tolerable. pass @@ -74,6 +75,8 @@ try: sys.stderr = StderrTee(sys.stderr, os.path.join(_mcp_log_dir, "mcp_stderr.log")) except Exception: + # Debug logging and the stderr tee are optional diagnostics; they must + # never prevent the MCP stdio server from starting. pass # 1. Import shared FastMCP instance diff --git a/mcp_server/tools/inspect_trace.py b/mcp_server/tools/inspect_trace.py index 6ee5d88..3e8358f 100644 --- a/mcp_server/tools/inspect_trace.py +++ b/mcp_server/tools/inspect_trace.py @@ -121,7 +121,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No try: with open(plan_path, encoding="utf-8") as f: plan_content = f.read() - except Exception: + except OSError: + # Unreadable plan file: the summary just omits the plan. pass try: @@ -149,7 +150,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No if step_dict.get(col): try: step_dict[col] = json.loads(step_dict[col]) - except Exception: + except (ValueError, TypeError): + # Non-JSON column value: keep the raw string. pass cursor.execute( @@ -163,7 +165,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No if t_dict.get("payload"): try: t_dict["payload"] = json.loads(t_dict["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: keep the raw string. pass events.append(t_dict) step_dict["interleaved_events"] = events @@ -200,10 +203,12 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No d_info = json.loads(row_dev["device_info"]) if isinstance(d_info, dict) and d_info.get("device_id"): device_serial = d_info["device_id"] - except Exception: + except (ValueError, TypeError): + # Malformed device_info JSON: leave the serial unset. pass conn_dev.close() - except Exception: + except sqlite3.Error: + # The serial enriches the summary header only; skip it. pass device_info_str = f" | **Device Serial:** `{device_serial}`" if device_serial else "" @@ -314,7 +319,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No if step_dict.get(col): try: step_dict[col] = json.loads(step_dict[col]) - except Exception: + except (ValueError, TypeError): + # Non-JSON column value: keep the raw string. pass cursor.execute( @@ -335,7 +341,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No if t_dict.get("payload"): try: t_dict["payload"] = json.loads(t_dict["payload"]) - except Exception: + except (ValueError, TypeError): + # Non-JSON payload: keep the raw string. pass events.append(t_dict) step_dict["interleaved_events"] = events diff --git a/mcp_server/tools/task_manager.py b/mcp_server/tools/task_manager.py index 41ac4c9..d38be80 100644 --- a/mcp_server/tools/task_manager.py +++ b/mcp_server/tools/task_manager.py @@ -64,14 +64,16 @@ def _session_tracked_by_lock(trace_id: str) -> bool: for q_item in DeviceExecutionLock.get_queued_tasks(): if str(q_item.get("session_id")) == trace_id: return True - except Exception: - pass + except Exception as exc: + # An unreadable queue may hide a live session; the caller then leans + # on DB/pid evidence, so surface the probe failure. + print(f"Could not read device queue for {trace_id}: {exc}", file=sys.stderr) try: for owner in DeviceExecutionLock.get_active_owners().values(): if owner.session_id and str(owner.session_id) == trace_id: return True - except Exception: - pass + except Exception as exc: + print(f"Could not read device owners for {trace_id}: {exc}", file=sys.stderr) return False @@ -115,8 +117,9 @@ def _reconcile_task_state( if row: db_status = row["status"] db_pid = row["pid"] - except Exception: - pass + except sqlite3.Error as exc: + # Reconciliation then relies on status.json and lock evidence only. + print(f"Could not read DB session row for {trace_id}: {exc}", file=sys.stderr) if db_pid and db_pid != pid: pid = db_pid @@ -188,6 +191,8 @@ def _mark_liveness_failure(trace_id: str, status_data: dict[str, Any]) -> None: payload={"trace_id": trace_id, "error": _LIVENESS_FAILURE_ERROR}, ) except Exception: + # Notification dispatch is best-effort; the failure verdict is + # already persisted in status.json. pass @@ -278,10 +283,13 @@ def mobile_manage_task( device_serial = d_info["device_id"] status_data["device_serial"] = device_serial trace_store.write_status(trace_id, status_data) - except Exception: + except (ValueError, TypeError, OSError): + # Malformed device_info or failed cache write: the + # serial simply stays unknown for this poll. pass conn_dev.close() - except Exception: + except sqlite3.Error: + # DB probe for the serial is optional enrichment. pass if not device_serial and pid: @@ -293,7 +301,8 @@ def mobile_manage_task( status_data["device_serial"] = device_serial trace_store.write_status(trace_id, status_data) break - except Exception: + except OSError: + # Lock-owner probe / cache write failed: serial stays unknown. pass response: dict[str, Any] = { @@ -329,7 +338,8 @@ def mobile_manage_task( "task_status": run_outcome.get("task_status"), **tests, } - except Exception: + except (OSError, ValueError, TypeError): + # Unreadable or malformed run_outcome.json: omit the summary. pass break @@ -394,7 +404,8 @@ def mobile_manage_task( progress["latest_thought"] = p_obj["response"][0].get( "text", "" ) - except Exception: + except (ValueError, TypeError, AttributeError): + # Unparseable thought payload: omit it. pass cur2.execute( @@ -411,7 +422,9 @@ def mobile_manage_task( act_payload = json.loads(row_action["payload"]).get( "args", {} ) - except Exception: + except (ValueError, TypeError, AttributeError): + # Unparseable action payload: show the + # bare action name. pass progress["latest_action"] = ( f"{act_name}({act_payload})" if act_payload else act_name @@ -433,7 +446,8 @@ def mobile_manage_task( try: with open(plan_path, encoding="utf-8") as f: plan_content = f.read() - except Exception: + except OSError: + # Unreadable plan file: progress omits the plan. pass progress["task_plan"] = plan_content else: @@ -458,16 +472,18 @@ def mobile_manage_task( try: if is_daemon_running(): stopped_via_daemon = stop_task_on_daemon(trace_id) - except Exception: - pass + except Exception as exc: + # Fall through to direct process termination below. + print(f"Daemon stop attempt failed for {trace_id}: {exc}", file=sys.stderr) # 2. Cancel queue reservation if present queue_ticket = status_data.get("queue_ticket") if queue_ticket: try: DeviceExecutionLock.cancel_reservation(queue_ticket) - except Exception: - pass + except OSError as exc: + # A stale reservation expires on its own; note the failure. + print(f"Could not cancel reservation {queue_ticket}: {exc}", file=sys.stderr) # 3. If process PID is missing, try to resolve from active device owners if not pid: @@ -479,8 +495,10 @@ def mobile_manage_task( status_data["pid"] = pid trace_store.write_status(trace_id, status_data) break - except Exception: - pass + except OSError as exc: + # Without a resolved pid the stop falls back to the + # daemon/queue outcome, so surface the probe failure. + print(f"Could not resolve owner pid for {trace_id}: {exc}", file=sys.stderr) # 4. If process PID could not be determined and not stopped via daemon or queue if not pid and not stopped_via_daemon and not queue_ticket: diff --git a/mcp_server/tools/task_runner.py b/mcp_server/tools/task_runner.py index 195163e..90f66a1 100644 --- a/mcp_server/tools/task_runner.py +++ b/mcp_server/tools/task_runner.py @@ -117,6 +117,8 @@ def _watch_spawn( payload={"trace_id": trace_id, "error": error_text}, ) except Exception: + # Notification dispatch is best-effort; the failure verdict is + # already persisted in status.json. pass return False @@ -321,8 +323,12 @@ def mobile_run_task( "stdout_log": str(trace_store.get_trace_stdout_log_path(trace_id)), "stderr_log": str(trace_store.get_trace_stderr_log_path(trace_id)), } - except Exception: - pass + except Exception as exc: + # The queue probe is a rescue path for a slow daemon reply; + # its failure downgrades the verdict to "failed" below. + logging.getLogger("mcp_server").warning( + f"Could not verify daemon queue for '{trace_id}': {exc}" + ) # When Daemon is running, refuse to launch a conflicting standalone runner on the same device logging.getLogger("mcp_server").warning( @@ -392,7 +398,8 @@ def mobile_run_task( ipc_port = read_ipc_port() if ipc_port: env["ARTEMIS_IPC_PORT"] = str(ipc_port) - except Exception: + except (OSError, ValueError): + # No readable IPC port file: the runner works without live telemetry. pass proc_kwargs = env_utils.get_detached_process_kwargs() diff --git a/mcp_server/utils/device_utils.py b/mcp_server/utils/device_utils.py index d2ca5a7..5782a8d 100644 --- a/mcp_server/utils/device_utils.py +++ b/mcp_server/utils/device_utils.py @@ -167,7 +167,8 @@ def ensure_emulator( if "1" in res.stdout: time.sleep(2) # Stabilization return True - except Exception: + except (subprocess.SubprocessError, OSError): + # adb not ready yet while the emulator boots: keep polling. pass time.sleep(2)