fix(agent): corrupt-session recovery guidance names the session's own state.db

The corruption explainer filled `{db_path}` from `_default_db_path()`, the
process default. A Desktop `serve` backend launched on the root home hosts
named-profile sessions whose SessionDB is `profiles/<name>/state.db`, so the
operator was told to inspect/repair a different profile's database. Pass the
agent's own `_session_db.db_path` from the turn finalizer; the process
default remains the fallback for agents without a bound store.

Reported in #105887.
This commit is contained in:
teknium1
2026-09-11 02:12:42 -07:00
committed by Teknium
parent df0eed4f6b
commit 16e4496d90
3 changed files with 28 additions and 4 deletions
+6 -3
View File
@@ -296,7 +296,7 @@ class TurnExplainersMixin:
@staticmethod
def _format_turn_completion_explanation(
turn_exit_reason: str, persistence_cause: Optional[str] = None
turn_exit_reason: str, persistence_cause: Optional[str] = None, db_path=None
) -> str:
"""User-facing explanation for an abnormal turn ending, or "" for normal / unknown reasons.
@@ -319,11 +319,14 @@ class TurnExplainersMixin:
persistence_cause or "unknown", _PERSISTENCE_DEFAULT_EXPLANATION
)
if persistence_cause == "corrupt":
# Copy-pasteable, so name the real store (profiles / HERMES_HOME do not live under ~/.hermes).
# Copy-pasteable, so name the store that actually failed: the agent's own
# SessionDB. A multi-profile backend (Desktop serve) hosts sessions whose
# state.db is NOT the process default, so the default would send the operator
# to inspect/repair the wrong profile's database (#105887).
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("{db_path}", str(db_path or _default_db_path()))
body = body.replace(
"{backups_dir}", str(get_default_hermes_root() / "backups")
)
+2 -1
View File
@@ -375,7 +375,8 @@ def _explain_abnormal_exit(agent, final_response, _turn_exit_reason, preserved_v
)
if _is_empty_terminal or _is_partial_fragment or str(_turn_exit_reason) == "partial_stream_recovery":
_explanation = agent._format_turn_completion_explanation(
_turn_exit_reason, getattr(agent, "_last_persistence_error_cause", None)
_turn_exit_reason, getattr(agent, "_last_persistence_error_cause", None),
db_path=getattr(getattr(agent, "_session_db", None), "db_path", None),
)
if _explanation:
# Replace the bare sentinel; keep a partial fragment and append why.
@@ -63,6 +63,26 @@ def test_gateway_corruption_banner_backups_dir_follows_hermes_home(monkeypatch,
assert "~/.hermes/backups" not in sent[0]
def test_format_turn_completion_corrupt_names_the_sessions_own_store(monkeypatch, tmp_path):
"""Recovery commands target the store that failed, not the process default (#105887).
A Desktop ``serve`` backend launched on the root home hosts named-profile sessions
whose SessionDB is ``profiles/<name>/state.db``; guidance built from the process
default would tell the operator to inspect/repair the root database.
"""
from run_agent import AIAgent
root = tmp_path / "root"
monkeypatch.setenv("HERMES_HOME", str(root))
failing = root / "profiles" / "research" / "state.db"
explanation = AIAgent._format_turn_completion_explanation(
"session_persistence_failed", "corrupt", db_path=failing
)
assert f"--source {failing} --inspect-only" in explanation
assert f"--source {root / 'state.db'}" not in explanation
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.