fix(hands): watchdog seen moves only on a new peer beat, tracked by message id

This commit is contained in:
hh committed 2026-09-27 18:20:50 +00:00
1 parent a39e0b27af
commit 78a0293819
2 files changed
+94 -7

No files matched your search

+38 -7
View File
@@ -20,9 +20,12 @@
«что нового с курсора»: курсор отдаёт строку соседа один раз, и следующий
прогон на живой комнате увидел бы пустое окно.
Опора тревоги - `seen`, то есть когда Я увидел строку: `at=` внутри строки
это показание соседа о себе, и остановившиеся у него часы не должны
выглядеть как живая цепь.
Опора тревоги - `seen`, то есть когда Я ВПЕРВЫЕ увидел строку: `at=` внутри
строки это показание соседа о себе, и остановившиеся у него часы не должны
выглядеть как живая цепь. Окно - это последние сообщения комнаты, поэтому
одна и та же отметка видна в нём много прогонов подряд, и новой её делает
новый id сообщения, а не текст: двигать `seen` на старой отметке значит
никогда не поднять тревогу в тихой комнате.
"""
from __future__ import annotations
@@ -264,6 +267,10 @@ class Watch:
"""Последний период, который сосед назвал читаемым числом."""
every_unknown: bool = False
"""В последней отметке стояло `every=unknown`: порог взят с запасом назад."""
beat_id: int = 0
"""id сообщения с последней зачтённой отметкой: по нему видно, что отметка
в окне новая, а не та же. Нуль - поля в файле не было (состояние старое)
или отметок не видели ни разу."""
top: int = 0
"""Максимальный id, который мы видели. Поехал назад - окно смотрит в прошлое."""
alerted: datetime | None = None
@@ -285,6 +292,7 @@ class Watch:
seen=_parse_iso(raw.get("seen")),
every=timedelta(seconds=every) if every else None,
every_unknown=bool(raw.get("every_unknown")),
beat_id=int(raw.get("beat_id", 0)),
top=int(raw.get("top", 0)),
alerted=_parse_iso(raw.get("alerted")),
unarmed_since=_parse_iso(raw.get("unarmed_since")),
@@ -298,6 +306,7 @@ class Watch:
"seen": _iso(self.seen),
"every": int(self.every.total_seconds()) if self.every else None,
"every_unknown": self.every_unknown,
"beat_id": self.beat_id,
"top": self.top,
"alerted": _iso(self.alerted),
"unarmed_since": _iso(self.unarmed_since),
@@ -327,6 +336,22 @@ class Report:
"""Строка для лога: почему прогон решил именно так."""
def _is_new_beat(state: Watch, eid: int, beat: Beat) -> bool:
"""Впервые ли мы видим ЭТУ отметку соседа.
Различитель - id сообщения: `at=` внутри строки это показание соседа о
себе, и повторить его он может как угодно. Нуля в `beat_id` два повода:
отметок не видели ни разу (тогда любая - новая) и состояние записано до
появления поля - там единственное, чем можно сверить отметку со зачтённой,
это её `at`, и одного прогона хватает, чтобы запомнить id.
"""
if state.beat_id:
return eid > state.beat_id
if state.seen is None:
return True
return beat.at != state.at
def look(
state: Watch,
events: Sequence[Event],
@@ -350,8 +375,8 @@ def look(
top = max((e.id for e in events), default=None)
stale = top is not None and top < state.top
beats = [
beat
for beat in (parse_beat(e.text) for e in events)
(event.id, beat)
for event, beat in ((event, parse_beat(event.text)) for event in events)
if beat is not None and beat.nick == peer
]
heard = bool(beats) or any(e.nick == peer for e in events)
@@ -360,9 +385,13 @@ def look(
else:
if top is not None:
state.top = max(state.top, top)
again = False
if beats:
last = beats[-1]
state.at, state.seen = last.at, now
eid, last = beats[-1]
again = not _is_new_beat(state, eid, last)
if not again:
state.seen = now
state.at, state.beat_id = last.at, eid
state.every_unknown = last.every is None
if last.every is not None:
state.every = last.every
@@ -371,6 +400,8 @@ def look(
f"отметок соседа в окне: {len(beats)} "
f"(сообщений комнаты: {len(events)} из {window}, {read})"
)
if again:
note += "; отметка та же, что в прошлый прогон - seen не двигаем"
armed = state.seen is not None
threshold = state.every * FACTOR if state.every is not None else None