mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-19 10:02:16 +00:00
Telegram does not echo a sendRichMessage's content back in reply_to_message (.text/.caption empty, .api_kwargs None), so replies to rich sends (briefings, the gateway's own rich finals) arrived with no quotable text and the [Replying to: ...] injection was skipped. Remember message_id -> text at send time in a best-effort JSON index (gateway/rich_sent_store.py), and recover it on inbound when text and caption are both empty. Best-effort and no-throw throughout: any failure degrades to prior behavior and never breaks a send or message. Salvaged from #47375 by @x1erra. Dropped the cross-platform run.py reply-prefix rewrite (out of scope; bloated every reply on every platform) and scrubbed a docstring reference to an out-of-repo script. Kept the inbound reply_to logging enrichment used to verify the fix.
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""Local index of text we've sent via ``sendRichMessage`` (Bot API 10.1).
|
|
|
|
Telegram does NOT echo a rich message's content back in ``reply_to_message``
|
|
when a user replies to it (verified: ``.text``/``.caption`` empty,
|
|
``.api_kwargs`` None). So replies to the launchd briefings / any rich send
|
|
arrive with no quotable text and the agent is blind to what was referenced.
|
|
|
|
Fix: remember ``message_id -> text`` at send time, look it up by
|
|
``reply_to_id`` on inbound. This module is the single source of truth for that
|
|
index.
|
|
|
|
Best-effort and dependency-free: every operation swallows errors and degrades
|
|
to a no-op / ``None`` so it can never break a send or an inbound message.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
from typing import Optional
|
|
|
|
_MAX_ENTRIES = 1000
|
|
_MAX_TEXT_CHARS = 2000
|
|
|
|
|
|
def _store_path() -> str:
|
|
home = os.environ.get("HERMES_HOME") or os.path.expanduser("~/.hermes")
|
|
return os.path.join(home, "state", "rich_sent_index.json")
|
|
|
|
|
|
def _key(chat_id, message_id) -> str:
|
|
return f"{chat_id}:{message_id}"
|
|
|
|
|
|
def record(chat_id, message_id, text: Optional[str]) -> None:
|
|
"""Persist ``text`` for ``(chat_id, message_id)``. No-op on any failure."""
|
|
if not text or message_id is None or chat_id is None:
|
|
return
|
|
path = _store_path()
|
|
try:
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
except (FileNotFoundError, ValueError):
|
|
data = {}
|
|
data[_key(chat_id, message_id)] = {
|
|
"t": text[:_MAX_TEXT_CHARS],
|
|
"ts": int(time.time()),
|
|
}
|
|
# Trim oldest by timestamp when over cap.
|
|
if len(data) > _MAX_ENTRIES:
|
|
for k, _ in sorted(
|
|
data.items(), key=lambda kv: kv[1].get("ts", 0)
|
|
)[: len(data) - _MAX_ENTRIES]:
|
|
data.pop(k, None)
|
|
tmp = f"{path}.tmp.{os.getpid()}"
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, ensure_ascii=False)
|
|
os.replace(tmp, path) # atomic; tolerates concurrent writers racing
|
|
except Exception:
|
|
return
|
|
|
|
|
|
def lookup(chat_id, message_id) -> Optional[str]:
|
|
"""Return stored text for ``(chat_id, message_id)`` or ``None``."""
|
|
if message_id is None or chat_id is None:
|
|
return None
|
|
try:
|
|
with open(_store_path(), "r", encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
entry = data.get(_key(chat_id, message_id))
|
|
if isinstance(entry, dict):
|
|
return entry.get("t") or None
|
|
except (FileNotFoundError, ValueError, AttributeError):
|
|
return None
|
|
return None
|