mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(telegram): treat "never checked" identity as stale on a fresh-boot clock
The identity-refresh TTL used 0.0 as the "never checked" sentinel and compared it against time.monotonic(). That epoch is arbitrary and starts near zero on a freshly-booted host, so on CI runners and containers `monotonic() - 0.0` was itself below the TTL — "never checked" read as "checked just now" and the first identity refresh was suppressed for the first 5 minutes of uptime. The stale-handle recovery therefore did nothing on exactly the machines most likely to be freshly booted. Invisible on a long-lived dev box (uptime >> TTL); caught by CI. - Sentinel is now None, meaning never checked and always stale. - Both TTL comparison sites route through _bot_identity_is_fresh(). - Regression test pins the invariant under a faked 12s-uptime clock. Verified by re-running the recheck tests against a simulated 3s-uptime host: green with the fix, and restoring the 0.0 sentinel reproduces the exact CI failure locally.
This commit is contained in:
parent
08130a26f6
commit
d7488a5579
2 changed files with 45 additions and 4 deletions
|
|
@ -759,7 +759,12 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
# pointing at the old handle until something calls getMe again. Every
|
||||
# mention/routing comparison reads _current_bot_username() instead.
|
||||
self._bot_username_observed: Optional[str] = None
|
||||
self._bot_identity_checked_at: float = 0.0
|
||||
# None = never checked. Must NOT be 0.0: these are compared against
|
||||
# time.monotonic(), whose epoch is arbitrary and on a freshly-booted
|
||||
# host starts near zero — so a 0.0 sentinel reads as "checked just
|
||||
# now" and suppresses the first refresh for the first TTL seconds of
|
||||
# uptime.
|
||||
self._bot_identity_checked_at: Optional[float] = None
|
||||
self._bot_identity_refresh_task: Optional[asyncio.Task] = None
|
||||
# Consecutive heartbeat probes that saw queued updates the running
|
||||
# poller is not consuming. get_me() can't see this — the send path is
|
||||
|
|
@ -7873,6 +7878,19 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
continue
|
||||
self._note_bot_username(getattr(candidate, "username", None))
|
||||
|
||||
def _bot_identity_is_fresh(self) -> bool:
|
||||
"""True when identity was re-read within the TTL.
|
||||
|
||||
``None`` means never checked, which is always stale. Do not fold the
|
||||
sentinel into ``0.0``: monotonic clocks have an arbitrary epoch that
|
||||
can legitimately be smaller than the TTL on a freshly-booted host,
|
||||
which would make "never" look like "just now".
|
||||
"""
|
||||
checked_at = getattr(self, "_bot_identity_checked_at", None)
|
||||
if checked_at is None:
|
||||
return False
|
||||
return (time.monotonic() - checked_at) < self._BOT_IDENTITY_TTL_SECONDS
|
||||
|
||||
async def _refresh_bot_identity(self, *, force: bool = False) -> None:
|
||||
"""Re-read the bot's identity from Telegram when the cache may be stale.
|
||||
|
||||
|
|
@ -7883,8 +7901,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
bot = self._bot
|
||||
if bot is None or not callable(getattr(bot, "get_me", None)):
|
||||
return
|
||||
now = time.monotonic()
|
||||
if not force and (now - getattr(self, "_bot_identity_checked_at", 0.0)) < self._BOT_IDENTITY_TTL_SECONDS:
|
||||
if not force and self._bot_identity_is_fresh():
|
||||
return
|
||||
try:
|
||||
me = await asyncio.wait_for(bot.get_me(), self._BOT_IDENTITY_PROBE_TIMEOUT)
|
||||
|
|
@ -8047,7 +8064,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
existing = getattr(self, "_bot_identity_refresh_task", None)
|
||||
if existing is not None and not existing.done():
|
||||
return
|
||||
if (time.monotonic() - getattr(self, "_bot_identity_checked_at", 0.0)) < self._BOT_IDENTITY_TTL_SECONDS:
|
||||
if self._bot_identity_is_fresh():
|
||||
return
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
|
|
|||
|
|
@ -1597,3 +1597,27 @@ def test_clean_bot_trigger_text_strips_the_current_handle():
|
|||
adapter._note_bot_username("new_helper_bot")
|
||||
|
||||
assert adapter._clean_bot_trigger_text("@new_helper_bot ship it") == "ship it"
|
||||
|
||||
|
||||
def test_identity_freshness_does_not_depend_on_host_uptime(monkeypatch):
|
||||
"""A never-checked identity is stale even when monotonic() is near zero.
|
||||
|
||||
time.monotonic() has an arbitrary epoch: on a freshly-booted host (CI
|
||||
runners, containers) it starts near 0. A 0.0 "never checked" sentinel
|
||||
therefore reads as "checked just now" for the first TTL seconds of
|
||||
uptime, suppressing the very first identity refresh — so the stale-handle
|
||||
recovery silently did nothing on exactly the machines most likely to be
|
||||
freshly booted. Caught by CI, invisible on a long-lived dev box.
|
||||
"""
|
||||
adapter = _make_adapter(require_mention=True)
|
||||
adapter._bot = _IdentityBot(cached="old_helper_bot", server="new_helper_bot")
|
||||
|
||||
# Simulate a host that booted 12 seconds ago.
|
||||
monkeypatch.setattr(
|
||||
"plugins.platforms.telegram.adapter.time.monotonic", lambda: 12.0
|
||||
)
|
||||
|
||||
assert adapter._bot_identity_is_fresh() is False
|
||||
|
||||
adapter._note_bot_username("new_helper_bot")
|
||||
assert adapter._bot_identity_is_fresh() is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue