feat: add prompt-first batches and pausable task guidance

This commit is contained in:
chenmingxuan committed 2026-09-23 22:59:48 +08:00
1 parent cec9898e4c
commit 057f0c3050
44 files changed
+1080 -191

No files matched your search

@@ -302,7 +302,7 @@ class TaskQueueService:
normalized = current_status.lower().strip() if isinstance(current_status, str) else None
if normalized == "success":
return "completed", True
if normalized in {"completed", "failed", "cancelled"}:
if normalized in {"completed", "failed", "cancelled", "interrupted"}:
return normalized, False
return ("completed" if returncode == 0 else "failed"), True
@@ -388,10 +388,9 @@ class TaskQueueService:
@classmethod
def _dispatch_pending_tasks(cls) -> None:
"""Launch every pending task admissible under the current concurrency limit."""
cls._sync_control_states()
limit = cls._concurrency_limit()
state.prune_finished_runs()
if limit == 1 and state.is_running:
return
# Dispatched-but-not-yet-spawned runs are only visible as queue items in
# "running" state, so admission must count those too -- active_runs alone
@@ -410,7 +409,9 @@ class TaskQueueService:
and item.get("pid") not in active_pids
for item in in_flight
)
capacity = limit - len(state.active_runs) - starting
capacity = (
limit - sum(not r.get("parked") for r in state.active_runs.values()) - starting
)
if capacity <= 0:
return
busy_devices = state.busy_device_ids | {
@@ -432,11 +433,15 @@ class TaskQueueService:
# A task without a resolved device may bind to any serial, so it
# only launches on an otherwise idle scheduler; the device lock
# then allocates freely without contending against active runs.
if device is None and (state.active_runs or in_flight or dispatched_any):
if device is None and (
any(not r.get("parked") for r in state.active_runs.values())
or in_flight
or dispatched_any
):
continue
if device is not None and target.lock_key in busy_devices:
continue
elif limit > 1 and device is not None and target.lock_key in busy_devices:
elif limit >= 1 and device is not None and target.lock_key in busy_devices:
# A second worker for this device would wait on its lock.
continue
@@ -454,6 +459,42 @@ class TaskQueueService:
if capacity <= 0:
break
@classmethod
def _sync_control_states(cls) -> None:
from artemis.runtime.task_control import read_state
from apps.admin_console.services.automation_store import store
for item in state.queue_items:
if not isinstance(item, dict):
continue
sid = str(item.get("session_id") or "")
run = state.active_runs.get(sid)
if not run:
continue
status = read_state(sid).get("status")
if status not in {"running", "task_paused", "resuming"}:
continue
old = item.get("status")
if status == old:
continue
parked = status in {"task_paused", "resuming"}
now = time.time()
if parked and not run.get("parked"):
run["non_execution_started_at"] = now
elif not parked and run.get("parked"):
run["non_execution_seconds"] = (
float(run.get("non_execution_seconds") or 0)
+ now
- float(run.pop("non_execution_started_at", now))
)
run["parked"] = parked
item["status"] = status
job = store.job(sid)
if job and job.get("status") != "finalizing":
job["status"] = status
store.save_job(job)
cls._broadcast_event("task_status", {"session_id": sid, "status": status})
@classmethod
def _begin_task_run(
cls,
@@ -531,6 +572,22 @@ class TaskQueueService:
env["ARTEMIS_SESSION_ID"] = str(sess_id)
env["ARTEMIS_TASK_INGRESS"] = str(task_item.get("ingress", "frontend"))
env["ARTEMIS_TASK_WORKER"] = "1"
overrides = task_item.get("runtime_overrides") or {}
for option, variable in {
"model": "OPENAI_MODEL",
"reasoning_effort": "OPENAI_REASONING_EFFORT",
"request_timeout_seconds": "OPENAI_TIMEOUT_SECONDS",
"max_retries": "OPENAI_MAX_RETRIES",
"retry_interval_seconds": "OPENAI_RETRY_INTERVAL_SECONDS",
"record_video": "ARTEMIS_RECORDING_ENABLED",
"analyze_video": "ARTEMIS_VIDEO_ENABLED",
}.items():
if overrides.get(option) is not None:
env[variable] = (
str(overrides[option]).lower()
if isinstance(overrides[option], bool)
else str(overrides[option])
)
target.endpoint.apply_to_environment(env)
env[DeviceExecutionLock.LOCK_SCOPE_ENV] = target.lock_scope
queue_ticket = task_item.get("queue_ticket")
@@ -664,6 +721,12 @@ class TaskQueueService:
returncode,
manual_stop,
)
if (
state.is_shutting_down
and not manual_stop
and current_status not in {"completed", "success", "failed", "interrupted"}
):
new_status, should_persist = "interrupted", True
if should_persist:
if session_repo.update_session_status(sess_id, new_status, time.time()):
print(f"[QueueWorker] Updated session {sess_id} status to '{new_status}'")
@@ -1069,6 +1132,7 @@ class TaskQueueService:
conversation_id: str | None = None,
verification_level: str | None = None,
explorer_mode: str | None = None,
runtime_overrides: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Enqueues one or more goals and wakes up the background worker.
@@ -1123,6 +1187,8 @@ class TaskQueueService:
verification_level=verification_level,
explorer_mode=explorer_mode,
)
if runtime_overrides:
task_item["runtime_overrides"] = runtime_overrides
state.queue_items.append(task_item)
enqueued_tasks.append(task_item)
cls._broadcast_startup_progress(