diff --git a/artemis/core/diagnostics/emulator_manager.py b/artemis/core/diagnostics/emulator_manager.py index 2a54f4d..9e26950 100644 --- a/artemis/core/diagnostics/emulator_manager.py +++ b/artemis/core/diagnostics/emulator_manager.py @@ -467,7 +467,9 @@ class EmulatorManager: stderr=subprocess.DEVNULL, timeout=5, ) - except Exception: + except (OSError, subprocess.SubprocessError): + # Graceful `adb emu kill` failed or timed out; the direct + # process terminate/kill below still stops the emulator. pass if self._proc and self._proc.poll() is None: diff --git a/artemis/core/diagnostics/engine.py b/artemis/core/diagnostics/engine.py index 37b010c..c5a9a43 100644 --- a/artemis/core/diagnostics/engine.py +++ b/artemis/core/diagnostics/engine.py @@ -194,8 +194,12 @@ class ReadinessEngine: if adb_result and adb_result.metadata.get("active_device"): try: active_device = DeviceInfo(**adb_result.metadata["active_device"]) - except Exception: - pass + except (TypeError, ValueError) as exc: + # Includes pydantic ValidationError (a ValueError subclass). + logger.warning( + f"Malformed active_device metadata from ADB probe; readiness" + f" report will omit the active device: {exc}" + ) return SystemReadinessReport( overall_ready=overall_ready, diff --git a/artemis/drivers/android/adb_driver.py b/artemis/drivers/android/adb_driver.py index 2d4c58f..e775fc7 100644 --- a/artemis/drivers/android/adb_driver.py +++ b/artemis/drivers/android/adb_driver.py @@ -355,8 +355,9 @@ class AndroidAdbDriver(BaseDeviceDriver): broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'" await asyncio.to_thread(self.device.shell, broadcast_cmd) return True - except Exception: - pass + except Exception as e: + # ADBKeyboard probe/broadcast failed; fall through to native input. + logger.debug(f"ADBKeyboard IME path failed, falling back to ADB input: {e}") # 3. Tier 3: Universal Native ADB input text fallback lines = norm_text.split("\n") @@ -410,8 +411,9 @@ class AndroidAdbDriver(BaseDeviceDriver): app_info = await asyncio.to_thread(self.device.current_app) if app_info and getattr(app_info, "package", None): return app_info.package - except Exception: - pass + except Exception as e: + # current_app is unsupported on some devices; use dumpsys below. + logger.debug(f"current_app query failed, falling back to dumpsys: {e}") # 2. Structured dumpsys extraction out = await asyncio.to_thread( @@ -467,8 +469,10 @@ class AndroidAdbDriver(BaseDeviceDriver): try: self._scrcpy_process.terminate() await asyncio.wait_for(self._scrcpy_process.wait(), timeout=5.0) - except Exception: - pass + except (ProcessLookupError, OSError, asyncio.TimeoutError) as e: + # Already exited, or did not stop within the timeout; a lingering + # scrcpy may keep the MKV file locked on Windows. + logger.debug(f"scrcpy process did not terminate cleanly: {e}") self._scrcpy_process = None mkv = getattr(self, "_recording_mkv_path", None) @@ -493,7 +497,8 @@ class AndroidAdbDriver(BaseDeviceDriver): if mp4.exists() and mp4.stat().st_size > 0: try: mkv.unlink() - except Exception: + except OSError: + # Best-effort cleanup of the intermediate MKV file. pass return str(mp4) except Exception as e: diff --git a/artemis/drivers/android/input_ime.py b/artemis/drivers/android/input_ime.py index 1cc14a5..723a85b 100644 --- a/artemis/drivers/android/input_ime.py +++ b/artemis/drivers/android/input_ime.py @@ -102,8 +102,9 @@ class AndroidInputIME: broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'" await asyncio.to_thread(self.device.shell, broadcast_cmd) return True - except Exception: - pass + except Exception as e: + # ADBKeyboard probe/broadcast failed; fall through to ADB input. + logger.debug(f"ADBKeyboard IME path failed, falling back to ADB input: {e}") # 4. Fallback: ADB input text lines = text.split("\n") diff --git a/artemis/interfaces/cli/commands/doctor.py b/artemis/interfaces/cli/commands/doctor.py index f9ac90d..4a69258 100644 --- a/artemis/interfaces/cli/commands/doctor.py +++ b/artemis/interfaces/cli/commands/doctor.py @@ -170,7 +170,8 @@ def doctor_command( state = parts[1] extra = " ".join(parts[2:]) devices_info.append((serial, state, extra)) - except Exception: + except (OSError, subprocess.SubprocessError): + # adb probe failed or timed out; report as "no devices detected". pass if devices_info: diff --git a/artemis/interfaces/cli/commands/init.py b/artemis/interfaces/cli/commands/init.py index c8fa063..46ae17b 100644 --- a/artemis/interfaces/cli/commands/init.py +++ b/artemis/interfaces/cli/commands/init.py @@ -78,7 +78,8 @@ def init_command() -> None: serial = parts[0] extra = " ".join(parts[2:]) detected_devices.append((serial, extra)) - except Exception: + except (OSError, subprocess.SubprocessError): + # adb probe failed or timed out; continue setup with no devices. pass selected_serial = "" diff --git a/artemis/interfaces/cli/commands/mcp.py b/artemis/interfaces/cli/commands/mcp.py index 5415655..6ec5d10 100644 --- a/artemis/interfaces/cli/commands/mcp.py +++ b/artemis/interfaces/cli/commands/mcp.py @@ -166,7 +166,8 @@ def _parse_json_lenient(text: str) -> dict | None: try: data = json.loads(text) return data if isinstance(data, dict) else None - except Exception: + except ValueError: + # Not strict JSON; fall through to the lenient JSONC cleanup below. pass # Remove // single-line comments cleaned = re.sub(r"//.*", "", text) @@ -215,8 +216,11 @@ def _merge_json_file( try: backup_path.write_text(raw_text, encoding="utf-8") logger.warning(f"Existing JSON in {file_path} could not be parsed; backup created at {backup_path}") - except Exception: - pass + except OSError as exc: + logger.warning( + f"Existing JSON in {file_path} could not be parsed AND the backup to" + f" {backup_path} failed ({exc}); the original content will be overwritten." + ) for remove_path in remove_paths: parent = data @@ -316,8 +320,10 @@ def _merge_codex_toml(file_path: Path, server_config: dict) -> bool: backup_path = file_path.with_suffix(file_path.suffix + ".bak") backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8") logger.warning(f"Could not update {file_path}; backup created at {backup_path}: {e}") - except Exception: - pass + except (OSError, UnicodeDecodeError) as backup_exc: + logger.warning( + f"Could not update {file_path} ({e}); backup also failed: {backup_exc}" + ) else: logger.warning(f"Could not create Codex MCP config file {file_path}: {e}") return False @@ -394,7 +400,8 @@ def _write_rule_file(file_path: Path, content: str) -> bool: try: if file_path.read_text(encoding="utf-8") == new_text: return True - except Exception: + except (OSError, UnicodeDecodeError): + # Unreadable existing file: skip the no-op check and rewrite it. pass file_path.write_text(new_text, encoding="utf-8") return True @@ -419,7 +426,8 @@ def _write_cursor_mdc(file_path: Path, content: str) -> bool: try: if file_path.read_text(encoding="utf-8") == new_text: return True - except Exception: + except (OSError, UnicodeDecodeError): + # Unreadable existing file: skip the no-op check and rewrite it. pass file_path.write_text(new_text, encoding="utf-8") return True diff --git a/artemis/interfaces/cli/commands/run.py b/artemis/interfaces/cli/commands/run.py index fb1691c..34ba6e8 100644 --- a/artemis/interfaces/cli/commands/run.py +++ b/artemis/interfaces/cli/commands/run.py @@ -408,8 +408,9 @@ def run_command( host=settings.ADB_HOST or "localhost", port=settings.ADB_PORT or 5037, ) - except Exception: - pass + except Exception as exc: + # Optional cosmetic device-status display; run continues without it. + logger.debug(f"Could not create ADB client for device status display: {exc}") display_device_status(console, adb_client=adb_client) @@ -420,7 +421,9 @@ def run_command( original_sigterm = signal.signal( signal.SIGTERM, lambda s, f: (_ for _ in ()).throw(KeyboardInterrupt()) ) - except Exception: + except (ValueError, OSError, RuntimeError): + # Not the main thread or platform does not support SIGTERM handlers; + # run proceeds without the graceful-cancel hook. pass try: @@ -471,7 +474,8 @@ def run_command( try: if original_sigterm is not None and hasattr(signal, "SIGTERM"): signal.signal(signal.SIGTERM, original_sigterm) - except Exception: + except (ValueError, OSError, RuntimeError): + # Best-effort restore of the previous handler on the way out. pass if cancelled: raise SystemExit(130) diff --git a/artemis/interfaces/cli/commands/server_lifecycle.py b/artemis/interfaces/cli/commands/server_lifecycle.py index aadcae6..e745861 100644 --- a/artemis/interfaces/cli/commands/server_lifecycle.py +++ b/artemis/interfaces/cli/commands/server_lifecycle.py @@ -265,7 +265,8 @@ def status_command( try: dt = datetime.datetime.fromtimestamp(metadata["started_at"]) started_at_str = dt.strftime("%Y-%m-%d %H:%M:%S") - except Exception: + except (TypeError, ValueError, OSError, OverflowError): + # Malformed or out-of-range timestamp in metadata: show "Unknown". pass table = Table(show_header=False, box=None, padding=(0, 2)) diff --git a/artemis/platform/windows.py b/artemis/platform/windows.py index b8d1c22..a9de5f3 100644 --- a/artemis/platform/windows.py +++ b/artemis/platform/windows.py @@ -107,7 +107,8 @@ class WindowsPlatformProcess(IPlatformProcess): ) if res.returncode == 0: return True - except Exception: + except (OSError, subprocess.SubprocessError): + # taskkill missing, not runnable, or timed out; try psutil below. pass # 2. Fallback to psutil @@ -131,12 +132,14 @@ class WindowsPlatformProcess(IPlatformProcess): if hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") - except Exception: + except (OSError, ValueError): + # Stream is detached/closed or does not support reconfigure. pass if hasattr(sys.stderr, "reconfigure"): try: sys.stderr.reconfigure(encoding="utf-8", errors="replace") - except Exception: + except (OSError, ValueError): + # Stream is detached/closed or does not support reconfigure. pass os.environ["PYTHONUTF8"] = "1" diff --git a/artemis/runtime/daemon_client.py b/artemis/runtime/daemon_client.py index ed7eb80..f300221 100644 --- a/artemis/runtime/daemon_client.py +++ b/artemis/runtime/daemon_client.py @@ -133,7 +133,7 @@ def spawn_daemon( if log_handle is not subprocess.DEVNULL: try: log_handle.close() - except Exception: + except OSError: pass diff --git a/artemis/runtime/device_pool.py b/artemis/runtime/device_pool.py index a861d1c..c186ce1 100644 --- a/artemis/runtime/device_pool.py +++ b/artemis/runtime/device_pool.py @@ -152,7 +152,8 @@ class DevicePool: if proc is not None: try: proc.kill() - except Exception: + except (ProcessLookupError, OSError): + # Process already exited; nothing left to clean up. pass return None @@ -256,7 +257,8 @@ class DevicePool: if proc is not None: try: proc.kill() - except Exception: + except (ProcessLookupError, OSError): + # Process already exited; nothing left to clean up. pass async def warm_up_async( diff --git a/artemis/runtime/supervisor.py b/artemis/runtime/supervisor.py index 4075db9..3aec1af 100644 --- a/artemis/runtime/supervisor.py +++ b/artemis/runtime/supervisor.py @@ -113,8 +113,9 @@ class ProcessSupervisor: logger.debug(f"Failed graceful stop for process {proc}: {e}") try: proc.kill() - except Exception: - pass + except (ProcessLookupError, OSError) as kill_exc: + # ProcessLookupError: already exited, nothing to do. + logger.debug(f"Failed to kill process {proc}: {kill_exc}") process_supervisor: ProcessSupervisor = ProcessSupervisor() diff --git a/artemis/toolchain/descriptors.py b/artemis/toolchain/descriptors.py index efe5bf9..2fd6a5b 100644 --- a/artemis/toolchain/descriptors.py +++ b/artemis/toolchain/descriptors.py @@ -26,6 +26,7 @@ def _get_embedded_adb() -> str | None: if hasattr(adbutils, "adb_path"): return adbutils.adb_path() except Exception: + # Optional dependency probe: not installed or no bundled binary. pass return None @@ -38,6 +39,8 @@ def _get_embedded_ffmpeg() -> str | None: if hasattr(imageio_ffmpeg, "get_ffmpeg_exe"): return imageio_ffmpeg.get_ffmpeg_exe() except Exception: + # Optional dependency probe: not installed, or imageio_ffmpeg raises + # RuntimeError when it ships no binary for this platform. pass return None diff --git a/artemis/toolchain/resolver.py b/artemis/toolchain/resolver.py index d445ea4..2096677 100644 --- a/artemis/toolchain/resolver.py +++ b/artemis/toolchain/resolver.py @@ -103,6 +103,8 @@ class ToolchainResolver: self._cache[key] = resolved return resolved except Exception: + # Probing an optional third-party embedded binary; any failure + # simply means "tool not found" and the caller handles None. pass # Do not permanently cache negative lookups so dynamic installs are detected immediately diff --git a/artemis/utils/coordinates.py b/artemis/utils/coordinates.py index ad96e90..ba6d307 100644 --- a/artemis/utils/coordinates.py +++ b/artemis/utils/coordinates.py @@ -63,7 +63,7 @@ def parse_swipe_parameters( if isinstance(p, (list, tuple)) and len(p) >= 2: try: return [int(float(p[0])), int(float(p[1]))] - except Exception: + except (ValueError, TypeError): pass if isinstance(p, str): nums = re.findall(r"-?\d+(?:\.\d+)?", p) @@ -88,7 +88,7 @@ def parse_swipe_parameters( if len(c) == 4: try: return "coords", [int(float(x)) for x in c], duration_int - except Exception: + except (ValueError, TypeError): pass elif ( len(c) == 2 and isinstance(c[0], (list, tuple)) and isinstance(c[1], (list, tuple)) @@ -112,7 +112,7 @@ def parse_swipe_parameters( pt2 = _parse_pt(parsed[1]) if pt1 and pt2: return "coords", [pt1[0], pt1[1], pt2[0], pt2[1]], duration_int - except Exception: + except (ValueError, TypeError): pass # Regex extract numbers @@ -448,7 +448,9 @@ def normalize_step_actions(step_dict: dict[str, Any]) -> dict[str, Any]: import ast seq = ast.literal_eval(seq) - except Exception: + except (ValueError, TypeError, SyntaxError): + # Unparseable sequence string: leave it as-is; the + # isinstance check below skips non-list values. pass if isinstance(seq, (list, tuple)): item["sequence"] = list(seq) @@ -481,7 +483,8 @@ def normalize_step_actions(step_dict: dict[str, Any]) -> dict[str, Any]: try: action_taken = json.loads(action_taken) step_dict["action_taken"] = action_taken - except Exception: + except ValueError: + # Not JSON: keep the raw string; downstream isinstance checks skip it. pass if isinstance(action_taken, dict): diff --git a/artemis/utils/credentials_validator.py b/artemis/utils/credentials_validator.py index 4636805..03b14f4 100644 --- a/artemis/utils/credentials_validator.py +++ b/artemis/utils/credentials_validator.py @@ -43,7 +43,8 @@ def _extract_error_message(resp: httpx.Response) -> str: return str(data["message"]) if "detail" in data: return str(data["detail"]) - except Exception: + except ValueError: + # Response body is not valid JSON; fall through to the text fallback. pass # Fallback to response text or HTTP status diff --git a/artemis/utils/notes.py b/artemis/utils/notes.py index c32a633..71b0e5f 100644 --- a/artemis/utils/notes.py +++ b/artemis/utils/notes.py @@ -20,6 +20,9 @@ from pathlib import Path import re from artemis.data_engine.engine import _CURRENT_DATA_ENGINE +from artemis.utils.logger import get_logger + +logger = get_logger(__name__) # Centralized Description and Documentation Constants (Single Source of Truth) READ_NOTE_DOCSTRING = ( @@ -153,22 +156,30 @@ def save_note_content(base_dir: str | Path, key: str, content: str) -> None: if key == "task_plan" and file_path.exists(): try: content_before = file_path.read_text(encoding="utf-8") - except Exception: - pass + except (OSError, UnicodeDecodeError) as exc: + logger.warning( + "Failed to read previous task_plan content from %s; subgoal" + " rename tracking will be skipped for this save: %s", + file_path, + exc, + ) file_path.write_text(content, encoding="utf-8") if key == "task_plan": try: record_subgoal_hash_chain(base_dir, content_before, content) - except Exception: - pass + except Exception as exc: + # Hash-chain bookkeeping must never break a note save, but a + # missed record can later confuse plan ratchet validation. + logger.warning("Failed to record subgoal hash chain for task_plan: %s", exc) try: if _CURRENT_DATA_ENGINE: _CURRENT_DATA_ENGINE._publish("note_saved", {"key": key, "content": content}) - except Exception: - pass + except Exception as exc: + # Telemetry side channel: publishing must never break a note save. + logger.debug("Failed to publish note_saved event for key '%s': %s", key, exc) def append_note_content(base_dir: str | Path, key: str, content: str) -> None: @@ -186,7 +197,9 @@ def append_note_content(base_dir: str | Path, key: str, content: str) -> None: last_char_bytes = f.read(1) if last_char_bytes != b"\n": prefix = "\n" - except Exception: + except OSError: + # Cannot inspect the last byte; worst case is a missing + # separator newline before the appended content. pass with open(file_path, "a", encoding="utf-8") as f: @@ -343,8 +356,10 @@ def update_note_content( if key == "task_plan": try: record_subgoal_hash_chain(base_dir, content, new_content_to_write) - except Exception: - pass + except Exception as exc: + # Hash-chain bookkeeping must never break a note edit, but a + # missed record can later confuse plan ratchet validation. + logger.warning("Failed to record subgoal hash chain for task_plan: %s", exc) return warning raise ValueError( @@ -410,8 +425,10 @@ def record_subgoal_hash_chain( if line.strip().startswith("- [ ]"): text = line.strip()[5:].strip() return hashlib.md5(text.encode("utf-8")).hexdigest() - except Exception: - pass + except Exception as exc: + # Pure string/hash operations; a failure here is unexpected and + # silently disables subgoal rename tracking. + logger.warning("Failed to compute active subgoal hash: %s", exc) return None old_active = get_active_hash(content_before) @@ -426,8 +443,13 @@ def record_subgoal_hash_chain( if chain_path.exists(): try: chain = json.loads(chain_path.read_text(encoding="utf-8")) - except Exception: - pass + except (OSError, ValueError) as exc: + logger.warning( + "Failed to load subgoal hash chain %s (starting a fresh" + " chain; previous rename history is lost): %s", + chain_path, + exc, + ) # Record mapping: old -> new chain[old_active] = new_active @@ -437,5 +459,12 @@ def record_subgoal_hash_chain( json.dumps(chain, indent=2, ensure_ascii=False), encoding="utf-8", ) - except Exception: - pass + except OSError as exc: + logger.warning( + "Failed to persist subgoal hash chain to %s (rename %s -> %s" + " not recorded): %s", + chain_path, + old_active, + new_active, + exc, + ) diff --git a/artemis/utils/video.py b/artemis/utils/video.py index 484284a..de1f5d2 100644 --- a/artemis/utils/video.py +++ b/artemis/utils/video.py @@ -703,7 +703,8 @@ def cleanup_video_segments(segments: list[Path], keep_path: Path | None = None) segment.unlink() if segment.parent.exists() and not any(segment.parent.iterdir()): segment.parent.rmdir() - except Exception: + except OSError: + # Best-effort cleanup of temporary segments; leftovers are harmless. pass diff --git a/artemis/utils/visualization.py b/artemis/utils/visualization.py index e906262..5143559 100644 --- a/artemis/utils/visualization.py +++ b/artemis/utils/visualization.py @@ -600,7 +600,9 @@ def draw_action_overlay_on_image( import ast sequence = ast.literal_eval(sequence_str) - except Exception: + except (ValueError, TypeError, SyntaxError): + # Unparseable sequence string: leave it as-is; the + # isinstance check below skips non-list values. pass if isinstance(sequence, list): diff --git a/tests/unit/test_credentials_validator.py b/tests/unit/test_credentials_validator.py index d4d3703..8ae63d8 100644 --- a/tests/unit/test_credentials_validator.py +++ b/tests/unit/test_credentials_validator.py @@ -27,7 +27,7 @@ def test_extract_error_message(): assert _extract_error_message(resp_openai) == "Incorrect API key." resp_fallback = MagicMock() - resp_fallback.json.side_effect = Exception("JSON parse error") + resp_fallback.json.side_effect = ValueError("JSON parse error") resp_fallback.text = "Raw error text" assert _extract_error_message(resp_fallback) == "Raw error text"