merge(errors): integrate core exception handling cleanup

This commit is contained in:
somew1nd committed 2026-09-01 15:55:22 -07:00
commit e6c64412de
21 files changed
+132 -58

No files matched your search

+3 -1
View File
@@ -467,7 +467,9 @@ class EmulatorManager:
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=5, 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 pass
if self._proc and self._proc.poll() is None: if self._proc and self._proc.poll() is None:
+6 -2
View File
@@ -194,8 +194,12 @@ class ReadinessEngine:
if adb_result and adb_result.metadata.get("active_device"): if adb_result and adb_result.metadata.get("active_device"):
try: try:
active_device = DeviceInfo(**adb_result.metadata["active_device"]) active_device = DeviceInfo(**adb_result.metadata["active_device"])
except Exception: except (TypeError, ValueError) as exc:
pass # 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( return SystemReadinessReport(
overall_ready=overall_ready, overall_ready=overall_ready,
+12 -7
View File
@@ -355,8 +355,9 @@ class AndroidAdbDriver(BaseDeviceDriver):
broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'" broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'"
await asyncio.to_thread(self.device.shell, broadcast_cmd) await asyncio.to_thread(self.device.shell, broadcast_cmd)
return True return True
except Exception: except Exception as e:
pass # 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 # 3. Tier 3: Universal Native ADB input text fallback
lines = norm_text.split("\n") lines = norm_text.split("\n")
@@ -410,8 +411,9 @@ class AndroidAdbDriver(BaseDeviceDriver):
app_info = await asyncio.to_thread(self.device.current_app) app_info = await asyncio.to_thread(self.device.current_app)
if app_info and getattr(app_info, "package", None): if app_info and getattr(app_info, "package", None):
return app_info.package return app_info.package
except Exception: except Exception as e:
pass # 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 # 2. Structured dumpsys extraction
out = await asyncio.to_thread( out = await asyncio.to_thread(
@@ -467,8 +469,10 @@ class AndroidAdbDriver(BaseDeviceDriver):
try: try:
self._scrcpy_process.terminate() self._scrcpy_process.terminate()
await asyncio.wait_for(self._scrcpy_process.wait(), timeout=5.0) await asyncio.wait_for(self._scrcpy_process.wait(), timeout=5.0)
except Exception: except (ProcessLookupError, OSError, asyncio.TimeoutError) as e:
pass # 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 self._scrcpy_process = None
mkv = getattr(self, "_recording_mkv_path", None) mkv = getattr(self, "_recording_mkv_path", None)
@@ -493,7 +497,8 @@ class AndroidAdbDriver(BaseDeviceDriver):
if mp4.exists() and mp4.stat().st_size > 0: if mp4.exists() and mp4.stat().st_size > 0:
try: try:
mkv.unlink() mkv.unlink()
except Exception: except OSError:
# Best-effort cleanup of the intermediate MKV file.
pass pass
return str(mp4) return str(mp4)
except Exception as e: except Exception as e:
+3 -2
View File
@@ -102,8 +102,9 @@ class AndroidInputIME:
broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'" broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'"
await asyncio.to_thread(self.device.shell, broadcast_cmd) await asyncio.to_thread(self.device.shell, broadcast_cmd)
return True return True
except Exception: except Exception as e:
pass # 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 # 4. Fallback: ADB input text
lines = text.split("\n") lines = text.split("\n")
+2 -1
View File
@@ -170,7 +170,8 @@ def doctor_command(
state = parts[1] state = parts[1]
extra = " ".join(parts[2:]) extra = " ".join(parts[2:])
devices_info.append((serial, state, extra)) devices_info.append((serial, state, extra))
except Exception: except (OSError, subprocess.SubprocessError):
# adb probe failed or timed out; report as "no devices detected".
pass pass
if devices_info: if devices_info:
+2 -1
View File
@@ -78,7 +78,8 @@ def init_command() -> None:
serial = parts[0] serial = parts[0]
extra = " ".join(parts[2:]) extra = " ".join(parts[2:])
detected_devices.append((serial, extra)) detected_devices.append((serial, extra))
except Exception: except (OSError, subprocess.SubprocessError):
# adb probe failed or timed out; continue setup with no devices.
pass pass
selected_serial = "" selected_serial = ""
+15 -7
View File
@@ -166,7 +166,8 @@ def _parse_json_lenient(text: str) -> dict | None:
try: try:
data = json.loads(text) data = json.loads(text)
return data if isinstance(data, dict) else None return data if isinstance(data, dict) else None
except Exception: except ValueError:
# Not strict JSON; fall through to the lenient JSONC cleanup below.
pass pass
# Remove // single-line comments # Remove // single-line comments
cleaned = re.sub(r"//.*", "", text) cleaned = re.sub(r"//.*", "", text)
@@ -215,8 +216,11 @@ def _merge_json_file(
try: try:
backup_path.write_text(raw_text, encoding="utf-8") 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}") logger.warning(f"Existing JSON in {file_path} could not be parsed; backup created at {backup_path}")
except Exception: except OSError as exc:
pass 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: for remove_path in remove_paths:
parent = data 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 = file_path.with_suffix(file_path.suffix + ".bak")
backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8") 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}") logger.warning(f"Could not update {file_path}; backup created at {backup_path}: {e}")
except Exception: except (OSError, UnicodeDecodeError) as backup_exc:
pass logger.warning(
f"Could not update {file_path} ({e}); backup also failed: {backup_exc}"
)
else: else:
logger.warning(f"Could not create Codex MCP config file {file_path}: {e}") logger.warning(f"Could not create Codex MCP config file {file_path}: {e}")
return False return False
@@ -394,7 +400,8 @@ def _write_rule_file(file_path: Path, content: str) -> bool:
try: try:
if file_path.read_text(encoding="utf-8") == new_text: if file_path.read_text(encoding="utf-8") == new_text:
return True return True
except Exception: except (OSError, UnicodeDecodeError):
# Unreadable existing file: skip the no-op check and rewrite it.
pass pass
file_path.write_text(new_text, encoding="utf-8") file_path.write_text(new_text, encoding="utf-8")
return True return True
@@ -419,7 +426,8 @@ def _write_cursor_mdc(file_path: Path, content: str) -> bool:
try: try:
if file_path.read_text(encoding="utf-8") == new_text: if file_path.read_text(encoding="utf-8") == new_text:
return True return True
except Exception: except (OSError, UnicodeDecodeError):
# Unreadable existing file: skip the no-op check and rewrite it.
pass pass
file_path.write_text(new_text, encoding="utf-8") file_path.write_text(new_text, encoding="utf-8")
return True return True
+8 -4
View File
@@ -408,8 +408,9 @@ def run_command(
host=settings.ADB_HOST or "localhost", host=settings.ADB_HOST or "localhost",
port=settings.ADB_PORT or 5037, port=settings.ADB_PORT or 5037,
) )
except Exception: except Exception as exc:
pass # 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) display_device_status(console, adb_client=adb_client)
@@ -420,7 +421,9 @@ def run_command(
original_sigterm = signal.signal( original_sigterm = signal.signal(
signal.SIGTERM, lambda s, f: (_ for _ in ()).throw(KeyboardInterrupt()) 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 pass
try: try:
@@ -471,7 +474,8 @@ def run_command(
try: try:
if original_sigterm is not None and hasattr(signal, "SIGTERM"): if original_sigterm is not None and hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, original_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 pass
if cancelled: if cancelled:
raise SystemExit(130) raise SystemExit(130)
@@ -265,7 +265,8 @@ def status_command(
try: try:
dt = datetime.datetime.fromtimestamp(metadata["started_at"]) dt = datetime.datetime.fromtimestamp(metadata["started_at"])
started_at_str = dt.strftime("%Y-%m-%d %H:%M:%S") 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 pass
table = Table(show_header=False, box=None, padding=(0, 2)) table = Table(show_header=False, box=None, padding=(0, 2))
+6 -3
View File
@@ -107,7 +107,8 @@ class WindowsPlatformProcess(IPlatformProcess):
) )
if res.returncode == 0: if res.returncode == 0:
return True return True
except Exception: except (OSError, subprocess.SubprocessError):
# taskkill missing, not runnable, or timed out; try psutil below.
pass pass
# 2. Fallback to psutil # 2. Fallback to psutil
@@ -131,12 +132,14 @@ class WindowsPlatformProcess(IPlatformProcess):
if hasattr(sys.stdout, "reconfigure"): if hasattr(sys.stdout, "reconfigure"):
try: try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception: except (OSError, ValueError):
# Stream is detached/closed or does not support reconfigure.
pass pass
if hasattr(sys.stderr, "reconfigure"): if hasattr(sys.stderr, "reconfigure"):
try: try:
sys.stderr.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception: except (OSError, ValueError):
# Stream is detached/closed or does not support reconfigure.
pass pass
os.environ["PYTHONUTF8"] = "1" os.environ["PYTHONUTF8"] = "1"
+1 -1
View File
@@ -133,7 +133,7 @@ def spawn_daemon(
if log_handle is not subprocess.DEVNULL: if log_handle is not subprocess.DEVNULL:
try: try:
log_handle.close() log_handle.close()
except Exception: except OSError:
pass pass
+4 -2
View File
@@ -152,7 +152,8 @@ class DevicePool:
if proc is not None: if proc is not None:
try: try:
proc.kill() proc.kill()
except Exception: except (ProcessLookupError, OSError):
# Process already exited; nothing left to clean up.
pass pass
return None return None
@@ -256,7 +257,8 @@ class DevicePool:
if proc is not None: if proc is not None:
try: try:
proc.kill() proc.kill()
except Exception: except (ProcessLookupError, OSError):
# Process already exited; nothing left to clean up.
pass pass
async def warm_up_async( async def warm_up_async(
+3 -2
View File
@@ -113,8 +113,9 @@ class ProcessSupervisor:
logger.debug(f"Failed graceful stop for process {proc}: {e}") logger.debug(f"Failed graceful stop for process {proc}: {e}")
try: try:
proc.kill() proc.kill()
except Exception: except (ProcessLookupError, OSError) as kill_exc:
pass # ProcessLookupError: already exited, nothing to do.
logger.debug(f"Failed to kill process {proc}: {kill_exc}")
process_supervisor: ProcessSupervisor = ProcessSupervisor() process_supervisor: ProcessSupervisor = ProcessSupervisor()
+3
View File
@@ -26,6 +26,7 @@ def _get_embedded_adb() -> str | None:
if hasattr(adbutils, "adb_path"): if hasattr(adbutils, "adb_path"):
return adbutils.adb_path() return adbutils.adb_path()
except Exception: except Exception:
# Optional dependency probe: not installed or no bundled binary.
pass pass
return None return None
@@ -38,6 +39,8 @@ def _get_embedded_ffmpeg() -> str | None:
if hasattr(imageio_ffmpeg, "get_ffmpeg_exe"): if hasattr(imageio_ffmpeg, "get_ffmpeg_exe"):
return imageio_ffmpeg.get_ffmpeg_exe() return imageio_ffmpeg.get_ffmpeg_exe()
except Exception: except Exception:
# Optional dependency probe: not installed, or imageio_ffmpeg raises
# RuntimeError when it ships no binary for this platform.
pass pass
return None return None
+2
View File
@@ -103,6 +103,8 @@ class ToolchainResolver:
self._cache[key] = resolved self._cache[key] = resolved
return resolved return resolved
except Exception: except Exception:
# Probing an optional third-party embedded binary; any failure
# simply means "tool not found" and the caller handles None.
pass pass
# Do not permanently cache negative lookups so dynamic installs are detected immediately # Do not permanently cache negative lookups so dynamic installs are detected immediately
+8 -5
View File
@@ -63,7 +63,7 @@ def parse_swipe_parameters(
if isinstance(p, (list, tuple)) and len(p) >= 2: if isinstance(p, (list, tuple)) and len(p) >= 2:
try: try:
return [int(float(p[0])), int(float(p[1]))] return [int(float(p[0])), int(float(p[1]))]
except Exception: except (ValueError, TypeError):
pass pass
if isinstance(p, str): if isinstance(p, str):
nums = re.findall(r"-?\d+(?:\.\d+)?", p) nums = re.findall(r"-?\d+(?:\.\d+)?", p)
@@ -88,7 +88,7 @@ def parse_swipe_parameters(
if len(c) == 4: if len(c) == 4:
try: try:
return "coords", [int(float(x)) for x in c], duration_int return "coords", [int(float(x)) for x in c], duration_int
except Exception: except (ValueError, TypeError):
pass pass
elif ( elif (
len(c) == 2 and isinstance(c[0], (list, tuple)) and isinstance(c[1], (list, tuple)) 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]) pt2 = _parse_pt(parsed[1])
if pt1 and pt2: if pt1 and pt2:
return "coords", [pt1[0], pt1[1], pt2[0], pt2[1]], duration_int return "coords", [pt1[0], pt1[1], pt2[0], pt2[1]], duration_int
except Exception: except (ValueError, TypeError):
pass pass
# Regex extract numbers # Regex extract numbers
@@ -448,7 +448,9 @@ def normalize_step_actions(step_dict: dict[str, Any]) -> dict[str, Any]:
import ast import ast
seq = ast.literal_eval(seq) 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 pass
if isinstance(seq, (list, tuple)): if isinstance(seq, (list, tuple)):
item["sequence"] = list(seq) item["sequence"] = list(seq)
@@ -481,7 +483,8 @@ def normalize_step_actions(step_dict: dict[str, Any]) -> dict[str, Any]:
try: try:
action_taken = json.loads(action_taken) action_taken = json.loads(action_taken)
step_dict["action_taken"] = action_taken step_dict["action_taken"] = action_taken
except Exception: except ValueError:
# Not JSON: keep the raw string; downstream isinstance checks skip it.
pass pass
if isinstance(action_taken, dict): if isinstance(action_taken, dict):
+2 -1
View File
@@ -43,7 +43,8 @@ def _extract_error_message(resp: httpx.Response) -> str:
return str(data["message"]) return str(data["message"])
if "detail" in data: if "detail" in data:
return str(data["detail"]) return str(data["detail"])
except Exception: except ValueError:
# Response body is not valid JSON; fall through to the text fallback.
pass pass
# Fallback to response text or HTTP status # Fallback to response text or HTTP status
+44 -15
View File
@@ -20,6 +20,9 @@ from pathlib import Path
import re import re
from artemis.data_engine.engine import _CURRENT_DATA_ENGINE 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) # Centralized Description and Documentation Constants (Single Source of Truth)
READ_NOTE_DOCSTRING = ( 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(): if key == "task_plan" and file_path.exists():
try: try:
content_before = file_path.read_text(encoding="utf-8") content_before = file_path.read_text(encoding="utf-8")
except Exception: except (OSError, UnicodeDecodeError) as exc:
pass 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") file_path.write_text(content, encoding="utf-8")
if key == "task_plan": if key == "task_plan":
try: try:
record_subgoal_hash_chain(base_dir, content_before, content) record_subgoal_hash_chain(base_dir, content_before, content)
except Exception: except Exception as exc:
pass # 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: try:
if _CURRENT_DATA_ENGINE: if _CURRENT_DATA_ENGINE:
_CURRENT_DATA_ENGINE._publish("note_saved", {"key": key, "content": content}) _CURRENT_DATA_ENGINE._publish("note_saved", {"key": key, "content": content})
except Exception: except Exception as exc:
pass # 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: 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) last_char_bytes = f.read(1)
if last_char_bytes != b"\n": if last_char_bytes != b"\n":
prefix = "\n" prefix = "\n"
except Exception: except OSError:
# Cannot inspect the last byte; worst case is a missing
# separator newline before the appended content.
pass pass
with open(file_path, "a", encoding="utf-8") as f: with open(file_path, "a", encoding="utf-8") as f:
@@ -343,8 +356,10 @@ def update_note_content(
if key == "task_plan": if key == "task_plan":
try: try:
record_subgoal_hash_chain(base_dir, content, new_content_to_write) record_subgoal_hash_chain(base_dir, content, new_content_to_write)
except Exception: except Exception as exc:
pass # 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 return warning
raise ValueError( raise ValueError(
@@ -410,8 +425,10 @@ def record_subgoal_hash_chain(
if line.strip().startswith("- [ ]"): if line.strip().startswith("- [ ]"):
text = line.strip()[5:].strip() text = line.strip()[5:].strip()
return hashlib.md5(text.encode("utf-8")).hexdigest() return hashlib.md5(text.encode("utf-8")).hexdigest()
except Exception: except Exception as exc:
pass # 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 return None
old_active = get_active_hash(content_before) old_active = get_active_hash(content_before)
@@ -426,8 +443,13 @@ def record_subgoal_hash_chain(
if chain_path.exists(): if chain_path.exists():
try: try:
chain = json.loads(chain_path.read_text(encoding="utf-8")) chain = json.loads(chain_path.read_text(encoding="utf-8"))
except Exception: except (OSError, ValueError) as exc:
pass 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 # Record mapping: old -> new
chain[old_active] = new_active chain[old_active] = new_active
@@ -437,5 +459,12 @@ def record_subgoal_hash_chain(
json.dumps(chain, indent=2, ensure_ascii=False), json.dumps(chain, indent=2, ensure_ascii=False),
encoding="utf-8", encoding="utf-8",
) )
except Exception: except OSError as exc:
pass logger.warning(
"Failed to persist subgoal hash chain to %s (rename %s -> %s"
" not recorded): %s",
chain_path,
old_active,
new_active,
exc,
)
+2 -1
View File
@@ -703,7 +703,8 @@ def cleanup_video_segments(segments: list[Path], keep_path: Path | None = None)
segment.unlink() segment.unlink()
if segment.parent.exists() and not any(segment.parent.iterdir()): if segment.parent.exists() and not any(segment.parent.iterdir()):
segment.parent.rmdir() segment.parent.rmdir()
except Exception: except OSError:
# Best-effort cleanup of temporary segments; leftovers are harmless.
pass pass
+3 -1
View File
@@ -600,7 +600,9 @@ def draw_action_overlay_on_image(
import ast import ast
sequence = ast.literal_eval(sequence_str) 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 pass
if isinstance(sequence, list): if isinstance(sequence, list):
+1 -1
View File
@@ -27,7 +27,7 @@ def test_extract_error_message():
assert _extract_error_message(resp_openai) == "Incorrect API key." assert _extract_error_message(resp_openai) == "Incorrect API key."
resp_fallback = MagicMock() 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" resp_fallback.text = "Raw error text"
assert _extract_error_message(resp_fallback) == "Raw error text" assert _extract_error_message(resp_fallback) == "Raw error text"