feat(hands,jobs): watchdog ring with a neighbour agent, daily heartbeat and silence alarm

This commit is contained in:
hh committed 2026-09-22 14:21:51 +00:00
1 parent 0475e09c81
commit f6cb17acaf
9 files changed
+1029

No files matched your search

+61
View File
@@ -204,6 +204,67 @@ publishes no reply, so the only voice in it is the `say` tool.
Switches: `досуг: выкл` as the first line of the file for every day, and
`LEISURE=off` in `.env` to drop the job entirely.
## The watchdog ring (`beaver_agent/jobs/heartbeat.py`)
An agreement with a foreign agent on someone else's home server: both print
one line a day into a vibegram room of their own, and both watch the other's
silence. If the neighbour goes quiet past the threshold, *their* human is the
one who needs to hear about it, so the alarm goes to the master thread as a
spawned triage job and the dispatcher decides whether to wake the owner.
The room is a **second, independent** `Vibegram` client (`VIBEGRAM_HB`), not
the one the dispatcher's `vibegram` tool and the triage job share. `/api/pending`
is a cursor on the hub's side: one client for both rooms would mean the triage
job eats the heartbeats, and the heartbeats drown in conversation. The model
gets no tool for this room - the watchdog is a mechanism.
The point of a watchdog is that it does not lie: an instrument that quietly
fails to watch is worse than no instrument, because it looks healthy. So there
are no defaults anywhere in it.
- `отметка` (17:12 local) prints `hb <nick> at=… boot=… every=… src=…`. The
period is not a constant: it is derived from that job's own registered cron
(`run.job.cron`). A schedule that does not reduce to "every N hours" gets
`every=unknown src=none` - lying with a number is worse than refusing.
- `сторож` (17:42 local) asks for an explicit window (`pending(limit=25)`),
finds the newest `hb claude-kai …`, and compares its age against
`every * 1.5` where `every` comes **from the neighbour's own line**. The
clock it trusts is `seen` - when *we* saw the line - not the `at=` inside it,
which is the neighbour's statement about himself.
- Every run prints `ack claude-kai last=… seen=… res=… win=… armed=… thr=…`
into the room. Anything the run could not measure says so out loud there:
`every=unknown` in his line falls back to the last known period and marks the
threshold `thr=36h-fallback`; a period never read once is `thr=unknown` and
no alarm is taken.
- Never having seen a beat is not an alarm, it is `armed=no` - there is nothing
to watch yet. Seven days of `armed=no` in a row tells the human once, so an
unfinished ring does not quietly become permanent.
- If the window's highest event id went **backwards** since the last run, the
window is looking at the past: `win=stale`, and an alarm in that run is
forbidden. Same for a hub that did not answer at all.
- The alarm names all three possible causes - the neighbour's machine, his
cron, or the channel (the room itself) - and if our own write into the room
went through in that same run while neither `hb` nor `ack` came back from
him, it says to suspect the channel before the neighbour's death. It repeats
at most once a day, not once a run.
State between runs is `мета/бобер/.состояние/сторож.json`; a lost file means
the ring disarms itself and starts the seven-day count again.
Setup is two variables in `.env` and one manual step:
```bash
# join the watchdog room with the invite code the neighbour sends
python3 scripts/vibegram_join.py <join-code> --nick <nick>
# put the token into .env as VIBEGRAM_HB_TOKEN, then
docker compose restart gateway
```
`VIBEGRAM_HB_NICK` is optional - without it the nick is `VIBEGRAM_NICK`. With
no `VIBEGRAM_HB_TOKEN` both jobs do nothing and the log says once at startup
that the watchdog is not configured. The neighbour's nick is the `PEER`
constant in `beaver_agent/hands/heartbeat.py`.
## Policy (`beaver_agent/policy.py`)
`bypassPermissions` everywhere; the boundary is `PreToolUse` rules declared