feat: enhance daemon dispatching, device readiness fallback, and live streaming UI

- Admin Console & Task Queue: add cancellation, session startup progress endpoints, and support ingress metadata
- Diagnostics & Runtime: enhance ADB probe to prefer unlocked devices and synchronize with readiness engine
- MCP Server: dispatch tasks through Artemis Daemon scheduler with fallback to standalone runner, improve action overlays
- Showcase UI: improve session auto-selection with pinning support, render startup progress, and refine stream styling
- CLI & Makefile: expose server lifecycle commands (restart, stop, status)
- Configuration: ignore runtime server state file in .gitignore
This commit is contained in:
somew1nd committed 2026-08-26 17:41:23 -07:00
1 parent 57655266cc
commit 8fdecf23d7
33 files changed
+1315 -293

No files matched your search

+12 -3
View File
@@ -90,7 +90,14 @@ class ReadinessEngine:
def get_active_device_serial(self) -> str | None:
"""Get currently selected active device serial."""
return self._active_device_serial
if self._report_cache and self._report_cache.active_device:
if self._report_cache.active_device.is_locked is False:
return self._report_cache.active_device.serial
if self._active_device_serial:
return self._active_device_serial
if self._report_cache and self._report_cache.active_device:
return self._report_cache.active_device.serial
return None
async def run_probe(self, probe_id: str) -> ProbeResult | None:
"""Execute a single specific probe by ID."""
@@ -99,9 +106,11 @@ class ReadinessEngine:
return None
return await probe.probe()
async def run_device_submission_probe(self) -> ProbeResult:
async def run_device_submission_probe(
self, target_serial: str | None = None
) -> ProbeResult:
"""Run the bounded device gate used by task submission."""
return await self._adb_probe.probe_submission_readiness()
return await self._adb_probe.probe_submission_readiness(target_serial=target_serial)
def invalidate_cache(self) -> None:
"""Invalidate the UI readiness snapshot after an explicit configuration change."""
+45 -7
View File
@@ -358,7 +358,9 @@ class AdbDeviceProbe(BaseProbe):
logger.debug(f"Failed submission-time ADB device check: {exc}")
return None
async def probe_submission_readiness(self) -> ProbeResult:
async def probe_submission_readiness(
self, target_serial: str | None = None
) -> ProbeResult:
"""Run the minimal fail-safe device check required before enqueueing.
The full diagnostics probe enriches device metadata, scans packages,
@@ -416,16 +418,42 @@ class AdbDeviceProbe(BaseProbe):
},
)
serial = (
self._target_serial
preferred = (
target_serial
if target_serial and target_serial in ready_serials
else self._target_serial
if self._target_serial in ready_serials
else ready_serials[0]
)
serial = preferred
is_locked = await self._get_confirmed_device_lock_state(
adb_path,
serial,
timeout_seconds=1.0,
)
# If preferred device is locked or unknown, check remaining ready devices for an unlocked one
if is_locked is not False and len(ready_serials) > 1:
for candidate in ready_serials:
if candidate == preferred:
continue
candidate_locked = await self._get_confirmed_device_lock_state(
adb_path,
candidate,
timeout_seconds=1.0,
)
if candidate_locked is False:
logger.info(
f"Device {preferred} is not ready/unlocked ({is_locked}); "
f"switching to unlocked ready device {candidate}"
)
serial = candidate
is_locked = False
break
elif candidate_locked is None and is_locked is True:
serial = candidate
is_locked = None
summary = (
"Device Locked"
if is_locked is True
@@ -796,11 +824,21 @@ class AdbDeviceProbe(BaseProbe):
)
# 4. Ready device available
active_dev = ready_devices[0]
preferred_dev = None
if self._target_serial:
matched = next((d for d in ready_devices if d.serial == self._target_serial), None)
if matched:
active_dev = matched
preferred_dev = next((d for d in ready_devices if d.serial == self._target_serial), None)
if preferred_dev and preferred_dev.is_locked is False:
active_dev = preferred_dev
else:
# Prefer a confirmed unlocked ready device
unlocked_dev = next((d for d in ready_devices if d.is_locked is False), None)
if unlocked_dev:
active_dev = unlocked_dev
else:
# If no confirmed unlocked device, prefer undetermined lock state over confirmed locked
unknown_dev = next((d for d in ready_devices if d.is_locked is None), None)
active_dev = preferred_dev or unknown_dev or ready_devices[0]
display_name = active_dev.model or active_dev.serial
if active_dev.screen_resolution:
+3 -3
View File
@@ -77,7 +77,6 @@ async def execute_task(
explorer_flash_mode: Override Explorer version mode for Flash execution profile.
explorer_pro_mode: Override Explorer version mode for Pro execution profile.
"""
publish_startup_progress("configuration", "Loading the run configuration")
effective_sid = (
session_id
or os.getenv("ARTEMIS_SESSION_ID")
@@ -85,6 +84,9 @@ async def execute_task(
)
if effective_sid:
os.environ["ARTEMIS_SESSION_ID"] = str(effective_sid)
publish_startup_progress(
"configuration", "Loading the run configuration", session_id=str(effective_sid) if effective_sid else None
)
llm_config = initialize_llm_config()
agent_profile = AgentProfile(name="default", llm_config=llm_config)
@@ -142,13 +144,11 @@ async def execute_task(
agent: Agent | None = None
try:
publish_startup_progress("device_check", "Checking the Android device")
agent = Agent(config=config.build(), session_id=effective_sid)
await agent.init(
retry_count=int(os.getenv("ARTEMIS_HEALTH_RETRIES", 5)),
retry_wait_seconds=int(os.getenv("ARTEMIS_HEALTH_DELAY", 2)),
)
publish_startup_progress("device_ready", "Android device connected")
task = agent.new_task(goal)
if locked_app_package:
@@ -23,12 +23,21 @@ from typing import Annotated
import urllib.error
import urllib.request
from artemis.interfaces.cli.commands.server_lifecycle import (
restart_command,
status_command,
stop_command,
)
from artemis.utils.logger import get_logger
import typer
logger = get_logger(__name__)
server_app = typer.Typer(help="Cloud proxy and server management commands.")
server_app.command("restart", help="Restart running Artemis Web UI & server.")(restart_command)
server_app.command("stop", help="Stop running Artemis Web UI & server.")(stop_command)
server_app.command("status", help="Display Artemis Web UI & server status.")(status_command)
cached_token = ""
token_expire_time = 0.0
+6
View File
@@ -158,6 +158,9 @@ def submit_task_to_daemon(
enable_outputter: bool | None = None,
locked_app_package: str | None = None,
app_path: str | None = None,
session_id: str | None = None,
ingress: str = "client",
conversation_id: str | None = None,
base_url: str | None = None,
timeout: float = 5.0,
) -> dict[str, Any] | None:
@@ -174,6 +177,9 @@ def submit_task_to_daemon(
"enable_outputter": enable_outputter,
"locked_app_package": locked_app_package,
"app_path": app_path,
"session_id": session_id,
"ingress": ingress,
"conversation_id": conversation_id,
}
try:
+10
View File
@@ -223,6 +223,16 @@ class DevicePool:
# If not detected by ADB, still return it so ADB can attempt connection
return preferred_serial
# Prefer active ready device from readiness engine if available
try:
from artemis.core.diagnostics import readiness_engine
active_serial = readiness_engine.get_active_device_serial()
if active_serial and any(d.serial == active_serial and d.state == "device" for d in all_devs):
return active_serial
except Exception:
pass
# Pick first idle device
for dev in ready_devs:
if not dev.is_busy:
+6 -4
View File
@@ -203,6 +203,7 @@ class Agent:
raise DeviceNotFoundError(error_msg)
# Initialize clients
publish_startup_progress("device_check", "Checking the Android device", session_id=self._session_id)
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
self._init_clients(
device_id=device_id,
@@ -221,6 +222,7 @@ class Agent:
device_id=device_id, platform=platform
)
logger.info(self._device_context.to_str())
publish_startup_progress("device_ready", "Android device connected", session_id=self._session_id)
# Asynchronously pre-warm LLM connection pools in the background
asyncio.create_task(self._prewarm_llm_connections(api_key))
@@ -580,15 +582,15 @@ class Agent:
if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
if self._ui_adb_client is not None:
publish_startup_progress(
"uiautomator", "Connecting to UI Automator", session_id=str(task.id)
"uiautomator", "Connecting to UI Automator", session_id=str(sess_id)
)
await asyncio.to_thread(self._ui_adb_client.connect)
publish_startup_progress(
"uiautomator_ready", "UI Automator is ready", session_id=str(task.id)
"uiautomator_ready", "UI Automator is ready", session_id=str(sess_id)
)
await self._ensure_device_unlocked()
publish_startup_progress(
"environment", "Preparing the device environment", session_id=str(task.id)
"environment", "Preparing the device environment", session_id=str(sess_id)
)
await self._prepare_app_installation(task=task)
await self._prepare_device_environment(context=context)
@@ -620,7 +622,7 @@ class Agent:
logger.error(f"[{task_name}] Failed to start screen recording: {e}")
publish_startup_progress(
"environment_ready", "Device environment is ready", session_id=str(task.id)
"environment_ready", "Device environment is ready", session_id=str(sess_id)
)
try:
if request.profile and request.profile.lower() == "flash":