merge(errors): integrate core exception handling cleanup
This commit is contained in:
commit
e6c64412de
21 files changed
+132
-58
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user