# NousResearch/hermes-agent · Cross-profile session routing bug and fixes

## Open now

- Nothing open on GitHub.

A bug was identified where disabling multiplex_profiles leaves orphaned sessions that cause incorrect profile routing. Two related fix attempts address cross-profile session recovery in the gateway.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #59325. Verify it addresses your case.
- **Recommended:** read PR #59325 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-59325`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #58032: Bug: multiplex_profiles: false leaves orphaned sessions that cause wrong profile routing

## Prior & competing PRs

- **#59325: fix(gateway): avoid cross-profile session recovery (#58119 salvage)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/session.py`, `tests/gateway/test_multiplex_phase0.py`
  - fix hunks:
  - `gateway/session.py`

    ```diff
    @@ -1225,6 +1225,45 @@ def _resolve_profile_for_key(self, source: Optional[SessionSource] = None) -> Op
             except Exception:
                 return None
     
    +    @staticmethod
    +    def _profile_from_session_key(session_key: Optional[str]) -> Optional[str]:
    +        """Extract the profile namespace encoded in a gateway session key."""
    +        if not session_key:
    +            return None
    +        parts = str(session_key).split(":")
    +        if len(parts) < 2 or parts[0] != "agent":
    +            return None
    +        namespace = parts[1] or "main"
    +        return "default" if namespace == "main" else namespace
    +
    +    @staticmethod
    +    def _active_profile_name() -> str:
    +        try:
    +            from hermes_cli.profiles import get_active_profile_name
    +            return get_active_profile_name() or "default"
    +        except Exception:
    +            return "default"
    +
    +    def _recovered_row_allowed_for_active_profile(
    +        self,
    +        *,
    +        requested_session_key: str,
    +        recovered: Dict[str, Any],
    +    ) -> bool:
    +        """Prevent non-multiplexed gateways from reviving another profile's row."""
    +        if getattr(self.config, "multiplex_profiles", False):
    +            return True
    +
    +        recovered_key = str(recovered.get("session_key") or "")
    +        if not recovered_key or recovered_key == requested_session_key:
    +            return True
    +
    +        recovered_profile = self._profile_from_session_key(recovered_key)
    +        if recovered_profile is None:
    +            return True
    +
    +        return recovered_profile == self._active_profile_name()
    +
         def _generate_session_key(self, source: SessionSource) -> str:
             """Generate a session key from a source."""
             return build_session_key(
    ```

    ```diff
    @@ -1285,6 +1324,18 @@ def _recover_session_from_db(
                 return None
             if not recovered:
                 return None
    +        if not self._recovered_row_allowed_for_active_profile(
    +            requested_session_key=session_key,
    +            recovered=recovered,
    +        ):
    +            logger.warning(
    +                "Gateway session DB recovery ignored %s for %s because "
    +                "multiplex_profiles is disabled and the row belongs to a "
    +                "different profile",
    +                recovered.get("session_key"),
    +                session_key,
    +            )
    +            return None
             try:
                 self._db.reopen_session(str(recovered["id"]))
             except Exception as exc:
    ```
  - `tests/gateway/test_multiplex_phase0.py`

    ```diff
    @@ -9,6 +9,7 @@
          on.
     """
     import pytest
    +from datetime import datetime
     from unittest.mock import patch
     import yaml
     
    ```

    ```diff
    @@ -201,3 +202,66 @@ def test_flag_on_default_profile_stays_legacy(self, tmp_path):
             with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"):
                 assert store._generate_session_key(s) == "agent:main:telegram:dm:99"
     
    +
    +class _RecoveringDB:
    +    def __init__(self, row):
    +        self.row = row
    +        self.reopened = []
    +
    +    def find_latest_gateway_session_for_peer(self, **_kwargs):
    +        return self.row
    +
    +    def reopen_session(self, session_id):
    +        self.reopened.append(session_id)
    +
    +
    +class TestSessionStoreUnmultiplexedRecovery:
    +    """Turning multiplexing off must not recover another profile's session."""
    +
    +    def _store_with_row(self, tmp_path, row, **cfg_kw):
    +        config = GatewayConfig(**cfg_kw)
    +        with patch("gateway.session.SessionStore._ensure_loaded"):
    +            store = SessionStore(sessions_dir=tmp_path, config=config)
    +        store._db = _RecoveringDB(row)
    +        store._loaded = True
    +        return store
    +
    +    def test_flag_off_rejects_other_profile_peer_fallback(self, tmp_path):
    +        row = {
    +            "id": "sess-coder",
    +            "started_at": 1700000000,
    +            "session_key": "agent:coder:telegram:dm:99",
    +        }
    +        store = self._store_with_row(tmp_path, row)
    +        source = _src(chat_id="99", chat_type="dm")
    +
    +        with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"):
    +            recovered = store._recover_session_from_db(
    +                session_key="agent:main:telegram:dm:99",
    +                source=source,
    +                now=datetime.fromtimestamp(1700000001),
    +            )
    +
    +        assert recovered is None
    +        assert store._db.reopened == []
    +
    +    def test_flag_off_allows_active_profile_peer_fallback(self, tmp_path):
    +        row = {
    +            "id": "sess-coder",
    +            "started_at": 1700000000,
    +            "session_key": "agent:coder:telegram:dm:99",
    +        }
    +        store = self._store_with_row(tmp_path, row)
    +        source = _src(chat_id="99", chat_type="dm")
    +
    +        with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"):
    +            recovered = store._recover_session_from_db(
    +                session_key="agent:main:telegram:dm:99",
    +                source=source,
    +                now=datetime.fromtimestamp(1700000001),
    +            )
    +
    +        assert recovered is not None
    +        assert recovered.session_id == "sess-coder"
    +        assert recovered.session_key == "agent:main:telegram:dm:99"
    +        assert store._db.reopened == ["sess-coder"]
    ```
- **#58119: fix(gateway): avoid cross-profile session recovery**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `f1fde49e453e508b9dc49ec5b70694fbbf0279c0`, `#59325`

## Fix status

- Merged to main in PR #59325 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-07`.
- Subsystem: `gateway`
