fix(sessions): release active-session leases against their acquisition registry

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.
This commit is contained in:
Teknium
2026-08-14 23:08:09 -07:00
parent aba4934274
commit 94ce8396e8
2 changed files with 91 additions and 4 deletions

View File

@@ -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:

View File

@@ -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"]