fix: correct task scheduling and simplify agent internals
Fix queue cancellation, concurrency limits and worker status updates. Use direct Explorer imports, split the Diagnoser loop into helpers and remove unused tool registration and prompt-builder code. Correct model caching, endpoint selection and retry handling.
This commit is contained in:
1 parent
67248c887e
commit
12d7902a82
92 files changed
+1085
-1763
No files matched your search
@@ -166,3 +166,8 @@ def current_adb_endpoint() -> AdbEndpoint:
|
||||
host = settings.ADB_HOST or os.environ.get("ADB_HOST") or DEFAULT_ADB_HOST
|
||||
port = settings.ADB_PORT or os.environ.get("ADB_PORT") or DEFAULT_ADB_PORT
|
||||
return AdbEndpoint.create(str(host), int(port))
|
||||
|
||||
|
||||
def adb_command(arguments: Sequence[str]) -> list[str]:
|
||||
"""Resolve the ADB binary and add the configured server's host and port."""
|
||||
return AdbSession(current_adb_endpoint()).command(arguments)
|
||||
@@ -28,6 +28,7 @@ import time
|
||||
import uuid
|
||||
|
||||
from artemis.config.paths import get_temp_dir
|
||||
from artemis.runtime.adb_endpoint import adb_command
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
from artemis.utils.logger import get_logger
|
||||
|
||||
@@ -148,7 +149,7 @@ class ScreenAwakeLease:
|
||||
def _run(self, args: list[str], description: str) -> subprocess.CompletedProcess[str] | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["adb", "-s", self.device_id, *args],
|
||||
adb_command(["-s", self.device_id, *args]),
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -25,6 +25,7 @@ from typing import Iterable
|
||||
from adbutils import AdbClient
|
||||
|
||||
from artemis.config import settings
|
||||
from artemis.runtime.adb_endpoint import adb_command
|
||||
from artemis.runtime.awake_lease import ScreenAwakeLease
|
||||
from artemis.utils.logger import get_logger
|
||||
|
||||
@@ -47,7 +48,7 @@ def _run_awake_adb_command(
|
||||
"""Run one non-fatal ADB command used by the USB awake policy."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["adb", "-s", device_id, *args],
|
||||
adb_command(["-s", device_id, *args]),
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -261,6 +261,20 @@ class DeviceExecutionLock:
|
||||
payload["lock_scope"] = str(lock_scope)
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def _atomic_write_json(cls, path: Path, payload: dict) -> bool:
|
||||
"""Replace a ticket atomically so queue scans cannot read partial JSON."""
|
||||
temp_path = path.with_name(f"{path.name}.{uuid.uuid4().hex}.tmp")
|
||||
try:
|
||||
temp_path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
if not cls._safe_replace(temp_path, path):
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
finally:
|
||||
cls._safe_unlink(temp_path)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _ticket_targets_device(
|
||||
cls,
|
||||
@@ -454,11 +468,7 @@ class DeviceExecutionLock:
|
||||
effective_scope = lock_scope or os.getenv(cls.LOCK_SCOPE_ENV) or None
|
||||
if effective_scope:
|
||||
payload["lock_scope"] = str(effective_scope)
|
||||
try:
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
return cls._atomic_write_json(path, payload)
|
||||
|
||||
def _find_reserved_ticket(self) -> Path | None:
|
||||
if not self.queue_ticket:
|
||||
@@ -482,7 +492,10 @@ class DeviceExecutionLock:
|
||||
ingress=self.ingress,
|
||||
lock_scope=self.lock_scope,
|
||||
)
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
if not self._atomic_write_json(path, payload):
|
||||
raise DeviceBusyError(
|
||||
"The Artemis queue reservation could not be claimed (ticket rewrite failed)."
|
||||
)
|
||||
return path
|
||||
|
||||
self.queue_ticket = self.reserve(
|
||||
@@ -567,6 +580,15 @@ class DeviceExecutionLock:
|
||||
return 0
|
||||
if self.max_concurrency is not None:
|
||||
return self.max_concurrency
|
||||
return self.resolve_env_concurrency()
|
||||
|
||||
@classmethod
|
||||
def resolve_env_concurrency(cls) -> int:
|
||||
"""Resolve the concurrency limit from the environment.
|
||||
|
||||
Shared with the admin scheduler. Returns 0 for per-device concurrency,
|
||||
or a positive global limit.
|
||||
"""
|
||||
env_mode = os.environ.get("ARTEMIS_CONCURRENCY_MODE", "").strip().lower()
|
||||
if env_mode in ("global", "serial", "1"):
|
||||
return 1
|
||||
@@ -690,9 +712,14 @@ class DeviceExecutionLock:
|
||||
return
|
||||
try:
|
||||
owner = self._read_owner(self.path)
|
||||
if owner is not None and (
|
||||
owner.token == self.token
|
||||
or (self.session_id and owner.session_id == self.session_id)
|
||||
# A matching session id cannot release another process's lease.
|
||||
if (
|
||||
owner is not None
|
||||
and owner.pid == os.getpid()
|
||||
and (
|
||||
owner.token == self.token
|
||||
or (self.session_id and owner.session_id == self.session_id)
|
||||
)
|
||||
):
|
||||
self._safe_unlink(self.path)
|
||||
finally:
|
||||
@@ -933,16 +960,7 @@ class DeviceExecutionLock:
|
||||
"session_id": str(session_id),
|
||||
"ingress": ingress or owner.ingress or "sdk",
|
||||
}
|
||||
temp_path = path.with_name(f"{path.name}.{uuid.uuid4().hex}.tmp")
|
||||
try:
|
||||
temp_path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
if not cls._safe_replace(temp_path, path):
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
finally:
|
||||
cls._safe_unlink(temp_path)
|
||||
return True
|
||||
return cls._atomic_write_json(path, payload)
|
||||
|
||||
@classmethod
|
||||
def cleanup_stale_locks(cls, device_id: str | None = None) -> int:
|
||||
|
||||
@@ -436,7 +436,15 @@ class DevicePool:
|
||||
selects the first ready device so the task will queue on it.
|
||||
Returns None if no devices are attached.
|
||||
"""
|
||||
all_devs = self.list_devices()
|
||||
return self._pick_device(self.list_devices(), preferred_serial)
|
||||
|
||||
async def select_device_async(self, preferred_serial: str | None = None) -> str | None:
|
||||
"""Select a device without blocking the caller on ADB enumeration."""
|
||||
return self._pick_device(await self.list_devices_async(), preferred_serial)
|
||||
|
||||
def _pick_device(
|
||||
self, all_devs: list[DeviceStatus], preferred_serial: str | None
|
||||
) -> str | None:
|
||||
ready_devs = [d for d in all_devs if d.state == "device"]
|
||||
|
||||
if preferred_serial:
|
||||
|
||||
@@ -326,6 +326,23 @@ def update_trace_status(
|
||||
return data
|
||||
|
||||
|
||||
def update_trace_pid(trace_id: str, pid: int) -> dict[str, Any] | None:
|
||||
"""Updates the pid field of the status.json for a given trace_id."""
|
||||
path = get_status_path(trace_id)
|
||||
with _status_lock(path):
|
||||
data = read_status(trace_id)
|
||||
if not data:
|
||||
if os.path.exists(path) or os.path.exists(f"{path}.corrupt"):
|
||||
logger.warning(
|
||||
f"Dropping pid update for trace {trace_id}: existing status.json is unreadable"
|
||||
)
|
||||
return None
|
||||
|
||||
data["pid"] = pid
|
||||
write_status(trace_id, data)
|
||||
return data
|
||||
|
||||
|
||||
def update_trace_device_serial(trace_id: str, device_serial: str) -> dict[str, Any] | None:
|
||||
"""Updates the device_serial field of the status.json for a given trace_id."""
|
||||
path = get_status_path(trace_id)
|
||||
|
||||
Reference in new issue
Block a user