mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Resume each configured Discord channel/thread after its last scanned message so busy sources cannot permanently starve later history windows.
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""Durable state for Discord reconnect message recovery."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import logging
|
|
import os
|
|
import sqlite3
|
|
import threading
|
|
from contextlib import suppress
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DB_FILENAME = "discord_message_recovery.db"
|
|
_RETENTION_DAYS = 30
|
|
|
|
|
|
class DiscordRecoveryStore:
|
|
"""Small profile-scoped SQLite ledger for completed Discord messages."""
|
|
|
|
def __init__(self, hermes_home: Path | None = None) -> None:
|
|
self._lock = threading.Lock()
|
|
self._initialized = False
|
|
self._hermes_home = Path(hermes_home or get_hermes_home())
|
|
|
|
def path(self) -> Path:
|
|
directory = self._hermes_home / "gateway"
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
return directory / _DB_FILENAME
|
|
|
|
def call(self, fn: Callable[[sqlite3.Connection], Any], default: Any = None) -> Any:
|
|
try:
|
|
with self._lock:
|
|
path = self.path()
|
|
conn = sqlite3.connect(path, timeout=0.1)
|
|
try:
|
|
if not self._initialized:
|
|
self._initialize(conn)
|
|
self._initialized = True
|
|
with suppress(OSError):
|
|
os.chmod(path, 0o600)
|
|
result = fn(conn)
|
|
conn.commit()
|
|
return result
|
|
finally:
|
|
conn.close()
|
|
except Exception as exc:
|
|
logger.warning("Discord recovery ledger unavailable: %s", exc)
|
|
return default
|
|
|
|
def _initialize(self, conn: sqlite3.Connection) -> None:
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS discord_messages (
|
|
message_id TEXT PRIMARY KEY,
|
|
channel_id TEXT,
|
|
thread_id TEXT,
|
|
parent_channel_id TEXT,
|
|
author_id TEXT,
|
|
created_at TEXT,
|
|
status TEXT NOT NULL,
|
|
replied INTEGER NOT NULL DEFAULT 0,
|
|
emoji_ack INTEGER NOT NULL DEFAULT 0,
|
|
outage_response INTEGER NOT NULL DEFAULT 0,
|
|
response_message_id TEXT,
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
last_attempt_at TEXT,
|
|
last_error TEXT,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS discord_recovery_scans (
|
|
scan_id TEXT PRIMARY KEY,
|
|
started_at TEXT NOT NULL,
|
|
completed_at TEXT,
|
|
status TEXT NOT NULL,
|
|
channels TEXT NOT NULL,
|
|
window_seconds REAL NOT NULL,
|
|
limit_count INTEGER NOT NULL,
|
|
scanned INTEGER NOT NULL DEFAULT 0,
|
|
missed INTEGER NOT NULL DEFAULT 0,
|
|
dispatched INTEGER NOT NULL DEFAULT 0,
|
|
error TEXT
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS discord_recovery_cursors (
|
|
channel_id TEXT PRIMARY KEY,
|
|
last_message_id TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
cutoff = (
|
|
dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=_RETENTION_DAYS)
|
|
).isoformat()
|
|
conn.execute("DELETE FROM discord_messages WHERE updated_at < ?", (cutoff,))
|
|
conn.execute(
|
|
"DELETE FROM discord_recovery_scans "
|
|
"WHERE COALESCE(completed_at, started_at) < ?",
|
|
(cutoff,),
|
|
)
|
|
conn.execute(
|
|
"DELETE FROM discord_recovery_cursors WHERE updated_at < ?",
|
|
(cutoff,),
|
|
)
|