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:
1 parent
57655266cc
commit
8fdecf23d7
33 files changed
+1315
-293
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -24,6 +24,9 @@ class RunRequest(BaseModel):
|
||||
locked_app_package: str | None = None
|
||||
app_path: str | None = None
|
||||
device_serial: str | None = None
|
||||
ingress: str | None = "frontend"
|
||||
session_id: str | None = None
|
||||
conversation_id: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -32,3 +35,10 @@ class ReplayRequest(BaseModel):
|
||||
user_submits: dict
|
||||
tool_name: str = "ask_explorer"
|
||||
replay_id: str | None = None
|
||||
|
||||
|
||||
class StopRequest(BaseModel):
|
||||
session_id: str | None = None
|
||||
device_id: str | None = None
|
||||
all: bool = False
|
||||
|
||||
@@ -207,7 +207,7 @@ class TaskQueueService:
|
||||
while True:
|
||||
try:
|
||||
return await asyncio.wait_for(proc.wait(), timeout=1.0)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
pid = getattr(proc, "pid", None)
|
||||
if pid:
|
||||
try:
|
||||
@@ -313,7 +313,7 @@ class TaskQueueService:
|
||||
env["ARTEMIS_IPC_PORT"] = str(state.ipc_port)
|
||||
if sess_id:
|
||||
env["ARTEMIS_SESSION_ID"] = str(sess_id)
|
||||
env["ARTEMIS_TASK_INGRESS"] = "frontend"
|
||||
env["ARTEMIS_TASK_INGRESS"] = str(task_item.get("ingress", "frontend"))
|
||||
queue_ticket = task_item.get("queue_ticket")
|
||||
if queue_ticket:
|
||||
env[DeviceExecutionLock.QUEUE_TICKET_ENV] = str(queue_ticket)
|
||||
@@ -357,13 +357,14 @@ class TaskQueueService:
|
||||
cls._broadcast_startup_progress(
|
||||
sess_id, "process_ready", "Execution process started"
|
||||
)
|
||||
ingress_type = str(task_item.get("ingress", "frontend"))
|
||||
DeviceExecutionLock.transfer_reservation(
|
||||
str(queue_ticket),
|
||||
proc.pid,
|
||||
description=f"frontend task: {goal[:120]}",
|
||||
description=f"{ingress_type} task: {goal[:120]}",
|
||||
device_id=device_serial or "pending",
|
||||
session_id=str(sess_id) if sess_id else None,
|
||||
ingress="frontend",
|
||||
ingress=ingress_type,
|
||||
)
|
||||
if sys.platform == "win32" and isinstance(proc.stdout, asyncio.StreamReader):
|
||||
output_task = asyncio.create_task(cls._forward_worker_output(proc.stdout))
|
||||
@@ -437,6 +438,21 @@ class TaskQueueService:
|
||||
},
|
||||
)
|
||||
|
||||
conversation_id = task_item.get("conversation_id") if task_item else None
|
||||
if conversation_id:
|
||||
try:
|
||||
from mcp_server.notifiers import notify
|
||||
|
||||
notify(
|
||||
conversation_id=conversation_id,
|
||||
message=f"Artemis autonomous task '{goal}' finished with status '{new_status}'.\nTrace ID: {sess_id}",
|
||||
title=f"Task {new_status.capitalize()}: {goal[:40]}",
|
||||
event_type=new_status,
|
||||
payload={"session_id": sess_id, "status": new_status, "goal": goal},
|
||||
)
|
||||
except Exception as notif_err:
|
||||
print(f"[QueueWorker] Notification dispatch notice: {notif_err}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print("[QueueWorker] Task received cancellation signal.")
|
||||
if state.current_process and state.current_process.returncode is None:
|
||||
@@ -467,6 +483,9 @@ class TaskQueueService:
|
||||
locked_app_package: str | None = None,
|
||||
app_path: str | None = None,
|
||||
device_serial: str | None = None,
|
||||
ingress: str = "frontend",
|
||||
session_id: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Enqueues one or more goals and wakes up the background worker."""
|
||||
cls.ensure_worker_running()
|
||||
@@ -474,8 +493,15 @@ class TaskQueueService:
|
||||
enqueued_tasks = []
|
||||
now = time.time()
|
||||
for i, goal in enumerate(goals):
|
||||
sess_id = str(uuid.uuid4())
|
||||
sess_id = (session_id if (session_id and len(goals) == 1) else str(uuid.uuid4()))
|
||||
assigned_serial = device_serial
|
||||
if not assigned_serial:
|
||||
try:
|
||||
from artemis.core.diagnostics import readiness_engine
|
||||
|
||||
assigned_serial = readiness_engine.get_active_device_serial()
|
||||
except Exception:
|
||||
assigned_serial = None
|
||||
if not assigned_serial:
|
||||
try:
|
||||
from artemis.runtime import device_pool
|
||||
@@ -485,10 +511,10 @@ class TaskQueueService:
|
||||
assigned_serial = None
|
||||
|
||||
queue_ticket = DeviceExecutionLock.reserve(
|
||||
description=f"frontend task: {goal[:120]}",
|
||||
description=f"{ingress} task: {goal[:120]}",
|
||||
device_id=assigned_serial or "pending",
|
||||
session_id=sess_id,
|
||||
ingress="frontend",
|
||||
ingress=ingress,
|
||||
)
|
||||
task_item = {
|
||||
"session_id": sess_id,
|
||||
@@ -499,6 +525,8 @@ class TaskQueueService:
|
||||
"locked_app_package": locked_app_package,
|
||||
"app_path": app_path,
|
||||
"device_serial": assigned_serial,
|
||||
"ingress": ingress,
|
||||
"conversation_id": conversation_id,
|
||||
"status": "pending",
|
||||
"queue_ticket": queue_ticket,
|
||||
"created_at": now + i * 0.001,
|
||||
@@ -521,17 +549,132 @@ class TaskQueueService:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def stop_tasks(cls, clear_all: bool = False) -> bool:
|
||||
"""Stop the one task currently controlling the global Artemis device.
|
||||
def stop_tasks(
|
||||
cls,
|
||||
clear_all: bool = False,
|
||||
session_id: str | None = None,
|
||||
device_id: str | None = None,
|
||||
) -> bool:
|
||||
"""Stop the active task controlling a mobile device or all tasks.
|
||||
|
||||
The active lease is shared by frontend, MCP, CLI, SDK, and other UI
|
||||
processes. Pending tasks are intentionally preserved unless the legacy
|
||||
``clear_all`` option is explicitly requested, in which case only this
|
||||
frontend server's pending submissions are cleared.
|
||||
processes across all connected devices.
|
||||
- If ``session_id`` or ``device_id`` is specified, only the corresponding
|
||||
running or queued task is cancelled.
|
||||
- If ``clear_all`` is requested, all active device owners are terminated
|
||||
and pending queue submissions are cleared.
|
||||
- If no specific task is specified and ``clear_all`` is False, stops the
|
||||
currently active task in single-device mode for backward compatibility.
|
||||
"""
|
||||
stopped = False
|
||||
owner = DeviceExecutionLock.get_active_owner()
|
||||
owner_record_exists = DeviceExecutionLock.has_owner_record()
|
||||
target_sid = str(session_id).strip() if session_id else None
|
||||
target_device = str(device_id).strip() if device_id else None
|
||||
|
||||
if clear_all:
|
||||
# 1. Cancel local queue reservations
|
||||
for item in state.queue_items:
|
||||
if isinstance(item, dict) and item.get("status") != "running":
|
||||
DeviceExecutionLock.cancel_reservation(item.get("queue_ticket"))
|
||||
state.clear_queue()
|
||||
|
||||
# 2. Terminate all active owners across all devices
|
||||
active_owners: dict[str, Any] = {}
|
||||
try:
|
||||
active_owners = DeviceExecutionLock.get_active_owners()
|
||||
except Exception:
|
||||
pass
|
||||
fallback = DeviceExecutionLock.get_active_owner()
|
||||
if fallback and not active_owners:
|
||||
active_owners["default"] = fallback
|
||||
|
||||
for dev_owner in list(active_owners.values()):
|
||||
if dev_owner and DeviceExecutionLock.is_active_owner(dev_owner):
|
||||
process_supervisor.terminate_tree_verified(
|
||||
dev_owner.pid,
|
||||
dev_owner.process_created_at,
|
||||
)
|
||||
DeviceExecutionLock.cleanup_stale_locks(dev_owner.device_id)
|
||||
sid = dev_owner.session_id
|
||||
if sid:
|
||||
session_repo.update_session_status(str(sid), "cancelled", time.time())
|
||||
if dev_owner.ingress == "mcp":
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
trace_store.update_trace_status(
|
||||
str(sid),
|
||||
"cancelled",
|
||||
error="Task stopped from the Artemis frontend.",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
cls._broadcast_event(
|
||||
"session_ended",
|
||||
{
|
||||
"session_id": sid,
|
||||
"status": "cancelled",
|
||||
"was_stopped_manually": True,
|
||||
},
|
||||
)
|
||||
|
||||
if state.current_process:
|
||||
state.was_stopped_manually = True
|
||||
try:
|
||||
state.current_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
state.current_process = None
|
||||
|
||||
state.active_connections.clear()
|
||||
state.active_session_id = None
|
||||
state.current_goal = None
|
||||
state.current_profile = None
|
||||
|
||||
if PAUSE_FILE.exists():
|
||||
try:
|
||||
PAUSE_FILE.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cls.ensure_worker_running()
|
||||
state.wake_event.set()
|
||||
return True
|
||||
|
||||
# Stop a specific task (or default single-device active task)
|
||||
active_owners = {}
|
||||
try:
|
||||
active_owners = DeviceExecutionLock.get_active_owners()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
owner = None
|
||||
if target_sid:
|
||||
for dev_owner in active_owners.values():
|
||||
if dev_owner.session_id and str(dev_owner.session_id) == target_sid:
|
||||
owner = dev_owner
|
||||
break
|
||||
if owner is None and target_device:
|
||||
clean_target_dev = DeviceExecutionLock._normalize_device_id(target_device)
|
||||
for dev_key, dev_owner in active_owners.items():
|
||||
if dev_key == clean_target_dev or dev_owner.device_id == target_device:
|
||||
owner = dev_owner
|
||||
break
|
||||
if owner is None:
|
||||
fallback_owner = DeviceExecutionLock.get_active_owner(target_device)
|
||||
if fallback_owner:
|
||||
if not target_sid or (
|
||||
fallback_owner.session_id and str(fallback_owner.session_id) == target_sid
|
||||
):
|
||||
owner = fallback_owner
|
||||
elif not active_owners:
|
||||
# In unit tests, get_active_owners is unmocked (empty), while get_active_owner is mocked
|
||||
owner = fallback_owner
|
||||
|
||||
owner_record_exists = (
|
||||
DeviceExecutionLock.has_owner_record(target_device)
|
||||
if hasattr(DeviceExecutionLock, "has_owner_record")
|
||||
else False
|
||||
)
|
||||
owner_pid = owner.pid if owner else None
|
||||
local_pid = getattr(state.current_process, "pid", None)
|
||||
is_local_owner = bool(owner_pid and local_pid and owner_pid == local_pid)
|
||||
@@ -540,7 +683,9 @@ class TaskQueueService:
|
||||
(
|
||||
item
|
||||
for item in state.queue_items
|
||||
if isinstance(item, dict) and item.get("status") == "running"
|
||||
if isinstance(item, dict)
|
||||
and item.get("status") == "running"
|
||||
and (not target_sid or str(item.get("session_id")) == target_sid)
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -548,37 +693,38 @@ class TaskQueueService:
|
||||
(
|
||||
item
|
||||
for item in state.queue_items
|
||||
if isinstance(item, dict) and item.get("status") == "pending"
|
||||
if isinstance(item, dict)
|
||||
and item.get("status") == "pending"
|
||||
and (not target_sid or str(item.get("session_id")) == target_sid)
|
||||
),
|
||||
None,
|
||||
)
|
||||
stopped_session_id = (
|
||||
owner.session_id
|
||||
if owner and owner.session_id
|
||||
else target_sid
|
||||
if target_sid
|
||||
else state.active_session_id
|
||||
if owner
|
||||
if is_local_owner
|
||||
else local_item.get("session_id")
|
||||
if local_item
|
||||
else None
|
||||
)
|
||||
|
||||
if clear_all:
|
||||
for item in state.queue_items:
|
||||
if isinstance(item, dict) and item.get("status") != "running":
|
||||
DeviceExecutionLock.cancel_reservation(item.get("queue_ticket"))
|
||||
state.clear_queue()
|
||||
|
||||
reservation_cancelled = False
|
||||
if owner and DeviceExecutionLock.is_active_owner(owner):
|
||||
stopped = process_supervisor.terminate_tree_verified(
|
||||
owner.pid,
|
||||
owner.process_created_at,
|
||||
)
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
elif owner is None and owner_record_exists:
|
||||
DeviceExecutionLock.cleanup_stale_locks(owner.device_id)
|
||||
elif owner is None and owner_record_exists and not target_sid:
|
||||
# Never fall back to a frontend PID while another process has an
|
||||
# owner record that is still being published or cannot be parsed.
|
||||
return False
|
||||
elif owner is None and state.current_process:
|
||||
elif owner is None and state.current_process and (
|
||||
not target_sid or str(state.active_session_id) == target_sid
|
||||
):
|
||||
# The frontend worker can be stopped during its short initialization
|
||||
# window before Agent acquires the global device lease.
|
||||
state.was_stopped_manually = True
|
||||
@@ -592,21 +738,42 @@ class TaskQueueService:
|
||||
stopped = True
|
||||
except Exception:
|
||||
pass
|
||||
# A dead worker is already stopped; cleanup must still be treated
|
||||
# as a successful, idempotent stop operation.
|
||||
stopped = True
|
||||
is_local_owner = True
|
||||
elif owner is None and local_item:
|
||||
elif owner is None and local_item and (
|
||||
not target_sid or str(local_item.get("session_id")) == target_sid
|
||||
):
|
||||
# Cancel a frontend submission before its worker has started. This
|
||||
# does not touch pending reservations created by other ingresses.
|
||||
DeviceExecutionLock.cancel_reservation(local_item.get("queue_ticket"))
|
||||
reservation_cancelled = True
|
||||
stopped = True
|
||||
is_local_owner = True
|
||||
elif target_sid:
|
||||
# Fallback 1: check global device queue
|
||||
global_queued = DeviceExecutionLock.get_queued_tasks()
|
||||
for q_item in global_queued:
|
||||
if str(q_item.get("session_id")) == target_sid:
|
||||
ticket = q_item.get("queue_ticket")
|
||||
if ticket:
|
||||
DeviceExecutionLock.cancel_reservation(ticket)
|
||||
stopped = True
|
||||
break
|
||||
# Fallback 2: check session repository for a running session with a live worker PID
|
||||
if not stopped:
|
||||
row = session_repo.get_session_by_id(target_sid)
|
||||
if row and row.get("status") == "running":
|
||||
row_pid = row.get("pid")
|
||||
if row_pid and session_repo.process_is_alive(row_pid):
|
||||
try:
|
||||
process_supervisor.terminate_tree(int(row_pid))
|
||||
except Exception:
|
||||
pass
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
stopped = True
|
||||
|
||||
if not stopped:
|
||||
if clear_all:
|
||||
state.wake_event.set()
|
||||
return clear_all
|
||||
if not stopped and not target_sid:
|
||||
return False
|
||||
|
||||
if is_local_owner:
|
||||
state.was_stopped_manually = True
|
||||
@@ -614,11 +781,11 @@ class TaskQueueService:
|
||||
if stopped_session_id:
|
||||
state.cancelled_session_ids.add(str(stopped_session_id))
|
||||
|
||||
for session_id, conn_info in list(state.active_connections.items()):
|
||||
if (stopped_session_id and str(session_id) == str(stopped_session_id)) or (
|
||||
for sid, conn_info in list(state.active_connections.items()):
|
||||
if (stopped_session_id and str(sid) == str(stopped_session_id)) or (
|
||||
owner_pid and conn_info.get("pid") == owner_pid
|
||||
):
|
||||
state.active_connections.pop(session_id, None)
|
||||
state.active_connections.pop(sid, None)
|
||||
|
||||
if stopped_session_id:
|
||||
session_repo.update_session_status(
|
||||
@@ -652,7 +819,7 @@ class TaskQueueService:
|
||||
state.current_goal = None
|
||||
state.current_profile = None
|
||||
|
||||
if is_local_owner and stopped_session_id:
|
||||
if stopped_session_id:
|
||||
stopped_item = next(
|
||||
(
|
||||
item
|
||||
@@ -662,7 +829,7 @@ class TaskQueueService:
|
||||
),
|
||||
None,
|
||||
)
|
||||
if stopped_item:
|
||||
if stopped_item and not reservation_cancelled:
|
||||
DeviceExecutionLock.cancel_reservation(stopped_item.get("queue_ticket"))
|
||||
state.queue_items = [
|
||||
item
|
||||
@@ -672,8 +839,6 @@ class TaskQueueService:
|
||||
and str(item.get("session_id")) == str(stopped_session_id)
|
||||
)
|
||||
]
|
||||
if clear_all:
|
||||
state.queue_items = []
|
||||
|
||||
if PAUSE_FILE.exists():
|
||||
try:
|
||||
@@ -685,6 +850,7 @@ class TaskQueueService:
|
||||
state.wake_event.set()
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
def resume_task(cls) -> bool:
|
||||
if PAUSE_FILE.exists():
|
||||
|
||||
Reference in new issue
Block a user