80 lines
2.1 KiB
Python
80 lines
2.1 KiB
Python
|
|
"""
|
||
|
|
Heartbeat writer for the Nexus consciousness loop.
|
||
|
|
|
||
|
|
Call write_heartbeat() at the end of each think cycle to let the
|
||
|
|
watchdog know the mind is alive. The file is written atomically
|
||
|
|
(write-to-temp + rename) to prevent the watchdog from reading a
|
||
|
|
half-written file.
|
||
|
|
|
||
|
|
Usage in nexus_think.py:
|
||
|
|
from nexus.heartbeat import write_heartbeat
|
||
|
|
|
||
|
|
class NexusMind:
|
||
|
|
def think_once(self):
|
||
|
|
# ... do the thinking ...
|
||
|
|
write_heartbeat(
|
||
|
|
cycle=self.cycle_count,
|
||
|
|
model=self.model,
|
||
|
|
status="thinking",
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
DEFAULT_HEARTBEAT_PATH = Path.home() / ".nexus" / "heartbeat.json"
|
||
|
|
|
||
|
|
|
||
|
|
def write_heartbeat(
|
||
|
|
cycle: int = 0,
|
||
|
|
model: str = "unknown",
|
||
|
|
status: str = "thinking",
|
||
|
|
path: Path = DEFAULT_HEARTBEAT_PATH,
|
||
|
|
) -> None:
|
||
|
|
"""Write a heartbeat file atomically.
|
||
|
|
|
||
|
|
The watchdog monitors this file to detect stale minds — processes
|
||
|
|
that are technically running but have stopped thinking (e.g., hung
|
||
|
|
on a blocking call, deadlocked, or crashed inside a catch-all
|
||
|
|
exception handler).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
cycle: Current think cycle number
|
||
|
|
model: Model identifier
|
||
|
|
status: Current state ("thinking", "perceiving", "acting", "idle")
|
||
|
|
path: Where to write the heartbeat file
|
||
|
|
"""
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
data = {
|
||
|
|
"pid": os.getpid(),
|
||
|
|
"timestamp": time.time(),
|
||
|
|
"cycle": cycle,
|
||
|
|
"model": model,
|
||
|
|
"status": status,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Atomic write: temp file in same directory + rename.
|
||
|
|
# This guarantees the watchdog never reads a partial file.
|
||
|
|
fd, tmp_path = tempfile.mkstemp(
|
||
|
|
dir=str(path.parent),
|
||
|
|
prefix=".heartbeat-",
|
||
|
|
suffix=".tmp",
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
with os.fdopen(fd, "w") as f:
|
||
|
|
json.dump(data, f)
|
||
|
|
os.replace(tmp_path, str(path))
|
||
|
|
except Exception:
|
||
|
|
# Best effort — never crash the mind over a heartbeat failure
|
||
|
|
try:
|
||
|
|
os.unlink(tmp_path)
|
||
|
|
except OSError:
|
||
|
|
pass
|