From 914d8a0bd6d136ed8388235ba8dfeadc5129d718 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:45:41 +0530 Subject: [PATCH] fix(recovery): name the real state.db in the copy-pasteable recovery banners The gateway broadcast and turn-failure explanation printed a literal ~/.hermes/state.db; now that the line is a command the operator is meant to run as-is, interpolate _default_db_path() so profile / HERMES_HOME installs are pointed at the store that actually failed. Also: split a comment that a merge fused onto the logger line in session_lost_and_found.py, and fix an inverted test docstring. --- gateway/run.py | 13 ++++++++++--- hermes_cli/session_lost_and_found.py | 4 +++- run_agent.py | 9 +++++++-- tests/hermes_cli/test_sqlite3_cli_salvage_gate.py | 6 +++--- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index afe4cad295..2602479525 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -27476,20 +27476,27 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if not error: return - from hermes_state import classify_persistence_error, format_session_db_unavailable + from hermes_state import ( + _default_db_path, + classify_persistence_error, + format_session_db_unavailable, + ) cause = classify_persistence_error(error) hint = format_session_db_unavailable() if cause == "corrupt": + # Copy-pasteable, so name the real store (profiles / HERMES_HOME + # do not live under ~/.hermes). + db_path = _default_db_path() message = ( "⚠️ Session database corruption detected. Messages may not be " "persisted. Recovery options:\n" "1. Run `hermes doctor --fix`\n" "2. Stop the gateway, then recover with:\n" - " hermes sessions recover --source ~/.hermes/state.db " + f" hermes sessions recover --source {db_path} " "--inspect-only\n" " (if it reports recoverable) hermes sessions recover " - "--source ~/.hermes/state.db --output recovered-state.db\n" + f"--source {db_path} --output recovered-state.db\n" " — recovery snapshots the damaged file first; do NOT run " "`sqlite3 ... \".recover\"` against the live state.db, a " "vulnerable sqlite3 CLI can corrupt it further\n" diff --git a/hermes_cli/session_lost_and_found.py b/hermes_cli/session_lost_and_found.py index cb1622783a..09a362076b 100644 --- a/hermes_cli/session_lost_and_found.py +++ b/hermes_cli/session_lost_and_found.py @@ -32,7 +32,9 @@ import tempfile from pathlib import Path from typing import Any, Optional -logger = logging.getLogger(__name__)# Hermes session ids are timestamps: 20260812_135332_ab12cd. This is the +logger = logging.getLogger(__name__) + +# Hermes session ids are timestamps: 20260812_135332_ab12cd. This is the # strongest sentinel available for classifying schema-less rows. SESSION_ID_PATTERN = re.compile(r"^\d{8}_\d{6}_") diff --git a/run_agent.py b/run_agent.py index 90a32b7714..8796236a9e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4423,6 +4423,11 @@ class AIAgent: "pending_messages/pending-*.json." ) if cause == "corrupt": + from hermes_state import _default_db_path + + # Copy-pasteable, so name the real store (profiles / + # HERMES_HOME do not live under ~/.hermes). + db_path = _default_db_path() return ( prefix + "the turn was stopped because the state database " @@ -4431,10 +4436,10 @@ class AIAgent: "not help. Recovery options:\n" "1. Run `hermes doctor --fix`\n" "2. Stop the gateway, then recover with:\n" - " hermes sessions recover --source ~/.hermes/state.db " + f" hermes sessions recover --source {db_path} " "--inspect-only\n" " (if it reports recoverable) hermes sessions recover " - "--source ~/.hermes/state.db --output recovered-state.db\n" + f"--source {db_path} --output recovered-state.db\n" " — recovery snapshots the damaged file first; do NOT " "run `sqlite3 ... \".recover\"` against the live " "state.db, a vulnerable sqlite3 CLI can corrupt it " diff --git a/tests/hermes_cli/test_sqlite3_cli_salvage_gate.py b/tests/hermes_cli/test_sqlite3_cli_salvage_gate.py index bda5a4b105..ec999aa4e5 100644 --- a/tests/hermes_cli/test_sqlite3_cli_salvage_gate.py +++ b/tests/hermes_cli/test_sqlite3_cli_salvage_gate.py @@ -307,9 +307,9 @@ class TestEmittedCommandsSatisfyCliContract: return argparse.Namespace(**fields) def test_old_v1_shape_is_still_rejected(self, tmp_path): - """Guard the test's own premise: neither --inspect-only nor - --output (the shape the v1 banner printed) is rejected with rc 2 - by the real dispatcher.""" + """Guard the test's own premise: the bare `--source ` shape the + v1 banner printed (neither --inspect-only nor --output) is rejected + with rc 2 by the real dispatcher.""" import hermes_cli.sessions_cmd as sc rc = sc.cmd_sessions(self._namespace(tmp_path / "state.db"))