diff --git a/agent/turn_explainers.py b/agent/turn_explainers.py index 3453902597..0ee38115e3 100644 --- a/agent/turn_explainers.py +++ b/agent/turn_explainers.py @@ -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") ) diff --git a/agent/turn_finalizer.py b/agent/turn_finalizer.py index 5cf658e388..0a9dec481f 100644 --- a/agent/turn_finalizer.py +++ b/agent/turn_finalizer.py @@ -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. diff --git a/tests/run_agent/test_corruption_recovery_guidance.py b/tests/run_agent/test_corruption_recovery_guidance.py index 2112ef1cc8..e942bdfa6c 100644 --- a/tests/run_agent/test_corruption_recovery_guidance.py +++ b/tests/run_agent/test_corruption_recovery_guidance.py @@ -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//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.