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

159 lines
5.8 KiB
Python

"""Agent record storage and JSONL-backed registry for tracked local processes."""
from __future__ import annotations
import json
import logging
from collections.abc import Iterable
from dataclasses import asdict, dataclass, field, replace
from datetime import UTC, datetime
from pathlib import Path
from config.constants import OPENSRE_HOME_DIR
logger = logging.getLogger(__name__)
_DEFAULT_REGISTRY_PATH = OPENSRE_HOME_DIR / "agents.jsonl"
@dataclass(frozen=True)
class AgentRecord:
"""Immutable snapshot of a registered local AI agent process."""
name: str
pid: int
command: str
registered_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
source: str = "registered"
waits_on: tuple[int, ...] = ()
provider: str | None = None
def to_dict(self) -> dict[str, object]:
return asdict(self)
@classmethod
def from_dict(cls, data: dict[str, object]) -> AgentRecord:
raw_pid = data["pid"]
raw_waits = data.get("waits_on", [])
pid = int(str(raw_pid))
waits_on = tuple(int(str(p)) for p in raw_waits) if isinstance(raw_waits, list) else ()
raw_provider = data.get("provider")
provider = str(raw_provider) if raw_provider is not None else None
return cls(
name=str(data["name"]),
pid=pid,
command=str(data["command"]),
registered_at=str(data.get("registered_at", datetime.now(UTC).isoformat())),
source=str(data.get("source", "registered")),
waits_on=waits_on,
provider=provider,
)
def add_waits_on(self, record: AgentRecord) -> AgentRecord:
"""Create a new record instead mutating to maintain the immutable contract."""
if record.pid in self.waits_on:
return self
return replace(self, waits_on=(*self.waits_on, record.pid))
class AgentRegistry:
"""JSONL-backed registry of locally running AI agent processes.
Persists to ``~/.opensre/agents.jsonl`` by default. The file is
append-only for ``register`` and fully rewritten on ``forget`` / ``clear``.
"""
def __init__(self, path: Path | None = None) -> None:
self._path = path or _DEFAULT_REGISTRY_PATH
self._records: dict[int, AgentRecord] = {}
self._load_from_disk()
def register(self, record: AgentRecord) -> None:
overwrite = record.pid in self._records
self._records[record.pid] = record
if overwrite:
self._rewrite()
else:
self._append(record)
def forget(self, pid: int) -> AgentRecord | None:
removed = self._records.pop(pid, None)
if removed is not None:
self._scrub_waits_on({pid})
self._rewrite()
return removed
def forget_many(self, pids: Iterable[int]) -> list[AgentRecord]:
"""Remove every PID in ``pids`` and rewrite the JSONL file once.
Functionally equivalent to calling :meth:`forget` in a loop,
but does only **one** disk rewrite at the end. Use this when
pruning many records at once (e.g. the boot-time sweep) so a
registry holding N dead PIDs doesn't trigger N full rewrites.
Returns the records that were actually removed (silently
skips PIDs not present in the registry).
"""
removed: list[AgentRecord] = []
for pid in pids:
record = self._records.pop(pid, None)
if record is not None:
removed.append(record)
if removed:
self._scrub_waits_on({r.pid for r in removed})
self._rewrite()
return removed
def _scrub_waits_on(self, removed_pids: set[int]) -> None:
"""Drop ``removed_pids`` from every remaining record's ``waits_on``."""
for pid, record in self._records.items():
if not record.waits_on:
continue
left = tuple(p for p in record.waits_on if p not in removed_pids)
if len(left) != len(record.waits_on):
self._records[pid] = replace(record, waits_on=left)
def list(self) -> list[AgentRecord]:
return list(self._records.values())
def get(self, pid: int) -> AgentRecord | None:
return self._records.get(pid)
def clear(self) -> None:
self._records.clear()
self._rewrite()
def _load_from_disk(self) -> None:
if not self._path.exists():
return
try:
lines = self._path.read_text(encoding="utf-8").strip().splitlines()
except OSError:
logger.warning("Failed to read agent registry from %s", self._path)
return
for line in lines:
try:
data = json.loads(line)
record = AgentRecord.from_dict(data)
self._records[record.pid] = record
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
logger.warning("Skipping corrupt agent registry line: %s", line[:80])
def _append(self, record: AgentRecord) -> None:
try:
self._path.parent.mkdir(parents=True, exist_ok=True)
with self._path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(record.to_dict()) + "\n")
except OSError:
logger.warning("Failed to append to agent registry at %s", self._path)
def _rewrite(self) -> None:
try:
self._path.parent.mkdir(parents=True, exist_ok=True)
tmp = self._path.with_name(self._path.name + ".tmp")
with tmp.open("w", encoding="utf-8") as fh:
for record in self._records.values():
fh.write(json.dumps(record.to_dict()) + "\n")
tmp.replace(self._path)
except OSError:
logger.warning("Failed to rewrite agent registry at %s", self._path)