refactor(errors): replace silent exception handling in console and server
This commit is contained in:
1 parent
41deb48005
commit
c76c9bb7e8
20 files changed
+267
-107
No files matched your search
@@ -13,11 +13,13 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from artemis.config import DB_PATH
|
from artemis.config import DB_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_initialized_dbs = set()
|
_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)
|
StorageManager(db_path=path, base_trace_dir=path.parent)
|
||||||
_initialized_dbs.add(path_key)
|
_initialized_dbs.add(path_key)
|
||||||
except Exception:
|
except (ImportError, OSError, sqlite3.Error) as exc:
|
||||||
pass
|
# 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 = sqlite3.connect(path, timeout=10.0)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
try:
|
try:
|
||||||
conn.execute("PRAGMA journal_mode=WAL")
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
conn.execute("PRAGMA busy_timeout=10000")
|
conn.execute("PRAGMA busy_timeout=10000")
|
||||||
except Exception:
|
except sqlite3.Error:
|
||||||
|
# Optional performance PRAGMAs; the connection works without them.
|
||||||
pass
|
pass
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from artemis.runtime import trace_store
|
from artemis.runtime import trace_store
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from admin_console.database.connection import db_session
|
from admin_console.database.connection import db_session
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -317,8 +320,15 @@ class SessionRepository:
|
|||||||
"failed",
|
"failed",
|
||||||
error="Process terminated prior to server startup.",
|
error="Process terminated prior to server startup.",
|
||||||
)
|
)
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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()
|
conn.commit()
|
||||||
return count
|
return count
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ class StepRepository:
|
|||||||
if isinstance(payload_raw, str):
|
if isinstance(payload_raw, str):
|
||||||
try:
|
try:
|
||||||
payload_obj = json.loads(payload_raw)
|
payload_obj = json.loads(payload_raw)
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: the isinstance(dict) guard below bails out.
|
||||||
pass
|
pass
|
||||||
if not isinstance(payload_obj, dict):
|
if not isinstance(payload_obj, dict):
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
@@ -175,7 +176,8 @@ class StepRepository:
|
|||||||
if isinstance(payload_raw, str):
|
if isinstance(payload_raw, str):
|
||||||
try:
|
try:
|
||||||
payload_obj = json.loads(payload_raw)
|
payload_obj = json.loads(payload_raw)
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: the isinstance(dict) guard below bails out.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not isinstance(payload_obj, dict):
|
if not isinstance(payload_obj, dict):
|
||||||
@@ -308,7 +310,8 @@ class StepRepository:
|
|||||||
if step_dict.get(json_col):
|
if step_dict.get(json_col):
|
||||||
try:
|
try:
|
||||||
step_dict[json_col] = json.loads(step_dict[json_col])
|
step_dict[json_col] = json.loads(step_dict[json_col])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON column value: keep the raw string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If post_image_name is identical to pre_image_name, normalize to None
|
# 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())
|
p_raw_th.append(str(th_text).strip())
|
||||||
elif thr["type"] == "thinking" and th_text:
|
elif thr["type"] == "thinking" and th_text:
|
||||||
p_nat_th.append(str(th_text).strip())
|
p_nat_th.append(str(th_text).strip())
|
||||||
except Exception:
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
# Unparseable or non-dict thinking payload: skip it.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
virtual_step = {
|
virtual_step = {
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ class TraceRepository:
|
|||||||
if row_dict.get("payload"):
|
if row_dict.get("payload"):
|
||||||
try:
|
try:
|
||||||
row_dict["payload"] = json.loads(row_dict["payload"])
|
row_dict["payload"] = json.loads(row_dict["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: keep the raw string for display.
|
||||||
pass
|
pass
|
||||||
nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
|
nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
|
||||||
|
|
||||||
@@ -94,7 +95,8 @@ class TraceRepository:
|
|||||||
if row_dict.get("payload"):
|
if row_dict.get("payload"):
|
||||||
try:
|
try:
|
||||||
row_dict["payload"] = json.loads(row_dict["payload"])
|
row_dict["payload"] = json.loads(row_dict["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: keep the raw string for display.
|
||||||
pass
|
pass
|
||||||
nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
|
nodes[row_dict["trace_id"]] = {**row_dict, "children": []}
|
||||||
|
|
||||||
|
|||||||
@@ -485,7 +485,8 @@ class ReplayManager:
|
|||||||
if td.get("payload"):
|
if td.get("payload"):
|
||||||
try:
|
try:
|
||||||
td["payload"] = json.loads(td["payload"])
|
td["payload"] = json.loads(td["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: keep the raw string.
|
||||||
pass
|
pass
|
||||||
traces.append(td)
|
traces.append(td)
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -721,8 +722,11 @@ class ReplayManager:
|
|||||||
with open(traces_json_path, encoding="utf-8") as f:
|
with open(traces_json_path, encoding="utf-8") as f:
|
||||||
traces = json.load(f)
|
traces = json.load(f)
|
||||||
return {str(t.get("trace_id")).lower() for t in traces if t.get("trace_id")}
|
return {str(t.get("trace_id")).lower() for t in traces if t.get("trace_id")}
|
||||||
except Exception:
|
except (OSError, ValueError) as e:
|
||||||
pass
|
print(
|
||||||
|
f"Warning: Failed to read preloaded trace ids from {traces_json_path}: {e}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def load_traces_from_db(self, db_path: Path, step_num: int, preloaded_trace_ids: set) -> list:
|
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"):
|
if td.get("payload"):
|
||||||
try:
|
try:
|
||||||
td["payload"] = json.loads(td["payload"])
|
td["payload"] = json.loads(td["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: normalized to {} just below.
|
||||||
pass
|
pass
|
||||||
if not isinstance(td.get("payload"), dict):
|
if not isinstance(td.get("payload"), dict):
|
||||||
td["payload"] = {}
|
td["payload"] = {}
|
||||||
@@ -808,7 +813,8 @@ class ReplayManager:
|
|||||||
session_id = None
|
session_id = None
|
||||||
try:
|
try:
|
||||||
session_id = temp_db_path.parents[1].name.split("_")[0]
|
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
|
pass
|
||||||
|
|
||||||
if not session_id:
|
if not session_id:
|
||||||
@@ -820,7 +826,8 @@ class ReplayManager:
|
|||||||
if row:
|
if row:
|
||||||
session_id = str(row[0])
|
session_id = str(row[0])
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except sqlite3.Error:
|
||||||
|
# Unreadable sandbox DB: fall back to single-step trace loading.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not session_id:
|
if not session_id:
|
||||||
@@ -890,8 +897,11 @@ class ReplayManager:
|
|||||||
with open(step_json_path, encoding="utf-8") as f:
|
with open(step_json_path, encoding="utf-8") as f:
|
||||||
step_data = json.load(f)
|
step_data = json.load(f)
|
||||||
current_step_id = step_data.get("step_id")
|
current_step_id = step_data.get("step_id")
|
||||||
except Exception:
|
except (OSError, ValueError) as e:
|
||||||
pass
|
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
|
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")
|
sid = step_data.get("step_id")
|
||||||
if sid:
|
if sid:
|
||||||
step_dirs[str(sid).lower()] = subdir
|
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
|
pass
|
||||||
|
|
||||||
for td in all_traces:
|
for td in all_traces:
|
||||||
@@ -1043,7 +1055,8 @@ class ReplayManager:
|
|||||||
return (
|
return (
|
||||||
f"file://{abs_path}" if abs_path.startswith("/") else f"file:///{abs_path}"
|
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
|
pass
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@@ -1104,7 +1117,8 @@ class ReplayManager:
|
|||||||
if r["payload"]:
|
if r["payload"]:
|
||||||
try:
|
try:
|
||||||
payload = json.loads(r["payload"])
|
payload = json.loads(r["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: treated as an empty tool payload.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
response_val = payload.get("result")
|
response_val = payload.get("result")
|
||||||
@@ -1116,7 +1130,8 @@ class ReplayManager:
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
response_val = json.loads(response_val)
|
response_val = json.loads(response_val)
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Looks like JSON but is not: keep the raw string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
args = payload.get("args", {})
|
args = payload.get("args", {})
|
||||||
@@ -1682,7 +1697,8 @@ class ReplayManager:
|
|||||||
if row and row[0]:
|
if row and row[0]:
|
||||||
try:
|
try:
|
||||||
device_info = json.loads(row[0])
|
device_info = json.loads(row[0])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Malformed device_info: simulate with the defaults below.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sim_device_id = device_info.get("device_id", "replay-device")
|
sim_device_id = device_info.get("device_id", "replay-device")
|
||||||
@@ -2294,8 +2310,11 @@ class ReplayManager:
|
|||||||
res_data = json.load(f)
|
res_data = json.load(f)
|
||||||
replay_outcome = res_data.get("result")
|
replay_outcome = res_data.get("result")
|
||||||
replay_id = res_data.get("replay_id")
|
replay_id = res_data.get("replay_id")
|
||||||
except Exception:
|
except (OSError, ValueError) as e:
|
||||||
pass
|
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
|
# Update virtual tool trace result with the actual formatted outcome if available
|
||||||
if replay_outcome is not None:
|
if replay_outcome is not None:
|
||||||
@@ -2464,8 +2483,11 @@ class ReplayManager:
|
|||||||
llm_calls_log = res_data.get("llm_calls") or []
|
llm_calls_log = res_data.get("llm_calls") or []
|
||||||
replay_outcome = res_data.get("result")
|
replay_outcome = res_data.get("result")
|
||||||
replay_id = res_data.get("replay_id")
|
replay_id = res_data.get("replay_id")
|
||||||
except Exception:
|
except (OSError, ValueError) as e:
|
||||||
pass
|
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
|
# Update virtual tool trace result with the actual formatted outcome if available
|
||||||
if replay_outcome is not None:
|
if replay_outcome is not None:
|
||||||
|
|||||||
@@ -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
|
d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw
|
||||||
if isinstance(d_info, dict):
|
if isinstance(d_info, dict):
|
||||||
device_id = d_info.get("device_id") or d_info.get("device_serial")
|
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
|
pass
|
||||||
if not device_id and recording:
|
if not device_id and recording:
|
||||||
device_id = recording.get("device_id")
|
device_id = recording.get("device_id")
|
||||||
@@ -157,8 +158,10 @@ def _list_sessions_sync():
|
|||||||
"failed",
|
"failed",
|
||||||
error="Process terminated unexpectedly (auto-harvested).",
|
error="Process terminated unexpectedly (auto-harvested).",
|
||||||
)
|
)
|
||||||
except Exception:
|
except OSError as e:
|
||||||
pass
|
# 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:
|
except Exception as e:
|
||||||
print(f"[list_sessions] Failed to update orphaned sessions: {e}")
|
print(f"[list_sessions] Failed to update orphaned sessions: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ async def get_trace(trace_id: str, session_id: str = None, step_number: int = No
|
|||||||
try:
|
try:
|
||||||
payload_obj = json.loads(trace_dict["payload"])
|
payload_obj = json.loads(trace_dict["payload"])
|
||||||
trace_dict["payload"] = media_service.unwrap_payload(payload_obj)
|
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
|
pass
|
||||||
|
|
||||||
return trace_dict
|
return trace_dict
|
||||||
|
|||||||
@@ -209,7 +209,8 @@ async def stop_task(
|
|||||||
target_sid = str(body["session_id"])
|
target_sid = str(body["session_id"])
|
||||||
if body.get("device_id"):
|
if body.get("device_id"):
|
||||||
target_dev = str(body["device_id"])
|
target_dev = str(body["device_id"])
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Empty or non-JSON body: fall back to the query parameters.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
stopped = task_queue_service.stop_tasks(
|
stopped = task_queue_service.stop_tasks(
|
||||||
|
|||||||
@@ -15,8 +15,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from admin_console.core.state import state
|
from admin_console.core.state import state
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -76,7 +79,8 @@ class IPCService:
|
|||||||
if isinstance(payload, str):
|
if isinstance(payload, str):
|
||||||
try:
|
try:
|
||||||
payload = json.loads(payload)
|
payload = json.loads(payload)
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Non-JSON payload string: cleaned as-is below.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if tr_type in ("tool", "action") and isinstance(payload, dict):
|
if tr_type in ("tool", "action") and isinstance(payload, dict):
|
||||||
@@ -102,7 +106,8 @@ class IPCService:
|
|||||||
if isinstance(pl, str):
|
if isinstance(pl, str):
|
||||||
try:
|
try:
|
||||||
pl = json.loads(pl)
|
pl = json.loads(pl)
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Non-JSON payload string: cleaned as-is below.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if tr_dict.get("type") == "llm_call":
|
if tr_dict.get("type") == "llm_call":
|
||||||
@@ -190,7 +195,14 @@ class IPCService:
|
|||||||
try:
|
try:
|
||||||
cb(event_type, data)
|
cb(event_type, data)
|
||||||
except Exception:
|
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:
|
except (ValueError, UnicodeDecodeError, TypeError) as e:
|
||||||
print(f"IPC parse error: {e}")
|
print(f"IPC parse error: {e}")
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -25,6 +26,8 @@ from fastapi import HTTPException
|
|||||||
|
|
||||||
from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
|
from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MediaService:
|
class MediaService:
|
||||||
"""Service handling media indexing, local file security, payload unwrapping and notes/plans."""
|
"""Service handling media indexing, local file security, payload unwrapping and notes/plans."""
|
||||||
@@ -76,7 +79,9 @@ class MediaService:
|
|||||||
str(result),
|
str(result),
|
||||||
)
|
)
|
||||||
return 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
|
pass
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@@ -167,8 +172,9 @@ class MediaService:
|
|||||||
url = cls.path_to_video_url(item)
|
url = cls.path_to_video_url(item)
|
||||||
idx[item.stem] = url
|
idx[item.stem] = url
|
||||||
idx[item.name] = url
|
idx[item.name] = url
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# Unreadable directory entry: index the rest of the tree.
|
||||||
|
logger.warning("Video index scan failed under %s: %s", d, exc)
|
||||||
return idx
|
return idx
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -219,7 +225,8 @@ class MediaService:
|
|||||||
item = cls.ensure_browser_playable_video(item)
|
item = cls.ensure_browser_playable_video(item)
|
||||||
v_url = cls.path_to_video_url(item)
|
v_url = cls.path_to_video_url(item)
|
||||||
break
|
break
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Unreadable session dir: no recording to resolve.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not v_url and row_dict.get("initial_goal"):
|
if not v_url and row_dict.get("initial_goal"):
|
||||||
@@ -274,7 +281,8 @@ class MediaService:
|
|||||||
obj.startswith("[") and obj.endswith("]")
|
obj.startswith("[") and obj.endswith("]")
|
||||||
):
|
):
|
||||||
return MediaService.unwrap_payload(json.loads(obj))
|
return MediaService.unwrap_payload(json.loads(obj))
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Looks like JSON but is not: treat it as a plain string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check if the string is a Base64 image
|
# Check if the string is a Base64 image
|
||||||
@@ -284,7 +292,8 @@ class MediaService:
|
|||||||
try:
|
try:
|
||||||
_, base64_data = obj.split(";base64,", 1)
|
_, base64_data = obj.split(";base64,", 1)
|
||||||
is_base64_img = True
|
is_base64_img = True
|
||||||
except Exception:
|
except ValueError:
|
||||||
|
# Malformed data URI: fall through to plain-string handling.
|
||||||
pass
|
pass
|
||||||
elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"):
|
elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"):
|
||||||
is_base64_img = True
|
is_base64_img = True
|
||||||
@@ -301,8 +310,10 @@ class MediaService:
|
|||||||
image_path.write_bytes(image_bytes)
|
image_path.write_bytes(image_bytes)
|
||||||
|
|
||||||
return f"image://{image_hash}"
|
return f"image://{image_hash}"
|
||||||
except Exception:
|
except (ValueError, OSError) as exc:
|
||||||
pass
|
# 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:
|
if len(obj) > 2000:
|
||||||
preview_start = obj[:100].replace("\n", " ")
|
preview_start = obj[:100].replace("\n", " ")
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ModelService:
|
class ModelService:
|
||||||
"""Service handling agent architecture profile detection and metadata formatting."""
|
"""Service handling agent architecture profile detection and metadata formatting."""
|
||||||
@@ -44,8 +47,10 @@ class ModelService:
|
|||||||
elif llm_cfg and llm_cfg.default:
|
elif llm_cfg and llm_cfg.default:
|
||||||
provider = str(llm_cfg.default.provider)
|
provider = str(llm_cfg.default.provider)
|
||||||
model_id = str(llm_cfg.default.model)
|
model_id = str(llm_cfg.default.model)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# 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)
|
cls._llm_info_cache = (now, provider, model_id)
|
||||||
return provider, model_id
|
return provider, model_id
|
||||||
|
|
||||||
@@ -94,7 +99,8 @@ class ModelService:
|
|||||||
return "pro"
|
return "pro"
|
||||||
elif "flash" in p_str:
|
elif "flash" in p_str:
|
||||||
return "flash"
|
return "flash"
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Malformed device_info JSON: try the other profile sources.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (row_dict.get("status") == "running") and running_profile:
|
if (row_dict.get("status") == "running") and running_profile:
|
||||||
@@ -132,7 +138,8 @@ class ModelService:
|
|||||||
for x in ("planner", "validator", "operator", "checker")
|
for x in ("planner", "validator", "operator", "checker")
|
||||||
):
|
):
|
||||||
return "pro"
|
return "pro"
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Malformed trace payload JSON: skip this trace.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import codecs
|
import codecs
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
import shutil
|
import shutil
|
||||||
@@ -48,6 +49,8 @@ from artemis.runtime import (
|
|||||||
trace_store,
|
trace_store,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskQueueService:
|
class TaskQueueService:
|
||||||
"""Service managing FIFO task execution, background worker, subprocess lifecycle,
|
"""Service managing FIFO task execution, background worker, subprocess lifecycle,
|
||||||
@@ -76,7 +79,14 @@ class TaskQueueService:
|
|||||||
try:
|
try:
|
||||||
cb(event_type, data)
|
cb(event_type, data)
|
||||||
except Exception:
|
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
|
@classmethod
|
||||||
def _broadcast_startup_progress(
|
def _broadcast_startup_progress(
|
||||||
@@ -194,7 +204,9 @@ class TaskQueueService:
|
|||||||
if log_file is not None:
|
if log_file is not None:
|
||||||
try:
|
try:
|
||||||
log_file.write(text)
|
log_file.write(text)
|
||||||
except Exception:
|
except (OSError, ValueError):
|
||||||
|
# Best-effort tee into stdout.log; console output above
|
||||||
|
# already carried the text.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -214,7 +226,8 @@ class TaskQueueService:
|
|||||||
if log_file is not None:
|
if log_file is not None:
|
||||||
try:
|
try:
|
||||||
log_file.close()
|
log_file.close()
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Flush-on-close of the best-effort tee failed; nothing to do.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -265,7 +278,8 @@ class TaskQueueService:
|
|||||||
return proc.returncode if proc.returncode is not None else -15
|
return proc.returncode if proc.returncode is not None else -15
|
||||||
except (psutil.NoSuchProcess, ProcessLookupError):
|
except (psutil.NoSuchProcess, ProcessLookupError):
|
||||||
return proc.returncode if proc.returncode is not None else -15
|
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
|
pass
|
||||||
else:
|
else:
|
||||||
return proc.returncode if proc.returncode is not None else -15
|
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())
|
state.worker_task = loop.create_task(cls.queue_worker())
|
||||||
try:
|
try:
|
||||||
state.wake_event.set()
|
state.wake_event.set()
|
||||||
except Exception:
|
except AttributeError:
|
||||||
|
# Partially initialized state (unit tests): the worker
|
||||||
|
# loop polls anyway.
|
||||||
pass
|
pass
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
@@ -515,8 +531,11 @@ class TaskQueueService:
|
|||||||
if st:
|
if st:
|
||||||
st["pid"] = proc.pid
|
st["pid"] = proc.pid
|
||||||
trace_store.write_status(str(sess_id), st)
|
trace_store.write_status(str(sess_id), st)
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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(
|
cls._broadcast_startup_progress(
|
||||||
sess_id, "process_ready", "Execution process started"
|
sess_id, "process_ready", "Execution process started"
|
||||||
)
|
)
|
||||||
@@ -583,8 +602,12 @@ class TaskQueueService:
|
|||||||
str(sess_id),
|
str(sess_id),
|
||||||
canonical_mcp_status,
|
canonical_mcp_status,
|
||||||
)
|
)
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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_info = session_repo.get_video_recording_for_session(sess_id)
|
||||||
rec_status = (rec_info or {}).get("status")
|
rec_status = (rec_info or {}).get("status")
|
||||||
recovered_video_path = None
|
recovered_video_path = None
|
||||||
@@ -848,8 +871,9 @@ class TaskQueueService:
|
|||||||
active_owners: dict[str, Any] = {}
|
active_owners: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
active_owners = DeviceExecutionLock.get_active_owners()
|
active_owners = DeviceExecutionLock.get_active_owners()
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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()
|
fallback = DeviceExecutionLock.get_active_owner()
|
||||||
if fallback and not active_owners:
|
if fallback and not active_owners:
|
||||||
active_owners["default"] = fallback
|
active_owners["default"] = fallback
|
||||||
@@ -872,8 +896,10 @@ class TaskQueueService:
|
|||||||
"cancelled",
|
"cancelled",
|
||||||
error="Task stopped from the Artemis frontend.",
|
error="Task stopped from the Artemis frontend.",
|
||||||
)
|
)
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
print(
|
||||||
|
f"[stop_tasks] Could not persist cancelled MCP status for {sid}: {exc}"
|
||||||
|
)
|
||||||
cls._broadcast_event(
|
cls._broadcast_event(
|
||||||
"session_ended",
|
"session_ended",
|
||||||
{
|
{
|
||||||
@@ -895,7 +921,8 @@ class TaskQueueService:
|
|||||||
if run_proc is not None and run_proc.returncode is None:
|
if run_proc is not None and run_proc.returncode is None:
|
||||||
try:
|
try:
|
||||||
run_proc.kill()
|
run_proc.kill()
|
||||||
except Exception:
|
except (ProcessLookupError, OSError):
|
||||||
|
# Process already exited between the check and the kill.
|
||||||
pass
|
pass
|
||||||
# active_runs entries are popped by each run's finalizer once the
|
# active_runs entries are popped by each run's finalizer once the
|
||||||
# process exit is observed; clearing them here would free the device
|
# process exit is observed; clearing them here would free the device
|
||||||
@@ -903,7 +930,8 @@ class TaskQueueService:
|
|||||||
if state.current_process:
|
if state.current_process:
|
||||||
try:
|
try:
|
||||||
state.current_process.kill()
|
state.current_process.kill()
|
||||||
except Exception:
|
except (ProcessLookupError, OSError):
|
||||||
|
# Process already exited between the check and the kill.
|
||||||
pass
|
pass
|
||||||
state.current_process = None
|
state.current_process = None
|
||||||
|
|
||||||
@@ -915,7 +943,9 @@ class TaskQueueService:
|
|||||||
if PAUSE_FILE.exists():
|
if PAUSE_FILE.exists():
|
||||||
try:
|
try:
|
||||||
PAUSE_FILE.unlink()
|
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
|
pass
|
||||||
|
|
||||||
cls.ensure_worker_running()
|
cls.ensure_worker_running()
|
||||||
@@ -926,8 +956,9 @@ class TaskQueueService:
|
|||||||
active_owners = {}
|
active_owners = {}
|
||||||
try:
|
try:
|
||||||
active_owners = DeviceExecutionLock.get_active_owners()
|
active_owners = DeviceExecutionLock.get_active_owners()
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# Unreadable lock dir: the local-process fallbacks below still apply.
|
||||||
|
print(f"[stop_tasks] Could not enumerate device owners: {exc}")
|
||||||
|
|
||||||
owner = None
|
owner = None
|
||||||
if target_sid:
|
if target_sid:
|
||||||
@@ -1066,7 +1097,8 @@ class TaskQueueService:
|
|||||||
try:
|
try:
|
||||||
local_proc.kill()
|
local_proc.kill()
|
||||||
stopped = True
|
stopped = True
|
||||||
except Exception:
|
except (ProcessLookupError, OSError):
|
||||||
|
# Process already exited; terminate_tree above may have got it.
|
||||||
pass
|
pass
|
||||||
stopped = True
|
stopped = True
|
||||||
is_local_owner = True
|
is_local_owner = True
|
||||||
@@ -1097,8 +1129,11 @@ class TaskQueueService:
|
|||||||
if row_pid and session_repo.process_is_alive(row_pid):
|
if row_pid and session_repo.process_is_alive(row_pid):
|
||||||
try:
|
try:
|
||||||
process_supervisor.terminate_tree(int(row_pid))
|
process_supervisor.terminate_tree(int(row_pid))
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# 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()
|
DeviceExecutionLock.cleanup_stale_locks()
|
||||||
stopped = True
|
stopped = True
|
||||||
|
|
||||||
@@ -1176,7 +1211,9 @@ class TaskQueueService:
|
|||||||
if PAUSE_FILE.exists():
|
if PAUSE_FILE.exists():
|
||||||
try:
|
try:
|
||||||
PAUSE_FILE.unlink()
|
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
|
pass
|
||||||
|
|
||||||
cls.ensure_worker_running()
|
cls.ensure_worker_running()
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def resolve_profile_file() -> str | None:
|
|||||||
os.path.join(app_dir, "llm-config.json"),
|
os.path.join(app_dir, "llm-config.json"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
except Exception:
|
except (ImportError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
candidates.extend(
|
candidates.extend(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ try:
|
|||||||
|
|
||||||
FastMCPSettings.model_rebuild()
|
FastMCPSettings.model_rebuild()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Version-compat shim: older/newer FastMCP releases may not expose
|
||||||
|
# Settings or need the rebuild; the server works without it.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Define the shared FastMCP instance for external IDE and agent clients.
|
# Define the shared FastMCP instance for external IDE and agent clients.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
if addr and token:
|
if addr and token:
|
||||||
self._save_shared_env(addr, token)
|
self._save_shared_env(addr, token)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Environment recovery is opportunistic; notifier construction
|
||||||
|
# must never fail because of it.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -59,7 +61,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
for cand in candidates:
|
for cand in candidates:
|
||||||
if os.path.exists(cand) and os.access(cand, os.X_OK):
|
if os.path.exists(cand) and os.access(cand, os.X_OK):
|
||||||
return cand
|
return cand
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Filesystem probe failed: report the binary as absent.
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -70,7 +73,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
return True
|
return True
|
||||||
if "ANTIGRAVITY_LS_ADDRESS" in os.environ:
|
if "ANTIGRAVITY_LS_ADDRESS" in os.environ:
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Filesystem probe failed: report the notifier as unavailable.
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -85,7 +89,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
)
|
)
|
||||||
shared_candidates.append(os.path.join(parent_dir, ".jetski_env"))
|
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
|
pass
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -128,7 +133,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
)
|
)
|
||||||
shared_candidates.append(os.path.join(parent_dir, ".jetski_env"))
|
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
|
pass
|
||||||
|
|
||||||
for shared_file in shared_candidates:
|
for shared_file in shared_candidates:
|
||||||
@@ -190,7 +196,8 @@ class AgentApiNotifier(BaseNotifier):
|
|||||||
)
|
)
|
||||||
if local_addr and local_token:
|
if local_addr and local_token:
|
||||||
proc_matches.append((mtime, local_addr, local_token))
|
proc_matches.append((mtime, local_addr, local_token))
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# /proc entries vanish or deny access as processes exit.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Sort matches descending by creation timestamp so active/newest sessions come first
|
# Sort matches descending by creation timestamp so active/newest sessions come first
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ def _rotate_log(path: str, max_bytes: int = _LOG_MAX_BYTES) -> None:
|
|||||||
if os.path.exists(old_path):
|
if os.path.exists(old_path):
|
||||||
os.remove(old_path)
|
os.remove(old_path)
|
||||||
os.replace(path, old_path)
|
os.replace(path, old_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Best-effort rotation; a locked or vanished log file is tolerable.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +75,8 @@ try:
|
|||||||
|
|
||||||
sys.stderr = StderrTee(sys.stderr, os.path.join(_mcp_log_dir, "mcp_stderr.log"))
|
sys.stderr = StderrTee(sys.stderr, os.path.join(_mcp_log_dir, "mcp_stderr.log"))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Debug logging and the stderr tee are optional diagnostics; they must
|
||||||
|
# never prevent the MCP stdio server from starting.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 1. Import shared FastMCP instance
|
# 1. Import shared FastMCP instance
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No
|
|||||||
try:
|
try:
|
||||||
with open(plan_path, encoding="utf-8") as f:
|
with open(plan_path, encoding="utf-8") as f:
|
||||||
plan_content = f.read()
|
plan_content = f.read()
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Unreadable plan file: the summary just omits the plan.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -149,7 +150,8 @@ async def mobile_inspect_trace(action: str, trace_id: str, step_number: int | No
|
|||||||
if step_dict.get(col):
|
if step_dict.get(col):
|
||||||
try:
|
try:
|
||||||
step_dict[col] = json.loads(step_dict[col])
|
step_dict[col] = json.loads(step_dict[col])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON column value: keep the raw string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cursor.execute(
|
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"):
|
if t_dict.get("payload"):
|
||||||
try:
|
try:
|
||||||
t_dict["payload"] = json.loads(t_dict["payload"])
|
t_dict["payload"] = json.loads(t_dict["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: keep the raw string.
|
||||||
pass
|
pass
|
||||||
events.append(t_dict)
|
events.append(t_dict)
|
||||||
step_dict["interleaved_events"] = events
|
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"])
|
d_info = json.loads(row_dev["device_info"])
|
||||||
if isinstance(d_info, dict) and d_info.get("device_id"):
|
if isinstance(d_info, dict) and d_info.get("device_id"):
|
||||||
device_serial = d_info["device_id"]
|
device_serial = d_info["device_id"]
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Malformed device_info JSON: leave the serial unset.
|
||||||
pass
|
pass
|
||||||
conn_dev.close()
|
conn_dev.close()
|
||||||
except Exception:
|
except sqlite3.Error:
|
||||||
|
# The serial enriches the summary header only; skip it.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
device_info_str = f" | **Device Serial:** `{device_serial}`" if device_serial else ""
|
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):
|
if step_dict.get(col):
|
||||||
try:
|
try:
|
||||||
step_dict[col] = json.loads(step_dict[col])
|
step_dict[col] = json.loads(step_dict[col])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON column value: keep the raw string.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cursor.execute(
|
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"):
|
if t_dict.get("payload"):
|
||||||
try:
|
try:
|
||||||
t_dict["payload"] = json.loads(t_dict["payload"])
|
t_dict["payload"] = json.loads(t_dict["payload"])
|
||||||
except Exception:
|
except (ValueError, TypeError):
|
||||||
|
# Non-JSON payload: keep the raw string.
|
||||||
pass
|
pass
|
||||||
events.append(t_dict)
|
events.append(t_dict)
|
||||||
step_dict["interleaved_events"] = events
|
step_dict["interleaved_events"] = events
|
||||||
|
|||||||
@@ -64,14 +64,16 @@ def _session_tracked_by_lock(trace_id: str) -> bool:
|
|||||||
for q_item in DeviceExecutionLock.get_queued_tasks():
|
for q_item in DeviceExecutionLock.get_queued_tasks():
|
||||||
if str(q_item.get("session_id")) == trace_id:
|
if str(q_item.get("session_id")) == trace_id:
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# 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:
|
try:
|
||||||
for owner in DeviceExecutionLock.get_active_owners().values():
|
for owner in DeviceExecutionLock.get_active_owners().values():
|
||||||
if owner.session_id and str(owner.session_id) == trace_id:
|
if owner.session_id and str(owner.session_id) == trace_id:
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
print(f"Could not read device owners for {trace_id}: {exc}", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -115,8 +117,9 @@ def _reconcile_task_state(
|
|||||||
if row:
|
if row:
|
||||||
db_status = row["status"]
|
db_status = row["status"]
|
||||||
db_pid = row["pid"]
|
db_pid = row["pid"]
|
||||||
except Exception:
|
except sqlite3.Error as exc:
|
||||||
pass
|
# 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:
|
if db_pid and db_pid != pid:
|
||||||
pid = db_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},
|
payload={"trace_id": trace_id, "error": _LIVENESS_FAILURE_ERROR},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Notification dispatch is best-effort; the failure verdict is
|
||||||
|
# already persisted in status.json.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -278,10 +283,13 @@ def mobile_manage_task(
|
|||||||
device_serial = d_info["device_id"]
|
device_serial = d_info["device_id"]
|
||||||
status_data["device_serial"] = device_serial
|
status_data["device_serial"] = device_serial
|
||||||
trace_store.write_status(trace_id, status_data)
|
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
|
pass
|
||||||
conn_dev.close()
|
conn_dev.close()
|
||||||
except Exception:
|
except sqlite3.Error:
|
||||||
|
# DB probe for the serial is optional enrichment.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not device_serial and pid:
|
if not device_serial and pid:
|
||||||
@@ -293,7 +301,8 @@ def mobile_manage_task(
|
|||||||
status_data["device_serial"] = device_serial
|
status_data["device_serial"] = device_serial
|
||||||
trace_store.write_status(trace_id, status_data)
|
trace_store.write_status(trace_id, status_data)
|
||||||
break
|
break
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Lock-owner probe / cache write failed: serial stays unknown.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
response: dict[str, Any] = {
|
response: dict[str, Any] = {
|
||||||
@@ -329,7 +338,8 @@ def mobile_manage_task(
|
|||||||
"task_status": run_outcome.get("task_status"),
|
"task_status": run_outcome.get("task_status"),
|
||||||
**tests,
|
**tests,
|
||||||
}
|
}
|
||||||
except Exception:
|
except (OSError, ValueError, TypeError):
|
||||||
|
# Unreadable or malformed run_outcome.json: omit the summary.
|
||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -394,7 +404,8 @@ def mobile_manage_task(
|
|||||||
progress["latest_thought"] = p_obj["response"][0].get(
|
progress["latest_thought"] = p_obj["response"][0].get(
|
||||||
"text", ""
|
"text", ""
|
||||||
)
|
)
|
||||||
except Exception:
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
# Unparseable thought payload: omit it.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cur2.execute(
|
cur2.execute(
|
||||||
@@ -411,7 +422,9 @@ def mobile_manage_task(
|
|||||||
act_payload = json.loads(row_action["payload"]).get(
|
act_payload = json.loads(row_action["payload"]).get(
|
||||||
"args", {}
|
"args", {}
|
||||||
)
|
)
|
||||||
except Exception:
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
# Unparseable action payload: show the
|
||||||
|
# bare action name.
|
||||||
pass
|
pass
|
||||||
progress["latest_action"] = (
|
progress["latest_action"] = (
|
||||||
f"{act_name}({act_payload})" if act_payload else act_name
|
f"{act_name}({act_payload})" if act_payload else act_name
|
||||||
@@ -433,7 +446,8 @@ def mobile_manage_task(
|
|||||||
try:
|
try:
|
||||||
with open(plan_path, encoding="utf-8") as f:
|
with open(plan_path, encoding="utf-8") as f:
|
||||||
plan_content = f.read()
|
plan_content = f.read()
|
||||||
except Exception:
|
except OSError:
|
||||||
|
# Unreadable plan file: progress omits the plan.
|
||||||
pass
|
pass
|
||||||
progress["task_plan"] = plan_content
|
progress["task_plan"] = plan_content
|
||||||
else:
|
else:
|
||||||
@@ -458,16 +472,18 @@ def mobile_manage_task(
|
|||||||
try:
|
try:
|
||||||
if is_daemon_running():
|
if is_daemon_running():
|
||||||
stopped_via_daemon = stop_task_on_daemon(trace_id)
|
stopped_via_daemon = stop_task_on_daemon(trace_id)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# 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
|
# 2. Cancel queue reservation if present
|
||||||
queue_ticket = status_data.get("queue_ticket")
|
queue_ticket = status_data.get("queue_ticket")
|
||||||
if queue_ticket:
|
if queue_ticket:
|
||||||
try:
|
try:
|
||||||
DeviceExecutionLock.cancel_reservation(queue_ticket)
|
DeviceExecutionLock.cancel_reservation(queue_ticket)
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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
|
# 3. If process PID is missing, try to resolve from active device owners
|
||||||
if not pid:
|
if not pid:
|
||||||
@@ -479,8 +495,10 @@ def mobile_manage_task(
|
|||||||
status_data["pid"] = pid
|
status_data["pid"] = pid
|
||||||
trace_store.write_status(trace_id, status_data)
|
trace_store.write_status(trace_id, status_data)
|
||||||
break
|
break
|
||||||
except Exception:
|
except OSError as exc:
|
||||||
pass
|
# 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
|
# 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:
|
if not pid and not stopped_via_daemon and not queue_ticket:
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ def _watch_spawn(
|
|||||||
payload={"trace_id": trace_id, "error": error_text},
|
payload={"trace_id": trace_id, "error": error_text},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# Notification dispatch is best-effort; the failure verdict is
|
||||||
|
# already persisted in status.json.
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -321,8 +323,12 @@ def mobile_run_task(
|
|||||||
"stdout_log": str(trace_store.get_trace_stdout_log_path(trace_id)),
|
"stdout_log": str(trace_store.get_trace_stdout_log_path(trace_id)),
|
||||||
"stderr_log": str(trace_store.get_trace_stderr_log_path(trace_id)),
|
"stderr_log": str(trace_store.get_trace_stderr_log_path(trace_id)),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# 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
|
# When Daemon is running, refuse to launch a conflicting standalone runner on the same device
|
||||||
logging.getLogger("mcp_server").warning(
|
logging.getLogger("mcp_server").warning(
|
||||||
@@ -392,7 +398,8 @@ def mobile_run_task(
|
|||||||
ipc_port = read_ipc_port()
|
ipc_port = read_ipc_port()
|
||||||
if ipc_port:
|
if ipc_port:
|
||||||
env["ARTEMIS_IPC_PORT"] = str(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
|
pass
|
||||||
|
|
||||||
proc_kwargs = env_utils.get_detached_process_kwargs()
|
proc_kwargs = env_utils.get_detached_process_kwargs()
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ def ensure_emulator(
|
|||||||
if "1" in res.stdout:
|
if "1" in res.stdout:
|
||||||
time.sleep(2) # Stabilization
|
time.sleep(2) # Stabilization
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
# adb not ready yet while the emulator boots: keep polling.
|
||||||
pass
|
pass
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user