From 94ce8396e84ccdaa96d2a85d1fc2ced4bc2e0c61 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:08:09 -0700 Subject: [PATCH] fix(sessions): release active-session leases against their acquisition registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A gateway active-session lease is acquired against the root HERMES_HOME, but release_active_session()/transfer_active_session() re-resolved the registry path from the *current* HERMES_HOME. Under native multiplex a routed turn runs agent cleanup inside _profile_runtime_scope, so the release looked under the named profile while the root entry stayed alive — after max_concurrent_sessions routed turns every new session was rejected with 'Hermes is at the active session limit' (#85431). Pin state/lock paths on the lease at acquisition time and prefer them on release and transfer. Fixes #85431. --- hermes_cli/active_sessions.py | 22 +++++-- tests/hermes_cli/test_active_sessions.py | 73 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/hermes_cli/active_sessions.py b/hermes_cli/active_sessions.py index a572c74093..13aa1e41b3 100644 --- a/hermes_cli/active_sessions.py +++ b/hermes_cli/active_sessions.py @@ -261,6 +261,14 @@ class ActiveSessionLease: surface: str enabled: bool = True released: bool = False + # Registry paths pinned at acquisition time. A lease acquired under the + # root ``HERMES_HOME`` must release against the same registry even when + # ``release()`` runs inside a profile home override (native multiplex + # routes turns under ``_profile_runtime_scope``), otherwise the root + # entry survives until process exit and the session cap fills with + # phantom leases (#85431). + state_path: Optional[Path] = None + lock_path: Optional[Path] = None def release(self) -> None: if self.released or not self.enabled: @@ -331,13 +339,18 @@ def try_acquire_active_session( lease_id=lease_id, session_id=str(session_id), surface=str(surface), + state_path=state_path, + lock_path=_lock_path(), ), None def release_active_session(lease: ActiveSessionLease) -> None: - state_path = _state_path() + # Prefer the registry the lease was acquired against: the caller may be + # running under a profile HERMES_HOME override (#85431). + state_path = lease.state_path or _state_path() + lock_path = lease.lock_path or _lock_path() try: - with _FileLock(_lock_path()): + with _FileLock(lock_path): entries = _prune_dead(_read_entries(state_path)) kept = [ entry @@ -366,8 +379,9 @@ def transfer_active_session( lease.session_id = new_session_id return True - state_path = _state_path() - with _FileLock(_lock_path()): + state_path = lease.state_path or _state_path() + lock_path = lease.lock_path or _lock_path() + with _FileLock(lock_path): entries = _prune_dead(_read_entries(state_path)) updated = False for entry in entries: diff --git a/tests/hermes_cli/test_active_sessions.py b/tests/hermes_cli/test_active_sessions.py index 2d4dd949ea..dcbc36af55 100644 --- a/tests/hermes_cli/test_active_sessions.py +++ b/tests/hermes_cli/test_active_sessions.py @@ -167,3 +167,76 @@ def test_release_orphaned_leases_reclaims_only_unowned_own_pid_entries(tmp_path, for entry in active_sessions.active_session_registry_snapshot() ) == ["kept", "other"] assert orphan is not None + + +def test_release_under_profile_home_override_targets_acquisition_registry( + tmp_path, monkeypatch +): + """Regression for #85431: a lease acquired against the root HERMES_HOME + must release from the root registry even when ``release()`` runs inside a + profile home override (native multiplex runs agent cleanup under + ``_profile_runtime_scope``). Before the fix the root entry survived and + the session cap filled with phantom leases.""" + from hermes_constants import ( + reset_hermes_home_override, + set_hermes_home_override, + ) + + root = tmp_path / "hermes" + profile = root / "profiles" / "worker" + profile.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(root)) + + lease, error = active_sessions.try_acquire_active_session( + session_id="agent:worker:telegram:dm:synthetic", + surface="gateway:telegram", + config={"max_concurrent_sessions": 2}, + ) + assert lease is not None and error is None + root_registry = root / "runtime" / "active_sessions.json" + assert root_registry.exists() + + token = set_hermes_home_override(str(profile)) + try: + lease.release() + finally: + reset_hermes_home_override(token) + + assert lease.released is True + remaining = active_sessions._read_entries(root_registry) + assert remaining == [] + # No phantom registry created under the profile home. + assert not (profile / "runtime" / "active_sessions.json").exists() + + +def test_transfer_under_profile_home_override_targets_acquisition_registry( + tmp_path, monkeypatch +): + """Sibling site of #85431: transfer must also update the registry the + lease was acquired against, not one resolved from the current override.""" + from hermes_constants import ( + reset_hermes_home_override, + set_hermes_home_override, + ) + + root = tmp_path / "hermes" + profile = root / "profiles" / "worker" + profile.mkdir(parents=True) + monkeypatch.setenv("HERMES_HOME", str(root)) + + lease, error = active_sessions.try_acquire_active_session( + session_id="before", + surface="gateway:telegram", + config={"max_concurrent_sessions": 2}, + ) + assert lease is not None and error is None + + token = set_hermes_home_override(str(profile)) + try: + assert active_sessions.transfer_active_session(lease, session_id="after") + finally: + reset_hermes_home_override(token) + + root_registry = root / "runtime" / "active_sessions.json" + entries = active_sessions._read_entries(root_registry) + assert [entry["session_id"] for entry in entries] == ["after"]