From c2c90a6378f8c835f1eb636e46a6384572942cd7 Mon Sep 17 00:00:00 2001
From: somew1nd
Date: Wed, 26 Aug 2026 15:53:14 -0700
Subject: [PATCH] feat: add multi-device concurrency, server lifecycle CLI, and
unified execution stream
- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization)
- Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration
- Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery
- Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution
- Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges
- Add comprehensive unit test coverage and edge-case verification scripts
---
README.md | 29 +-
README_CN.md | 29 +-
apps/admin_console/core/state.py | 2 +
apps/admin_console/index.html | 4 +-
apps/admin_console/routers/sessions.py | 32 +-
apps/admin_console/routers/system.py | 93 ++++
apps/admin_console/routers/tasks.py | 49 +-
apps/admin_console/schemas/task_schema.py | 2 +
apps/admin_console/server.py | 35 +-
.../services/task_preset_catalog.py | 18 +-
.../services/task_queue_service.py | 27 +-
.../agent-stream/agent-stream.component.html | 10 +
.../agent-stream/agent-stream.component.scss | 62 ++-
.../agent-stream/agent-stream.component.ts | 41 +-
.../chat-interface.component.html | 10 +
.../chat-interface.component.scss | 62 ++-
.../chat-interface.component.ts | 41 +-
.../floating-video-player.component.html | 249 ++++++---
.../floating-video-player.component.scss | 311 +++++++++--
.../floating-video-player.component.ts | 113 +++-
.../legacy-agent-stream.component.html | 2 +-
.../src/app/core/data/smart-tasks.data.ts | 18 +-
.../src/app/core/models/session.model.ts | 5 +
.../src/app/core/models/stream.model.ts | 8 +
.../src/app/services/agent.service.ts | 210 +++++---
.../app/utils/action-formatter.util.spec.ts | 19 +-
.../src/app/utils/action-formatter.util.ts | 85 +--
artemis/__init__.py | 3 +-
artemis/agents/flash/runner.py | 29 ++
artemis/config/constants.py | 1 +
artemis/config/paths.py | 9 +
artemis/config/runtime.py | 80 ++-
artemis/context.py | 10 +-
.../platform_specific_commands_controller.py | 12 +-
artemis/data_engine/engine.py | 28 +-
artemis/engine/graph_runner.py | 2 +-
artemis/engine/pipeline.py | 6 +-
artemis/engine/reactive_runner.py | 2 +-
artemis/interfaces/cli/commands/run.py | 56 +-
.../cli/commands/server_lifecycle.py | 255 +++++++++
artemis/interfaces/cli/commands/ui.py | 48 ++
artemis/interfaces/cli/main.py | 8 +
artemis/interfaces/sdk/__init__.py | 7 +-
artemis/interfaces/sdk/client.py | 388 +++++++++++---
artemis/interfaces/sdk/task.py | 14 +
artemis/llm/router.py | 6 +-
artemis/mcp/adb_server.py | 37 +-
artemis/runtime/__init__.py | 37 +-
artemis/runtime/awake_service.py | 20 +-
artemis/runtime/daemon_client.py | 193 +++++++
artemis/runtime/device_lock.py | 488 ++++++++++++++----
artemis/runtime/device_pool.py | 243 +++++++++
artemis/runtime/server_lifecycle.py | 318 ++++++++++++
artemis/sdk/agent.py | 128 ++++-
artemis/sdk/builders/agent_config_builder.py | 41 +-
artemis/sdk/builders/task_request_builder.py | 2 +-
artemis/sdk/types/agent.py | 3 +-
artemis/sdk/types/task.py | 2 +-
artemis/toolchain/__init__.py | 18 +
artemis/utils/startup_progress.py | 8 +-
mcp_server/README.md | 15 +-
mcp_server/background/task_runner.py | 76 ++-
mcp_server/rules.md | 15 +-
mcp_server/tools/device_state.py | 30 +-
mcp_server/tools/inspect_trace.py | 46 +-
mcp_server/tools/task_manager.py | 53 +-
mcp_server/tools/task_runner.py | 69 ++-
mcp_server/utils/trace_store.py | 16 +
scripts/real_device_video_acceptance.py | 14 +-
scripts/run_edge_case_part.py | 257 +++++++++
scripts/run_ui.py | 84 ++-
scripts/test_all_edge_cases.py | 258 +++++++++
scripts/verify_concurrency_and_devices.py | 312 +++++++++++
.../test_session_list_batching.py | 41 ++
.../admin_console/test_task_queue_service.py | 17 +-
tests/unit/mcp/test_mcp_tools.py | 133 ++++-
tests/unit/runtime/test_daemon_client.py | 103 ++++
tests/unit/runtime/test_device_lock.py | 94 ++++
tests/unit/runtime/test_device_pool.py | 85 +++
.../runtime/test_server_process_lifecycle.py | 201 ++++++++
.../unit/sdk/test_agent_trace_finalization.py | 31 ++
tests/unit/test_cli.py | 109 +++-
tests/unit/test_prompt_builder.py | 2 +-
tests/unit/test_sdk.py | 128 +++++
84 files changed, 5612 insertions(+), 645 deletions(-)
create mode 100644 artemis/interfaces/cli/commands/server_lifecycle.py
create mode 100644 artemis/runtime/daemon_client.py
create mode 100644 artemis/runtime/device_pool.py
create mode 100644 artemis/runtime/server_lifecycle.py
create mode 100644 scripts/run_edge_case_part.py
create mode 100644 scripts/test_all_edge_cases.py
create mode 100644 scripts/verify_concurrency_and_devices.py
create mode 100644 tests/unit/runtime/test_daemon_client.py
create mode 100644 tests/unit/runtime/test_device_pool.py
create mode 100644 tests/unit/runtime/test_server_process_lifecycle.py
diff --git a/README.md b/README.md
index eb6d340..b8dfd90 100644
--- a/README.md
+++ b/README.md
@@ -208,21 +208,28 @@ Embed the mobile automation engine into your Python workflows in just a few line
```python
import asyncio
-from artemis.interfaces.sdk import ArtemisClient
+from artemis import ArtemisClient, ConcurrencyMode
async def main():
- # Initialize client (choose "flash" for fast UI checks or "pro" for deep reasoning & self-healing)
- client = ArtemisClient(default_profile="flash")
-
- # Execute natural language end-to-end test case
- result = await client.run(
- "Open System Settings, go to 'Battery', verify battery percentage is displayed, and check for any crash dialogs."
+ # 1. Initialize client with optional device targeting and concurrency strategy:
+ # - concurrency_mode="per_device" (default): 1 task per device, allows multi-device parallel execution
+ # - concurrency_mode="global": 1 task globally across all devices
+ client = ArtemisClient(
+ device_serial="emulator-5554", # optional: target specific device serial
+ default_profile="flash", # "flash" (fast reactive) or "pro" (deep reasoning)
+ concurrency_mode="per_device", # or ConcurrencyMode.GLOBAL
)
- # Structured assertions & execution tracing
- assert result.status == "SUCCESS", f"Test failed: {result.failure_reason}"
- print(f"✅ Test Passed! Turns: {result.turns} | Trace ID: {result.trace_id}")
+ # 2. Execute natural language end-to-end test case (can also override device per-run)
+ result = await client.run(
+ "Open System Settings, go to 'Battery', verify battery percentage is displayed, and check for any crash dialogs.",
+ device_serial="emulator-5554", # optional: override target device for this specific run
+ )
+
+ # 3. Structured assertions & execution tracing
+ assert result.status == "SUCCESS", f"Test failed: {result.error}"
+ print(f"✅ Test Passed! Device: {result.device_id} | Turns: {result.turns} | Trace ID: {result.trace_id}")
if __name__ == "__main__":
@@ -239,7 +246,7 @@ if __name__ == "__main__":
Console Overview: ① View Switcher (Home / Workspace) · ② Model & Replay (Flash/Pro status & video replay) · ③ Live Agent Stream (Action perception, target coordinates & structured results) · ④ Prompt Dock (Natural language dispatch) · ⑤ Task Queue & Dashboard (Lifecycle & history)
-* **Web Visual Test Console (`uv run artemis ui`)**: Real-time screen projection and interactive panel, supporting natural language test dispatch, live reasoning telemetry, action trajectories, and execution replay;
+* **Web Visual Test Console (`uv run artemis ui`)**: Real-time screen projection and interactive panel, supporting natural language test dispatch, live reasoning telemetry, action trajectories, and execution replay; manage server lifecycle anytime from any terminal using `uv run artemis restart`, `uv run artemis stop`, and `uv run artemis status`;
* **Native MCP Protocol (IDE Collaboration)**: Operates as a standard MCP server seamlessly integrating with **Antigravity, Claude Code, Windsurf**, etc., directly driving real devices inside the IDE to verify bugs and run test cases;
* **Developer CLI (`uv run artemis run`)**: Direct terminal execution for automated test cases, exploratory stability inspection, or AndroidWorld benchmarks with high-fidelity structured terminal output;
* **Python SDK**: Integrates as a standard Python library into existing automated testing frameworks (e.g., pytest) or CI/CD pipelines with strongly typed Pydantic structured outputs and assertion support.
diff --git a/README_CN.md b/README_CN.md
index 1927ce2..a7ce945 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -210,21 +210,28 @@ PYTHONPATH = "/path/to/artemis"
```python
import asyncio
-from artemis.interfaces.sdk import ArtemisClient
+from artemis import ArtemisClient, ConcurrencyMode
async def main():
- # 初始化测试客户端(支持 "flash" 极速校验模式 或 "pro" 深度推理与自愈模式)
- client = ArtemisClient(default_profile="flash")
-
- # 执行自然语言端到端测试用例
- result = await client.run(
- "打开系统设置,进入『电池』页面,验证是否正常显示电量百分比,确认页面无异常报错弹窗。"
+ # 1. 初始化测试客户端(支持指定设备序列号与并发策略):
+ # - concurrency_mode="per_device"(默认):单设备排队,跨设备支持多任务并发执行
+ # - concurrency_mode="global":全局单一并发,整个集群跨所有设备同一时刻只执行一个任务
+ client = ArtemisClient(
+ device_serial="emulator-5554", # 可选:指定目标设备序列号(不传则自动选择空闲设备)
+ default_profile="flash", # "flash" 极速校验 或 "pro" 深度推理自愈
+ concurrency_mode="per_device", # 或 ConcurrencyMode.GLOBAL
)
- # 结构化断言与执行追溯
- assert result.status == "SUCCESS", f"测试执行失败: {result.failure_reason}"
- print(f"✅ 测试通过!耗时步数: {result.turns} | Trace ID: {result.trace_id}")
+ # 2. 执行端到端任务(也可在调用时动态覆盖指定设备与并发模式)
+ result = await client.run(
+ "打开系统设置,进入『电池』页面,验证是否正常显示电量百分比,确认页面无异常报错弹窗。",
+ device_serial="emulator-5554", # 可选:覆盖当前调用的目标设备
+ )
+
+ # 3. 结构化断言与执行追溯
+ assert result.status == "SUCCESS", f"测试执行失败: {result.error}"
+ print(f"✅ 测试通过!设备: {result.device_id} | 步数: {result.turns} | Trace ID: {result.trace_id}")
if __name__ == "__main__":
@@ -241,7 +248,7 @@ if __name__ == "__main__":
控制台功能概览:① 视图切换(主页与工作区) · ② 运行模式与回放(Flash/Pro 状态与视频回放) · ③ 实时感知推理流(动作感知、目标坐标与结构化总结) · ④ 提示词输入坞 (Prompt Dock)(自然语言下发) · ⑤ 任务队列看板(生命周期与历史回溯)
-* **Web 可视化测试控制台 (`uv run artemis ui`)**:集成设备实时投屏与交互面板,支持通过自然语言下发测试用例,实时观测推理步骤、操作轨迹、截图留存与异常状态回放;
+* **Web 可视化测试控制台 (`uv run artemis ui`)**:集成设备实时投屏与交互面板,支持通过自然语言下发测试用例,实时观测推理步骤、操作轨迹、截图留存与异常状态回放;支持在任意终端使用 `uv run artemis restart`、`uv run artemis stop`、`uv run artemis status` 一键重启、关停或查看服务状态;
* **原生 MCP 协议 (IDE 协同)**:作为标准 MCP 服务器无缝接入 **Antigravity、Claude Code、Windsurf** 等开发环境,在 IDE 中直接驱动真机完成自动化测试与 Bug 复现验证;
* **开发者命令行 CLI (`uv run artemis run`)**:支持通过终端直接执行自动化测试用例、探索性稳定性巡检或 AndroidWorld 基准评测,提供高保真结构化终端输出;
* **Python SDK**:作为标准 Python 库无缝集成至现有自动化测试框架(如 pytest)或 CI/CD 流水线,提供基于 Pydantic 的强类型结构化结果与断言支持。
diff --git a/apps/admin_console/core/state.py b/apps/admin_console/core/state.py
index 9aafe2f..8149161 100644
--- a/apps/admin_console/core/state.py
+++ b/apps/admin_console/core/state.py
@@ -30,6 +30,8 @@ class ServerState:
self.ipc_server: asyncio.Server | None = None
self.ipc_serve_task: asyncio.Task | None = None
self.ipc_port: int | None = None
+ self.port: int = 8000
+ self.host: str = "0.0.0.0"
self.is_shutting_down: bool = False
self.current_process: asyncio.subprocess.Process | None = None
diff --git a/apps/admin_console/index.html b/apps/admin_console/index.html
index d5157fb..d28f9ea 100644
--- a/apps/admin_console/index.html
+++ b/apps/admin_console/index.html
@@ -1148,13 +1148,15 @@
const time = new Date(session.start_time * 1000).toLocaleString();
const titleText = session.initial_goal || 'Unnamed Session';
const sidDisplay = session.session_id || '';
+ const deviceSerial = session.device_serial || session.device_id || '';
+ const devicePrefix = deviceSerial ? `${escapeHtml(deviceSerial)} · ` : '';
div.innerHTML = `
${titleText}
ID: ${sidDisplay}
-
${time}
+
${devicePrefix}${time}
diff --git a/apps/admin_console/routers/sessions.py b/apps/admin_console/routers/sessions.py
index 91c71f9..16638f1 100644
--- a/apps/admin_console/routers/sessions.py
+++ b/apps/admin_console/routers/sessions.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import json
import time
from uuid import UUID
from fastapi import APIRouter, HTTPException
@@ -49,12 +50,39 @@ async def list_sessions():
unresolved_profiles = []
result = []
+ try:
+ from artemis.runtime import DeviceExecutionLock
+ active_owners = DeviceExecutionLock.get_active_owners()
+ active_owner_sids = {
+ str(owner.session_id)
+ for owner in active_owners.values()
+ if owner.session_id
+ }
+ except Exception:
+ active_owner_sids = set()
+
for row_dict in rows:
s_id = str(row_dict.get("session_id"))
recording = latest_recordings.get(s_id)
+ d_info_raw = row_dict.get("device_info")
+ device_id = None
+ if d_info_raw:
+ try:
+ d_info = json.loads(d_info_raw) if isinstance(d_info_raw, str) else d_info_raw
+ if isinstance(d_info, dict):
+ device_id = d_info.get("device_id") or d_info.get("device_serial")
+ except Exception:
+ pass
+ if not device_id and recording:
+ device_id = recording.get("device_id")
+ row_dict["device_id"] = device_id
+ row_dict["device_serial"] = device_id
+
if row_dict.get("status") == "running":
- is_active = s_id in state.active_connections or (
- state.is_running and s_id == str(state.active_session_id)
+ is_active = (
+ s_id in active_owner_sids
+ or s_id in state.active_connections
+ or (state.is_running and s_id == str(state.active_session_id))
)
worker_is_alive = session_repo.process_is_alive(row_dict.get("pid"))
if not is_active and not worker_is_alive:
diff --git a/apps/admin_console/routers/system.py b/apps/admin_console/routers/system.py
index 910ac74..7a9107e 100644
--- a/apps/admin_console/routers/system.py
+++ b/apps/admin_console/routers/system.py
@@ -360,3 +360,96 @@ async def get_model_config_and_env():
"env_filename": ".env",
"env_vars": env_vars,
}
+
+
+@router.get("/server-status")
+async def get_server_runtime_status():
+ """Retrieve runtime status, PID, port, and uptime of the Artemis server."""
+ import os
+ from artemis.runtime.server_lifecycle import get_server_status
+
+ try:
+ from apps.admin_console.core.state import state
+ except ImportError:
+ from admin_console.core.state import state
+
+ port = getattr(state, "port", 8000)
+ status = get_server_status(port=port)
+ status["current_pid"] = os.getpid()
+ return status
+
+
+@router.post("/restart")
+async def restart_server_endpoint():
+ """Request a graceful restart of the Artemis server from thin clients/UI."""
+ import asyncio
+ import os
+ import sys
+ import threading
+
+ try:
+ from apps.admin_console.core.state import state
+ except ImportError:
+ from admin_console.core.state import state
+
+ port = getattr(state, "port", 8000)
+ current_pid = os.getpid()
+
+ def _restart_worker():
+ import time
+
+ time.sleep(0.6)
+ if sys.platform != "win32":
+ try:
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+ except Exception:
+ import subprocess
+
+ subprocess.Popen([sys.executable] + sys.argv)
+ os._exit(0)
+ else:
+ import subprocess
+
+ subprocess.Popen([sys.executable] + sys.argv)
+ os._exit(0)
+
+ threading.Thread(target=_restart_worker, daemon=True).start()
+
+ return {
+ "status": "restarting",
+ "message": "Artemis server is restarting. Client reconnection should occur in 2-3 seconds.",
+ "previous_pid": current_pid,
+ "port": port,
+ }
+
+
+@router.post("/shutdown")
+async def shutdown_server_endpoint():
+ """Request a graceful shutdown of the Artemis server."""
+ import os
+ import signal
+ import threading
+
+ try:
+ from apps.admin_console.core.state import state
+ except ImportError:
+ from admin_console.core.state import state
+
+ current_pid = os.getpid()
+
+ def _shutdown_worker():
+ import time
+
+ time.sleep(0.5)
+ try:
+ os.kill(current_pid, signal.SIGTERM)
+ except Exception:
+ os._exit(0)
+
+ threading.Thread(target=_shutdown_worker, daemon=True).start()
+
+ return {
+ "status": "shutting_down",
+ "message": "Artemis server is shutting down.",
+ "pid": current_pid,
+ }
diff --git a/apps/admin_console/routers/tasks.py b/apps/admin_console/routers/tasks.py
index ccc9071..ec32dd0 100644
--- a/apps/admin_console/routers/tasks.py
+++ b/apps/admin_console/routers/tasks.py
@@ -18,7 +18,7 @@ import json
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from artemis.core.diagnostics import readiness_engine
-from artemis.runtime import DeviceExecutionLock
+from artemis.runtime import DeviceExecutionLock, device_pool
try:
from admin_console.core.state import state
@@ -99,9 +99,17 @@ async def run_task(request: RunRequest):
enable_outputter=request.enable_outputter,
locked_app_package=request.locked_app_package,
app_path=request.app_path,
+ device_serial=request.device_serial,
)
+@router.get("/api/devices")
+async def list_devices():
+ """List all connected Android devices with their busy / idle status."""
+ devices = await device_pool.list_devices_async()
+ return {"devices": [d.to_dict() for d in devices]}
+
+
@router.post("/api/stop")
async def stop_task(all: bool = False):
stopped = task_queue_service.stop_tasks(clear_all=all)
@@ -138,10 +146,24 @@ async def get_status():
None,
)
+ active_owners = DeviceExecutionLock.get_active_owners()
+ active_tasks = [
+ {
+ "device_id": owner.device_id,
+ "session_id": owner.session_id,
+ "goal": owner.description,
+ "pid": owner.pid,
+ "ingress": owner.ingress,
+ "acquired_at": owner.acquired_at,
+ }
+ for owner in active_owners.values()
+ ]
+
running_sid = (
(global_owner.session_id if global_owner else None)
or state.active_session_id
or (running_task.get("session_id") if running_task else None)
+ or (active_tasks[0]["session_id"] if active_tasks and active_tasks[0].get("session_id") else None)
or (session_repo.get_running_session_id() if is_running else None)
)
owner_connection = (
@@ -152,6 +174,7 @@ async def get_status():
or (global_owner.description if global_owner else None)
or state.current_goal
or (running_task.get("goal") if running_task else None)
+ or (active_tasks[0]["goal"] if active_tasks else None)
)
active_profile = owner_connection.get("profile") or state.current_profile or (
@@ -168,7 +191,23 @@ async def get_status():
)
model_info = model_service.get_active_model_info(active_profile)
- queue_data = state.queue_tasks
+
+ # Unified Global Queue: merge web tasks and external SDK/CLI device queue tickets
+ global_queued = DeviceExecutionLock.get_queued_tasks()
+ seen_ids = set()
+ queue_data: list[dict[str, Any]] = []
+
+ for item in state.queue_tasks:
+ sid = item.get("session_id")
+ if sid:
+ seen_ids.add(str(sid))
+ queue_data.append(item)
+
+ for g_item in global_queued:
+ sid = str(g_item.get("session_id"))
+ if sid not in seen_ids:
+ seen_ids.add(sid)
+ queue_data.append(g_item)
if is_running:
is_paused = state.is_paused
@@ -187,6 +226,7 @@ async def get_status():
"background_tasks": bg_tasks,
"queue": queue_data,
"model_info": model_info,
+ "active_tasks": active_tasks,
}
if latest_session_id and str(latest_session_id) in state.active_connections:
@@ -203,6 +243,8 @@ async def get_status():
"background_tasks": bg_tasks,
"queue": queue_data,
"model_info": conn_model_info,
+ "active_tasks": active_tasks,
+ "ipc_port": state.ipc_port,
}
return {
@@ -210,7 +252,8 @@ async def get_status():
"session_id": latest_session_id,
"background_tasks": bg_tasks,
"queue": queue_data,
- "model_info": model_info,
+ "active_tasks": active_tasks,
+ "ipc_port": state.ipc_port,
}
diff --git a/apps/admin_console/schemas/task_schema.py b/apps/admin_console/schemas/task_schema.py
index 49c8033..f9f543c 100644
--- a/apps/admin_console/schemas/task_schema.py
+++ b/apps/admin_console/schemas/task_schema.py
@@ -23,6 +23,8 @@ class RunRequest(BaseModel):
enable_outputter: bool | None = None
locked_app_package: str | None = None
app_path: str | None = None
+ device_serial: str | None = None
+
class ReplayRequest(BaseModel):
diff --git a/apps/admin_console/server.py b/apps/admin_console/server.py
index 464fada..f760761 100644
--- a/apps/admin_console/server.py
+++ b/apps/admin_console/server.py
@@ -49,8 +49,10 @@ import uvicorn
from artemis.runtime import (
DeviceExecutionLock,
+ clear_server_info,
shutdown_awake_service,
start_awake_service,
+ write_server_info,
)
try:
@@ -108,6 +110,7 @@ async def on_startup():
"""Startup lifecycle hooks."""
state.is_shutting_down = False
state.shutdown_event.clear()
+ write_server_info(port=getattr(state, "port", 8000), host=getattr(state, "host", "0.0.0.0"))
await asyncio.to_thread(start_awake_service)
cleaned_device_locks = DeviceExecutionLock.cleanup_stale_locks()
if cleaned_device_locks:
@@ -160,6 +163,7 @@ async def on_shutdown():
await ipc_service.stop_server()
state.ipc_subscribers.clear()
await asyncio.to_thread(shutdown_awake_service)
+ clear_server_info()
# Mount modular routers
@@ -382,23 +386,30 @@ class ArtemisUvicornServer(uvicorn.Server):
def run_ui_server(host: str, port: int, reload: bool = False) -> None:
"""Run the UI server with bounded, signal-aware graceful shutdown."""
- if reload:
- uvicorn.run(
+ state.host = host
+ state.port = port
+ write_server_info(port=port, host=host)
+
+ try:
+ if reload:
+ uvicorn.run(
+ "apps.admin_console.server:app",
+ host=host,
+ port=port,
+ reload=True,
+ timeout_graceful_shutdown=5,
+ )
+ return
+
+ config = uvicorn.Config(
app,
host=host,
port=port,
- reload=True,
timeout_graceful_shutdown=5,
)
- return
-
- config = uvicorn.Config(
- app,
- host=host,
- port=port,
- timeout_graceful_shutdown=5,
- )
- ArtemisUvicornServer(config).run()
+ ArtemisUvicornServer(config).run()
+ finally:
+ clear_server_info()
def main():
diff --git a/apps/admin_console/services/task_preset_catalog.py b/apps/admin_console/services/task_preset_catalog.py
index 10f51e5..b878360 100644
--- a/apps/admin_console/services/task_preset_catalog.py
+++ b/apps/admin_console/services/task_preset_catalog.py
@@ -334,12 +334,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
required_packages=["com.google.android.apps.photos"],
priority=82,
),
- # 9. WeChat (微信)
+ # 9. WeChat
TaskPreset(
id="wechat_browse",
title="Check WeChat Messages",
description="Open WeChat and view top recent chat conversations",
- goal="Open WeChat (微信) and view the top recent chat messages.",
+ goal="Open WeChat and view the top recent chat messages.",
profile="flash",
category="flash",
tag="WeChat",
@@ -351,7 +351,7 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
id="pro_wechat_to_calendar",
title="WeChat Notice to Calendar",
description="Extract meeting notice from WeChat chat and add to Calendar",
- goal="Open WeChat (微信), locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.",
+ goal="Open WeChat, locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.",
profile="pro",
category="cross_app",
tag="WeChat + Calendar",
@@ -367,12 +367,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
required_packages=["com.tencent.mm"],
priority=93,
),
- # 10. Xiaohongshu (小红书)
+ # 10. Xiaohongshu
TaskPreset(
id="xhs_coffee_guide",
title="RED Cafe Guide Search",
description="Search trending specialty cafe reviews on Xiaohongshu",
- goal="Open Xiaohongshu (小红书), search for top-rated specialty coffee shops, and view the top post.",
+ goal="Open Xiaohongshu, search for top-rated specialty coffee shops, and view the top post.",
profile="flash",
category="flash",
tag="Xiaohongshu",
@@ -384,12 +384,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
required_packages=["com.xingin.xhs"],
priority=87,
),
- # 11. Meituan / Dianping (美团 / 大众点评)
+ # 11. Meituan / Dianping
TaskPreset(
id="meituan_ramen_search",
title="Meituan Food Search",
description="Search top-rated Ramen nearby on Meituan or Dianping",
- goal="Open Meituan (美团) or Dianping (大众点评), search for top-rated Ramen nearby, and view top restaurant rating.",
+ goal="Open Meituan or Dianping, search for top-rated Ramen nearby, and view top restaurant rating.",
profile="flash",
category="flash",
tag="Meituan",
@@ -401,12 +401,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
required_packages=["com.sankuai.meituan", "com.dianping.v1"],
priority=86,
),
- # 12. Bilibili (哔哩哔哩)
+ # 12. Bilibili
TaskPreset(
id="bilibili_stream",
title="Bilibili Tech Video",
description="Search and play an AI Agent tutorial video on Bilibili",
- goal='Open Bilibili (哔哩哔哩), search for "AI Agent 架构实战", and play the top matching video.',
+ goal='Open Bilibili, search for "AI Agent Architecture", and play the top matching video.',
profile="flash",
category="flash",
tag="Bilibili",
diff --git a/apps/admin_console/services/task_queue_service.py b/apps/admin_console/services/task_queue_service.py
index 0570201..5f7ad93 100644
--- a/apps/admin_console/services/task_queue_service.py
+++ b/apps/admin_console/services/task_queue_service.py
@@ -241,6 +241,11 @@ class TaskQueueService:
async def queue_worker(cls):
"""Persistent, self-healing background worker loop processing tasks in FIFO order."""
print("[QueueWorker] Background worker loop initialized and running.")
+ try:
+ DeviceExecutionLock.cleanup_stale_locks()
+ except Exception as exc:
+ print(f"[QueueWorker] Initial stale lock cleanup notice: {exc}")
+
while True:
task_item = None
sess_id = None
@@ -289,6 +294,7 @@ class TaskQueueService:
"session_id": sess_id,
"initial_goal": goal,
"profile": profile,
+ "device_serial": task_item.get("device_serial"),
},
)
@@ -322,6 +328,8 @@ class TaskQueueService:
"--test-name",
test_name,
]
+ if sess_id:
+ cmd.extend(["--session-id", str(sess_id)])
if expected_output:
cmd.extend(["--output-description", str(expected_output)])
if enable_outputter is not None:
@@ -330,9 +338,13 @@ class TaskQueueService:
cmd.extend(["--locked-app", str(locked_app)])
if app_path:
cmd.extend(["--app-path", str(app_path)])
+ device_serial = task_item.get("device_serial")
+ if device_serial:
+ cmd.extend(["--device-serial", str(device_serial)])
+ env["ADB_DEVICE_SERIAL"] = str(device_serial)
print(
- f"[QueueWorker] Starting task [{sess_id}]: '{goal}' (profile: {profile}, outputter: {bool(expected_output or enable_outputter)})"
+ f"[QueueWorker] Starting task [{sess_id}]: '{goal}' (profile: {profile}, device: {device_serial or 'auto'}, outputter: {bool(expected_output or enable_outputter)})"
)
proc = await asyncio.create_subprocess_exec(
*cmd,
@@ -349,6 +361,7 @@ class TaskQueueService:
str(queue_ticket),
proc.pid,
description=f"frontend task: {goal[:120]}",
+ device_id=device_serial or "pending",
session_id=str(sess_id) if sess_id else None,
ingress="frontend",
)
@@ -453,6 +466,7 @@ class TaskQueueService:
enable_outputter: bool | None = None,
locked_app_package: str | None = None,
app_path: str | None = None,
+ device_serial: str | None = None,
) -> dict[str, Any]:
"""Enqueues one or more goals and wakes up the background worker."""
cls.ensure_worker_running()
@@ -461,8 +475,18 @@ class TaskQueueService:
now = time.time()
for i, goal in enumerate(goals):
sess_id = str(uuid.uuid4())
+ assigned_serial = device_serial
+ if not assigned_serial:
+ try:
+ from artemis.runtime import device_pool
+
+ assigned_serial = device_pool.select_device()
+ except Exception:
+ assigned_serial = None
+
queue_ticket = DeviceExecutionLock.reserve(
description=f"frontend task: {goal[:120]}",
+ device_id=assigned_serial or "pending",
session_id=sess_id,
ingress="frontend",
)
@@ -474,6 +498,7 @@ class TaskQueueService:
"enable_outputter": enable_outputter,
"locked_app_package": locked_app_package,
"app_path": app_path,
+ "device_serial": assigned_serial,
"status": "pending",
"queue_ticket": queue_ticket,
"created_at": now + i * 0.001,
diff --git a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
index 17be5d7..1736409 100644
--- a/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
+++ b/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html
@@ -68,6 +68,11 @@
{{ getTaskStatus(session) | uppercase }}