feat: prepare 2.2.0 with UI and execution fixes

Add dark theme, richer Markdown and readable plugin results.
Improve Android input evidence, response integrity and diagnostics.
Unify lifecycle, guidance receipts and evidence sources across clients.
Keep direct action dispatch without an added AI quota approval layer.
Align version metadata at 2.2.0 and bundle Helper 1.2.2.

Validation: 3063 backend tests before the version bump; 44 after.
Frontend: 317 tests and production build passed.
This commit is contained in:
chenmingxuan committed 2026-10-26 00:44:13 +08:00
1 parent 83bcfd54d5
commit fac4ccb760
146 files changed
+5689 -2033

No files matched your search

+67 -24
View File
@@ -6,12 +6,15 @@ import json
import logging
import os
from pathlib import Path
import sys
import threading
import time
import uuid
from artemis.config import TRACES_PATH
from artemis.runtime.device_lock import DeviceExecutionLock
from artemis.runtime.cancellation import CancellationReason, cancellation_reason
from artemis.runtime.task_progress import TERMINAL, number
logger = logging.getLogger(__name__)
@@ -64,13 +67,41 @@ def _write_atomic(path: Path, data: dict) -> None:
def read_state(session_id: str) -> dict:
try:
return json.loads(
value = json.loads(
(control_dir(session_id) / "task_control_state.json").read_text(encoding="utf-8")
)
return value if isinstance(value, dict) else {}
except (OSError, ValueError):
return {}
def write_control_state(session_id: str, status: str, *, reset: bool = False, **fields) -> None:
"""The worker owns transition timing in its existing control-state file."""
now = time.time()
previous = {} if reset else read_state(session_id)
timing = previous.get("timing")
timing = timing if isinstance(timing, dict) else {}
since = number(timing.get("state_started_at"))
if previous.get("pid") != os.getpid() or since is None or since > now:
timing = {"started_at": now, "state_started_at": now, "seconds": {}}
elif previous.get("status") != status:
seconds = timing.get("seconds")
seconds = seconds if isinstance(seconds, dict) else {}
old = previous.get("status")
seconds[old] = (number(seconds.get(old)) or 0) + now - since
timing = {**timing, "seconds": seconds, "state_started_at": now}
_write_atomic(
control_dir(session_id) / "task_control_state.json",
{
**fields,
"status": status,
"updated_at": now,
"pid": os.getpid(),
"timing": timing,
},
)
def request_pause(session_id: str, *, reason: str | None = None) -> None:
_unlink_control_file(control_dir(session_id) / "resume.request")
_write_atomic(
@@ -109,32 +140,48 @@ def add_instruction(
raise ValueError("指导编号已用于不同内容")
return record
path = directory / f"{instruction_id or f'{time.time_ns():020d}-{uuid.uuid4().hex}'}.json"
accepted_at = time.time()
record = {
"id": path.stem,
"instruction": instruction.strip(),
"release_loop": bool(release_loop),
"source": source,
"timestamp": time.time(),
"timestamp": accepted_at,
"accepted_at": accepted_at,
}
_write_atomic(path, record)
return record
def list_instructions(session_id: str) -> list[dict]:
def list_instructions(session_id: str, *, task_status: str | None = None) -> list[dict]:
"""Show submissions separately from guidance the runner actually consumed."""
directory = control_dir(session_id) / "instructions"
records = []
for folder, status in ((directory, "pending"), (directory / "consumed", "consumed")):
for path in folder.glob("*.json") if folder.exists() else []:
records = {}
for folder, pattern, status in (
(directory, "*.json", "pending"),
(directory, "*.reading", "reading"),
(directory / "consumed", "*.json", "consumed"),
):
for path in folder.glob(pattern) if folder.exists() else []:
try:
record = json.loads(path.read_text(encoding="utf-8"))
if isinstance(record, dict) and record.get("instruction"):
records.append(
{**record, "id": record.get("id") or path.stem, "status": status}
)
if task_status in TERMINAL or task_status == "finalizing":
status_value = {"pending": "unapplied", "reading": "unconfirmed"}.get(
status, status
)
else:
status_value = status
key = record.get("id") or path.stem
records[key] = {
**record,
"id": key,
"status": status_value,
"accepted_at": record.get("accepted_at", record.get("timestamp")),
}
except (OSError, ValueError):
continue
return sorted(records, key=lambda record: (record.get("timestamp", 0), record["id"]))
return sorted(records.values(), key=lambda record: (record.get("timestamp", 0), record["id"]))
def has_pending_instruction(base_dir: str | Path) -> bool:
@@ -187,15 +234,11 @@ class TaskControl:
self.pause_reason: str | None = None
def _state(self, status: str) -> None:
_write_atomic(
self.directory / "task_control_state.json",
{
"status": status,
"updated_at": time.time(),
"pid": os.getpid(),
"budget_epoch": self.budget_epoch,
"pause_reason": self.pause_reason,
},
write_control_state(
self.session_id,
status,
budget_epoch=self.budget_epoch,
pause_reason=self.pause_reason,
)
async def checkpoint(self, *, on_pause=None, on_resume=None) -> bool:
@@ -271,8 +314,8 @@ class TaskControl:
running = asyncio.ensure_future(operation)
pause = self.directory / "pause.request"
async def stop_operation():
running.cancel()
async def stop_operation(reason: CancellationReason):
running.cancel(reason)
result = (await asyncio.gather(running, return_exceptions=True))[0]
if isinstance(result, BaseException) and not isinstance(result, asyncio.CancelledError):
raise result
@@ -284,16 +327,16 @@ class TaskControl:
# that requested pause). The next checkpoint handles the request.
return False, running.result()
if pause.exists():
await stop_operation()
await stop_operation(CancellationReason.TASK_PAUSED)
await self.checkpoint(on_pause=on_pause, on_resume=on_resume)
return True, None
if has_pending_instruction(self.directory):
await stop_operation()
await stop_operation(CancellationReason.GUIDANCE_REPLACED)
return True, None
await asyncio.wait({running}, timeout=0.2)
finally:
if not running.done():
await stop_operation()
await stop_operation(cancellation_reason(sys.exception()))
def release(self) -> None:
self.device_lock.release()