Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

185 lines
6.5 KiB
Python

"""Persistent registry for in-flight interactive-shell tasks (/tasks, /cancel).
The task value types (:class:`TaskStatus`, :class:`TaskKind`,
:class:`TaskRecord`) live in :mod:`platform.common.task_types` so non-CLI
packages (e.g. ``tools.system.watch_dog``) can share the task contract without
importing the CLI package. This module owns only the CLI-runtime registry
that stores and persists those records across REPL sessions.
"""
from __future__ import annotations
import contextlib
import json
import secrets
import threading
from collections import deque
from pathlib import Path
import config.constants as const_module
from platform.common.task_types import TaskKind, TaskRecord, TaskStatus
_TASK_ID_BYTES = 4
_MAX_REGISTRY = 100
_TASKS_STORE_FILENAME = "interactive_tasks.json"
def _tasks_store_path() -> Path:
return const_module.OPENSRE_HOME_DIR / _TASKS_STORE_FILENAME
class TaskRegistry:
"""Recent tasks for /tasks and /cancel, optionally persisted across REPL sessions."""
def __init__(
self,
*,
max_tasks: int = _MAX_REGISTRY,
persist_path: Path | None = None,
load: bool = False,
) -> None:
self._tasks: deque[TaskRecord] = deque(maxlen=max_tasks)
self._lock = threading.Lock()
self._persist_lock = threading.Lock()
self._persist_path = persist_path
self._max_tasks = max_tasks
if load:
self._load_persisted()
@classmethod
def persistent(cls, *, max_tasks: int = _MAX_REGISTRY) -> TaskRegistry:
return cls(max_tasks=max_tasks, persist_path=_tasks_store_path(), load=True)
def _attach(self, record: TaskRecord) -> TaskRecord:
record._on_change = self._persist
return record
def _load_persisted(self) -> None:
if self._persist_path is None or not self._persist_path.exists():
return
try:
payload = json.loads(self._persist_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return
if not isinstance(payload, list):
return
records = [
self._attach(record)
for item in payload
if isinstance(item, dict)
if (record := TaskRecord.from_dict(item)) is not None
]
for record in records[-self._max_tasks :]:
record.refresh_rehydrated_status()
self._tasks.append(record)
self._persist()
def _persist(self) -> None:
if self._persist_path is None:
return
with self._persist_lock:
with self._lock:
payload = [task.to_dict() for task in self._tasks]
tmp_path: Path | None = None
try:
self._persist_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
tmp_path = self._persist_path.with_name(
f"{self._persist_path.name}.{threading.get_ident()}.{secrets.token_hex(4)}.tmp"
)
tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
tmp_path.replace(self._persist_path)
except OSError:
if tmp_path is not None:
with contextlib.suppress(OSError):
tmp_path.unlink()
return
def _refresh_rehydrated(self) -> None:
with self._lock:
items = list(self._tasks)
for task in items:
task.refresh_rehydrated_status()
def _tasks_from_disk(self) -> list[TaskRecord]:
"""Read the persisted store and return records not already in memory.
Called by :meth:`list_recent` so that tasks created by other REPL
sessions (which share the same on-disk store) are visible without
requiring a full restart.
"""
if self._persist_path is None or not self._persist_path.exists():
return []
try:
payload = json.loads(self._persist_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return []
if not isinstance(payload, list):
return []
with self._lock:
known_ids = {task.task_id for task in self._tasks}
records: list[TaskRecord] = []
for item in payload:
if not isinstance(item, dict):
continue
record = TaskRecord.from_dict(item)
if record is None or record.task_id in known_ids:
continue
record._rehydrated = True
record.refresh_rehydrated_status()
records.append(record)
return records
def create(self, kind: TaskKind, *, command: str | None = None) -> TaskRecord:
task_id = secrets.token_hex(_TASK_ID_BYTES)
record = self._attach(TaskRecord(task_id=task_id, kind=kind, command=command))
with self._lock:
self._tasks.append(record)
self._persist()
return record
def candidates(self, task_id_prefix: str) -> list[TaskRecord]:
self._refresh_rehydrated()
needle = task_id_prefix.strip().lower()
if not needle:
return []
with self._lock:
items = list(self._tasks)
return [t for t in items if t.task_id.lower().startswith(needle)]
def get(self, task_id_prefix: str) -> TaskRecord | None:
matches = self.candidates(task_id_prefix)
if len(matches) != 1:
return None
return matches[0]
def running_count(self) -> int:
"""Count in-memory running tasks (no disk merge — safe for hot prompt refresh)."""
with self._lock:
return sum(1 for task in self._tasks if task.status == TaskStatus.RUNNING)
def list_recent(self, n: int = 20) -> list[TaskRecord]:
"""Return up to ``n`` tasks, newer tasks first.
Merges any tasks written to the on-disk store by other REPL sessions
(e.g. a parallel terminal) so the view is always up-to-date across
concurrent sessions that share the same persistence file.
"""
self._refresh_rehydrated()
disk_extras = self._tasks_from_disk()
with self._lock:
items = list(self._tasks)
combined = items + disk_extras
combined.sort(key=lambda t: t.started_at)
return list(reversed(combined[-n:]))
def clear(self) -> None:
with self._lock:
self._tasks.clear()
self._persist()
def __contains__(self, task_id: str) -> bool:
return self.get(task_id) is not None
__all__ = ["TaskRegistry"]