fix(agent): interpolate the live backups dir into corruption recovery guidance
The corrupt-cause recovery guidance hardcoded `~/.hermes/backups/` while
every other path in the same message follows the active HERMES_HOME
(`{db_path}` is already interpolated). A custom-home or named-profile
deployment was told to restore from a directory that may not exist at all,
mid data-loss incident. Both sites (turn-completion explainer and gateway
startup broadcast) now interpolate `<hermes_root>/backups` via
get_default_hermes_root(), matching hermes_cli/backup.py's real backup
location.
Fixes #104250
This commit is contained in:
@@ -123,7 +123,7 @@ _PERSISTENCE_CAUSE_EXPLANATIONS: Dict[str, str] = {
|
||||
"run `sqlite3 ... \".recover\"` against the live "
|
||||
"state.db, a vulnerable sqlite3 CLI can corrupt it "
|
||||
"further\n"
|
||||
"3. Restore from a backup in ~/.hermes/backups/\n"
|
||||
"3. Restore from a backup in {backups_dir}/\n"
|
||||
"Then send your message again."
|
||||
),
|
||||
"disk": (
|
||||
@@ -295,7 +295,11 @@ class TurnExplainersMixin:
|
||||
)
|
||||
if persistence_cause == "corrupt":
|
||||
# Copy-pasteable, so name the real store (profiles / HERMES_HOME do not live under ~/.hermes).
|
||||
from hermes_constants import get_default_hermes_root
|
||||
from hermes_state import _default_db_path
|
||||
|
||||
body = body.replace("{db_path}", str(_default_db_path()))
|
||||
body = body.replace(
|
||||
"{backups_dir}", str(get_default_hermes_root() / "backups")
|
||||
)
|
||||
return _NO_REPLY + body if body else ""
|
||||
|
||||
@@ -722,10 +722,12 @@ class GatewayNotificationsMixin:
|
||||
error = getattr(self, "_session_db_init_error", None)
|
||||
if not error:
|
||||
return
|
||||
from hermes_constants import get_default_hermes_root
|
||||
from hermes_state import _default_db_path, classify_persistence_error, format_session_db_unavailable
|
||||
if classify_persistence_error(error) == "corrupt":
|
||||
# Copy-pasteable, so name the real store (profiles / HERMES_HOME do not live under ~/.hermes).
|
||||
db_path = _default_db_path()
|
||||
backups_dir = get_default_hermes_root() / "backups"
|
||||
message = (
|
||||
"⚠️ Session database corruption detected. Messages may not be "
|
||||
"persisted. Recovery options:\n"
|
||||
@@ -738,7 +740,7 @@ class GatewayNotificationsMixin:
|
||||
" — 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"
|
||||
"3. Restore from a backup in ~/.hermes/backups/\n"
|
||||
f"3. Restore from a backup in {backups_dir}/\n"
|
||||
"Run `hermes doctor` for sanitized diagnostics."
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -31,6 +31,38 @@ def test_format_turn_completion_corrupt_includes_recovery_options():
|
||||
assert "Freeing disk space will not help" in explanation
|
||||
|
||||
|
||||
def test_gateway_corruption_banner_backups_dir_follows_hermes_home(monkeypatch, tmp_path):
|
||||
"""The gateway broadcast's step 3 must name the live backups dir, not ~/.hermes (#104250).
|
||||
|
||||
Pre-update backups live at ``<hermes_root>/backups`` (``hermes_cli/backup.py``); a
|
||||
custom-HERMES_HOME gateway must not be told to restore from a directory that never
|
||||
held its backups.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import gateway.run as gateway_run
|
||||
|
||||
custom_home = tmp_path / "custom-hermes-home"
|
||||
monkeypatch.setenv("HERMES_HOME", str(custom_home))
|
||||
|
||||
runner = object.__new__(gateway_run.GatewayRunner)
|
||||
runner._session_db_init_error = "database disk image is malformed"
|
||||
sent = []
|
||||
monkeypatch.setattr(
|
||||
runner, "_home_channel_transports", lambda: [("telegram", {}, "home-chat", object())]
|
||||
)
|
||||
|
||||
async def _capture_send(_platform, _home, _transport, message, _log_fmt):
|
||||
sent.append(message)
|
||||
|
||||
monkeypatch.setattr(runner, "_send_home_channel_message", _capture_send)
|
||||
asyncio.run(runner._send_session_db_warning_notifications())
|
||||
|
||||
assert sent, "warning must be broadcast to home channels"
|
||||
assert f"{custom_home / 'backups'}" in sent[0]
|
||||
assert "~/.hermes/backups" not in sent[0]
|
||||
|
||||
|
||||
def test_format_turn_completion_corrupt_never_names_the_live_db():
|
||||
"""The 'corrupt' cause must not direct a raw sqlite3 shell at the live DB.
|
||||
|
||||
|
||||
@@ -155,6 +155,24 @@ def test_explanation_persistence_corrupt_cause_never_says_free_space():
|
||||
assert "full disk" not in lower
|
||||
|
||||
|
||||
def test_explanation_persistence_corrupt_backups_dir_follows_hermes_home(monkeypatch, tmp_path):
|
||||
"""Step 3 must name the backups dir under the ACTIVE home, not ~/.hermes (#104250).
|
||||
|
||||
Pre-update backups live at ``<hermes_root>/backups`` (``hermes_cli/backup.py``), so a
|
||||
custom-HERMES_HOME deployment told to restore from ``~/.hermes/backups/`` is misdirected
|
||||
mid data-loss incident: that directory may not exist at all, or may hold an unrelated
|
||||
install's backups.
|
||||
"""
|
||||
custom_home = tmp_path / "custom-hermes-home"
|
||||
monkeypatch.setenv("HERMES_HOME", str(custom_home))
|
||||
out = AIAgent._format_turn_completion_explanation(
|
||||
"session_persistence_failed", "corrupt"
|
||||
)
|
||||
assert f"{custom_home / 'backups'}" in out
|
||||
assert "~/.hermes/backups" not in out
|
||||
assert "{backups_dir}" not in out
|
||||
|
||||
|
||||
def test_explanation_persistence_replaced_cause_forbids_inplace_repair():
|
||||
out = AIAgent._format_turn_completion_explanation(
|
||||
"session_persistence_failed", "replaced"
|
||||
|
||||
Reference in New Issue
Block a user