diff --git a/hermes_state_portability.py b/hermes_state_portability.py index be1939be00..1893742414 100644 --- a/hermes_state_portability.py +++ b/hermes_state_portability.py @@ -42,7 +42,7 @@ _IMPORT_SESSION_INSERT_SQL = """INSERT INTO sessions ( cwd, git_branch, git_repo_root, billing_provider, billing_base_url, billing_mode, estimated_cost_usd, actual_cost_usd, cost_status, cost_source, - pricing_version, title, api_call_count, archived, pinned + pricing_version, title, api_call_count, archived, pinned, hidden ) VALUES ( :id, :source, :user_id, :model, :model_config, @@ -53,7 +53,7 @@ _IMPORT_SESSION_INSERT_SQL = """INSERT INTO sessions ( :billing_provider, :billing_base_url, :billing_mode, :estimated_cost_usd, :actual_cost_usd, :cost_status, :cost_source, :pricing_version, :title, - :api_call_count, :archived, :pinned + :api_call_count, :archived, :pinned, :hidden )""" # Columns copied verbatim from the payload; typed columns are converted below. _IMPORT_PASSTHROUGH_COLS = ( @@ -66,7 +66,9 @@ _IMPORT_INT_COLS = ( _IMPORT_FLOAT_COLS = ("ended_at", "estimated_cost_usd", "actual_cost_usd") # Durable user decisions, not live runtime state: a restore that drops ``pinned`` hands the "keep" # session to the next startup prune and the stale-archive sweep, the two things the pin opts out of. -_IMPORT_FLAG_COLS = ("archived", "pinned") +# ``hidden`` is what makes an adopted Bot Mode chat canonical: without it the chat shows in every +# listing, loses its rename guard and becomes a stale-archive candidate. +_IMPORT_FLAG_COLS = ("archived", "pinned", "hidden") def _rich_select(select_cols: str, where: str, tail: str = "", prompt_select: Optional[str] = "") -> str: diff --git a/tests/hermes_state/test_session_import_keep_flags.py b/tests/hermes_state/test_session_import_keep_flags.py index 3469abd22b..927f271d81 100644 --- a/tests/hermes_state/test_session_import_keep_flags.py +++ b/tests/hermes_state/test_session_import_keep_flags.py @@ -1,9 +1,9 @@ """A restored or adopted session keeps the user's durable flags. ``import_sessions`` (the dashboard import of a ``hermes sessions export`` backup, and stranded-session -adoption) restored ``archived`` but not ``pinned``. Pinned is the "keep" flag the startup prune and the -stale-archive sweep both exempt, so an old pinned session came back unpinned and the next startup -deleted it. +adoption) restored ``archived`` but not ``pinned`` or ``hidden``. Pinned is the "keep" flag the startup +prune and the stale-archive sweep both exempt, so an old pinned session came back unpinned and the next +startup deleted it; an adopted Bot Mode chat came back visible and no longer canonical. """ from __future__ import annotations @@ -39,3 +39,24 @@ def test_restored_pinned_session_survives_the_startup_prune(tmp_path): finally: source.close() target.close() + + +def test_adopted_bot_chat_stays_the_hidden_canonical_chat(tmp_path): + """Stranded-session adoption imports the donor's Bot Mode chat into the profile store; Bot Mode + chats are hidden, and hidden is what keeps the canonical one out of listings and the stale sweep.""" + donor = SessionDB(db_path=tmp_path / "default.db") + profile = SessionDB(db_path=tmp_path / "profile.db") + try: + donor.create_session(SESSION_ID, source="tui") + donor.set_session_title(SESSION_ID, SessionDB.CANONICAL_BOT_CHAT_TITLE) + donor.set_session_hidden(SESSION_ID, True) + donor.append_message(SESSION_ID, "user", "hi bot") + + assert profile.adopt_session_lineage_from(donor, SESSION_ID)["adopted"] + + assert profile.get_session(SESSION_ID)["hidden"] + assert SESSION_ID not in {s["id"] for s in profile.list_sessions_rich(limit=50)} + assert profile.archive_stale_sessions(idle_days=0) == 0 + finally: + donor.close() + profile.close()