Files
Clawd BotandClaude Opus 4.6 ca9b510922 chore: align with upstream openclaw/openclaw and overlay local additions
- Reset master to upstream/main (16,697 commits)
- Overlay 2,271 local-only files (skills, tools, workspace, configs, apps)
- Restore IDENTITY.md and USER.md templates
- Build verified, gateway running, Discord working

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-03 07:40:46 +01:00

368 lines
12 KiB
Python

#!/usr/bin/env python3
import json
import os
import shutil
import subprocess
import time
import uuid
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
from james.lib.log import get_logger
from james.lib.nodes import node_name, node_role, register_node, registry_payload
from james.lib.schema import validate_event
log = get_logger("watcher")
@dataclass
class CpuSample:
total: int
idle: int
def read_cpu_sample() -> CpuSample:
with open("/proc/stat", "r", encoding="utf-8") as handle:
parts = handle.readline().split()
values = list(map(int, parts[1:]))
total = sum(values)
idle = values[3] + values[4] if len(values) > 4 else values[3]
return CpuSample(total=total, idle=idle)
def cpu_percent(prev: CpuSample, cur: CpuSample) -> float:
total_delta = cur.total - prev.total
idle_delta = cur.idle - prev.idle
if total_delta <= 0:
return 0.0
return max(0.0, min(100.0, 100.0 * (total_delta - idle_delta) / total_delta))
def read_mem() -> Tuple[int, int, float]:
mem_total = 0
mem_available = 0
with open("/proc/meminfo", "r", encoding="utf-8") as handle:
for line in handle:
if line.startswith("MemTotal:"):
mem_total = int(line.split()[1])
elif line.startswith("MemAvailable:"):
mem_available = int(line.split()[1])
mem_used = max(0, mem_total - mem_available)
percent = (mem_used / mem_total * 100.0) if mem_total else 0.0
return mem_total, mem_used, percent
def read_disk(path: str = "/") -> Tuple[int, int, float]:
usage = shutil.disk_usage(path)
used = usage.total - usage.free
percent = (used / usage.total * 100.0) if usage.total else 0.0
return usage.total, used, percent
def read_loadavg() -> Tuple[float, float, float]:
return os.getloadavg()
def docker_containers() -> Dict[str, str]:
try:
result = subprocess.run(
["docker", "ps", "-a", "--format", "{{.ID}} {{.Names}} {{.State}}"],
check=True,
capture_output=True,
text=True,
)
except Exception as exc:
log.warning("docker ps failed", error=str(exc))
return {}
containers = {}
for line in result.stdout.splitlines():
parts = line.strip().split(maxsplit=2)
if len(parts) != 3:
continue
_, name, state = parts
containers[name] = state
return containers
def gpu_stats() -> Optional[List[dict]]:
if shutil.which("nvidia-smi") is None:
return None
try:
result = subprocess.run(
[
"nvidia-smi",
"--query-gpu=index,name,utilization.gpu,utilization.memory,memory.total,memory.used,temperature.gpu",
"--format=csv,noheader,nounits",
],
check=True,
capture_output=True,
text=True,
)
except Exception as exc:
log.warning("nvidia-smi failed", error=str(exc))
return None
stats = []
for line in result.stdout.splitlines():
parts = [part.strip() for part in line.split(",")]
if len(parts) < 7:
continue
index, name, util_gpu, util_mem, mem_total, mem_used, temp = parts[:7]
try:
total = float(mem_total)
used = float(mem_used)
vram_percent = round((used / total * 100.0) if total else 0.0, 2)
except ValueError as exc:
log.warning("gpu vram parse error", gpu=index, error=str(exc))
vram_percent = 0.0
stats.append(
{
"index": index,
"name": name,
"util_gpu_percent": float(util_gpu) if util_gpu else 0.0,
"util_mem_percent": float(util_mem) if util_mem else 0.0,
"vram_total_mb": float(mem_total) if mem_total else 0.0,
"vram_used_mb": float(mem_used) if mem_used else 0.0,
"vram_used_percent": vram_percent,
"temp_c": float(temp) if temp else 0.0,
}
)
return stats or None
def redis_url() -> str:
url = os.environ.get("REDIS_URL")
if url:
return url
host = os.environ.get("REDIS_HOST", "127.0.0.1")
port = os.environ.get("REDIS_PORT", "6379")
password = os.environ.get("REDIS_PASSWORD")
auth = f":{password}@" if password else ""
return f"redis://{auth}{host}:{port}/0"
def publish(
r,
channel: str,
event: dict,
rate_limit_window: float,
rate_limit_severities: set,
low_ttl_seconds: int,
) -> None:
validate_event(event)
if rate_limit_window > 0 and event.get("severity") in rate_limit_severities:
key = f"james:rate:{channel}:{event['source']}:{event['type']}:{event['severity']}"
if not r.set(key, "1", nx=True, ex=int(rate_limit_window)):
log.debug("rate-limited", event_id=event.get("id"), channel=channel)
return
r.publish(channel, json.dumps(event))
if low_ttl_seconds > 0 and event.get("severity") == "low":
cache_key = f"james:low:{channel}:{event['source']}:{event['type']}"
r.setex(cache_key, int(low_ttl_seconds), json.dumps(event))
def base_event(event_type: str, severity: str, payload: dict) -> dict:
return {
"id": f"evt-{time.strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}",
"source": node_name(),
"type": event_type,
"timestamp": int(time.time()),
"severity": severity,
"payload": payload,
}
def main() -> int:
interval = float(os.environ.get("JAMES_INTERVAL", "30"))
disk_threshold = float(os.environ.get("JAMES_DISK_WARN", "80"))
mem_threshold = float(os.environ.get("JAMES_MEM_WARN", "85"))
cpu_threshold = float(os.environ.get("JAMES_CPU_WARN", "90"))
gpu_vram_threshold = float(os.environ.get("JAMES_GPU_VRAM_WARN", "90"))
channel = os.environ.get("JAMES_CHANNEL", "james.events.system")
role = node_role()
rate_limit_window = float(os.environ.get("JAMES_RATE_LIMIT_WINDOW", "30"))
rate_limit_severities = {
item.strip()
for item in os.environ.get("JAMES_RATE_LIMIT_SEVERITIES", "low,medium").split(",")
if item.strip()
}
low_ttl_seconds = int(os.environ.get("JAMES_LOW_EVENT_TTL", "300"))
registry_enabled = os.environ.get("JAMES_NODE_REGISTRY", "1").strip().lower() not in {
"0",
"false",
"no",
"off",
}
registry_ttl = int(os.environ.get("JAMES_NODE_REGISTRY_TTL", "180"))
try:
import redis
except Exception as exc: # pragma: no cover - runtime guard
log.error("redis is required", exc_info=True)
return 2
url = redis_url()
try:
r = redis.Redis.from_url(url)
r.ping()
except Exception:
log.error("redis connection failed", exc_info=True)
return 1
log.info("started", channel=channel, interval=interval, role=role)
prev_cpu = read_cpu_sample()
prev_containers = docker_containers()
docker_available = shutil.which("docker") is not None
gpu_available = shutil.which("nvidia-smi") is not None
if registry_enabled:
try:
register_node(r, registry_payload(interval, docker_available, gpu_available), registry_ttl)
except Exception:
log.warning("node registry update failed", exc_info=True)
while True:
time.sleep(interval)
try:
if registry_enabled:
try:
register_node(r, registry_payload(interval, docker_available, gpu_available), registry_ttl)
except Exception:
log.warning("node registry update failed", exc_info=True)
cur_cpu = read_cpu_sample()
cpu = cpu_percent(prev_cpu, cur_cpu)
prev_cpu = cur_cpu
mem_total, mem_used, mem_percent = read_mem()
disk_total, disk_used, disk_percent = read_disk("/")
load1, load5, load15 = read_loadavg()
containers = docker_containers()
running = sum(1 for state in containers.values() if state == "running")
stopped = sum(1 for state in containers.values() if state != "running")
gpus = gpu_stats()
payload = {
"role": role,
"cpu_percent": round(cpu, 2),
"loadavg": [load1, load5, load15],
"mem_total_kb": mem_total,
"mem_used_kb": mem_used,
"mem_used_percent": round(mem_percent, 2),
"disk_total_bytes": disk_total,
"disk_used_bytes": disk_used,
"disk_used_percent": round(disk_percent, 2),
"docker": {
"running": running,
"stopped": stopped,
},
}
if gpus:
payload["gpu"] = gpus
publish(
r,
channel,
base_event("system.metrics", "low", payload),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
if disk_percent >= disk_threshold:
publish(
r,
channel,
base_event(
"disk.high_usage",
"high" if disk_percent >= 90 else "medium",
{"disk_used_percent": round(disk_percent, 2)},
),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
if mem_percent >= mem_threshold:
publish(
r,
channel,
base_event(
"memory.high_usage",
"high" if mem_percent >= 92 else "medium",
{"mem_used_percent": round(mem_percent, 2)},
),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
if cpu >= cpu_threshold:
publish(
r,
channel,
base_event(
"cpu.high_usage",
"high" if cpu >= 95 else "medium",
{"cpu_percent": round(cpu, 2)},
),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
if gpus:
for gpu in gpus:
if gpu.get("vram_used_percent", 0.0) >= gpu_vram_threshold:
publish(
r,
channel,
base_event(
"gpu.vram_high",
"high" if gpu["vram_used_percent"] >= 95 else "medium",
{
"index": gpu.get("index"),
"name": gpu.get("name"),
"vram_used_percent": gpu.get("vram_used_percent"),
"vram_used_mb": gpu.get("vram_used_mb"),
"vram_total_mb": gpu.get("vram_total_mb"),
},
),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
for name, state in containers.items():
prev = prev_containers.get(name)
if prev and prev != state:
event_type = "container.stopped" if state != "running" else "container.running"
log.info("container state changed", container=name, prev=prev, state=state)
publish(
r,
channel,
base_event(
event_type,
"medium",
{"container": name, "state": state},
),
rate_limit_window,
rate_limit_severities,
low_ttl_seconds,
)
prev_containers = containers
except Exception:
log.error("collection loop error", exc_info=True)
if __name__ == "__main__":
raise SystemExit(main())