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:
somew1nd committed 2026-09-02 22:58:00 -07:00
1 parent 67248c887e
commit 12d7902a82
92 files changed
+1085 -1763

No files matched your search

+5
View File
@@ -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)
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+37 -19
View File
@@ -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:
+9 -1
View File
@@ -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:
+17
View File
@@ -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)