Files
hermes-agent/plugins/platforms/discord/recovery.py
T
Teknium 53db597201 simplify(compat): hermes_state — drop 81 re-exports + 3 registry aliases + 3 shims, repoint 45 callers + 60 test files
hermes_state.py: delete every '# noqa: F401 (re-exported...)' import block (hermes_state_common/errors/guard/
readpool/sessions/fts/dbfile/wal/repair/registry + agent.context_compressor _DB_PERSISTED_MARKER_KEY); keep
only the names hermes_state.py itself uses, without noqa.
hermes_state_registry.py: drop get_shared_session_db/release_shared_session_db/close_shared_session_dbs
aliases; every caller (gateway/, tools/, tui_gateway/, cron/, mcp_serve, run_agent, tests) now imports
acquire/release/close_all/release_or_close from hermes_state_registry.
hermes_state_titles.py: drop set_auto_title_if_empty shim (title_generator keeps its getattr fallback).
Re-remove shim-only names restored by 34abf954bd: latest_user_message_row_id (tests call
latest_message_row_id(key, role='user'); role-targeting assertions kept) and get_session_activity (tests
build the snapshot via agent.session_activity.build_activity_snapshot over db.get_session(sid)).
hermes_state_wal._log_once resolves its dedupe sets as module globals instead of via hermes_state;
hermes_state_repair helpers call module globals directly (tests patch hermes_state_repair.<name>).
Frozen updater surface untouched (update_cmd_maint imports only SessionDB from hermes_state).
2026-09-03 13:46:50 -07:00

81 lines
3.4 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:
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(conn, db_label="discord_recovery.db")
conn.executescript("""
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
);
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
);
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,))