53db597201
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).
81 lines
3.4 KiB
Python
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,))
|