| """Host CPU / RAM sampling for embodied and emotional counterweight. |
| |
| Optional dependency: ``psutil``. If missing or disabled via env, callers get |
| ``ok: false`` and should behave as if load is zero. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import time |
| from typing import Any, Dict, Optional |
|
|
| _MIN_INTERVAL = float(os.getenv("INFJ_HOST_LOAD_INTERVAL_SEC", "2.5")) |
|
|
| _cache: Optional[Dict[str, Any]] = None |
| _last_mono: float = 0.0 |
| _cpu_warmed: bool = False |
|
|
|
|
| def _stress_from_samples(cpu_percent: float, memory_percent: float) -> float: |
| """Map instantaneous CPU% and memory% to a single 0..1 strain scalar.""" |
| c = max(0.0, min(1.0, cpu_percent / 100.0)) |
| m = max(0.0, min(1.0, memory_percent / 100.0)) |
| |
| return max(0.0, min(1.0, c * 0.42 + m * 0.58)) |
|
|
|
|
| def sample_host_load(*, force: bool = False) -> Dict[str, Any]: |
| """ |
| Return a cached snapshot of host load. |
| |
| Keys: |
| ok: bool |
| stress: float 0..1 (only meaningful when ok) |
| cpu_percent, memory_percent: float | None |
| available_mb: float | None |
| """ |
| global _cache, _last_mono, _cpu_warmed |
|
|
| if os.getenv("INFJ_DISABLE_HOST_LOAD", "").strip().lower() in ( |
| "1", |
| "true", |
| "yes", |
| "on", |
| ): |
| return { |
| "ok": False, |
| "stress": 0.0, |
| "cpu_percent": None, |
| "memory_percent": None, |
| "available_mb": None, |
| "reason": "disabled", |
| } |
|
|
| now = time.monotonic() |
| if not force and _cache is not None and now - _last_mono < _MIN_INTERVAL: |
| return _cache |
|
|
| try: |
| import psutil |
| except ImportError: |
| out: Dict[str, Any] = { |
| "ok": False, |
| "stress": 0.0, |
| "cpu_percent": None, |
| "memory_percent": None, |
| "available_mb": None, |
| "reason": "no_psutil", |
| } |
| _cache, _last_mono = out, now |
| return out |
|
|
| try: |
| vm = psutil.virtual_memory() |
| mem_pct = float(vm.percent) |
| avail_mb = float(vm.available) / (1024.0 * 1024.0) |
| if not _cpu_warmed: |
| psutil.cpu_percent(interval=0.05) |
| _cpu_warmed = True |
| cpu_pct = float(psutil.cpu_percent(interval=None)) |
| st = _stress_from_samples(cpu_pct, mem_pct) |
| out = { |
| "ok": True, |
| "stress": st, |
| "cpu_percent": cpu_pct, |
| "memory_percent": mem_pct, |
| "available_mb": avail_mb, |
| } |
| except Exception as exc: |
| out = { |
| "ok": False, |
| "stress": 0.0, |
| "cpu_percent": None, |
| "memory_percent": None, |
| "available_mb": None, |
| "reason": str(exc), |
| } |
| _cache, _last_mono = out, now |
| return out |
|
|
|
|
| def host_stress_01() -> float: |
| """Convenience for plugins: 0..1 strain or 0 if unavailable.""" |
| s = sample_host_load() |
| if not s.get("ok"): |
| return 0.0 |
| return float(s.get("stress", 0.0)) |
|
|
|
|
| def stress_from_cpu_mem(cpu_percent: float, memory_percent: float) -> float: |
| """Exposed for tests — same mapping as :func:`sample_host_load` internals.""" |
| return _stress_from_samples(cpu_percent, memory_percent) |
|
|