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).
This commit is contained in:
Teknium
2026-09-03 13:46:50 -07:00
parent a9b0dd6742
commit 53db597201
111 changed files with 558 additions and 629 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ def _surface(layer: str, code: str, retryable: bool, provider: str = "", model:
def _disk_full(candidate: Any) -> bool:
try:
from hermes_state import is_disk_full_error
from hermes_state_errors import is_disk_full_error
return bool(is_disk_full_error(candidate))
except Exception: # pragma: no cover - defensive import guard
+2 -4
View File
@@ -242,10 +242,8 @@ def _db_flush_failed(agent, e: Exception, batch_rows: List[Dict[str, Any]], adop
agent._db_flush_scan_prefix = None # full re-scan next flush: an exception mid-loop leaves mixed dispositions
# The only place the SQLite error is visible before it becomes a bare False — classify it so the turn-end
# explanation can distinguish lock contention from disk-full/read-only.
from hermes_state import (
CompressionSessionClosedError, StateDbCorruptError, StateDbReplacedError, classify_persistence_error,
divert_session_transcript_jsonl,
)
from hermes_state import StateDbCorruptError, StateDbReplacedError, classify_persistence_error, divert_session_transcript_jsonl
from hermes_state_errors import CompressionSessionClosedError
agent._last_persistence_error_cause = classify_persistence_error(e)
if isinstance(e, (StateDbReplacedError, StateDbCorruptError)):
# A replaced/quarantined handle will not take this batch again — keep it on disk.
+1 -1
View File
@@ -112,7 +112,7 @@ def _db_path() -> Path:
def _connect() -> sqlite3.Connection:
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
path = _db_path()
path.parent.mkdir(parents=True, exist_ok=True)
+1 -1
View File
@@ -89,7 +89,7 @@ def _transaction() -> Iterator[sqlite3.Connection]:
pass
conn.row_factory = sqlite3.Row
try:
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
conn.execute("PRAGMA busy_timeout=5000")
apply_wal_with_fallback(conn, db_label="cron/deliveries.db")
+1 -1
View File
@@ -44,7 +44,7 @@ def prepare_ledger(
conn: sqlite3.Connection, *, db_label: str, synchronous_full: bool = True
) -> None:
"""Row factory + busy timeout + WAL (with fallback) + optional ``synchronous=FULL``."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout=5000")
+6 -6
View File
@@ -60,7 +60,7 @@ def _close_late_session_db_result(future: "concurrent.futures.Future") -> None:
with contextlib.suppress(Exception):
db = future.result()
if db is not None:
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(db)
@@ -1684,15 +1684,15 @@ def _open_cron_session_db(job: dict):
# timeout proceeds without a session store instead of blocking the run forever.
_session_db_timeout = _get_session_db_timeout()
try:
from hermes_state import get_shared_session_db
from hermes_state_registry import acquire
if _session_db_timeout <= 0:
return get_shared_session_db()
return acquire()
_session_db_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
# Copy the context so a profile run resolves ITS OWN home/state.db on the worker thread
# instead of the process-global default.
_session_db_context = contextvars.copy_context()
_session_db_future = _session_db_pool.submit(_session_db_context.run, get_shared_session_db)
_session_db_future = _session_db_pool.submit(_session_db_context.run, acquire)
try:
return _session_db_future.result(timeout=_session_db_timeout)
except concurrent.futures.TimeoutError:
@@ -1884,7 +1884,7 @@ def _final_response_from_result(result: dict, job_id: str, job_name: str, AIAgen
# Render every persistence-cause variant or cause-refined text slips through.
_explainer_variants = []
try:
from hermes_state import PERSISTENCE_ERROR_CAUSES as _causes
from hermes_state_errors import PERSISTENCE_ERROR_CAUSES as _causes
except Exception:
_causes = ("locked", "disk", "unknown")
for _cause in (None, *_causes):
@@ -1982,7 +1982,7 @@ def _finalize_cron_session(session_db, agent, job_id: str, job_name: str, cron_s
except (Exception, KeyboardInterrupt) as e:
logger.debug("Job '%s': failed to end session: %s", job_id, e)
try:
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(_session_db)
except (Exception, KeyboardInterrupt) as e:
logger.debug("Job '%s': failed to close SQLite session store: %s", job_id, e)
+2 -2
View File
@@ -342,8 +342,8 @@ def _entries_from_origins(platform_name: str, source: str, origins_fn) -> List[D
def _build_from_sessions_db(platform_name: str) -> List[Dict[str, str]]:
"""Pull channels/contacts from state.db gateway session rows."""
def _origins() -> Iterable[Tuple[Dict[str, Any], Any]]:
from hermes_state import get_shared_session_db, release_or_close
db = get_shared_session_db()
from hermes_state_registry import acquire, release_or_close
db = acquire()
try:
lister = getattr(db, "list_gateway_sessions", None)
if not callable(lister):
+1 -1
View File
@@ -62,7 +62,7 @@ def _connect() -> sqlite3.Connection:
def _initialize_schema(conn: sqlite3.Connection) -> None:
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(conn, db_label="state.db (delivery_ledger)")
conn.execute(
"""CREATE TABLE IF NOT EXISTS delivery_obligations (
+1 -1
View File
@@ -106,7 +106,7 @@ class HostedRoomPolicyCheckpoint:
conn.execute(ddl)
def _connect(self) -> sqlite3.Connection:
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path, timeout=10)
conn.row_factory = sqlite3.Row
+1 -1
View File
@@ -113,7 +113,7 @@ def connect(
from the journal-mode pragma is retried (it may ignore the busy timeout while another
first opener initializes the DB, especially on Windows).
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
path = Path(db_path)
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path, timeout=10)
+4 -4
View File
@@ -74,8 +74,8 @@ def _find_session_id(platform: str, chat_id: str, thread_id: Optional[str] = Non
for pre-migration databases.
"""
try:
from hermes_state import get_shared_session_db, release_or_close
db = get_shared_session_db()
from hermes_state_registry import acquire, release_or_close
db = acquire()
try:
finder = getattr(db, "find_session_by_origin", None)
session_id = finder(platform=platform, chat_id=chat_id, thread_id=thread_id, user_id=user_id) if callable(finder) else None
@@ -117,9 +117,9 @@ def _find_session_id(platform: str, chat_id: str, thread_id: Optional[str] = Non
def _append_to_sqlite(session_id: str, message: dict) -> None:
"""Append a message to the SQLite session database."""
try:
from hermes_state import get_shared_session_db, release_or_close
from hermes_state_registry import acquire, release_or_close
db = get_shared_session_db()
db = acquire()
try:
db.append_message(session_id=session_id, role=message.get("role", "assistant"), content=message.get("content"))
finally:
+1 -1
View File
@@ -685,7 +685,7 @@ class ResponseStore:
self._conn = sqlite3.connect(":memory:", check_same_thread=False)
self._db_path = None
# Shared WAL-fallback so response_store.db degrades gracefully on NFS/SMB/FUSE homes.
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(self._conn, db_label="response_store.db")
self._conn.execute(
"CREATE TABLE IF NOT EXISTS responses ("
@@ -80,7 +80,7 @@ class RunIdempotencyStore:
"process memory, so replay will not survive a restart: %s", exc)
self._conn = sqlite3.connect(":memory:", check_same_thread=False)
self._db_path = None
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(self._conn, db_label="runs_idempotency.db")
self._conn.execute(
"""CREATE TABLE IF NOT EXISTS run_idempotency (
+6 -5
View File
@@ -3629,7 +3629,8 @@ class GatewayRunner(
after recording that recoverable state so ``__init__`` can record ``_session_db_init_error`` for the
#88235 broadcast.
"""
from hermes_state import AsyncSessionDB, _default_db_path, get_shared_session_db
from hermes_state import AsyncSessionDB, _default_db_path
from hermes_state_registry import acquire
from gateway.session_db_recovery import RecoverableHandleCache
path = Path(_default_db_path())
cache = getattr(self, "_session_db_handle_cache", None)
@@ -3659,7 +3660,7 @@ class GatewayRunner(
# Store handle unavailable: opening our own would resurrect the duplicate borrowed away.
raise RuntimeError("SessionStore SQLite handle unavailable")
try:
return AsyncSessionDB(get_shared_session_db())
return AsyncSessionDB(acquire())
except Exception as exc:
logger.warning("SQLite session store not available: %s", exc)
raise
@@ -3698,7 +3699,7 @@ class GatewayRunner(
return
# Shared instances no-op on close() (the registry owns the lifecycle). Release the refcount
# instead (#90837).
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
try:
release_or_close(inner)
except Exception as exc:
@@ -4447,10 +4448,10 @@ def _housekeeping_auto_archive() -> None:
"""Stale-session auto-archive on a live timer (the startup hook fires once); maybe_auto_archive()
is gated by sessions.min_interval_hours. Opens its own SessionDB — SQLite connections are thread-bound."""
from hermes_cli.config import load_config as _load_full_config
from hermes_state import get_shared_session_db, release_or_close
from hermes_state_registry import acquire, release_or_close
_sess_cfg = (_load_full_config().get("sessions") or {})
if _sess_cfg.get("auto_archive", False):
_adb = get_shared_session_db()
_adb = acquire()
try:
_adb.maybe_auto_archive(
idle_days=float(_sess_cfg.get("auto_archive_days", 3)),
+2 -2
View File
@@ -1656,8 +1656,8 @@ class GatewayShutdownMixin:
# Shared SessionDB instances still held by the process-wide registry (tools, cron, mirror).
# This is the safety net that guarantees no WAL write lock survives past gateway shutdown
# (#90837).
from hermes_state import close_shared_session_dbs
closed = close_shared_session_dbs()
from hermes_state_registry import close_all
closed = close_all()
if closed:
logger.debug("Closed %d shared SessionDB instance(s) at shutdown", closed)
+4 -3
View File
@@ -51,13 +51,14 @@ class SessionPersistenceMixin:
Resolving here rather than once in ``__init__`` is the whole fix for #88532: it lets the scoping
that the multiplexed inbound path already performs actually reach session storage.
"""
from hermes_state import _default_db_path, get_shared_session_db
from hermes_state import _default_db_path
from hermes_state_registry import acquire
path = Path(db_path) if db_path is not None else Path(_default_db_path())
def _open():
try:
return get_shared_session_db(path) # process-wide registry: one writer per path
return acquire(path) # process-wide registry: one writer per path
except Exception as e:
if not _is_live_system_guard(e):
print(f"[gateway] Warning: SQLite session store unavailable, falling back to JSONL: {e}")
@@ -195,7 +196,7 @@ class SessionPersistenceMixin:
would strand secondary profiles' handles with their WAL lock held ('database is locked' on
restart). Drained under the lock, closed outside it; a pinned handle is the pinner's."""
def _close(db) -> None:
from hermes_state import release_or_close # shared instances no-op on close()
from hermes_state_registry import release_or_close # shared instances no-op on close()
try:
release_or_close(db)
except Exception as exc:
+2 -3
View File
@@ -243,9 +243,8 @@ class SessionTranscriptMixin:
try:
self._append_transcript_message(session_id, msg)
except Exception as exc:
from hermes_state import (
CompressionSessionClosedError, StateDbCorruptError, StateDbReplacedError,
)
from hermes_state import StateDbCorruptError, StateDbReplacedError
from hermes_state_errors import CompressionSessionClosedError
if isinstance(exc, (StateDbReplacedError, StateDbCorruptError)):
self._divert_transcript_after_db_replaced(session_id, queue_session_id, exc)
return
+3 -3
View File
@@ -209,8 +209,8 @@ def recover_pending_to_db(session_db=None) -> int:
return 0
own_db = session_db is None
if own_db:
from hermes_state import get_shared_session_db
session_db = get_shared_session_db()
from hermes_state_registry import acquire
session_db = acquire()
recovered = 0
try:
for path in flush_files:
@@ -224,7 +224,7 @@ def recover_pending_to_db(session_db=None) -> int:
finally:
if own_db: # shutdown cancellation/interrupt must not strand an owned DB
with contextlib.suppress(Exception):
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(session_db)
if recovered:
logger.info("Recovered %d pending message(s) from shutdown flush", recovered)
+3 -3
View File
@@ -612,16 +612,16 @@ class GatewayStatusCommandsMixin:
days = int(flag) if flag.isdigit() else days
i += 1
try:
from hermes_state import get_shared_session_db
from hermes_state_registry import acquire
from agent.insights import InsightsEngine
def _run_insights():
db = get_shared_session_db()
db = acquire()
try:
engine = InsightsEngine(db)
return engine.format_gateway(engine.generate(days=days, source=source))
finally:
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(db)
# Not a bare hop: ``SessionDB()`` resolves ``get_hermes_home()`` at call time, a
+2 -1
View File
@@ -709,7 +709,8 @@ def _sessions_repair(_engine: HermesConsoleEngine, args: list[str]) -> None:
ns = _parse(
"sessions repair", args, (("--check-only",), dict(action="store_true")),
(("--no-backup",), dict(action="store_true")))
from hermes_state import DEFAULT_DB_PATH, _db_opens_cleanly, repair_state_db_schema
from hermes_state import DEFAULT_DB_PATH
from hermes_state_repair import _db_opens_cleanly, repair_state_db_schema
db_path = DEFAULT_DB_PATH
if not db_path.exists():
print(f"No session database at {db_path} (nothing to repair).")
+3 -3
View File
@@ -171,7 +171,7 @@ def _repair_state_db(f: Finding, should_fix: bool, state_db_path: Path, kind: st
ok_label, not_fixed_label, failed_issue, fix_hint = _STATE_DB_REPAIRS[kind]
if not should_fix:
return f.issues.append(fix_hint)
from hermes_state import repair_state_db_schema
from hermes_state_repair import repair_state_db_schema
report = repair_state_db_schema(state_db_path)
if not report.get("repaired"):
check_warn(not_fixed_label, f"({report.get('error')}; backup: {report.get('backup_path')})")
@@ -192,7 +192,7 @@ def _state_db_health(f: Finding, should_fix: bool, state_db_path: Path, _DHH: st
check_ok(f"{_DHH}/state.db exists ({_session_count(state_db_path)} sessions)")
# COUNT(*) succeeds even when the FTS index is corrupt and every write fails through the triggers;
# _db_opens_cleanly drives a rolled-back write to surface that.
from hermes_state import _db_opens_cleanly
from hermes_state_repair import _db_opens_cleanly
# `_db_opens_cleanly` now drives a rolled-back write so this otherwise-silent corruption class is
# surfaced (and repaired in place with --fix). See #50502.
_write_reason = _db_opens_cleanly(state_db_path)
@@ -213,7 +213,7 @@ def _state_db_stats(issues: list, state_db_path: Path) -> None:
"""Health/stats snapshot: strictly read-only (mode=ro) so it is safe against a live DB held by
the gateway; any failure degrades to one info line rather than failing doctor."""
with warn_on_error("state.db stats unavailable ({e})", "", report=lambda t, _d: check_info(t)):
from hermes_state import collect_state_db_stats, count_db_holders
from hermes_state_dbfile import collect_state_db_stats, count_db_holders
rows = _render_state_db_stats(collect_state_db_stats(state_db_path), holders=count_db_holders(state_db_path))
for _kind, _text, _detail in rows:
if _kind != "warn":
+7 -7
View File
@@ -231,12 +231,12 @@ def _maybe_checkpoint_wal(conn: sqlite3.Connection, db_path: Path) -> None:
key = str(db_path)
now = time.monotonic()
with _WAL_CHECKPOINT_LOCK:
last = _kb._LAST_WAL_CHECKPOINT.get(key)
last = _LAST_WAL_CHECKPOINT.get(key)
if last is not None and (now - last) < _WAL_CHECKPOINT_INTERVAL_SECONDS:
return
# Claim the slot first so concurrent same-process ticks don't
# double-checkpoint on the boundary.
_kb._LAST_WAL_CHECKPOINT[key] = now
_LAST_WAL_CHECKPOINT[key] = now
try:
row = conn.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone()
_kb._log.debug(
@@ -582,7 +582,7 @@ def repair_db(db_path: Optional[Path] = None, *, board: Optional[str] = None) ->
if _missing_or_empty(resolved):
return RepairResult(status="missing", db_path=resolved)
with _kb._cross_process_init_lock(resolved):
with _cross_process_init_lock(resolved):
messages, reason = _probe_for_corruption(resolved)
if messages is None:
# Same quarantine the connect-time guard takes when sqlite
@@ -641,8 +641,8 @@ def _open_configured(path: Path, under_lock) -> tuple[sqlite3.Connection, Any]:
conn.row_factory = sqlite3.Row
with _INIT_LOCK:
# WAL doesn't work on network filesystems; the helper falls back to
# DELETE with one ERROR log (see hermes_state._WAL_INCOMPAT_MARKERS).
from hermes_state import apply_wal_with_fallback
# DELETE with one ERROR log (see hermes_state_wal._WAL_INCOMPAT_MARKERS).
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(conn, db_label=f"kanban.db ({path.name})")
# FULL (not NORMAL): fsync before each checkpoint to narrow the
# crash window that can leave a b-tree page header torn.
@@ -699,7 +699,7 @@ def connect(db_path: Optional[Path] = None, *, board: Optional[str] = None) -> s
path,
)
with _kb._cross_process_init_lock(path):
with _cross_process_init_lock(path):
# Read-only file/sidecar preflight first, so a stray read-only kanban.db
# fails actionably instead of "attempt to write a readonly database".
# See #12508.
@@ -1187,5 +1187,5 @@ def write_txn(conn: sqlite3.Connection, *, allow_nested: bool = False):
# Late-bound origin namespace (see module docstring); imported LAST so this
# module is fully populated before ``kanban_db`` re-exports from it.
# module is fully populated before ``kanban_db`` imports from it.
from hermes_cli import kanban_db as _kb # noqa: E402
+1 -1
View File
@@ -123,7 +123,7 @@ def connect(db_path: Optional[Path] = None) -> sqlite3.Connection:
conn = sqlite3.connect(str(path))
try:
conn.row_factory = sqlite3.Row
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(conn, db_label="projects.db")
conn.execute("PRAGMA foreign_keys=ON")
+4 -2
View File
@@ -16,7 +16,9 @@ from contextlib import contextmanager
from pathlib import Path
from typing import Any, Callable, Iterator, Optional
from hermes_state import (FTS_STORAGE_VERSION, SCHEMA_VERSION, SessionDB, _db_opens_cleanly)
from hermes_state import SessionDB
from hermes_state_common import FTS_STORAGE_VERSION, SCHEMA_VERSION
from hermes_state_repair import _db_opens_cleanly
ProgressCallback = Callable[[dict[str, Any]], None]
@@ -180,7 +182,7 @@ def _copy_source_bundle(source: Path, snapshot_dir: Path) -> tuple[Path, list[st
The whole copy runs inside ``offline_file_access`` (holds the connection-lifecycle lock). Recovery
normally runs as its own CLI process against an offline file, so the refusal should never fire; the
guard keeps this path consistent with ``hermes_state._backup_db_file``.
guard keeps this path consistent with ``hermes_state_repair._backup_db_file``.
Checking for a live connection and *then* copying would be a check/use race: a connection could open in
that window, and the copy's ``close()`` would cancel its POSIX advisory locks -- the failure class
+2 -1
View File
@@ -21,7 +21,8 @@ from hermes_cli.web_deps import late
from hermes_cli.web_models import (
BulkDeleteSessions, SessionImport, SessionOwnerBackfill, SessionPrune, SessionRename)
from hermes_cli.web_routers._common import log as _log, http_failure
from hermes_state import is_malformed_db_error, is_transient_sqlite_error
from hermes_state import is_malformed_db_error
from hermes_state_errors import is_transient_sqlite_error
list_router = APIRouter()
search_router = APIRouter()
+26 -71
View File
@@ -24,73 +24,35 @@ from contextlib import contextmanager
from pathlib import Path
from agent.message_sanitization import _sanitize_surrogates
# Known-durable message marker (run_agent keeps a copy: circular import; a test pins them in sync).
from agent.context_compressor import ( # noqa: F401 (re-exported; tests import it from here)
# Intrinsic persistence marker stamped on message dicts that are known-durable (#92231). One shared
# constant with agent.context_compressor (this module already imports agent.* at module level, and
# context_compressor is a transitive dependency via hermes_state_common). run_agent keeps its own
# predating copy — hermes_state cannot import run_agent (circular) — guarded by
# test_marker_constant_in_sync.
_DB_PERSISTED_MARKER as _DB_PERSISTED_MARKER_KEY,
)
from hermes_constants import get_hermes_home
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, TypeVar, cast
from hermes_state_common import ( # noqa: F401 (re-exported; tests import from hermes_state)
AUTO_VACUUM_MIN_FREELIST_RATIO, _FTS_TRIGGERS, escape_like as _escape_like, FTS_CJK_STALE_KEY,
FTS_REBUILD_DEFERRAL_KEY, FTS_SQL, FTS_STALE_KEY, FTS_STORAGE_VERSION, FTS_TRIGRAM_SQL, LEGACY_FTS_SQL,
LEGACY_FTS_TRIGRAM_SQL, SCHEMA_SQL, SCHEMA_VERSION, stat_db_file_identity as _stat_db_file_identity,
from hermes_state_common import escape_like as _escape_like, stat_db_file_identity as _stat_db_file_identity
from hermes_state_errors import (
_DELETED_WAL_GENERATION_MSG, _DISK_IO_ERROR_MARKER, _STATE_DB_CORRUPT_MSG, _STATE_DB_GENERATION_KEY,
_STATE_DB_REPLACED_MSG, DeletedWalGenerationError, SessionCompressionInProgressError, StateDbCorruptError,
StateDbReplacedError, _is_no_more_rows, classify_persistence_error, is_malformed_db_error,
is_malformed_schema_error,
)
from hermes_state_errors import ( # noqa: F401 (re-exported; the historical import path)
_DELETED_WAL_GENERATION_MSG, _DISK_IO_ERROR_MARKER, _STATE_DB_APPLICATION_ID_OFFSET,
_STATE_DB_CORRUPT_MSG, _STATE_DB_GENERATION_KEY, _STATE_DB_REPLACED_MSG, PERSISTENCE_ERROR_CAUSES,
CompressionSessionBusyError, CompressionSessionClosedError, DeletedWalGenerationError,
SessionCompressionInProgressError, SessionTurnLeaseLostError, StateDbCorruptError,
StateDbReplacedError, _is_no_more_rows, classify_persistence_error, is_disk_full_error,
is_malformed_db_error, is_malformed_schema_error, is_transient_sqlite_error,
from hermes_state_guard import (
_STATE_DB_GUARD_BYPASS_ENV, _in_test_context, _is_production_state_db, _real_platform_state_root,
_set_last_init_error, get_last_init_error,
)
from hermes_state_guard import ( # noqa: F401 (re-exported; tests patch hermes_state.<name>)
_STATE_DB_GUARD_BYPASS_ENV, _in_test_context, _is_production_state_db, _process_looks_like_pytest,
_real_platform_state_root, _running_under_pytest, _set_last_init_error, get_last_init_error,
)
from hermes_state_readpool import ( # noqa: F401 (re-exported; tests import from hermes_state)
_READ_POOL_MAX, _proc_fd_targets, _process_read_permits, _read_budget_for,
)
from hermes_state_sessions import ( # noqa: F401 (re-exported; the historical import path)
SessionSessionsMixin, _cwd_prefix_clause, workspace_key,
)
from hermes_state_fts import SessionFtsSetupMixin, load_fts5_cjk_extension # noqa: F401 (re-exported)
from hermes_state_readpool import _READ_POOL_MAX, _proc_fd_targets, _read_budget_for
from hermes_state_sessions import SessionSessionsMixin
from hermes_state_fts import SessionFtsSetupMixin, load_fts5_cjk_extension
from hermes_state_portability import SessionPortabilityMixin
from hermes_state_telegram import SessionTelegramTopicsMixin
from hermes_state_schema import SessionSchemaMixin
import hermes_state_holders as _state_holders
from hermes_state_dbfile import ( # noqa: F401 (re-exported; tests patch hermes_state.<name>)
_canonical_sqlite_path, _concrete_state_db_holder_pids, _connect_tracked_db,
_is_inactive_orphan_desktop_holder, _read_sqlite_application_id, _stat_sqlite_sidecar_identity, _watched_sqlite_sidecar_paths,
collect_state_db_stats, count_db_holders, is_zeroed_state_db, iter_deleted_sqlite_sidecar_holders,
quarantine_cross_process_lock, quarantine_zeroed_state_db, refuse_deleted_wal_generation,
from hermes_state_dbfile import (
_canonical_sqlite_path, _connect_tracked_db, _read_sqlite_application_id, _stat_sqlite_sidecar_identity,
_watched_sqlite_sidecar_paths, is_zeroed_state_db, quarantine_cross_process_lock, quarantine_zeroed_state_db,
refuse_deleted_wal_generation,
)
from hermes_state_messages import SessionMessagesMixin
from hermes_state_wal import ( # noqa: F401 (re-exported; tests patch hermes_state.<name>)
WalUnsupportedError, _WAL_INCOMPAT_MARKERS, _apply_macos_checkpoint_barrier, _apply_synchronous_pragma,
_database_has_content, _delete_overridden_warned_paths, _enforce_macos_synchronous_full,
_journal_upgrade_warned_paths, _on_disk_journal_mode, _wal_fallback_warned_paths,
_wal_reset_bug_warned_paths, _wal_reset_repair_hint, apply_database_pragmas, apply_wal_with_fallback,
is_sqlite_wal_reset_vulnerable, resolve_journal_mode, resolve_synchronous_level, sqlite_source_id,
)
from hermes_state_repair import ( # noqa: F401 (re-exported; tests patch hermes_state.<name>)
_MAX_MALFORMED_BACKUPS, _MAX_PERSISTENT_REPAIR_ATTEMPTS, _REPAIR_BACKUP_MIN_FREE_BYTES,
_REPAIR_SNAPSHOT_MIN_THROUGHPUT_BYTES_PER_SECOND, _backup_content_identity, _backup_db_file,
_claim_repair_attempt, _connect_repair_durable, _copy_database_snapshot, _cross_process_repair_lock,
_db_fingerprint, _db_opens_cleanly, _existing_malformed_backups, _live_writer_holds_db,
_persistent_repair_attempts_exhausted, _probe_journal_mode_for_repair, _prune_malformed_backups,
_read_repair_ledger, _record_repair_outcome, _release_auto_maintenance_lock,
_repair_backup_headroom_bytes, _repair_ledger_path, _repair_scratch_space_error,
_repair_snapshot_timeout_seconds, _repair_state_db_schema_locked, _restore_journal_mode_after_repair,
_exclusive_repair_db_guard, _run_repair_strategies,
_try_acquire_auto_maintenance_lock, _unlink_db_triple, apply_durability_barriers,
preflight_db_writability, repair_state_db_schema,
)
from hermes_state_wal import _WAL_INCOMPAT_MARKERS, apply_database_pragmas, apply_wal_with_fallback
from hermes_state_repair import _claim_repair_attempt, preflight_db_writability, repair_state_db_schema
from hermes_state_titles import SessionTitlesMixin
from hermes_state_usage import SessionUsageMixin
from hermes_state_maintenance import SessionMaintenanceMixin
@@ -350,23 +312,16 @@ def divert_session_transcript_jsonl(session_id: str, messages) -> "Optional[Path
# Process-wide shared SessionDB registry: long-lived in-process callers share ONE writer
# connection per resolved path via get_shared_session_db(); one-shots use SessionDB() + close().
# connection per resolved path via hermes_state_registry.acquire(); one-shots use SessionDB() + close().
def _foreign_state_db_holders(db_path: Path) -> List[Tuple[int, str]]:
"""Compatibility delegate to the state-holder authority."""
return _state_holders.foreign_state_db_holders(db_path)
# ── Process-wide shared SessionDB registry (#90837) ── The registry itself lives in
# hermes_state_registry.py — a bounded module owning acquisition, generation identity, refcounting,
# retirement, and teardown. These re-exports keep the historical import path (``from hermes_state import
# get_shared_session_db``) working for every call site and test that imports from here. Routing rules (see
# hermes_state_registry for the full lifecycle): - Long-lived in-process callers (gateway, tui_gateway,
# cron, in-process tools) share ONE writer connection per resolved path via get_shared_session_db(). - CLI
# one-shots, recovery flows, and read-only cross-profile opens keep using SessionDB() directly with their
# own close().
from hermes_state_registry import ( # noqa: F401 (re-export)
close_shared_session_dbs, get_shared_session_db, release_or_close,
)
# ── Process-wide shared SessionDB registry (#90837) ── lives in hermes_state_registry.py (acquire /
# release / close_all / release_or_close). Long-lived in-process callers (gateway, tui_gateway, cron,
# in-process tools) share ONE writer connection per resolved path via hermes_state_registry.acquire(); CLI
# one-shots, recovery flows, and read-only cross-profile opens use SessionDB() directly with their own close().
class SessionDB(
@@ -509,8 +464,8 @@ class SessionDB(
self._token_writer_thread: Optional[threading.Thread] = None
self._token_writer_stop = self._token_writer_busy = False
self._token_atexit_hook: Optional[Callable[[], None]] = None
# Opened via get_shared_session_db(): close() releases a refcount instead.
# Set True when this instance is opened via get_shared_session_db(). Makes close() a no-op so the
# Opened via hermes_state_registry.acquire(): close() releases a refcount instead.
# Set True when this instance is opened via hermes_state_registry.acquire(). Makes close() a no-op so the
# registry (not individual callers) controls the connection lifecycle (#90837).
self._shared_registry_owned = False
initialization_complete = False
@@ -1174,7 +1129,7 @@ class SessionDB(
Drains queued token deltas first (the background writer needs the connection). Read-only connections
never request a checkpoint. See #45383.
When this instance is shared (opened via ``get_shared_session_db``), ``close()`` RELEASES one
When this instance is shared (opened via ``hermes_state_registry.acquire``), ``close()`` RELEASES one
refcount instead of tearing down the connection: the registry owns the lifecycle and only closes on
the final release (#90837). This prevents one caller's close from tearing down the writer connection
that other callers in the same process are still using — while still letting legacy ``close()`` call
+3 -3
View File
@@ -1,5 +1,5 @@
"""Shared constants and helpers for the SessionDB family of modules. Lives outside hermes_state so
the mixin modules can import it without a cycle; hermes_state re-exports every name."""
the mixin modules can import it without a cycle."""
import contextlib
import errno
@@ -817,7 +817,7 @@ END;
# structural rebuild (FTS5 'rebuild' or `_recover_stale_fts`'s drop/recreate) must run in ONE at a time —
# concurrent rebuilds corrupted state.db in production. Gates `rebuild_fts()`, `_rebuild_fts_indexes()`,
# `_recover_stale_fts()`; the chunked backfill (`fts_rebuild_step`) is deliberately NOT routed through it (it
# claims progress under SQLite transaction authority). Mirrors `hermes_state._cross_process_repair_lock`:
# claims progress under SQLite transaction authority). Mirrors `hermes_state_repair._cross_process_repair_lock`:
# portable (msvcrt/flock), bounded wait, FAIL CLOSED; orphaned-fd holders (see `_acquire_db_flock`) are broken
# only when provably dead, indeterminate liveness defers. `<db>.fts_rebuild.lock` is distinct from
# `<db>.repair.lock` (offline schema surgery, minutes in VACUUM). Lives here: mixins cannot import hermes_state.
@@ -832,7 +832,7 @@ END;
# `SessionSchemaMixin._rebuild_fts_indexes()` (via `_init_schema`), and
# `SessionSchemaMixin._recover_stale_fts()`. The chunked deferred backfill (`fts_rebuild_step`) is
# deliberately NOT routed through it — it claims progress under `_execute_write`'s SQLite transaction
# authority and is intentionally multi-process. Semantics mirror `hermes_state._cross_process_repair_lock`
# authority and is intentionally multi-process. Semantics mirror `hermes_state_repair._cross_process_repair_lock`
# (the schema- surgery authority): portable (msvcrt on Windows, flock elsewhere), bounded wait, and FAIL
# CLOSED — a caller that cannot acquire the lock must NOT rebuild. The kernel drops both lock types when the
# holder dies — UNLESS a forked child inherited the lock fd (flock rides the open file description, which
+1 -1
View File
@@ -198,7 +198,7 @@ class SessionCompressionMixin:
See #75316.
``None`` = unbounded (no internal flush happened). See #47202.
"""
from hermes_state import CompressionSessionBusyError
from hermes_state_errors import CompressionSessionBusyError
def _do(conn):
if require_lease_refresh and compression_lock_holder:
conn.execute(
+6 -6
View File
@@ -2,10 +2,9 @@
Header probes (application_id / zeroed-file detection), deleted-WAL-sidecar
holder scans, quarantine of zeroed databases, ``collect_state_db_stats`` and
holder-process classification. Every name is re-imported into ``hermes_state``
so ``hermes_state.<name>`` keeps resolving — and tests that monkeypatch it keep
intercepting, because intra-module calls to patched helpers go through a lazy
``from hermes_state import ...`` at call time.
holder-process classification. Helpers that hermes_state itself imports and
calls (``_connect_tracked_db`` & co) are looked up lazily from ``hermes_state`` at
call time, so tests that monkeypatch ``hermes_state.<name>`` keep intercepting.
"""
from __future__ import annotations
@@ -81,7 +80,7 @@ def _pread_db_header(db_path: Path, length: int) -> "Optional[bytes]":
def _read_sqlite_application_id(db_path: Path) -> "Optional[int]":
"""application_id from the SQLite header, via the lock-safe :func:`_pread_db_header`."""
from hermes_state import _STATE_DB_APPLICATION_ID_OFFSET
from hermes_state_errors import _STATE_DB_APPLICATION_ID_OFFSET
end = _STATE_DB_APPLICATION_ID_OFFSET + 4
header = _pread_db_header(db_path, end)
if header is None or len(header) < end or header[:16] != b"SQLite format 3\x00":
@@ -142,7 +141,8 @@ def iter_deleted_sqlite_sidecar_holders(db_path) -> List[Tuple[int, str]]:
def refuse_deleted_wal_generation(db_path) -> None:
"""Raise if any process holds a deleted WAL/SHM generation for *db_path*; called
*before* ``sqlite3.connect`` so a second opener cannot mint a replacement WAL inode."""
from hermes_state import DeletedWalGenerationError, _DELETED_WAL_GENERATION_MSG
from hermes_state import DeletedWalGenerationError
from hermes_state_errors import _DELETED_WAL_GENERATION_MSG
if not iter_deleted_sqlite_sidecar_holders(db_path):
return
logger.error(_DELETED_WAL_GENERATION_MSG)
+2 -1
View File
@@ -147,7 +147,8 @@ class SessionGatewayMixin:
Fails closed: anything whose parent, age, argv, or network connections
cannot be proved safe remains a repair-blocking holder."""
from hermes_state import _concrete_state_db_holder_pids, _is_inactive_orphan_desktop_holder, psutil
from hermes_state import psutil
from hermes_state_dbfile import _concrete_state_db_holder_pids, _is_inactive_orphan_desktop_holder
if not sys.platform.startswith("linux") or psutil is None:
return []
try:
+4 -3
View File
@@ -28,7 +28,7 @@ def _like(value: str) -> str:
def _cwd_prefix_filter(value: str) -> Tuple[List[str], list]:
from hermes_state import _cwd_prefix_clause
from hermes_state_sessions import _cwd_prefix_clause
clause, params = _cwd_prefix_clause(value)
return [clause], list(params)
@@ -107,7 +107,8 @@ class SessionMaintenanceMixin:
def _write_guards_reject(self, conn, sid: str, **kwargs) -> bool:
"""True when a live turn lease / compression lock protects ``sid``; expired or
dead-holder guards are reclaimed and fenced as a side effect."""
from hermes_state import SessionCompressionInProgressError, SessionTurnLeaseLostError
from hermes_state import SessionCompressionInProgressError
from hermes_state_errors import SessionTurnLeaseLostError
try:
self._check_transcript_write_guards(
conn, sid, compression_lock_holder=None, turn_lease_holder=None,
@@ -376,7 +377,7 @@ class SessionMaintenanceMixin:
``request_dump_*``) for pruned sessions are removed as part of the same sweep (issue #3015).
Messaging and UI sources are never touched here. See #54189.
"""
from hermes_state import _release_auto_maintenance_lock, _try_acquire_auto_maintenance_lock
from hermes_state_repair import _release_auto_maintenance_lock, _try_acquire_auto_maintenance_lock
result: Dict[str, Any] = {"skipped": False, "pruned": 0, "closed": 0, "vacuumed": False}
maintenance_lock = _try_acquire_auto_maintenance_lock(self.db_path)
if maintenance_lock is None:
+3 -11
View File
@@ -189,7 +189,8 @@ class SessionMessagesMixin:
#74478 patience note below). User-initiated transcript mutations may opt in to rejecting an active
unowned turn lease in that same transaction.
"""
from hermes_state import CompressionSessionClosedError, SessionCompressionInProgressError, SessionTurnLeaseLostError
from hermes_state import SessionCompressionInProgressError
from hermes_state_errors import CompressionSessionClosedError, SessionTurnLeaseLostError
# NOTE (#75316 redesign): appends do NOT check compression_locks. The lock's job is to stop two
# COMPRESSIONS colliding, not to fence ordinary transcript writes. Concurrent appends during a
# compression are safe by construction: archive_and_compact() commits against a watermark captured
@@ -410,15 +411,6 @@ class SessionMessagesMixin:
(session_id, role, int(offset)))
return row[0] if row else None
def latest_user_message_row_id(self, session_id: str) -> Optional[int]:
"""Row id of the most recent active user message, or ``None``.
The agent's default reaction target: "the message that triggered me",
so the model never has to thread row ids through a tool call (mirrors
the photon adapter's ``_record_last_inbound``).
"""
return self.latest_message_row_id(session_id, role="user")
def get_message_role(self, session_id: str, row_id: int) -> Optional[str]:
"""Role of the active message at *row_id* in *session_id*, or ``None``."""
if not session_id:
@@ -461,7 +453,7 @@ class SessionMessagesMixin:
is inserted as fresh active rows exactly as in the destructive path, so the live view is identical
either way; only the durability of the dropped turns differs.
"""
from hermes_state import CompressionSessionClosedError
from hermes_state_errors import CompressionSessionClosedError
def _do(conn):
if reject_active_turn_lease:
self._check_transcript_write_guards(
-6
View File
@@ -214,12 +214,6 @@ def stats() -> Dict[str, int]:
}
# Backwards-compatible aliases (hermes_state re-exports them).
get_shared_session_db = acquire
release_shared_session_db = release
close_shared_session_dbs = close_all
def release_or_close(db: "SessionDB") -> None:
"""Release a shared instance, or close it when it is not registry-managed. Drop-in for a
plain ``db.close()``: read-only opens, CLI one-shots and test fakes fall back."""
+11 -22
View File
@@ -1,7 +1,6 @@
"""state.db repair, backup and writability preflight (split from hermes_state).
Every name is re-imported into ``hermes_state``; intra-module calls to patchable helpers go through a lazy
``from hermes_state import ...`` at call time so monkeypatches there still intercept.
Patchable helpers are looked up as module globals at call time, so tests patch ``hermes_state_repair.<name>``.
"""
from __future__ import annotations
@@ -291,7 +290,7 @@ def _backup_free_space_error(db_path: Path) -> Optional[str]:
def _repair_snapshot_timeout_seconds(source_path: Path) -> float:
"""Bound one SQLite snapshot by source size incl. sidecars (a WAL can hold committed rows not yet in the
main file), so a healthy large-database copy is not cut off by the repair-lock timeout."""
from hermes_state import _REPAIR_LOCK_TIMEOUT_SECONDS, _REPAIR_SNAPSHOT_MIN_THROUGHPUT_BYTES_PER_SECOND
from hermes_state import _REPAIR_LOCK_TIMEOUT_SECONDS
source_bytes = 0
for candidate in (source_path, *_sidecars(source_path)):
with contextlib.suppress(FileNotFoundError): # a sidecar may vanish mid-walk
@@ -584,15 +583,14 @@ def _connect_repair_durable(db_path: Path, *, timeout: float = 5.0) -> sqlite3.C
def _repair_conn(db_path: Path, *, timeout: float = 5.0):
"""A :func:`_connect_repair_durable` connection as a context manager, closed on exit."""
from hermes_state import _connect_repair_durable as _connect # call-time lookup: tests patch hermes_state.<name>
return contextlib.closing(_connect(db_path, timeout=timeout))
return contextlib.closing(_connect_repair_durable(db_path, timeout=timeout))
def _reapply_durability_barriers(conn: sqlite3.Connection) -> bool:
"""Best-effort (re)application of the macOS write barriers; True if accepted.
Call before ``VACUUM``/``REINDEX`` once the schema parses: a connection opened
on a malformed schema could not take them at open time. Never raises."""
from hermes_state import _apply_macos_checkpoint_barrier, _enforce_macos_synchronous_full
from hermes_state_wal import _apply_macos_checkpoint_barrier, _enforce_macos_synchronous_full
try:
_apply_macos_checkpoint_barrier(conn)
_enforce_macos_synchronous_full(conn)
@@ -605,7 +603,7 @@ def apply_durability_barriers(conn: sqlite3.Connection) -> bool:
"""Durability barriers for guest users of ``state.db`` that must inherit its owner's journal mode. Also
applies the configured ``database.synchronous`` level, a per-connection pragma that otherwise only
rides on the journal-mode setup path guests must not run."""
from hermes_state import _apply_synchronous_pragma
from hermes_state_wal import _apply_synchronous_pragma
ok = _reapply_durability_barriers(conn)
with contextlib.suppress(Exception):
from hermes_cli.config import cfg_get, load_config_readonly # local: avoids an import cycle
@@ -624,8 +622,7 @@ def _close_unpinned(conn: sqlite3.Connection) -> None:
def _open_exclusive(db_path: Path, begin: str) -> sqlite3.Connection:
"""Zero-timeout connection holding ``locking_mode=EXCLUSIVE`` after a rolled-back
*begin*; closed (unpinned) and re-raised when exclusion cannot be taken."""
from hermes_state import _connect_repair_durable as _connect # call-time lookup: tests patch hermes_state.<name>
conn = _connect(db_path, timeout=0.0)
conn = _connect_repair_durable(db_path, timeout=0.0)
try:
for statement in ("PRAGMA locking_mode=EXCLUSIVE", begin, "ROLLBACK"):
conn.execute(statement)
@@ -702,8 +699,7 @@ def _db_opens_cleanly(db_path: Path) -> Optional[str]:
# of entries in index" when a B-tree index (e.g. idx_sessions_handoff_state) falls out of sync with its
# base table. REINDEX rewrites the index b-tree from the canonical table rows using the existing index
# definition, fixing the mismatch without touching data or FTS schema.
from hermes_state import _connect_repair_durable as _connect # call-time lookup: tests patch hermes_state.<name>
conn = _connect(db_path)
conn = _connect_repair_durable(db_path)
try:
with contextlib.closing(conn):
# Best-effort tokenizer load: messages_fts_cjk needs cjk_unicode61 before any statement can touch it;
@@ -766,8 +762,7 @@ def _live_writer_holds_db(db_path: Path) -> bool:
repair is then serialised only by the cross-process repairer lock. Before probing, the foreign-holder scan
(``hermes_state_holders``) fails closed on deleted-WAL-generation, uninspectable, or unknown holders."""
import hermes_state_holders as _state_holders
from hermes_state import _connect_repair_durable as _connect # call-time lookup: tests patch hermes_state.<name>
return _state_holders.live_writer_holds_db(db_path, connect_repair_durable=_connect)
return _state_holders.live_writer_holds_db(db_path, connect_repair_durable=_connect_repair_durable)
def _repair_skip(report: Dict[str, Any], verb: str, error: str, exc: Optional[BaseException] = None) -> Dict[str, Any]:
@@ -794,9 +789,6 @@ def repair_state_db_schema(db_path: Path, *, backup: bool = True) -> Dict[str, A
See #50502.
"""
from hermes_state import (_cross_process_repair_lock, _db_opens_cleanly, _live_writer_holds_db,
_persistent_repair_attempts_exhausted, _probe_journal_mode_for_repair,
_record_repair_outcome, _repair_state_db_schema_locked)
report: Dict[str, Any] = {"repaired": False, "strategy": None, "backup_path": None, "error": None}
# Startup-watchdog lease: repair is I/O-bound (near-zero CPU), which the watchdog's CPU fallback would
# misread as a parked deadlock. One lease (clamped to _MAX_LEASE_S=900) beats per-chunk renewal complexity.
@@ -858,7 +850,7 @@ def _probe_journal_mode_for_repair(db_path: Path) -> Optional[str]:
"""Best-effort journal-mode probe: ``wal``/``delete``, or ``None`` when the file cannot be opened or
probed (malformed header, concurrent opener's locks — both expected on the repair path); callers then
fall back to ``database.journal_mode``."""
from hermes_state import _on_disk_journal_mode
from hermes_state_wal import _on_disk_journal_mode
try:
with _repair_conn(db_path) as conn:
return _on_disk_journal_mode(conn)
@@ -885,7 +877,7 @@ def _restore_journal_mode_after_repair(db_path: Path, before_mode: Optional[str]
The transactional promotion already leaves the destination in its pre-repair mode, so on that path this
is mostly the WAL-companion re-assertion; the reopen is the hazard, not the mode. See #101064.
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
try:
if conn is None:
with _repair_conn(db_path) as owned:
@@ -917,9 +909,6 @@ def _repair_state_db_schema_locked(
so recovery still depends on a human noticing a ``.malformed-backup-*`` file and knowing what to do with
it. Not mutating the original in the first place is the property that holds without a human in the loop.
"""
from hermes_state import (_backup_db_file, _copy_database_snapshot, _db_opens_cleanly, _exclusive_repair_db_guard,
_repair_scratch_space_error, _restore_journal_mode_after_repair, _run_repair_strategies,
_unlink_db_triple)
scratch = db_path.with_name(f"{db_path.name}.repair-scratch")
if (cleanup_error := _unlink_db_triple(scratch)) is not None:
return _repair_skip(report, "aborted", f"could not remove a stale repair snapshot before probing state.db: {cleanup_error}")
@@ -1074,7 +1063,7 @@ _REPAIR_STRATEGIES = (
def _run_repair_strategies(db_path: Path, report: Dict[str, Any]) -> Dict[str, Any]:
"""Escalating repair attempts, applied to *db_path* IN PLACE — only ever a scratch copy nothing else holds open,
never the user's database. The "could not recover" log lives in the caller so it names the user's database."""
from hermes_state import _db_opens_cleanly
for name, body, success_msg, failure_msg in _REPAIR_STRATEGIES:
try:
with _repair_conn(db_path) as conn:
-15
View File
@@ -601,21 +601,6 @@ class SessionSessionsMixin:
("", ActivityProvenance.UNKNOWN.value, session_id), patience_s=self._ACTIVITY_WRITE_PATIENCE_S,
)
def get_session_activity(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Return the durable activity snapshot for *session_id*, or None."""
if not session_id:
return None
row = self.get_session(session_id)
if not row:
return None
from agent.session_activity import build_activity_snapshot
return build_activity_snapshot(
last_activity_at=row.get("last_activity_at"),
last_activity_description=row.get("last_activity_description"),
last_activity_provenance=row.get("last_activity_provenance"),
)
def update_session_meta(
self, session_id: str, model_config_json: str, model: Optional[str] = None,
) -> None:
-4
View File
@@ -127,10 +127,6 @@ class SessionTitlesMixin:
raise ValueError(f"invalid automatic title source: {source!r}")
return self._set_session_title(session_id, title, source=source)
def set_auto_title_if_empty(self, session_id: str, title: str) -> bool:
"""Back-compat shim (third-party plugins reference it by name)."""
return self.set_auto_title(session_id, title, source=self.TITLE_SOURCE_LLM)
def get_session_title(self, session_id: str) -> Optional[str]:
"""Get the title for a session, or None."""
row = self._read_one("SELECT title FROM sessions WHERE id = ?", (session_id,))
+4 -7
View File
@@ -1,7 +1,6 @@
"""SQLite journal-mode and PRAGMA policy for state.db (split from hermes_state).
Every name is re-imported into ``hermes_state``; intra-module calls to patchable helpers go through a lazy
``from hermes_state import ...`` at call time so monkeypatches there still intercept.
Patchable helpers are looked up as module globals at call time, so tests patch ``hermes_state_wal.<name>``.
"""
from __future__ import annotations
@@ -30,7 +29,7 @@ _WAL_INCOMPAT_MARKERS = ("locking protocol", "not authorized", "disk i/o error")
_WAL_SIZE_LIMIT_BYTES = 64 * 1024 * 1024 # 64 MiB
# Once-per-process-per-db_label dedup sets (kanban_db.connect() runs on every kanban operation, so an undeduped
# line would repeat per connection). Tests clear these via ``hermes_state.<name>``; ``_log_once`` resolves them there.
# line would repeat per connection). Tests clear these via ``hermes_state_wal.<name>``.
_wal_fallback_warned_paths: set[str] = set()
_wal_fallback_warned_lock = threading.Lock()
_wal_reset_bug_warned_paths: set[str] = set()
@@ -174,7 +173,7 @@ def _verify_configured_delete(actual: str) -> str:
def apply_wal_with_fallback(conn: sqlite3.Connection, *, db_label: str = "state.db", require_wal: bool = False) -> str:
"""Set ``journal_mode=WAL`` on ``conn``, falling back to DELETE on failure.
Returns the mode actually set. Shared by :class:`SessionDB` and ``hermes_cli.kanban_db.connect``.
Returns the mode actually set. Shared by :class:`SessionDB` and ``hermes_cli.kanban_db_connect.connect``.
WAL-incompatible filesystems either raise ``OperationalError`` ("locking protocol" / "disk I/O error") or —
macOS NFS / SMB / AgentFS — silently refuse and stay in DELETE; either way log ERROR once per process per
``db_label`` and fall back. ``require_wal=True`` raises :class:`WalUnsupportedError` instead. WAL-reset-bug
@@ -193,7 +192,6 @@ def apply_wal_with_fallback(conn: sqlite3.Connection, *, db_label: str = "state.
still documents the WAL-reset bug as real through 3.51.2 with serious consequences. Until a fixed
runtime is delivered, keep new databases out of WAL.
"""
from hermes_state import is_sqlite_wal_reset_vulnerable, resolve_journal_mode
configured = resolve_journal_mode()
# Vulnerable SQLite: never enable WAL on non-WAL files (configured mode resolved first so an explicit DELETE
@@ -397,9 +395,8 @@ _ONCE_LOGS = {
def _log_once(kind: str, db_label: str, *args: Any) -> None:
"""Emit ``_ONCE_LOGS[kind]`` once per (process, db_label). Callable *args* are
resolved only after the dedupe check, so install-method probes run once."""
import hermes_state
lock, set_name, level, message = _ONCE_LOGS[kind]
seen = getattr(hermes_state, set_name)
seen = globals()[set_name]
with lock:
if db_label in seen:
return
+3 -3
View File
@@ -78,8 +78,8 @@ def _close_quietly(db, what: str) -> None:
def _get_session_db():
"""SessionDB instance for reading message transcripts, or None."""
try:
from hermes_state import get_shared_session_db
return get_shared_session_db()
from hermes_state_registry import acquire
return acquire()
except Exception as e:
logger.debug("SessionDB unavailable: %s", e)
return None
@@ -96,7 +96,7 @@ def _load_session_messages(session_id: str):
return None, f"Failed to read messages: {e}"
finally:
try:
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(db)
except Exception:
logger.debug("Failed to close MCP SessionDB", exc_info=True)
+1 -1
View File
@@ -125,7 +125,7 @@ class MemoryStore:
def _init_db(self) -> None:
"""Create schema, enable WAL via the shared fallback helper (NFS/SMB/FUSE degrade gracefully), add hrr_vector to pre-HRR DBs."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
apply_wal_with_fallback(self._conn, db_label="memory_store.db (holographic)")
self._conn.executescript(_SCHEMA)
if "hrr_vector" not in {row[1] for row in self._conn.execute("PRAGMA table_info(facts)").fetchall()}:
+1 -1
View File
@@ -53,7 +53,7 @@ class DiscordRecoveryStore:
return default
def _initialize(self, conn: sqlite3.Connection) -> None:
from hermes_state import apply_wal_with_fallback
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 (
+3 -3
View File
@@ -289,9 +289,9 @@ class AIAgent(
if self._session_db is not None:
return self._session_db
try:
from hermes_state import get_shared_session_db
from hermes_state_registry import acquire
self._session_db = get_shared_session_db()
self._session_db = acquire()
self._owns_session_db = True # we opened it, so close() must release it
return self._session_db
except Exception:
@@ -998,7 +998,7 @@ class AIAgent(
self._owns_session_db = False
# Shared instances no-op on close(); release the refcount so the registry closes on the last caller.
# See #90837.
from hermes_state import release_or_close
from hermes_state_registry import release_or_close
release_or_close(session_db)
def _hydrate_todo_store(self, history: List[Dict[str, Any]]) -> None:
@@ -3,7 +3,8 @@
from types import SimpleNamespace
from agent.conversation_compression import recover_rotated_compression_session
from hermes_state import CompressionSessionClosedError, SessionDB
from hermes_state import SessionDB
from hermes_state_errors import CompressionSessionClosedError
def test_recover_rotated_compression_session_reopens_legacy_orphan(tmp_path):
+5 -5
View File
@@ -99,7 +99,7 @@ if _hermes_home_points_at_production(os.environ.get("HERMES_HOME", "")):
# the child at the same moment the child lost the HERMES_HOME redirect.
# HERMES_TEST_ISOLATION is OUR marker: exported here (before any test module
# imports), inherited by every child by default, and honored by
# hermes_state._running_under_pytest() as a test-context signal. A child
# hermes_state_guard._running_under_pytest() as a test-context signal. A child
# that carries it and still resolves the production state.db fails hard.
# Tests that legitimately need a child to look like a non-test process AND
# open a real DB must export HERMES_STATE_DB_GUARD_BYPASS=1 in that child's
@@ -647,7 +647,7 @@ def _neutralize_macos_keychain_creds(request, monkeypatch):
# ── Kanban write guard (#69283) ─────────────────────────────────────────────
# When hermetic isolation is bypassed (stale checkout, wrong rootdir, direct
# invocation), kanban writes silently pollute the real ~/.hermes. This autouse
# fixture patches ``kanban_db.connect`` to refuse writes whose resolved DB
# fixture patches ``kanban_db_connect.connect`` to refuse writes whose resolved DB
# path lands under the REAL kanban root (captured at import time, before any
# fixture rewires the environment). A deny-list is used instead of an
# allow-list because test-level fixtures legitimately move HERMES_HOME to
@@ -711,8 +711,8 @@ def _kanban_write_guard(_hermetic_environment, monkeypatch):
# doesn't exist yet (AttributeError flake, caught in a full-suite run).
# A half-imported module has no callers yet either — nothing to guard
# this round; the next test's fixture will patch the completed module.
_orig_connect = getattr(_kdb, "connect", None)
if _orig_connect is None:
_orig_connect = getattr(_kdbc, "connect", None)
if _orig_connect is None or getattr(_kdb, "kanban_db_path", None) is None:
return
def _guarded_connect(db_path=None, *args, **kwargs):
@@ -736,7 +736,7 @@ def _kanban_write_guard(_hermetic_environment, monkeypatch):
f"to the real ~/.hermes. See #69283."
)
monkeypatch.setattr(_kdb, "connect", _guarded_connect)
monkeypatch.setattr(_kdbc, "connect", _guarded_connect)
# ── Live state.db write guard ───────────────────────────────────────────────
+2 -2
View File
@@ -58,7 +58,7 @@ def test_run_job_bounds_sessiondb_finalization(tmp_path):
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=_RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls, \
patch("cron.scheduler._cron_cleanup_timeout_seconds", return_value=0.02):
@@ -118,7 +118,7 @@ def test_dispatch_guard_releases_after_sessiondb_finalization_hang(tmp_path):
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=_RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls, \
patch("cron.scheduler._cron_cleanup_timeout_seconds", return_value=0.02), \
+3 -3
View File
@@ -54,7 +54,7 @@ def _run_with_current_provider(job, current_provider, tmp_path):
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -259,7 +259,7 @@ def _run_with_current_provider_and_model(
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -423,7 +423,7 @@ class TestRuntimeResolutionTargetModel:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
side_effect=_capture,
+1 -1
View File
@@ -35,7 +35,7 @@ class TestRunJobRequestOverrides:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("dotenv.load_dotenv"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
+14 -14
View File
@@ -553,7 +553,7 @@ class TestRunJobSessionPersistence:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -611,7 +611,7 @@ class TestRunJobSessionPersistence:
patch("cron.scheduler_delivery._resolve_origin", return_value=None),
patch("hermes_cli.env_loader.load_hermes_dotenv"),
patch("hermes_cli.env_loader.reset_secret_source_cache"),
patch("hermes_state.get_shared_session_db", return_value=fake_db),
patch("hermes_state_registry.acquire", return_value=fake_db),
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -753,7 +753,7 @@ class TestRunJobSessionPersistence:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._preflight_job_config", return_value=None), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -813,7 +813,7 @@ class TestRunJobSessionPersistence:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._preflight_job_config", return_value=None), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -874,7 +874,7 @@ class TestRunJobSessionPersistence:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._preflight_job_config", return_value=None), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -924,7 +924,7 @@ class TestRunJobSessionPersistence:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.reset_secret_source_cache", _record_reset), \
patch("hermes_cli.env_loader.load_hermes_dotenv", _record_load), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -985,7 +985,7 @@ class TestRunJobSessionPersistence:
with patch("cron.scheduler._hermes_home", tmp_path), \
patch("cron.scheduler._preflight_job_config", return_value=None), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -1083,7 +1083,7 @@ class TestRunJobConfigEnvVarExpansion:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1233,7 +1233,7 @@ class TestRunJobConfigEnvVarExpansion:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1278,7 +1278,7 @@ class TestRunJobModelResolution:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1304,7 +1304,7 @@ class TestRunJobModelResolution:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1336,7 +1336,7 @@ class TestRunJobModelResolution:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1361,7 +1361,7 @@ class TestRunJobModelResolution:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=self._RUNTIME), \
patch("run_agent.AIAgent") as mock_agent_cls:
@@ -1406,7 +1406,7 @@ class TestRunJobSkillBacked:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -94,7 +94,7 @@ def test_run_job_cron_execute_code_deny_does_not_pollute_later_gateway_execute_c
monkeypatch.setattr(approval_module, "_YOLO_MODE_FROZEN", False)
monkeypatch.setattr(approval_module, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(approval_module, "_get_cron_approval_mode", lambda: "deny")
monkeypatch.setattr("hermes_state.get_shared_session_db", _DummySessionDB)
monkeypatch.setattr("hermes_state_registry.acquire", _DummySessionDB)
monkeypatch.setattr("run_agent.AIAgent", _FakeCronAgent)
monkeypatch.setattr(
"hermes_constants.resolve_reasoning_config", lambda *_args, **_kwargs: None
+1 -1
View File
@@ -250,7 +250,7 @@ def test_long_running_script_refreshes_owned_claim_in_profile_store(
with (
jobs.use_cron_store(profile_home),
patch("hermes_state.get_shared_session_db", return_value=MagicMock()),
patch("hermes_state_registry.acquire", return_value=MagicMock()),
):
success, _doc, _response, error = scheduler.run_job(claimed_job)
profile_claim = jobs.get_job("long-script")["run_claim"]
+7 -7
View File
@@ -102,7 +102,7 @@ class TestSessionDbInitTimeout:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", side_effect=make_session_db), \
patch("hermes_state_registry.acquire", side_effect=make_session_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=_RUNTIME,
@@ -131,7 +131,7 @@ class TestSessionDbInitTimeout:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db"), \
patch("hermes_state_registry.acquire"), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=_RUNTIME,
@@ -166,7 +166,7 @@ class TestSessionDbInitTimeout:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", return_value=fake_db), \
patch("hermes_state_registry.acquire", return_value=fake_db), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=_RUNTIME,
@@ -209,7 +209,7 @@ class TestSessionDbInitTimeout:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db"), \
patch("hermes_state_registry.acquire"), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=_RUNTIME,
@@ -259,7 +259,7 @@ class TestDispatchGuardReleasedAfterHang:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db"), \
patch("hermes_state_registry.acquire"), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value=_RUNTIME,
@@ -357,7 +357,7 @@ class TestLateSessionDbClosedAfterTimeout:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db", side_effect=_hanging_then_capture), \
patch("hermes_state_registry.acquire", side_effect=_hanging_then_capture), \
patch(
"hermes_cli.runtime_provider.resolve_runtime_provider",
return_value={
@@ -415,7 +415,7 @@ class TestSessionDbInitAfterEarlyReturns:
patch("cron.scheduler_delivery._resolve_origin", return_value=None), \
patch("hermes_cli.env_loader.load_hermes_dotenv"), \
patch("hermes_cli.env_loader.reset_secret_source_cache"), \
patch("hermes_state.get_shared_session_db") as mock_db_cls, \
patch("hermes_state_registry.acquire") as mock_db_cls, \
patch(
"cron.scheduler._run_job_script_with_claim_heartbeat",
return_value=(True, '{"wakeAgent": false}'),
+4 -4
View File
@@ -11,6 +11,7 @@ import pytest
from gateway import hosted_room_driver as driver
from gateway import hosted_rooms as rooms
import hermes_state
import hermes_state_wal
from gateway.hosted_room_policy_checkpoint import HostedRoomPolicyCheckpoint
from hermes_state import SessionDB
@@ -167,7 +168,7 @@ def test_first_database_open_retries_only_transient_journal_lock(
tmp_path,
monkeypatch,
):
original = hermes_state.apply_wal_with_fallback
original = hermes_state_wal.apply_wal_with_fallback
attempts = 0
def transient_lock(conn, **kwargs):
@@ -177,7 +178,7 @@ def test_first_database_open_retries_only_transient_journal_lock(
raise sqlite3.OperationalError("database is locked")
return original(conn, **kwargs)
monkeypatch.setattr(hermes_state, "apply_wal_with_fallback", transient_lock)
monkeypatch.setattr(hermes_state_wal, "apply_wal_with_fallback", transient_lock)
assert _create(tmp_path / "state.db")["room_id"] == "room-1"
assert attempts == 3
@@ -197,8 +198,7 @@ def test_first_database_open_does_not_retry_other_journal_errors(
)
monkeypatch.setattr(
hermes_state,
"apply_wal_with_fallback",
hermes_state_wal, "apply_wal_with_fallback",
configured_delete_refusal,
)
+2 -2
View File
@@ -125,9 +125,9 @@ class TestAppendToSqlite:
mock_db = MagicMock()
released = []
with patch("hermes_state.get_shared_session_db", return_value=mock_db), \
with patch("hermes_state_registry.acquire", return_value=mock_db), \
patch(
"hermes_state.release_or_close",
"hermes_state_registry.release_or_close",
side_effect=lambda db: released.append(db),
):
_append_to_sqlite("sess_1", {"role": "assistant", "content": "hello"})
+3 -1
View File
@@ -7,7 +7,7 @@ survivor. A gateway closing a 500MB WAL store runs a PASSIVE checkpoint in
autocheckpoint threshold) that does not reliably finish in 5s. A SIGKILL
landing mid-checkpoint leaves half-written b-tree pages — macOS ``fsync``
guarantees neither data-on-platter nor write ordering, which is exactly why
``hermes_state._enforce_macos_synchronous_full`` exists.
``hermes_state_wal._enforce_macos_synchronous_full`` exists.
The port-rebinding reason the 5s deadline was introduced still holds, so the
force-kill stays — it just must not fire on a process that is still shutting
@@ -17,6 +17,8 @@ from __future__ import annotations
import pytest
import hermes_state_wal
from hermes_cli.gateway import (
_ORPHAN_EXIT_GRACE_SECONDS,
_await_gateway_exit,
+1 -1
View File
@@ -1475,7 +1475,7 @@ class TestGatewaySessionDbRecovery:
def test_transcript_reroute_migrates_remaining_backlog_to_child(self):
import threading
from types import SimpleNamespace
from hermes_state import CompressionSessionClosedError
from hermes_state_errors import CompressionSessionClosedError
class FakeDb:
def get_compression_tip(self, session_id):
+3 -2
View File
@@ -108,6 +108,7 @@ def test_runtime_health_is_sanitized_and_recovers() -> None:
def test_session_store_and_runner_reopen_after_failed_construction(monkeypatch, tmp_path) -> None:
import hermes_state
import hermes_state_registry
from gateway.run import GatewayRunner, _SESSION_DB_UNPINNED
from gateway.session import SessionStore
from gateway.session_persistence import _DB_UNPINNED
@@ -124,7 +125,7 @@ def test_session_store_and_runner_reopen_after_failed_construction(monkeypatch,
opened.append(handle)
return handle
monkeypatch.setattr(hermes_state, "get_shared_session_db", fail_once_session_db)
monkeypatch.setattr(hermes_state_registry, "acquire", fail_once_session_db)
monkeypatch.setattr(hermes_state, "_default_db_path", lambda: db_path)
store = object.__new__(SessionStore)
@@ -153,7 +154,7 @@ def test_session_store_and_runner_reopen_after_failed_construction(monkeypatch,
runner_opened.append(handle)
return handle
monkeypatch.setattr(hermes_state, "get_shared_session_db", runner_fail_once)
monkeypatch.setattr(hermes_state_registry, "acquire", runner_fail_once)
monkeypatch.setattr(hermes_state, "AsyncSessionDB", lambda db: ("async", db))
runner = object.__new__(GatewayRunner)
runner._session_db_pinned = _SESSION_DB_UNPINNED
+2 -2
View File
@@ -123,9 +123,9 @@ def test_recover_closes_owned_db_when_unexpected_exception_escapes(
raise KeyboardInterrupt
db = InterruptingDB()
monkeypatch.setattr("hermes_state.get_shared_session_db", lambda: db)
monkeypatch.setattr("hermes_state_registry.acquire", lambda: db)
monkeypatch.setattr(
"hermes_state.release_or_close", lambda _: setattr(db, "released", True)
"hermes_state_registry.release_or_close", lambda _: setattr(db, "released", True)
)
with pytest.raises(KeyboardInterrupt):
+3 -1
View File
@@ -19,6 +19,8 @@ from pathlib import Path
import pytest
import hermes_state_repair
import hermes_startup_watchdog as sw
from hermes_startup_watchdog import (
SERVICE_RESTART_EXIT_CODE,
@@ -453,7 +455,7 @@ class TestProgressLease:
import hermes_state
src = inspect.getsource(hermes_state.repair_state_db_schema)
src = inspect.getsource(hermes_state_repair.repair_state_db_schema)
assert "report_startup_progress" in src
+11 -1
View File
@@ -16,6 +16,16 @@ from agent.session_activity import ActivityProvenance, build_activity_snapshot
from hermes_state import SessionDB
def _activity_snapshot(db, session_id):
"""Durable activity snapshot for *session_id* (what gateway/delegate readers build from the row)."""
row = db.get_session(session_id)
return build_activity_snapshot(
last_activity_at=row.get("last_activity_at"),
last_activity_description=row.get("last_activity_description"),
last_activity_provenance=row.get("last_activity_provenance"),
)
# ── S1: observational activity writes must not ride the 20s patience ────────
@@ -77,7 +87,7 @@ def test_s1_clear_labels_noop_skips_transaction(tmp_path, monkeypatch):
calls.clear()
db.clear_session_activity_labels(sid)
assert len(calls) == 1
activity = db.get_session_activity(sid)
activity = _activity_snapshot(db, sid)
assert activity["last_activity_description"] == ""
+11 -10
View File
@@ -15,6 +15,7 @@ from pathlib import Path
import pytest
import hermes_state
import hermes_state_wal
from hermes_cli import kanban_db as kb
@@ -74,7 +75,7 @@ def test_cross_process_init_lock_uses_windows_byte_range_lock(tmp_path, monkeypa
monkeypatch.setitem(sys.modules, "msvcrt", fake_msvcrt)
db_path = tmp_path / "kanban.db"
with kb._cross_process_init_lock(db_path):
with kbc._cross_process_init_lock(db_path):
# Acquired exactly once via the non-blocking byte-range lock.
assert [call[1:] for call in calls] == [(fake_msvcrt.LK_NBLCK, 1)]
@@ -873,7 +874,7 @@ class TestSharedBoardPaths:
# ---------------------------------------------------------------------------
# NFS / network-filesystem fallback (see hermes_state.apply_wal_with_fallback)
# NFS / network-filesystem fallback (see hermes_state_wal.apply_wal_with_fallback)
# ---------------------------------------------------------------------------
def test_connect_falls_back_to_delete_on_locking_protocol(tmp_path, monkeypatch, caplog):
@@ -903,16 +904,16 @@ def test_connect_falls_back_to_delete_on_locking_protocol(tmp_path, monkeypatch,
# These tests exercise the WAL-attempt path; assume a fixed SQLite so the
# WAL-reset vulnerability gate doesn't short-circuit before the pragma.
import hermes_state as _hermes_state
import hermes_state_wal as _hermes_state_wal
monkeypatch.setattr(
_hermes_state, "is_sqlite_wal_reset_vulnerable",
_hermes_state_wal, "is_sqlite_wal_reset_vulnerable",
lambda version_info=None: False,
)
_hermes_state._wal_fallback_warned_paths.clear()
_hermes_state_wal._wal_fallback_warned_paths.clear()
# Clear module cache so a fresh connect() is attempted
kb._INITIALIZED_PATHS.clear()
hermes_state._wal_fallback_warned_paths.clear()
hermes_state_wal._wal_fallback_warned_paths.clear()
real_connect = _sqlite3.connect
@@ -963,10 +964,10 @@ def test_connect_works_when_wal_is_silently_refused(tmp_path, monkeypatch, caplo
monkeypatch.setattr(Path, "home", lambda: tmp_path)
kb._INITIALIZED_PATHS.clear()
hermes_state._wal_fallback_warned_paths.clear()
hermes_state_wal._wal_fallback_warned_paths.clear()
# Assume a fixed SQLite so the WAL-reset gate doesn't short-circuit.
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable",
hermes_state_wal, "is_sqlite_wal_reset_vulnerable",
lambda version_info=None: False,
)
@@ -1033,7 +1034,7 @@ def test_sqlite_connect_closes_tracked_conn_on_setup_failure(tmp_path, monkeypat
monkeypatch.setattr(kb.sqlite3, "connect", failing_connect)
with pytest.raises(sqlite3.OperationalError, match="simulated setup failure"):
kb._sqlite_connect(db_path)
kbc._sqlite_connect(db_path)
with sqlite_safe_read._live_lock:
after = sqlite_safe_read._live_connections.get(key, 0)
@@ -1627,7 +1628,7 @@ def test_write_txn_check_reads_correct_header_fields(tmp_path):
# connect_closing(): context manager that actually closes the FD
# Regression coverage for #33159 (kanban.db FD leak — gateway crashes after
# ~4 days). sqlite3.Connection's built-in __exit__ commits/rollbacks but
# does NOT close, so `with kb.connect() as conn:` leaks the FD in
# does NOT close, so `with kbc.connect() as conn:` leaks the FD in
# long-lived processes (gateway run_slash, dashboard decompose handler).
# `connect_closing()` is the leak-safe replacement.
# ---------------------------------------------------------------------------
+2 -1
View File
@@ -12,7 +12,8 @@ from types import SimpleNamespace
import pytest
import hermes_state
from hermes_state import FTS_STORAGE_VERSION, SCHEMA_VERSION, SessionDB
from hermes_state import SessionDB
from hermes_state_common import FTS_STORAGE_VERSION, SCHEMA_VERSION
from hermes_cli import session_recovery
from hermes_cli.session_recovery import (
SessionRecoverySafetyError,
@@ -26,6 +26,8 @@ from unittest.mock import patch
import pytest
import hermes_state_repair
from hermes_cli.session_lost_and_found import (
_parse_sqlite3_cli_version,
_wal_reset_vulnerable,
@@ -245,7 +247,7 @@ class TestGuidanceNeverNamesLiveDb:
must not embed a raw sqlite3 command against the live path."""
import hermes_state
body = inspect.getsource(hermes_state._backup_db_file)
body = inspect.getsource(hermes_state_repair._backup_db_file)
assert ".recover\"`" not in body
assert "sessions recover --source" in body
assert "--inspect-only" in body
@@ -15,22 +15,18 @@ from pathlib import Path
import pytest
import hermes_state
from hermes_state import (
DeletedWalGenerationError,
SessionDB,
classify_persistence_error,
iter_deleted_sqlite_sidecar_holders,
refuse_deleted_wal_generation,
)
import hermes_state_wal
from hermes_state import DeletedWalGenerationError, SessionDB, classify_persistence_error, refuse_deleted_wal_generation
from hermes_state_dbfile import iter_deleted_sqlite_sidecar_holders
@pytest.fixture
def force_wal(monkeypatch):
"""Pin WAL so this host's vulnerable SQLite still matches production topology."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
)
monkeypatch.setattr(hermes_state, "resolve_journal_mode", lambda: "wal")
monkeypatch.setattr(hermes_state_wal, "resolve_journal_mode", lambda: "wal")
def _make_db(path: Path, session_id: str, content: str) -> SessionDB:
@@ -87,9 +83,9 @@ def test_clean_open_and_second_open_still_work(tmp_path, force_wal):
def test_delete_journal_two_writers_still_work(tmp_path, monkeypatch):
monkeypatch.setattr(hermes_state, "resolve_journal_mode", lambda: "delete")
monkeypatch.setattr(hermes_state_wal, "resolve_journal_mode", lambda: "delete")
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
)
path = tmp_path / "state.db"
a = _make_db(path, "s", "from-a")
@@ -20,6 +20,7 @@ import sys
from pathlib import Path
import hermes_state
import hermes_state_guard
REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -27,13 +28,14 @@ _CHILD_PROBE = r"""
import json, os, sys
sys.path.insert(0, {repo!r})
import hermes_state as hs
import hermes_state_guard
fired = False
try:
hs._ensure_test_isolation(hs._real_platform_state_root() / "state.db")
except RuntimeError:
fired = True
print(json.dumps({{
"armed": hs._running_under_pytest(),
"armed": hermes_state_guard._running_under_pytest(),
"fired": fired,
}}))
"""
@@ -78,7 +80,7 @@ def test_marker_alone_reports_test_context(monkeypatch):
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("PYTEST_VERSION", raising=False)
monkeypatch.setenv("HERMES_TEST_ISOLATION", "/tmp/some-isolation-root")
assert hermes_state._running_under_pytest() is True
assert hermes_state_guard._running_under_pytest() is True
def test_no_signals_reports_production(monkeypatch):
@@ -87,7 +89,7 @@ def test_no_signals_reports_production(monkeypatch):
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
monkeypatch.delenv("PYTEST_VERSION", raising=False)
monkeypatch.delenv("HERMES_TEST_ISOLATION", raising=False)
assert hermes_state._running_under_pytest() is False
assert hermes_state_guard._running_under_pytest() is False
def test_child_with_rebuilt_env_keeping_marker_refuses_production_db():
@@ -29,6 +29,7 @@ from pathlib import Path
import pytest
import hermes_state
import hermes_state_guard
REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -137,7 +138,7 @@ class TestPytestProcessRecognition:
],
)
def test_recognises_pytest_invocations(self, cmdline):
assert hermes_state._process_looks_like_pytest(self._FakeProc(cmdline))
assert hermes_state_guard._process_looks_like_pytest(self._FakeProc(cmdline))
@pytest.mark.parametrize(
"cmdline",
@@ -150,11 +151,11 @@ class TestPytestProcessRecognition:
],
)
def test_ignores_non_pytest_invocations(self, cmdline):
assert not hermes_state._process_looks_like_pytest(self._FakeProc(cmdline))
assert not hermes_state_guard._process_looks_like_pytest(self._FakeProc(cmdline))
def test_unreadable_process_is_not_pytest(self):
class _Denied:
def cmdline(self):
raise PermissionError("access denied")
assert not hermes_state._process_looks_like_pytest(_Denied())
assert not hermes_state_guard._process_looks_like_pytest(_Denied())
@@ -165,7 +165,7 @@ def test_flush_adopts_exactly_once_no_retry_loop(tmp_path: Path, monkeypatch) ->
"""Adoption budget: the tip lookup runs at most once per flush, and a
second closed-parent write after adoption fails closed instead of looping.
"""
from hermes_state import CompressionSessionClosedError
from hermes_state_errors import CompressionSessionClosedError
db = SessionDB(db_path=tmp_path / "state.db")
try:
@@ -205,11 +205,8 @@ def test_flush_adopts_exactly_once_no_retry_loop(tmp_path: Path, monkeypatch) ->
def test_compression_closed_error_classifies_as_compression_closed() -> None:
from hermes_state import (
PERSISTENCE_ERROR_CAUSES,
CompressionSessionClosedError,
classify_persistence_error,
)
from hermes_state import classify_persistence_error
from hermes_state_errors import CompressionSessionClosedError, PERSISTENCE_ERROR_CAUSES
cause = classify_persistence_error(CompressionSessionClosedError("session-abc"))
assert cause == "compression_closed"
@@ -222,7 +219,8 @@ def test_compression_closed_error_classifies_as_compression_closed() -> None:
def test_compression_closed_wording_never_mentions_disk() -> None:
from hermes_state import CompressionSessionClosedError, classify_persistence_error
from hermes_state import classify_persistence_error
from hermes_state_errors import CompressionSessionClosedError
text = AIAgent._format_turn_completion_explanation(
"session_persistence_failed",
@@ -48,7 +48,7 @@ class TestCronJobCleanup:
"model": "test/model",
}
with patch("hermes_state.get_shared_session_db", return_value=mock_db), \
with patch("hermes_state_registry.acquire", return_value=mock_db), \
patch.object(scheduler, "_build_job_prompt", return_value="hello"), \
patch.object(sched_delivery, "_resolve_origin", return_value=None), \
patch.object(scheduler, "_resolve_delivery_target", return_value=None), \
@@ -68,8 +68,10 @@ def test_session_search_lazily_opens_db_when_entrypoint_did_not_pass_one(monkeyp
hermes_state = ModuleType("hermes_state")
hermes_state.SessionDB = FakeSessionDB
hermes_state.get_shared_session_db = lambda db_path=None: sentinel_db
monkeypatch.setitem(sys.modules, "hermes_state", hermes_state)
hermes_state_registry = ModuleType("hermes_state_registry")
hermes_state_registry.acquire = lambda db_path=None: sentinel_db
monkeypatch.setitem(sys.modules, "hermes_state_registry", hermes_state_registry)
session_search_mod = ModuleType("tools.session_search_tool")
@@ -17,6 +17,7 @@ suite (we patch ``agent.process_bootstrap.OpenAI`` and drive ``agent.client``),
pass identically in CI and locally.
"""
import hermes_state_errors
import os
import uuid
from types import SimpleNamespace
@@ -249,7 +250,7 @@ def test_classify_persistence_error_corruption_beats_disk_bucket():
def test_classify_persistence_error_reuses_disk_full_markers():
"""The disk bucket delegates to hermes_state.is_disk_full_error, so
"""The disk bucket delegates to hermes_state_errors.is_disk_full_error, so
every marker that helper recognizes (ENOSPC, 'not enough space', ...)
must classify as 'disk' — the two classifiers can never drift apart."""
import errno
@@ -270,10 +271,8 @@ def test_classify_persistence_error_compression_busy_is_distinct():
storage damage — but its message contains neither 'locked' nor 'busy',
so it must classify by exception type (and by phrase for RPC-wrapped
strings). This is the exact failure mode of issue #81227."""
from hermes_state import (
CompressionSessionBusyError,
SessionCompressionInProgressError,
)
from hermes_state import SessionCompressionInProgressError
from hermes_state_errors import CompressionSessionBusyError
from hermes_state import classify_persistence_error
assert classify_persistence_error(
@@ -294,7 +293,8 @@ def test_classify_persistence_error_compression_busy_is_distinct():
def test_classify_persistence_error_turn_lease_lost_is_distinct():
from hermes_state import SessionTurnLeaseLostError, classify_persistence_error
from hermes_state import classify_persistence_error
from hermes_state_errors import SessionTurnLeaseLostError
assert classify_persistence_error(
SessionTurnLeaseLostError(
@@ -309,7 +309,8 @@ def test_classify_persistence_error_turn_lease_lost_is_distinct():
def test_persistence_error_causes_tuple_matches_classifier():
"""PERSISTENCE_ERROR_CAUSES must cover every value the classifier can
return (consumers like cron suppression iterate it)."""
from hermes_state import PERSISTENCE_ERROR_CAUSES, classify_persistence_error
from hermes_state import classify_persistence_error
from hermes_state_errors import PERSISTENCE_ERROR_CAUSES
probes = (
"database is locked",
@@ -11,7 +11,8 @@ from unittest.mock import patch
import pytest
from hermes_state import SessionDB, CompressionSessionBusyError
from hermes_state import SessionDB
from hermes_state_errors import CompressionSessionBusyError
def _setup_db(tmp_path):
+1 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import errno
import sqlite3
from hermes_state import is_disk_full_error
from hermes_state_errors import is_disk_full_error
def test_enospc_oserror():
+8 -6
View File
@@ -27,7 +27,8 @@ from pathlib import Path
import pytest
import hermes_state_common
from hermes_state import FTS_STALE_KEY, SessionDB, _FTS_TRIGGERS
from hermes_state import SessionDB
from hermes_state_common import FTS_STALE_KEY, _FTS_TRIGGERS
pytestmark = pytest.mark.skipif(
sys.platform == "win32", reason="POSIX flock child-process harness"
@@ -338,9 +339,9 @@ class TestOrphanedHolderStalenessBreak:
import os, sys, time
sys.path.insert(0, {repo!r})
from pathlib import Path
import hermes_state
import hermes_state_repair
lock_cm = hermes_state._cross_process_repair_lock(Path({db!r}))
lock_cm = hermes_state_repair._cross_process_repair_lock(Path({db!r}))
assert lock_cm.__enter__() is True
pid = os.fork()
if pid == 0:
@@ -358,9 +359,9 @@ os._exit(1)
grandchild = int(proc.stdout.readline().strip().split()[1])
proc.wait(timeout=10)
try:
import hermes_state as hs
import hermes_state_repair
with hs._cross_process_repair_lock(db_path) as holding:
with hermes_state_repair._cross_process_repair_lock(db_path) as holding:
assert holding is True
finally:
with contextlib.suppress(OSError):
@@ -463,6 +464,7 @@ class TestNonContentionErrnoFailsFast:
import fcntl
import hermes_state
import hermes_state_repair
monkeypatch.setattr(hermes_state, "_REPAIR_LOCK_TIMEOUT_SECONDS", 30.0)
@@ -471,7 +473,7 @@ class TestNonContentionErrnoFailsFast:
monkeypatch.setattr(fcntl, "flock", _flock)
t0 = time.monotonic()
with hermes_state._cross_process_repair_lock(tmp_path / "state.db") as ok:
with hermes_state_repair._cross_process_repair_lock(tmp_path / "state.db") as ok:
assert ok is False
assert time.monotonic() - t0 < 2.0
@@ -6,7 +6,8 @@ import sqlite3
import pytest
from hermes_state import FTS_TRIGRAM_SQL, SCHEMA_VERSION, SessionDB
from hermes_state import SessionDB
from hermes_state_common import FTS_TRIGRAM_SQL, SCHEMA_VERSION
@pytest.fixture
@@ -9,7 +9,8 @@ from __future__ import annotations
import pytest
from hermes_state import SCHEMA_VERSION, SessionDB
from hermes_state import SessionDB
from hermes_state_common import SCHEMA_VERSION
from hermes_state_common import FTS_TRIGRAM_EXCLUDED_SOURCES, fts_trigram_session_sql
@@ -5,7 +5,8 @@ from __future__ import annotations
import sqlite3
import threading
from hermes_state import SCHEMA_VERSION, SessionDB
from hermes_state import SessionDB
from hermes_state_common import SCHEMA_VERSION
def _open_pair(tmp_path):
+2 -1
View File
@@ -11,7 +11,8 @@ from types import SimpleNamespace
import pytest
import hermes_state
from hermes_state import SessionDB, SessionTurnLeaseLostError
from hermes_state import SessionDB
from hermes_state_errors import SessionTurnLeaseLostError
def test_turn_lease_serializes_separate_session_db_instances(tmp_path):
@@ -5,7 +5,7 @@ work on a file several Hermes processes share (gateway service, the Desktop
app's `hermes serve` backend, CLI sessions, the TUI slash worker):
* `hermes_state_common.fts_rebuild_admission` — full structural FTS rebuilds
* `hermes_state._cross_process_repair_lock` — writable_schema surgery / VACUUM
* `hermes_state_repair._cross_process_repair_lock` — writable_schema surgery / VACUUM
Both document themselves as fail-closed, and both honoured that only for a
*timed-out* acquire. When the lock file could not be `open()`ed at all they
@@ -34,8 +34,10 @@ from pathlib import Path
import pytest
import hermes_state
import hermes_state_repair
import hermes_state_common
from hermes_state import SessionDB, repair_state_db_schema
from hermes_state import SessionDB
from hermes_state_repair import repair_state_db_schema
def _make_unopenable(lock_path: Path) -> None:
@@ -130,7 +132,7 @@ def test_repair_lock_fails_closed_when_lock_file_is_unopenable(tmp_path):
db_path = tmp_path / "state.db"
_make_unopenable(db_path.with_name(db_path.name + ".repair.lock"))
with hermes_state._cross_process_repair_lock(db_path) as holding:
with hermes_state_repair._cross_process_repair_lock(db_path) as holding:
assert holding is False
@@ -145,7 +147,7 @@ def test_repair_skips_surgery_when_lock_file_is_unopenable(tmp_path):
db_path = tmp_path / "state.db"
_build_healthy_db(db_path)
_corrupt_duplicate_fts(db_path)
assert hermes_state._db_opens_cleanly(db_path) is not None
assert hermes_state_repair._db_opens_cleanly(db_path) is not None
before = db_path.read_bytes()
_make_unopenable(db_path.with_name(db_path.name + ".repair.lock"))
+11 -10
View File
@@ -12,7 +12,8 @@ import sqlite3
import pytest
import hermes_state
from hermes_state import repair_state_db_schema
import hermes_state_repair
from hermes_state_repair import repair_state_db_schema
def _make_db(path):
@@ -31,9 +32,9 @@ def test_wal_restoration_reuses_exclusive_repair_connection(tmp_path, monkeypatc
def fail_if_reopened(_path):
pytest.fail("WAL restoration reopened state.db outside the repair guard")
monkeypatch.setattr(hermes_state, "_connect_repair_durable", fail_if_reopened)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", fail_if_reopened)
hermes_state._restore_journal_mode_after_repair(db_path, None, conn=conn)
hermes_state_repair._restore_journal_mode_after_repair(db_path, None, conn=conn)
# The mode itself is whatever apply_wal_with_fallback resolves on this
# runtime (WAL, or DELETE on WAL-reset-vulnerable SQLite builds); the
# contract under test is the connection reuse, asserted above.
@@ -46,20 +47,20 @@ def test_repair_never_reopens_after_the_guard_releases(tmp_path, monkeypatch):
opens is opened while the exclusive guard is still held, and none after."""
db = tmp_path / "state.db"
_make_db(db)
monkeypatch.setattr(hermes_state, "_db_opens_cleanly", lambda path: "forced-unhealthy")
monkeypatch.setattr(hermes_state_repair, "_db_opens_cleanly", lambda path: "forced-unhealthy")
# The scratch-space pre-flight wants ~10GB headroom; irrelevant here.
monkeypatch.setattr(hermes_state, "_repair_scratch_space_error", lambda path: None)
monkeypatch.setattr(hermes_state_repair, "_repair_scratch_space_error", lambda path: None)
def fake_strategies(scratch_path, report):
report["repaired"] = True
report["strategy"] = "test_strategy"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", fake_strategies)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", fake_strategies)
events: list[str] = []
real_guard = hermes_state._exclusive_repair_db_guard
real_connect = hermes_state._connect_repair_durable
real_guard = hermes_state_repair._exclusive_repair_db_guard
real_connect = hermes_state_repair._connect_repair_durable
from contextlib import contextmanager
@@ -74,8 +75,8 @@ def test_repair_never_reopens_after_the_guard_releases(tmp_path, monkeypatch):
events.append("connect")
return real_connect(path, *a, **kw)
monkeypatch.setattr(hermes_state, "_exclusive_repair_db_guard", tracing_guard)
monkeypatch.setattr(hermes_state, "_connect_repair_durable", tracing_connect)
monkeypatch.setattr(hermes_state_repair, "_exclusive_repair_db_guard", tracing_guard)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", tracing_connect)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is True
+1 -1
View File
@@ -14,7 +14,7 @@ import sqlite3
import pytest
from hermes_state import is_sqlite_wal_reset_vulnerable
from hermes_state_wal import is_sqlite_wal_reset_vulnerable
from tests.conftest import _wal_is_usable
+3 -2
View File
@@ -11,7 +11,8 @@ from pathlib import Path
import pytest
from hermes_state import FTS_CJK_STALE_KEY, SessionDB
from hermes_state import SessionDB
from hermes_state_common import FTS_CJK_STALE_KEY
REPO = Path(__file__).resolve().parent.parent
SRC = REPO / "native" / "fts5_cjk" / "fts5_cjk.c"
@@ -149,7 +150,7 @@ def test_legacy_v22_optimize_lands_on_cjk(cjk_so, tmp_path, monkeypatch):
cjk index in the same run."""
import time as _time
from hermes_state import SCHEMA_SQL
from hermes_state_common import SCHEMA_SQL
monkeypatch.setenv("HERMES_FTS5_CJK_SO", str(cjk_so))
db_path = tmp_path / "state.db"
+1 -1
View File
@@ -12,7 +12,7 @@ import sqlite3
import pytest
import hermes_state
from hermes_state import apply_durability_barriers
from hermes_state_repair import apply_durability_barriers
def _config(monkeypatch, database_section):
+32 -26
View File
@@ -10,14 +10,21 @@ from unittest import mock
import pytest
import hermes_state
from agent.session_activity import ActivityProvenance
from hermes_state import (
FTS_SQL,
FTS_STORAGE_VERSION,
SCHEMA_SQL,
SCHEMA_VERSION,
SessionDB,
)
import hermes_state_wal
import hermes_state_common
from agent.session_activity import ActivityProvenance, build_activity_snapshot
from hermes_state import SessionDB
from hermes_state_common import FTS_SQL, FTS_STORAGE_VERSION, SCHEMA_SQL, SCHEMA_VERSION
def _activity_snapshot(db, session_id):
"""Durable activity snapshot for *session_id* (what gateway/delegate readers build from the row)."""
row = db.get_session(session_id)
return build_activity_snapshot(
last_activity_at=row.get("last_activity_at"),
last_activity_description=row.get("last_activity_description"),
last_activity_provenance=row.get("last_activity_provenance"),
)
class _NoFtsCursor(sqlite3.Cursor):
@@ -1741,7 +1748,7 @@ class TestSanitizeTitle:
class TestSchemaInit:
def test_wal_mode(self, db):
"""Prefer WAL on fixed SQLite; DELETE on WAL-reset-vulnerable builds (#69784)."""
from hermes_state import is_sqlite_wal_reset_vulnerable
from hermes_state_wal import is_sqlite_wal_reset_vulnerable
cursor = db._conn.execute("PRAGMA journal_mode")
mode = cursor.fetchone()[0].lower()
@@ -1796,7 +1803,7 @@ class TestSchemaInit:
This is the architectural invariant: SCHEMA_SQL declares the
desired schema, _reconcile_columns ensures it matches reality.
"""
from hermes_state import SCHEMA_SQL
from hermes_state_common import SCHEMA_SQL
expected = SessionDB._parse_schema_columns(SCHEMA_SQL)
for table_name, declared_cols in expected.items():
@@ -2375,7 +2382,7 @@ class TestListSessionsRich:
assert row["last_activity_description"] == "starting API call #1"
assert row["last_activity_provenance"] == "unknown"
activity = db.get_session_activity("s1")
activity = _activity_snapshot(db, "s1")
assert activity["last_activity_at"] == heartbeat
assert activity["last_activity_description"] == "starting API call #1"
assert "phase" not in activity
@@ -2405,7 +2412,7 @@ class TestListSessionsRich:
assert row["last_activity_at"] == heartbeat
assert row["last_activity_description"] == ""
assert row["last_activity_provenance"] == "unknown"
activity = db.get_session_activity("s1")
activity = _activity_snapshot(db, "s1")
assert activity["last_activity_at"] == heartbeat
assert activity["last_activity_description"] == ""
assert activity["last_activity_provenance"] == "unknown"
@@ -2448,7 +2455,7 @@ class TestListSessionsRich:
rows = db.list_gateway_sessions(active_only=True)
assert len(rows) == 1
assert rows[0]["last_active"] == heartbeat
activity = db.get_session_activity("gw-1")
activity = _activity_snapshot(db, "gw-1")
assert activity["last_activity_description"] == "compressing context"
def test_order_by_last_active_surfaces_recently_touched_older_session_first(self, db):
@@ -3108,7 +3115,7 @@ class TestVacuum:
def test_auto_maintenance_freelist_ratio_exactly_at_threshold_skips(self, db, monkeypatch):
"""Gate is strictly greater-than: 25.0% reclaimable does not VACUUM."""
from hermes_state import AUTO_VACUUM_MIN_FREELIST_RATIO
from hermes_state_common import AUTO_VACUUM_MIN_FREELIST_RATIO
monkeypatch.setattr(db, "prune_sessions", lambda **_kwargs: 1)
monkeypatch.setattr(db, "_freelist_ratio", lambda: AUTO_VACUUM_MIN_FREELIST_RATIO)
@@ -3148,7 +3155,7 @@ class TestVacuum:
def test_freelist_ratio_reads_real_pragmas(self, db):
"""Real-DB check: freeing most of the file pushes the ratio past the gate."""
from hermes_state import AUTO_VACUUM_MIN_FREELIST_RATIO
from hermes_state_common import AUTO_VACUUM_MIN_FREELIST_RATIO
db.create_session(session_id="keep", source="cli")
db.append_message(session_id="keep", role="user", content="hi")
@@ -3534,7 +3541,7 @@ class TestFTS5ToolCallMigration:
assert len(session_db.search_messages("LEGACYARG")) == 1, \
"v23 optimize must index tool_calls JSON into FTS"
# schema_version bumped once the FTS layer is v23
from hermes_state import SCHEMA_VERSION
from hermes_state_common import SCHEMA_VERSION
row = session_db._conn.execute(
"SELECT version FROM schema_version LIMIT 1"
).fetchone()
@@ -3806,7 +3813,7 @@ class TestFTSExternalContentMigration:
Mirrors what happened when ``_ensure_fts_schema`` ran inside
``_execute_write`` and the process died before the marker writes.
"""
from hermes_state import FTS_SQL, FTS_TRIGRAM_SQL
from hermes_state_common import FTS_SQL, FTS_TRIGRAM_SQL
conn = db._conn
db._drop_fts_triggers(conn)
@@ -3871,7 +3878,7 @@ class TestFTSExternalContentMigration:
assert db.fts_rebuild_status() is None
assert db.fts_optimize_available() is False
assert db.get_meta("fts_storage_version") == str(
hermes_state.FTS_STORAGE_VERSION
hermes_state_common.FTS_STORAGE_VERSION
)
assert db._conn.execute(
"SELECT name FROM sqlite_master WHERE name LIKE '%_v22_trash%'"
@@ -3913,7 +3920,7 @@ class TestFTSExternalContentMigration:
"INSERT INTO state_meta (key, value) VALUES "
"('fts_storage_version', ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
(str(hermes_state.FTS_STORAGE_VERSION),),
(str(hermes_state_common.FTS_STORAGE_VERSION),),
)
db._conn.commit()
@@ -3928,7 +3935,7 @@ class TestFTSExternalContentMigration:
assert result["ok"] is True
assert len(db.search_messages("deployment")) == 1
assert db.get_meta("fts_storage_version") == str(
hermes_state.FTS_STORAGE_VERSION
hermes_state_common.FTS_STORAGE_VERSION
)
assert db.fts_optimize_available() is False
finally:
@@ -4275,14 +4282,14 @@ class TestApplyWalProbe:
import hermes_state
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
)
def test_sets_wal_on_fresh_connection(self, tmp_path):
"""Probe sees 'delete', then set-pragma runs and returns 'wal'."""
import sqlite3
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
class _TracingConn(sqlite3.Connection):
def __init__(self, *a, **kw):
@@ -4315,7 +4322,7 @@ class TestApplyWalProbe:
import sys
import threading
import sqlite3
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
db_path = tmp_path / "concurrent.db"
errors = []
@@ -4361,7 +4368,7 @@ class TestApplyWalProbe:
def test_returns_wal_not_delete_from_probe(self, tmp_path):
"""Early-return only on 'wal'; 'delete' or 'memory' must fall through to set-pragma."""
import sqlite3
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
class _TracingConn(sqlite3.Connection):
def __init__(self, *a, **kw):
@@ -5699,8 +5706,7 @@ class TestPerformancePragmasEndToEnd:
# path. Force WAL eligibility so _get_read_conn is truly exercised
# (established pattern used by the WAL tests above).
monkeypatch.setattr(
hermes_state,
"is_sqlite_wal_reset_vulnerable",
hermes_state_wal, "is_sqlite_wal_reset_vulnerable",
lambda version_info=None: False,
)
home = tmp_path / "hermes_home"
+8 -12
View File
@@ -19,13 +19,9 @@ from unittest.mock import patch
import pytest
import hermes_state
from hermes_state import (
SessionDB,
WalUnsupportedError,
apply_wal_with_fallback,
format_session_db_unavailable,
get_last_init_error,
)
import hermes_state_wal
from hermes_state import SessionDB, format_session_db_unavailable, get_last_init_error
from hermes_state_wal import WalUnsupportedError, apply_wal_with_fallback
# ``sqlite3.Connection.execute`` is a C-level slot and can't be monkeypatched
@@ -88,20 +84,20 @@ def _reset_last_init_error():
@pytest.fixture(autouse=True)
def _reset_wal_fallback_warned_paths():
"""Reset the WAL-fallback warned-paths set so dedup doesn't leak between tests."""
hermes_state._wal_fallback_warned_paths.clear()
hermes_state_wal._wal_fallback_warned_paths.clear()
yield
hermes_state._wal_fallback_warned_paths.clear()
hermes_state_wal._wal_fallback_warned_paths.clear()
@pytest.fixture(autouse=True)
def _assume_fixed_sqlite(monkeypatch):
"""NFS-fallback tests assume a SQLite build without the WAL-reset bug."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: False
)
hermes_state._wal_reset_bug_warned_paths.clear()
hermes_state_wal._wal_reset_bug_warned_paths.clear()
yield
hermes_state._wal_reset_bug_warned_paths.clear()
hermes_state_wal._wal_reset_bug_warned_paths.clear()
class TestApplyWalWithFallback:
+20 -17
View File
@@ -5,6 +5,8 @@ from __future__ import annotations
import sqlite3
import pytest
import hermes_state_wal
import yaml
@@ -24,7 +26,7 @@ def _configure_mode(monkeypatch: pytest.MonkeyPatch, tmp_path, mode: object) ->
def _disable_vulnerable_gate(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: False,
)
@@ -35,9 +37,9 @@ def _reset_configured_delete_override_warned_paths():
once-per-process-per-db_label dedup doesn't leak between tests."""
import hermes_state
hermes_state._delete_overridden_warned_paths.clear()
hermes_state_wal._delete_overridden_warned_paths.clear()
yield
hermes_state._delete_overridden_warned_paths.clear()
hermes_state_wal._delete_overridden_warned_paths.clear()
def test_database_journal_mode_has_a_canonical_default():
@@ -47,14 +49,14 @@ def test_database_journal_mode_has_a_canonical_default():
def test_resolve_journal_mode_uses_real_database_config(monkeypatch, tmp_path):
from hermes_state import resolve_journal_mode
from hermes_state_wal import resolve_journal_mode
_configure_mode(monkeypatch, tmp_path, "DELETE")
assert resolve_journal_mode() == "delete"
def test_new_nonsecret_hermes_env_override_is_not_exposed(monkeypatch, tmp_path):
from hermes_state import resolve_journal_mode
from hermes_state_wal import resolve_journal_mode
_configure_mode(monkeypatch, tmp_path, "wal")
monkeypatch.setenv("HERMES_JOURNAL_MODE", "delete")
@@ -63,7 +65,7 @@ def test_new_nonsecret_hermes_env_override_is_not_exposed(monkeypatch, tmp_path)
@pytest.mark.parametrize("value", ["bogus", "truncate", None, 42, {"bad": "shape"}])
def test_invalid_config_value_falls_back_to_wal(monkeypatch, tmp_path, value):
from hermes_state import resolve_journal_mode
from hermes_state_wal import resolve_journal_mode
_configure_mode(monkeypatch, tmp_path, value)
assert resolve_journal_mode() == "wal"
@@ -73,14 +75,14 @@ def test_invalid_config_value_falls_back_to_wal(monkeypatch, tmp_path, value):
def test_malformed_database_section_falls_back_to_wal(
monkeypatch, tmp_path, database
):
from hermes_state import resolve_journal_mode
from hermes_state_wal import resolve_journal_mode
_write_config(monkeypatch, tmp_path, {"database": database})
assert resolve_journal_mode() == "wal"
def test_apply_wal_with_fallback_honors_delete_config(monkeypatch, tmp_path):
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
_disable_vulnerable_gate(monkeypatch)
@@ -93,7 +95,7 @@ def test_apply_wal_with_fallback_honors_delete_config(monkeypatch, tmp_path):
def test_apply_wal_with_fallback_defaults_to_wal(monkeypatch, tmp_path):
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -107,11 +109,11 @@ def test_apply_wal_with_fallback_defaults_to_wal(monkeypatch, tmp_path):
def test_configured_delete_validates_vulnerable_sqlite_result(monkeypatch, tmp_path):
"""The safety gate must not report DELETE when SQLite returns MEMORY."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: True,
)
conn = sqlite3.connect(":memory:")
@@ -127,7 +129,7 @@ def test_configured_delete_never_live_downgrades_existing_wal(monkeypatch, tmp_p
"""Keeping WAL is correct, but the operator must be told their configured
delete had no effect (otherwise the DB silently stays WAL and the protection
they configured never applies)."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
db_path = tmp_path / "existing-wal.db"
@@ -135,7 +137,7 @@ def test_configured_delete_never_live_downgrades_existing_wal(monkeypatch, tmp_p
try:
assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: True,
)
with caplog.at_level("ERROR", logger="hermes_state"):
@@ -155,7 +157,7 @@ def test_configured_delete_overridden_warns_on_non_vulnerable_runtime_too(monkey
3.51.3+ upgrade), the on-disk WAL + configured-delete case reaches the
read-only probe path instead of the vulnerability path. That path used to
return WAL with no signal at all; it must emit the same override warning."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
_disable_vulnerable_gate(monkeypatch)
@@ -182,11 +184,11 @@ def test_configured_delete_overridden_warns_on_non_vulnerable_runtime_too(monkey
def test_configured_delete_overridden_warning_fires_once_per_db(monkeypatch, tmp_path, caplog):
"""The override warning is deduped per process per db_label (same discipline
as the WAL-fallback warning), so repeated connections don't flood the log."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: True,
)
db_path = tmp_path / "existing-wal.db"
@@ -211,7 +213,7 @@ def test_configured_delete_with_require_wal_and_existing_wal_returns_wal(monkeyp
existing-WAL probe branch returns "wal" unconditionally (require_wal only
governs the WAL-refusal fallback paths), so the override warning fires and no
WalUnsupportedError is raised."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
_disable_vulnerable_gate(monkeypatch)
@@ -242,6 +244,7 @@ def test_real_db_openers_honor_configured_delete(monkeypatch, tmp_path):
from gateway import delivery_ledger
from gateway.platforms.api_server import ResponseStore
from hermes_cli import kanban_db, projects_db
from hermes_cli import kanban_db_connect as kbc
from hermes_state import SessionDB
from plugins.memory.holographic.store import MemoryStore
from plugins.platforms.discord.recovery import DiscordRecoveryStore
+21 -19
View File
@@ -26,6 +26,8 @@ from __future__ import annotations
import sqlite3
import pytest
import hermes_state_wal
import yaml
@@ -42,7 +44,7 @@ def _configure_mode(monkeypatch: pytest.MonkeyPatch, tmp_path, mode: object) ->
def _disable_vulnerable_gate(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: False,
)
@@ -64,16 +66,16 @@ def _reset_dedup():
"""Order-independence: the warning is deduped per process per db_label."""
import hermes_state
hermes_state._journal_upgrade_warned_paths.clear()
hermes_state_wal._journal_upgrade_warned_paths.clear()
yield
hermes_state._journal_upgrade_warned_paths.clear()
hermes_state_wal._journal_upgrade_warned_paths.clear()
class TestTheContentProbe:
"""``_database_has_content`` is what keeps fresh installs quiet."""
def test_a_brand_new_database_has_no_content(self, tmp_path):
from hermes_state import _database_has_content
from hermes_state_wal import _database_has_content
conn = sqlite3.connect(str(tmp_path / "new.db"))
try:
@@ -82,7 +84,7 @@ class TestTheContentProbe:
conn.close()
def test_a_database_with_a_table_has_content(self, tmp_path):
from hermes_state import _database_has_content
from hermes_state_wal import _database_has_content
path = tmp_path / "used.db"
_make_delete_db_with_content(path)
@@ -98,7 +100,7 @@ class TestTheContentProbe:
Answering True on an error would emit the warning for a database we
could not measure, which includes every fresh one.
"""
from hermes_state import _database_has_content
from hermes_state_wal import _database_has_content
conn = sqlite3.connect(":memory:")
try:
@@ -113,7 +115,7 @@ class TestTheWarningFires:
def test_an_existing_delete_database_warns_when_flipped(
self, monkeypatch, tmp_path, caplog
):
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -139,7 +141,7 @@ class TestTheWarningFires:
Telling an operator their mode changed, without telling them which
lever survives an open, leaves them doing the same PRAGMA again.
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -165,7 +167,7 @@ class TestTheWarningFires:
(managed_uv repairs it on update, citing ~2600x slower appends), so
this must warn about the change without preventing it.
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -183,7 +185,7 @@ class TestTheWarningFires:
self, monkeypatch, tmp_path, caplog
):
"""kanban opens a connection per operation; undeduped this is a flood."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -205,7 +207,7 @@ class TestTheWarningFires:
self, monkeypatch, tmp_path, caplog
):
"""#89293 saw four databases flip. Dedup is per label, not global."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -236,7 +238,7 @@ class TestTheWarningStaysQuiet:
every opener applies WAL before creating any schema -- so without
this guard the warning fires on every first run of every install.
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -252,7 +254,7 @@ class TestTheWarningStaysQuiet:
def test_an_existing_wal_database_is_silent(self, monkeypatch, tmp_path, caplog):
"""No flip happens: the probe returns early. Nothing to report."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
@@ -271,7 +273,7 @@ class TestTheWarningStaysQuiet:
def test_configured_delete_is_silent(self, monkeypatch, tmp_path, caplog):
"""The operator used the durable lever. There is nothing to tell them."""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
_disable_vulnerable_gate(monkeypatch)
@@ -299,11 +301,11 @@ class TestTheWarningStaysQuiet:
the SQLite upgrade -- warning here would blame the guard that was
doing its job.
"""
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: True,
)
path = tmp_path / "vulnerable.db"
@@ -327,7 +329,7 @@ class TestTheExistingContractIsUnchanged:
"""Behaviour preservation for the rules this change sits next to."""
def test_on_disk_wal_is_still_never_live_downgraded(self, monkeypatch, tmp_path):
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "delete")
path = tmp_path / "existing-wal.db"
@@ -335,7 +337,7 @@ class TestTheExistingContractIsUnchanged:
try:
assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0].lower() == "wal"
monkeypatch.setattr(
"hermes_state.is_sqlite_wal_reset_vulnerable",
"hermes_state_wal.is_sqlite_wal_reset_vulnerable",
lambda **kwargs: True,
)
assert apply_wal_with_fallback(conn, db_label="existing-wal.db") == "wal"
@@ -346,7 +348,7 @@ class TestTheExistingContractIsUnchanged:
def test_default_config_still_yields_wal_on_a_fresh_database(
self, monkeypatch, tmp_path
):
from hermes_state import apply_wal_with_fallback
from hermes_state_wal import apply_wal_with_fallback
_configure_mode(monkeypatch, tmp_path, "wal")
_disable_vulnerable_gate(monkeypatch)
+3 -3
View File
@@ -152,17 +152,17 @@ def test_reacting_never_mutates_message_content(session, db):
def test_latest_user_message_is_the_agents_default_target(session, db):
"""The agent reacts to "the message that triggered me" without an id."""
key, rows = session
assert db.latest_user_message_row_id(key) == rows[0]
assert db.latest_message_row_id(key, role="user") == rows[0]
db.append_message(key, "user", "thanks!")
newest = db.get_messages_as_conversation(key, include_row_ids=True)[-1]["_row_id"]
assert db.latest_user_message_row_id(key) == newest
assert db.latest_message_row_id(key, role="user") == newest
# Role-targeting contract: a newer ASSISTANT message must not become the
# agent's default target — it always means the latest USER message.
db.append_message(key, "assistant", "you're welcome")
assert db.latest_user_message_row_id(key) == newest
assert db.latest_message_row_id(key, role="user") == newest
assert db.latest_message_row_id(key, role="assistant") != newest
+3 -2
View File
@@ -35,6 +35,7 @@ holds POSIX locks on that inode, so raw descriptor counts lag the real
connection count and make such assertions flaky.
"""
import hermes_state_readpool
import queue
import threading
@@ -540,10 +541,10 @@ def test_peak_is_bounded_across_many_database_files(tmp_path):
finally:
for d in dbs:
d.close()
assert hermes_state._process_read_permits.acquire(blocking=False), (
assert hermes_state_readpool._process_read_permits.acquire(blocking=False), (
"close() stranded a process permit"
)
hermes_state._process_read_permits.release()
hermes_state_readpool._process_read_permits.release()
@pytest.mark.requires_wal
+2 -1
View File
@@ -8,7 +8,8 @@ import time
import pytest
from hermes_state import SCHEMA_VERSION, SessionDB
from hermes_state import SessionDB
from hermes_state_common import SCHEMA_VERSION
@pytest.fixture()
+3 -2
View File
@@ -1,4 +1,4 @@
"""Session <-> workspace grouping key (hermes_state.workspace_key).
"""Session <-> workspace grouping key (hermes_state_sessions.workspace_key).
The key is what `hermes sessions list --workspace` groups/filters on. It is a
coarse workspace identity derived from fields already recorded on sessions
@@ -6,7 +6,8 @@ coarse workspace identity derived from fields already recorded on sessions
NOT part of the key.
"""
from hermes_state import workspace_key
import hermes_state_sessions
from hermes_state_sessions import workspace_key
def test_repo_root_is_the_key_when_known():
+17 -22
View File
@@ -18,18 +18,15 @@ from types import SimpleNamespace
import pytest
import hermes_state
from hermes_state import (
apply_wal_with_fallback,
is_sqlite_wal_reset_vulnerable,
sqlite_source_id,
)
import hermes_state_wal
from hermes_state_wal import apply_wal_with_fallback, is_sqlite_wal_reset_vulnerable, sqlite_source_id
@pytest.fixture(autouse=True)
def _reset_wal_reset_bug_warnings():
hermes_state._wal_reset_bug_warned_paths.clear()
hermes_state_wal._wal_reset_bug_warned_paths.clear()
yield
hermes_state._wal_reset_bug_warned_paths.clear()
hermes_state_wal._wal_reset_bug_warned_paths.clear()
class TestIsSqliteWalResetVulnerable:
@@ -61,7 +58,7 @@ class TestIsSqliteWalResetVulnerable:
class TestApplyWalWalResetGate:
def test_fresh_db_uses_delete_when_vulnerable(self, tmp_path, monkeypatch, caplog):
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
conn = sqlite3.connect(str(tmp_path / "fresh.db"))
with caplog.at_level("WARNING", logger="hermes_state"):
@@ -77,7 +74,7 @@ class TestApplyWalWalResetGate:
):
"""Already-WAL DBs must not be live-downgraded under concurrent openers."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
path = tmp_path / "prior_wal.db"
seed = sqlite3.connect(str(path))
@@ -109,7 +106,7 @@ class TestApplyWalWalResetGate:
def test_warning_deduped_per_label(self, tmp_path, monkeypatch, caplog):
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
with caplog.at_level("WARNING", logger="hermes_state"):
for name in ("a.db", "a.db", "b.db"):
@@ -151,7 +148,7 @@ class TestNoDowngradeUnderConcurrentOpeners:
All blocked-state assertions run WHILE the holder owns the DB."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
db = tmp_path / "live_wal.db"
seed = sqlite3.connect(str(db))
@@ -215,7 +212,7 @@ class TestNoDowngradeUnderConcurrentOpeners:
as 'not WAL' and flipping anyway (the incident's exact confusion).
Assertions run WHILE the holder's exclusive lock is live."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
db = tmp_path / "locked_wal.db"
seed = sqlite3.connect(str(db))
@@ -266,7 +263,7 @@ class TestNoDowngradeUnderConcurrentOpeners:
"""No concurrent openers → the vulnerable-SQLite DELETE gate still
applies exactly as before."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
conn = sqlite3.connect(str(tmp_path / "exclusive.db"))
try:
@@ -287,7 +284,7 @@ class TestNoDowngradeUnderConcurrentOpeners:
between probe and flip), the gate returns the observed mode instead of
raising or waiting the lock out."""
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
class _FlipLockedConnection(sqlite3.Connection):
@@ -316,11 +313,10 @@ class TestNoDowngradeUnderConcurrentOpeners:
refuse to downgrade when the mode probe is blocked by a concurrent
opener's exclusive lock — raise, never flip blind."""
monkeypatch.setattr(
hermes_state,
"is_sqlite_wal_reset_vulnerable",
hermes_state_wal, "is_sqlite_wal_reset_vulnerable",
lambda version_info=None: False,
)
monkeypatch.setattr(hermes_state, "resolve_journal_mode", lambda: "delete")
monkeypatch.setattr(hermes_state_wal, "resolve_journal_mode", lambda: "delete")
db = tmp_path / "cfg_delete.db"
seed = sqlite3.connect(str(db))
try:
@@ -358,11 +354,10 @@ class TestNoDowngradeUnderConcurrentOpeners:
"""The filesystem-incompat fallback must not downgrade when the on-disk
mode cannot be verified (possible concurrent openers)."""
monkeypatch.setattr(
hermes_state,
"is_sqlite_wal_reset_vulnerable",
hermes_state_wal, "is_sqlite_wal_reset_vulnerable",
lambda version_info=None: False,
)
hermes_state._wal_fallback_warned_paths.clear()
hermes_state_wal._wal_fallback_warned_paths.clear()
class _LockedProbeConnection(sqlite3.Connection):
def execute(self, sql, *args, **kwargs): # type: ignore[override]
@@ -394,9 +389,9 @@ def test_doctor_warns_without_adding_issues(monkeypatch, tmp_path, capsys):
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setattr("hermes_constants.get_hermes_home", lambda: home)
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda version_info=None: True
)
monkeypatch.setattr(hermes_state, "sqlite_source_id", lambda: "testid-abc")
monkeypatch.setattr(hermes_state_wal, "sqlite_source_id", lambda: "testid-abc")
monkeypatch.setattr(sqlite3, "sqlite_version", "3.50.4", raising=False)
args = SimpleNamespace(fix=False, ack=None)
+20 -24
View File
@@ -23,11 +23,10 @@ from pathlib import Path
import pytest
import hermes_state
from hermes_state import (
SessionDB,
is_malformed_db_error,
repair_state_db_schema,
)
import hermes_state_repair
import hermes_state_wal
from hermes_state import SessionDB, is_malformed_db_error
from hermes_state_repair import repair_state_db_schema
def _build_healthy_db(db_path: Path) -> str:
@@ -80,10 +79,9 @@ def test_generic_malformed_open_does_not_attempt_schema_surgery(
def _generic_corruption(*_args, **_kwargs):
raise sqlite3.DatabaseError("database disk image is malformed")
monkeypatch.setattr(hermes_state, "apply_wal_with_fallback", _generic_corruption)
monkeypatch.setattr(hermes_state, "apply_wal_with_fallback", _generic_corruption) # SessionDB open path
monkeypatch.setattr(
hermes_state,
"repair_state_db_schema",
hermes_state, "repair_state_db_schema",
lambda *args, **kwargs: repair_calls.append((args, kwargs)),
)
@@ -188,7 +186,7 @@ def _corrupt_fts_shadow_segments(db_path: Path) -> None:
def test_fts_read_corruption_repaired_in_place(tmp_path):
"""``repair_state_db_schema`` rebuilds the FTS index so reads resume."""
from hermes_state import _db_opens_cleanly
from hermes_state_repair import _db_opens_cleanly
db_path = tmp_path / "state.db"
_build_healthy_db(db_path)
@@ -275,7 +273,7 @@ def _corrupt_fts_index_data(db_path: Path) -> None:
def test_fts_write_corruption_detected_by_write_probe(tmp_path):
"""_db_opens_cleanly's rolled-back write probe flags FTS write corruption."""
from hermes_state import _db_opens_cleanly
from hermes_state_repair import _db_opens_cleanly
db_path = tmp_path / "state.db"
_build_healthy_db(db_path)
@@ -296,7 +294,7 @@ def test_fts_write_corruption_detected_by_write_probe(tmp_path):
def test_fts_write_corruption_repaired_in_place(tmp_path):
"""repair_state_db_schema rebuilds the FTS index; reads + writes resume."""
from hermes_state import _db_opens_cleanly
from hermes_state_repair import _db_opens_cleanly
db_path = tmp_path / "state.db"
_build_healthy_db(db_path)
@@ -380,7 +378,7 @@ def test_repair_rebuilds_stale_btree_indexes(tmp_path):
_corrupt_btree_index(db_path, "idx_messages_session")
# The real detector must see the real corruption...
reason = hermes_state._db_opens_cleanly(db_path)
reason = hermes_state_repair._db_opens_cleanly(db_path)
assert reason is not None
assert "wrong # of entries in index idx_messages_session" in reason
@@ -391,7 +389,7 @@ def test_repair_rebuilds_stale_btree_indexes(tmp_path):
# Post-repair the DB is genuinely healthy: detector and raw
# integrity_check both agree, and the repaired index answers queries.
assert hermes_state._db_opens_cleanly(db_path) is None
assert hermes_state_repair._db_opens_cleanly(db_path) is None
raw = sqlite3.connect(str(db_path))
assert raw.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
n = raw.execute(
@@ -481,7 +479,7 @@ def test_repair_skips_surgery_while_another_process_holds_the_lock(
# No surgery ran: no backup was taken and the DB is still malformed.
assert report["backup_path"] is None
assert not list(tmp_path.glob("state.db.malformed-backup-*"))
assert hermes_state._db_opens_cleanly(db_path) is not None
assert hermes_state_repair._db_opens_cleanly(db_path) is not None
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX flock test")
@@ -503,7 +501,7 @@ def test_repair_reports_success_when_the_holder_already_healed_the_db(
_REPAIR_SCRIPT = """
import sys, json
sys.path.insert(0, {root!r})
from hermes_state import repair_state_db_schema
from hermes_state_repair import repair_state_db_schema
print(json.dumps(repair_state_db_schema({db!r})), flush=True)
"""
@@ -597,8 +595,7 @@ def test_backup_refusal_hard_stops_the_repair(tmp_path, monkeypatch):
original_bytes = db_path.read_bytes()
monkeypatch.setattr(
hermes_state,
"_backup_db_file",
hermes_state_repair, "_backup_db_file",
lambda p: (None, "a connection to it is still open in this process"),
)
@@ -610,7 +607,7 @@ def test_backup_refusal_hard_stops_the_repair(tmp_path, monkeypatch):
assert "still open" in report["error"]
# No mutating strategy ran: the damaged source bytes are untouched.
assert db_path.read_bytes() == original_bytes
assert hermes_state._db_opens_cleanly(db_path) is not None
assert hermes_state_repair._db_opens_cleanly(db_path) is not None
def test_backup_copy_failure_hard_stops_the_repair(tmp_path, monkeypatch):
@@ -620,8 +617,7 @@ def test_backup_copy_failure_hard_stops_the_repair(tmp_path, monkeypatch):
_corrupt_duplicate_fts(db_path)
monkeypatch.setattr(
hermes_state,
"_backup_db_file",
hermes_state_repair, "_backup_db_file",
lambda p: (None, "backup copy failed: [Errno 28] No space left on device"),
)
@@ -671,7 +667,7 @@ def _configure_journal_mode(monkeypatch, tmp_path, mode) -> None:
yaml.safe_dump({"database": {"journal_mode": mode}}), encoding="utf-8",
)
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda **kwargs: False,
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda **kwargs: False,
)
@@ -722,7 +718,7 @@ def test_repair_restore_matches_canonical_on_vulnerable_sqlite(
db_path = tmp_path / "state.db"
_configure_journal_mode(monkeypatch, tmp_path, "wal")
monkeypatch.setattr(
hermes_state, "is_sqlite_wal_reset_vulnerable", lambda **kwargs: True
hermes_state_wal, "is_sqlite_wal_reset_vulnerable", lambda **kwargs: True
)
_build_healthy_db(db_path)
conn = sqlite3.connect(str(db_path))
@@ -756,7 +752,7 @@ def test_repair_logs_mode_change_when_probe_succeeded(
with (
patch.object(
hermes_state, "_probe_journal_mode_for_repair", return_value="delete"
hermes_state_repair, "_probe_journal_mode_for_repair", return_value="delete"
),
caplog.at_level(logging.WARNING, logger="hermes_state"),
):
@@ -813,7 +809,7 @@ def test_repair_restore_failure_is_nonfatal_and_logged(
raise sqlite3.OperationalError("database is locked")
with (
patch.object(hermes_state, "apply_wal_with_fallback", _refused),
patch.object(hermes_state_wal, "apply_wal_with_fallback", _refused),
caplog.at_level(logging.WARNING, logger="hermes_state"),
):
report = repair_state_db_schema(db_path)
+2 -1
View File
@@ -14,7 +14,8 @@ from unittest.mock import MagicMock
import pytest
from hermes_state import SessionDB, StateDbCorruptError, _on_disk_journal_mode
from hermes_state import SessionDB, StateDbCorruptError
from hermes_state_wal import _on_disk_journal_mode
class _NotADbOnce:
+10 -13
View File
@@ -29,11 +29,10 @@ from pathlib import Path
import pytest
import hermes_state
import hermes_state_repair
import hermes_state_holders
from hermes_state import (
SessionDB,
repair_state_db_schema,
)
from hermes_state import SessionDB
from hermes_state_repair import repair_state_db_schema
def _make_wal_db(tmp_path: Path) -> Path:
@@ -101,7 +100,7 @@ def test_repair_checks_foreign_holders_before_opening_sqlite(tmp_path, monkeypat
def _unexpected_probe(*_args, **_kwargs):
pytest.fail("repair opened SQLite before excluding foreign holders")
monkeypatch.setattr(hermes_state, "_connect_repair_durable", _unexpected_probe)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", _unexpected_probe)
report = repair_state_db_schema(db, backup=False)
@@ -181,7 +180,7 @@ def test_uninspectable_watched_descriptor_blocks_repair_before_sqlite(
def _unexpected_probe(*_args, **_kwargs):
pytest.fail("repair opened SQLite with unproven descriptor identity")
monkeypatch.setattr(hermes_state, "_connect_repair_durable", _unexpected_probe)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", _unexpected_probe)
report = repair_state_db_schema(db, backup=False)
@@ -227,8 +226,7 @@ def test_uninspectable_unknown_descriptor_uses_hermes_identity_at_repair_boundar
pytest.fail("repair opened SQLite with an unproven Hermes descriptor")
monkeypatch.setattr(
hermes_state,
"_connect_repair_durable",
hermes_state_repair, "_connect_repair_durable",
_unexpected_probe,
)
report = repair_state_db_schema(db, backup=False)
@@ -236,7 +234,7 @@ def test_uninspectable_unknown_descriptor_uses_hermes_identity_at_repair_boundar
assert report["repaired"] is False
assert "live writer" in (report["error"] or "").lower()
else:
real_connect = hermes_state._connect_repair_durable
real_connect = hermes_state_repair._connect_repair_durable
probe_reached = False
def _record_probe(*args, **kwargs):
@@ -245,8 +243,7 @@ def test_uninspectable_unknown_descriptor_uses_hermes_identity_at_repair_boundar
return real_connect(*args, **kwargs)
monkeypatch.setattr(
hermes_state,
"_connect_repair_durable",
hermes_state_repair, "_connect_repair_durable",
_record_probe,
)
report = repair_state_db_schema(db, backup=False)
@@ -288,7 +285,7 @@ def test_uninspectable_watched_identity_blocks_alias_before_sqlite(
def _unexpected_probe(*_args, **_kwargs):
pytest.fail("repair opened SQLite with an unproven watched identity")
monkeypatch.setattr(hermes_state, "_connect_repair_durable", _unexpected_probe)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", _unexpected_probe)
report = repair_state_db_schema(db, backup=False)
@@ -332,7 +329,7 @@ def test_uninspectable_alias_descriptor_for_hermes_blocks_before_sqlite(
def _unexpected_probe(*_args, **_kwargs):
pytest.fail("repair opened SQLite with an unproven Hermes alias fd")
monkeypatch.setattr(hermes_state, "_connect_repair_durable", _unexpected_probe)
monkeypatch.setattr(hermes_state_repair, "_connect_repair_durable", _unexpected_probe)
report = repair_state_db_schema(db, backup=False)
+3 -12
View File
@@ -25,17 +25,8 @@ from pathlib import Path
from unittest.mock import patch
import hermes_state
from hermes_state import (
_MAX_MALFORMED_BACKUPS,
_MAX_PERSISTENT_REPAIR_ATTEMPTS,
_backup_db_file,
_existing_malformed_backups,
_persistent_repair_attempts_exhausted,
_prune_malformed_backups,
_record_repair_outcome,
_repair_ledger_path,
repair_state_db_schema,
)
import hermes_state_repair
from hermes_state_repair import _MAX_MALFORMED_BACKUPS, _MAX_PERSISTENT_REPAIR_ATTEMPTS, _backup_db_file, _existing_malformed_backups, _persistent_repair_attempts_exhausted, _prune_malformed_backups, _record_repair_outcome, _repair_ledger_path, repair_state_db_schema
def _make_unrepairable_db(tmp_path: Path) -> Path:
@@ -76,7 +67,7 @@ class TestPersistentAttemptCap:
# Budget burned: the next call must refuse WITHOUT running surgery
# (and without taking another backup).
backups_before = len(_existing_malformed_backups(db))
with patch.object(hermes_state, "_repair_state_db_schema_locked") as surgery:
with patch.object(hermes_state_repair, "_repair_state_db_schema_locked") as surgery:
report = repair_state_db_schema(db)
surgery.assert_not_called()
assert report["repaired"] is False
+3 -13
View File
@@ -29,18 +29,8 @@ from pathlib import Path
from unittest.mock import patch
import hermes_state
from hermes_state import (
_MAX_MALFORMED_BACKUPS,
_MAX_PERSISTENT_REPAIR_ATTEMPTS,
_REPAIR_BACKUP_MIN_FREE_BYTES,
_backup_content_identity,
_backup_db_file,
_db_fingerprint,
_existing_malformed_backups,
_persistent_repair_attempts_exhausted,
_record_repair_outcome,
_repair_backup_headroom_bytes,
)
import hermes_state_repair
from hermes_state_repair import _MAX_MALFORMED_BACKUPS, _MAX_PERSISTENT_REPAIR_ATTEMPTS, _REPAIR_BACKUP_MIN_FREE_BYTES, _backup_content_identity, _backup_db_file, _db_fingerprint, _existing_malformed_backups, _persistent_repair_attempts_exhausted, _record_repair_outcome, _repair_backup_headroom_bytes
def _damaged_db(tmp_path: Path, size: int = 200_000) -> Path:
@@ -255,7 +245,7 @@ def test_repair_aborts_when_backup_refused_for_disk(tmp_path):
"Usage", (), {"total": 0, "used": 0, "free": _REPAIR_BACKUP_MIN_FREE_BYTES // 2}
)()
with patch("shutil.disk_usage", return_value=tight):
report = hermes_state.repair_state_db_schema(db)
report = hermes_state_repair.repair_state_db_schema(db)
assert not report.get("repaired")
assert "free" in (report.get("error") or "").lower()
+57 -57
View File
@@ -66,7 +66,8 @@ from types import SimpleNamespace
import pytest
import hermes_state
from hermes_state import repair_state_db_schema
import hermes_state_repair
from hermes_state_repair import repair_state_db_schema
PAGE_SIZE = 4096
@@ -130,7 +131,7 @@ def _leave_hot_wal_row(db_path: str) -> None:
def _probe_repair_lock_from_child(db_path: str, result) -> None:
"""Attempt the repair lock with a short timeout from another process."""
hermes_state._REPAIR_LOCK_TIMEOUT_SECONDS = 0.5
with hermes_state._cross_process_repair_lock(Path(db_path)) as holding:
with hermes_state_repair._cross_process_repair_lock(Path(db_path)) as holding:
result.put(holding)
@@ -196,13 +197,13 @@ def test_strategies_never_receive_the_live_database(corrupt_db, monkeypatch):
"""Every strategy mutates its argument in place, so the property that
makes them safe is simply that the argument is never the real file."""
seen: list[Path] = []
real = hermes_state._run_repair_strategies
real = hermes_state_repair._run_repair_strategies
def spy(path, report):
seen.append(path)
return real(path, report)
monkeypatch.setattr(hermes_state, "_run_repair_strategies", spy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", spy)
repair_state_db_schema(corrupt_db)
assert seen, "the repair path did not run at all"
@@ -259,7 +260,7 @@ def test_successful_repair_is_promoted_over_the_original(tmp_path, monkeypatch):
# Force the "already healthy" short-circuit off so the staging path runs,
# and have the strategy pass mark a repair after writing a marker row.
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda path: "forced-unhealthy"
)
def fake_strategies(scratch_path, report):
@@ -271,7 +272,7 @@ def test_successful_repair_is_promoted_over_the_original(tmp_path, monkeypatch):
report["strategy"] = "test_strategy"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", fake_strategies)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", fake_strategies)
report = repair_state_db_schema(db)
assert report["repaired"] is True
@@ -303,7 +304,7 @@ def test_committed_writer_after_staging_is_never_lost(
_make_repair_test_db(db, journal_mode=journal_mode)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
ready = multiprocessing.get_context("spawn").Event()
start = multiprocessing.get_context("spawn").Event()
@@ -329,7 +330,7 @@ def test_committed_writer_after_staging_is_never_lost(
report["strategy"] = "race_test"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", staged_strategy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", staged_strategy)
report = repair_state_db_schema(db, backup=False)
writer.join(20)
if writer.is_alive():
@@ -361,11 +362,10 @@ def test_environmental_aborts_do_not_burn_repair_ledger(tmp_path, monkeypatch):
db = tmp_path / "state.db"
_make_repair_test_db(db)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
monkeypatch.setattr(
hermes_state,
"_repair_scratch_space_error",
hermes_state_repair, "_repair_scratch_space_error",
lambda _path: "temporary disk pressure",
)
@@ -374,10 +374,10 @@ def test_environmental_aborts_do_not_burn_repair_ledger(tmp_path, monkeypatch):
assert report["repaired"] is False
assert report["error"] == "temporary disk pressure"
ledger_path = hermes_state._repair_ledger_path(db)
ledger_path = hermes_state_repair._repair_ledger_path(db)
assert not ledger_path.exists(), "environmental aborts must not consume attempts"
monkeypatch.setattr(hermes_state, "_repair_scratch_space_error", lambda _path: None)
monkeypatch.setattr(hermes_state_repair, "_repair_scratch_space_error", lambda _path: None)
def successful_strategy(scratch_path, report):
with sqlite3.connect(str(scratch_path)) as conn:
@@ -387,7 +387,7 @@ def test_environmental_aborts_do_not_burn_repair_ledger(tmp_path, monkeypatch):
report["strategy"] = "after_environmental_aborts"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", successful_strategy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", successful_strategy)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is True
assert report["strategy"] == "after_environmental_aborts"
@@ -397,7 +397,7 @@ def test_actual_strategy_failure_still_consumes_one_attempt(tmp_path, monkeypatc
db = tmp_path / "state.db"
_make_repair_test_db(db)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
def failed_strategy(_scratch_path, report):
@@ -405,10 +405,10 @@ def test_actual_strategy_failure_still_consumes_one_attempt(tmp_path, monkeypatc
report["strategy"] = None
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", failed_strategy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", failed_strategy)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is False
ledger = hermes_state._read_repair_ledger(db)
ledger = hermes_state_repair._read_repair_ledger(db)
assert ledger["failed_attempts"] == 1
@@ -419,7 +419,7 @@ def test_repair_outcome_is_recorded_while_cross_process_lock_is_held(
db = tmp_path / "state.db"
_make_repair_test_db(db)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
def failed_strategy(_scratch_path, report):
@@ -427,10 +427,10 @@ def test_repair_outcome_is_recorded_while_cross_process_lock_is_held(
report["strategy"] = None
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", failed_strategy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", failed_strategy)
observed = []
lock_released = threading.Event()
real_repair_lock = hermes_state._cross_process_repair_lock
real_repair_lock = hermes_state_repair._cross_process_repair_lock
@contextlib.contextmanager
def tracking_repair_lock(path):
@@ -439,7 +439,7 @@ def test_repair_outcome_is_recorded_while_cross_process_lock_is_held(
lock_released.set()
monkeypatch.setattr(
hermes_state, "_cross_process_repair_lock", tracking_repair_lock
hermes_state_repair, "_cross_process_repair_lock", tracking_repair_lock
)
def record_outcome(_db_path, *, repaired, fingerprint=None):
@@ -462,7 +462,7 @@ def test_repair_outcome_is_recorded_while_cross_process_lock_is_held(
probe.join(5)
assert repaired is False
monkeypatch.setattr(hermes_state, "_record_repair_outcome", record_outcome)
monkeypatch.setattr(hermes_state_repair, "_record_repair_outcome", record_outcome)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is False
@@ -484,9 +484,9 @@ def test_exhaustion_is_rechecked_after_acquiring_repair_lock(tmp_path, monkeypat
return len(exhaustion_checks) >= 2
monkeypatch.setattr(
hermes_state, "_persistent_repair_attempts_exhausted", exhaustion_probe
hermes_state_repair, "_persistent_repair_attempts_exhausted", exhaustion_probe
)
monkeypatch.setattr(hermes_state, "_live_writer_holds_db", lambda _path: False)
monkeypatch.setattr(hermes_state_repair, "_live_writer_holds_db", lambda _path: False)
surgery_calls = []
def unexpected_surgery(_db_path, *, backup, report):
@@ -494,7 +494,7 @@ def test_exhaustion_is_rechecked_after_acquiring_repair_lock(tmp_path, monkeypat
return report
monkeypatch.setattr(
hermes_state, "_repair_state_db_schema_locked", unexpected_surgery
hermes_state_repair, "_repair_state_db_schema_locked", unexpected_surgery
)
report = repair_state_db_schema(db, backup=False)
@@ -513,7 +513,7 @@ def test_scratch_budget_counts_sidecars_in_vacuum_multiplier(tmp_path, monkeypat
db.write_bytes(b"m" * main_bytes)
db.with_name(db.name + "-wal").write_bytes(b"w" * wal_bytes)
total = 10_000_000_000
headroom = hermes_state._repair_backup_headroom_bytes(total)
headroom = hermes_state_repair._repair_backup_headroom_bytes(total)
# This exactly satisfies the obsolete ``snapshot + 2*main + headroom``
# calculation, but is below the corrected ``3*snapshot + headroom``.
old_required = main_bytes + wal_bytes + (2 * main_bytes) + headroom
@@ -523,7 +523,7 @@ def test_scratch_budget_counts_sidecars_in_vacuum_multiplier(tmp_path, monkeypat
lambda _path: SimpleNamespace(total=total, free=old_required),
)
error = hermes_state._repair_scratch_space_error(db)
error = hermes_state_repair._repair_scratch_space_error(db)
assert error is not None
assert "VACUUM may need another" in error
@@ -536,7 +536,7 @@ def test_environmental_promotion_failures_do_not_burn_ledger(
db = tmp_path / "state.db"
_make_repair_test_db(db)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
def successful_strategy(_scratch_path, report):
@@ -544,8 +544,8 @@ def test_environmental_promotion_failures_do_not_burn_ledger(
report["strategy"] = "promotion_environment_test"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", successful_strategy)
real_copy = hermes_state._copy_database_snapshot
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", successful_strategy)
real_copy = hermes_state_repair._copy_database_snapshot
copy_calls = 0
def fail_promotion_three_times(source, destination, **kwargs):
@@ -556,17 +556,17 @@ def test_environmental_promotion_failures_do_not_burn_ledger(
return real_copy(source, destination, **kwargs)
monkeypatch.setattr(
hermes_state, "_copy_database_snapshot", fail_promotion_three_times
hermes_state_repair, "_copy_database_snapshot", fail_promotion_three_times
)
for _ in range(3):
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is False
assert "could not be promoted" in report["error"]
ledger = hermes_state._read_repair_ledger(db)
ledger = hermes_state_repair._read_repair_ledger(db)
assert ledger.get("failed_attempts", 0) == 0
monkeypatch.setattr(hermes_state, "_copy_database_snapshot", real_copy)
monkeypatch.setattr(hermes_state_repair, "_copy_database_snapshot", real_copy)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is True
assert report["strategy"] == "promotion_environment_test"
@@ -577,7 +577,7 @@ def test_corrupt_promotion_failure_consumes_one_attempt(tmp_path, monkeypatch):
db = tmp_path / "state.db"
_make_repair_test_db(db)
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
def successful_strategy(_scratch_path, report):
@@ -585,8 +585,8 @@ def test_corrupt_promotion_failure_consumes_one_attempt(tmp_path, monkeypatch):
report["strategy"] = "corrupt-promotion-test"
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", successful_strategy)
real_copy = hermes_state._copy_database_snapshot
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", successful_strategy)
real_copy = hermes_state_repair._copy_database_snapshot
calls = 0
def fail_promotion_with_corruption(source, destination, **kwargs):
@@ -597,12 +597,12 @@ def test_corrupt_promotion_failure_consumes_one_attempt(tmp_path, monkeypatch):
return real_copy(source, destination, **kwargs)
monkeypatch.setattr(
hermes_state, "_copy_database_snapshot", fail_promotion_with_corruption
hermes_state_repair, "_copy_database_snapshot", fail_promotion_with_corruption
)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is False
assert hermes_state._read_repair_ledger(db)["failed_attempts"] == 1
assert hermes_state_repair._read_repair_ledger(db)["failed_attempts"] == 1
def test_snapshot_includes_committed_wal_frames(tmp_path):
@@ -617,7 +617,7 @@ def test_snapshot_includes_committed_wal_frames(tmp_path):
assert db.with_name(db.name + "-wal").exists()
scratch = tmp_path / "state.db.repair-scratch"
hermes_state._copy_database_snapshot(db, scratch)
hermes_state_repair._copy_database_snapshot(db, scratch)
with sqlite3.connect(str(scratch)) as check:
assert check.execute("SELECT body FROM messages").fetchall() == [
("committed-only-in-wal",)
@@ -646,7 +646,7 @@ def test_failed_wal_repair_preserves_committed_rows_semantically(
pytest.skip("SQLite/filesystem did not retain a hot WAL sidecar")
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
strategy_calls = []
@@ -657,7 +657,7 @@ def test_failed_wal_repair_preserves_committed_rows_semantically(
report["strategy"] = None
return report
monkeypatch.setattr(hermes_state, "_run_repair_strategies", failed_strategy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", failed_strategy)
report = repair_state_db_schema(db, backup=False)
assert report["repaired"] is False
assert strategy_calls == [True], "the test must exercise strategy failure"
@@ -673,12 +673,12 @@ def test_snapshot_deadline_has_a_floor_and_scales_with_source_size(
tmp_path, monkeypatch
):
"""Large snapshots get more than the lock floor without huge fixtures."""
deadline = getattr(hermes_state, "_repair_snapshot_timeout_seconds", None)
deadline = getattr(hermes_state_repair, "_repair_snapshot_timeout_seconds", None)
assert callable(deadline), "repair snapshots need a size-scaled deadline"
# Lower the throughput only for this arithmetic test so a 72 MiB fixture
# crosses the floor without allocating a multi-GB file.
monkeypatch.setattr(
hermes_state, "_REPAIR_SNAPSHOT_MIN_THROUGHPUT_BYTES_PER_SECOND", 256 * 1024
hermes_state_repair, "_REPAIR_SNAPSHOT_MIN_THROUGHPUT_BYTES_PER_SECOND", 256 * 1024
)
db = tmp_path / "state.db"
@@ -711,7 +711,7 @@ def test_transactional_promotion_preserves_a_live_wal_reader(tmp_path):
reader.execute("BEGIN")
assert reader.execute("SELECT body FROM messages").fetchall() == [("old",)]
hermes_state._copy_database_snapshot(scratch, live)
hermes_state_repair._copy_database_snapshot(scratch, live)
assert reader.execute("SELECT body FROM messages").fetchall() == [("old",)]
with sqlite3.connect(str(live)) as fresh:
@@ -742,7 +742,7 @@ def test_interrupted_snapshot_rolls_back_destination(tmp_path, monkeypatch):
monkeypatch.setattr(hermes_state.time, "monotonic", lambda: next(ticks))
with pytest.raises(TimeoutError):
hermes_state._copy_database_snapshot(source, destination)
hermes_state_repair._copy_database_snapshot(source, destination)
with sqlite3.connect(str(destination)) as conn:
assert conn.execute("SELECT value FROM marker").fetchall() == [("original",)]
@@ -752,7 +752,7 @@ def test_failed_promotion_returns_failure_and_preserves_original(tmp_path, monke
db = tmp_path / "state.db"
_write_populated_db(db)
before = hashlib.sha256(db.read_bytes()).hexdigest()
real_copy = hermes_state._copy_database_snapshot
real_copy = hermes_state_repair._copy_database_snapshot
calls = 0
def fail_second_copy(source, destination, **kwargs):
@@ -768,10 +768,10 @@ def test_failed_promotion_returns_failure_and_preserves_original(tmp_path, monke
return report
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
monkeypatch.setattr(hermes_state, "_copy_database_snapshot", fail_second_copy)
monkeypatch.setattr(hermes_state, "_run_repair_strategies", fake_strategies)
monkeypatch.setattr(hermes_state_repair, "_copy_database_snapshot", fail_second_copy)
monkeypatch.setattr(hermes_state_repair, "_run_repair_strategies", fake_strategies)
report = repair_state_db_schema(db, backup=False)
@@ -785,12 +785,12 @@ def test_failed_promotion_returns_failure_and_preserves_original(tmp_path, monke
def test_scratch_space_guard_accounts_for_snapshot_and_vacuum(tmp_path, monkeypatch):
db = tmp_path / "state.db"
db.write_bytes(b"x" * 4096)
headroom = hermes_state._repair_backup_headroom_bytes(10_000_000_000)
headroom = hermes_state_repair._repair_backup_headroom_bytes(10_000_000_000)
free = (3 * db.stat().st_size) + headroom - 1
usage = SimpleNamespace(total=10_000_000_000, free=free)
monkeypatch.setattr(shutil, "disk_usage", lambda _path: usage)
error = hermes_state._repair_scratch_space_error(db)
error = hermes_state_repair._repair_scratch_space_error(db)
assert error is not None
assert "VACUUM may need" in error
@@ -808,7 +808,7 @@ def test_stale_scratch_is_removed_before_health_check(tmp_path, monkeypatch):
assert not scratch.exists()
return None
monkeypatch.setattr(hermes_state, "_db_opens_cleanly", fake_health)
monkeypatch.setattr(hermes_state_repair, "_db_opens_cleanly", fake_health)
report = repair_state_db_schema(db, backup=False)
@@ -826,7 +826,7 @@ def test_stale_scratch_is_removed_before_space_check(tmp_path, monkeypatch):
checked_space = False
monkeypatch.setattr(
hermes_state, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
hermes_state_repair, "_db_opens_cleanly", lambda _path: "forced-unhealthy"
)
def fake_space_check(_path):
@@ -836,7 +836,7 @@ def test_stale_scratch_is_removed_before_space_check(tmp_path, monkeypatch):
return "forced low space"
monkeypatch.setattr(
hermes_state, "_repair_scratch_space_error", fake_space_check
hermes_state_repair, "_repair_scratch_space_error", fake_space_check
)
report = repair_state_db_schema(db, backup=False)
@@ -857,9 +857,9 @@ def test_stale_scratch_cleanup_failure_aborts_before_probe(tmp_path, monkeypatch
probed = True
return "forced-unhealthy"
monkeypatch.setattr(hermes_state, "_db_opens_cleanly", fake_health)
monkeypatch.setattr(hermes_state_repair, "_db_opens_cleanly", fake_health)
monkeypatch.setattr(
hermes_state, "_unlink_db_triple", lambda _path: "scratch is locked"
hermes_state_repair, "_unlink_db_triple", lambda _path: "scratch is locked"
)
report = {
"repaired": False,
@@ -868,7 +868,7 @@ def test_stale_scratch_cleanup_failure_aborts_before_probe(tmp_path, monkeypatch
"error": None,
}
result = hermes_state._repair_state_db_schema_locked(
result = hermes_state_repair._repair_state_db_schema_locked(
db, backup=False, report=report
)
+4 -5
View File
@@ -16,10 +16,9 @@ import sys
import pytest
import hermes_state
from hermes_state import (
apply_database_pragmas,
resolve_synchronous_level,
)
import hermes_state_wal
from hermes_state import apply_database_pragmas
from hermes_state_wal import resolve_synchronous_level
def _wal_conn(tmp_path):
@@ -186,7 +185,7 @@ class TestMacOSFloor:
conn = _wal_conn(tmp_path)
try:
monkeypatch.setattr(sys, "platform", "darwin")
hermes_state._enforce_macos_synchronous_full(conn)
hermes_state_wal._enforce_macos_synchronous_full(conn)
assert _level(conn) == 2
with caplog.at_level("WARNING"):
apply_database_pragmas(conn, db_label="state.db")
+20 -16
View File
@@ -3936,7 +3936,7 @@ def test_session_resume_profile_uses_profile_db_cwd(monkeypatch, tmp_path):
monkeypatch.setenv("TERMINAL_CWD", str(launch_cwd))
monkeypatch.setattr(server, "_profile_home", lambda _profile: profile_home)
monkeypatch.setattr("hermes_state.get_shared_session_db", lambda db_path=None: profile_db)
monkeypatch.setattr("hermes_state_registry.acquire", lambda db_path=None: profile_db)
monkeypatch.setattr(server, "_get_db", lambda: launch_db)
monkeypatch.setattr(server, "_enable_gateway_prompts", lambda: None)
monkeypatch.setattr(server, "_set_session_context", lambda target: [])
@@ -7895,7 +7895,7 @@ def test_ensure_session_db_row_stamps_profile_name(monkeypatch, tmp_path):
def close(self):
pass
monkeypatch.setattr("hermes_state.get_shared_session_db", _ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", _ProfileDB)
monkeypatch.setattr(server, "_resolve_model", lambda: "test-model")
server._ensure_session_db_row(
@@ -9792,7 +9792,7 @@ def test_config_set_model_recovers_failed_profile_resume_after_build_completes(
"hermes_cli.model_selection_guards.combined_selection_warning",
lambda *args, **kwargs: None,
)
monkeypatch.setattr("hermes_state.get_shared_session_db", FakeDb)
monkeypatch.setattr("hermes_state_registry.acquire", FakeDb)
monkeypatch.setattr(server, "_make_agent", fake_make_agent)
monkeypatch.setattr(server, "_transfer_db_to_agent", barrier_transfer)
monkeypatch.setattr(
@@ -14620,8 +14620,10 @@ def test_get_db_degrades_cleanly_when_sessiondb_init_fails(monkeypatch):
def _broken_shared(_db_path=None):
raise RuntimeError("locking protocol")
fake_mod.get_shared_session_db = _broken_shared
monkeypatch.setitem(sys.modules, "hermes_state", fake_mod)
fake_registry = types.ModuleType("hermes_state_registry")
fake_registry.acquire = _broken_shared
monkeypatch.setitem(sys.modules, "hermes_state_registry", fake_registry)
monkeypatch.setattr(server, "_db", None)
monkeypatch.setattr(server, "_db_error", None)
@@ -14643,8 +14645,10 @@ def test_ensure_session_db_row_false_when_store_unavailable(monkeypatch):
def _broken_shared(_db_path=None):
raise RuntimeError("utf-8 boom")
fake_mod.get_shared_session_db = _broken_shared
monkeypatch.setitem(sys.modules, "hermes_state", fake_mod)
fake_registry = types.ModuleType("hermes_state_registry")
fake_registry.acquire = _broken_shared
monkeypatch.setitem(sys.modules, "hermes_state_registry", fake_registry)
monkeypatch.setattr(server, "_db", None)
monkeypatch.setattr(server, "_db_error", None)
@@ -14996,7 +15000,7 @@ def test_session_list_honors_params_profile_opens_profile_db(monkeypatch, tmp_pa
monkeypatch.setattr(server, "_profile_home", lambda p: profile_home if p == "mlperf" else None)
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
resp = server.handle_request(
{
@@ -15037,7 +15041,7 @@ def test_session_most_recent_honors_params_profile(monkeypatch, tmp_path):
monkeypatch.setattr(server, "_profile_home", lambda p: profile_home if p == "mlperf" else None)
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB2)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB2)
resp = server.handle_request(
{
@@ -15164,7 +15168,7 @@ def test_session_delete_honors_params_profile_sessions_dir(monkeypatch, tmp_path
monkeypatch.setattr(server, "_profile_home", lambda p: profile_home if p == "mlperf" else None)
monkeypatch.setattr(server, "_get_db", lambda: None)
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
resp = server.handle_request(
{
@@ -15231,7 +15235,7 @@ def test_session_title_uses_session_profile_db_not_launch(monkeypatch, tmp_path)
"last_active": 1.0,
}
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
try:
set_resp = server.handle_request(
{
@@ -15288,7 +15292,7 @@ def test_session_history_uses_session_profile_db(monkeypatch, tmp_path):
"last_active": 1.0,
}
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
try:
resp = server.handle_request(
{"id": "1", "method": "session.history", "params": {"session_id": "sid"}}
@@ -15375,7 +15379,7 @@ def test_session_status_uses_session_profile_db(monkeypatch, tmp_path):
"last_active": 1.0,
}
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
try:
resp = server.handle_request(
{"id": "1", "method": "session.status", "params": {"session_id": "sid"}}
@@ -15417,7 +15421,7 @@ def test_teardown_ends_session_in_profile_db(monkeypatch, tmp_path):
seen["closed"] = True
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
session = {
"session_key": "ml-sess",
"profile_home": str(profile_home),
@@ -15510,7 +15514,7 @@ def test_session_branch_writes_to_parent_profile_db(monkeypatch, tmp_path):
}
server._sessions["parent"] = parent
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
monkeypatch.setattr(server, "_claim_active_session_slot", lambda *a, **k: (None, None))
def _fake_make_agent(*a, **k):
@@ -15932,7 +15936,7 @@ def test_session_branch_installs_parent_profile_secret_scope(monkeypatch, tmp_pa
}
server._sessions["parent"] = parent
monkeypatch.setattr(server, "_get_db", lambda: ProfileDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
monkeypatch.setattr(server, "_claim_active_session_slot", lambda *a, **k: (None, None))
def _fake_make_agent(*a, **k):
@@ -16049,7 +16053,7 @@ def test_session_branch_uses_persisted_display_history_after_compaction(monkeypa
}
server._sessions["parent"] = parent
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
monkeypatch.setattr(server, "_claim_active_session_slot", lambda *args, **kwargs: (None, None))
monkeypatch.setattr(server, "_make_agent", lambda *args, **kwargs: FakeAgent())
monkeypatch.setattr(server, "_set_session_context", lambda *args, **kwargs: {})
@@ -16109,7 +16113,7 @@ def test_pending_title_finalizer_uses_session_profile_db(monkeypatch, tmp_path):
seen["closed"] = True
monkeypatch.setattr(server, "_get_db", lambda: LaunchDB())
monkeypatch.setattr("hermes_state.get_shared_session_db", ProfileDB)
monkeypatch.setattr("hermes_state_registry.acquire", ProfileDB)
session = {
"session_key": "ml-sess",
"pending_title": "deferred-title",
+2 -2
View File
@@ -151,9 +151,9 @@ class TestBrowseShape:
return []
db = _DB()
monkeypatch.setattr("hermes_state.get_shared_session_db", lambda: db)
monkeypatch.setattr("hermes_state_registry.acquire", lambda: db)
monkeypatch.setattr(
"hermes_state.release_or_close",
"hermes_state_registry.release_or_close",
lambda _: setattr(db, "released", db.released + 1),
)
@@ -55,7 +55,7 @@ def homes(monkeypatch, tmp_path):
homes = {name: tmp_path / name for name in ("a", "b")}
for home in homes.values():
home.mkdir()
monkeypatch.setattr("hermes_state.get_shared_session_db", _DB)
monkeypatch.setattr("hermes_state_registry.acquire", _DB)
monkeypatch.setattr(server, "_get_db", lambda: _DB())
monkeypatch.setattr(server, "_profile_home", lambda p: homes.get(p) if p else None)
monkeypatch.setattr(server, "_profile_configured_cwd", lambda _home: str(tmp_path))
@@ -178,7 +178,7 @@ def test_lazy_recall_open_is_owned_by_the_agent(monkeypatch):
opened.append(db)
return db
monkeypatch.setattr("hermes_state.get_shared_session_db", _factory)
monkeypatch.setattr("hermes_state_registry.acquire", _factory)
agent = _bare_agent(_session_db=None, _persist_disabled=False)
got = agent._get_session_db_for_recall()
@@ -272,7 +272,7 @@ def build_env(monkeypatch, tmp_path):
opened.append(db)
return db
monkeypatch.setattr("hermes_state.get_shared_session_db", _factory)
monkeypatch.setattr("hermes_state_registry.acquire", _factory)
for name, value in [
("_set_session_context", lambda _key: []),
("_clear_session_context", lambda _tokens: None),

Some files were not shown because too many files have changed in this diff Show More