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

+8
View File
@@ -161,6 +161,14 @@ async def get_session_background_tasks(session_id: str):
return []
@router.get("/api/sessions/{session_id}/startup_progress")
async def get_session_startup_progress(session_id: str):
try:
return state.get_startup_progress(session_id)
except Exception:
return []
@router.post("/api/cleanup")
async def cleanup_history_endpoint():
try:
+93 -10
View File
@@ -16,7 +16,7 @@ import asyncio
from contextlib import suppress
import json
from typing import Any
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from artemis.core.diagnostics import readiness_engine
from artemis.runtime import DeviceExecutionLock, device_pool
@@ -84,15 +84,30 @@ async def run_task(request: RunRequest):
# polling intervals cannot start through a stale Ready state. Use the
# bounded submission probe: the full diagnostics path also scans packages,
# emulator installations, Android version, and screen size.
device_probe = await readiness_engine.run_device_submission_probe()
target_serial = request.device_serial or readiness_engine.get_active_device_serial()
device_probe = await readiness_engine.run_device_submission_probe(
target_serial=target_serial
)
if device_probe and device_probe.summary in {"Device Locked", "Lock State Unknown"}:
locked_serial = (
device_probe.metadata.get("active_device", {}).get("serial")
or target_serial
or ""
)
detail = (
"Android device is locked. Unlock it and enter the home screen before running a task."
f"Android device {locked_serial} is locked. Unlock it and enter the home screen before running a task.".replace(" ", " ").strip()
if device_probe.summary == "Device Locked"
else "Android device lock state could not be verified. Keep it unlocked on the home screen and try again."
else f"Android device {locked_serial} lock state could not be verified. Keep it unlocked on the home screen and try again.".replace(" ", " ").strip()
)
raise HTTPException(status_code=409, detail=detail)
if device_probe and device_probe.metadata.get("active_device"):
verified_serial = device_probe.metadata["active_device"].get("serial")
if verified_serial:
target_serial = verified_serial
if readiness_engine.get_active_device_serial() != verified_serial:
readiness_engine.set_active_device_serial(verified_serial)
return await task_queue_service.enqueue_tasks(
incoming_goals,
profile=request.profile or "flash",
@@ -100,7 +115,10 @@ 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,
device_serial=target_serial,
ingress=request.ingress or "frontend",
session_id=request.session_id,
conversation_id=request.conversation_id,
)
@@ -112,13 +130,39 @@ async def list_devices():
@router.post("/api/stop")
async def stop_task(all: bool = False):
stopped = task_queue_service.stop_tasks(clear_all=all)
async def stop_task(
request: Request,
all: bool = False,
session_id: str | None = None,
device_id: str | None = None,
):
target_all = all
target_sid = session_id
target_dev = device_id
try:
body = await request.json()
if isinstance(body, dict):
if "all" in body:
target_all = bool(body["all"]) or target_all
if body.get("session_id"):
target_sid = str(body["session_id"])
if body.get("device_id"):
target_dev = str(body["device_id"])
except Exception:
pass
stopped = task_queue_service.stop_tasks(
clear_all=target_all,
session_id=target_sid,
device_id=target_dev,
)
if stopped:
return {"status": "stopped"}
return {"status": "stopped", "session_id": target_sid}
return {"status": "no_running_task"}
@router.post("/api/resume")
async def resume_task():
resumed = task_queue_service.resume_task()
@@ -258,8 +302,9 @@ async def get_status():
}
@router.get("/api/stream")
@router.get("/api/stream/{session_id}")
async def stream_events(session_id: str, client: str | None = None):
async def stream_events(session_id: str = "active", client: str | None = None):
async def event_generator():
queue = asyncio.Queue()
event_loop = asyncio.get_running_loop()
@@ -295,7 +340,45 @@ async def stream_events(session_id: str, client: str | None = None):
state.add_subscriber(callback)
yield f'event: info\ndata: {{"message": "Subscribed to session {session_id}"}}\n\n'
if session_id not in ("all", "active"):
if session_id in ("all", "active"):
active_sid = state.active_session_id
if not active_sid:
running_item = next(
(t for t in state.queue_items if isinstance(t, dict) and t.get("status") == "running"),
None,
)
if running_item:
active_sid = running_item.get("session_id")
if not active_sid:
owner = DeviceExecutionLock.get_active_owner()
if owner and owner.session_id:
active_sid = owner.session_id
if active_sid:
goal = state.current_goal or ""
profile = state.current_profile or "flash"
yield (
"event: session_started\n"
f"data: {json.dumps({'session_id': str(active_sid), 'initial_goal': goal, 'profile': profile}, default=str)}\n\n"
)
for progress_event in state.get_startup_progress(str(active_sid)):
yield (
"event: startup_progress\n"
f"data: {json.dumps(progress_event, default=str)}\n\n"
)
try:
from apps.admin_console.database.repositories.step_repository import step_repo
recorded_steps = step_repo.get_session_steps(str(active_sid))
for step_dict in recorded_steps:
yield (
"event: step_recorded\n"
f"data: {json.dumps(step_dict, default=str)}\n\n"
)
except Exception as exc:
print(f"[Stream] Could not replay active steps: {exc}")
else:
for progress_event in state.get_startup_progress(session_id):
yield (
"event: startup_progress\n"