fix(gateway): use process-level HERMES_HOME for identity files (#56993 salvage) (#59341)

* fix(gateway): use process-level HERMES_HOME for identity files

Gateway identity files (PID, lock, runtime status, takeover/stop markers)
were written via get_hermes_home() which honours the _HERMES_HOME_OVERRIDE
contextvar used for per-session profile dispatch.  When a profile-context
task happened to be active at write time, files landed in the wrong profile
directory.

Add _get_process_hermes_home() that skips the contextvar and uses only the
HERMES_HOME env var or platform default, and route all gateway identity file
paths through it.

Fixes #56986

* chore(release): map liuhao1024 author email for PR #56993 salvage

---------

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Ben <ben@nousresearch.com>
This commit is contained in:
Teknium 2026-07-07 02:05:21 -07:00 committed by GitHub
parent 4b9d9b205b
commit 043e71f1f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 7 deletions

View file

@ -20,7 +20,7 @@ import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_constants import get_hermes_home, _get_platform_default_hermes_home
from typing import Any, Optional
from utils import atomic_json_write
@ -42,9 +42,25 @@ _gateway_lock_handle = None
_WINDOWS_LOCK_OFFSET = 1024 * 1024
def _get_process_hermes_home() -> Path:
"""Return the process-level HERMES_HOME, skipping context-local overrides.
Gateway identity files (PID, lock, runtime status, takeover/stop markers)
must always live in the directory the gateway process was launched with.
``get_hermes_home()`` honors ``_HERMES_HOME_OVERRIDE`` contextvar used for
per-session profile dispatch, which would route these files into the wrong
profile directory when a profile-context task happens to be active at write
time. See issue #56986.
"""
val = os.environ.get("HERMES_HOME", "").strip()
if val:
return Path(val)
return _get_platform_default_hermes_home()
def _get_pid_path() -> Path:
"""Return the path to the gateway PID file, respecting HERMES_HOME."""
home = get_hermes_home()
home = _get_process_hermes_home()
return home / "gateway.pid"
@ -52,7 +68,7 @@ def _get_gateway_lock_path(pid_path: Optional[Path] = None) -> Path:
"""Return the path to the runtime gateway lock file."""
if pid_path is not None:
return pid_path.with_name(_GATEWAY_LOCK_FILENAME)
home = get_hermes_home()
home = _get_process_hermes_home()
return home / _GATEWAY_LOCK_FILENAME
@ -1136,13 +1152,13 @@ _PLANNED_STOP_MARKER_TTL_S = 60
def _get_takeover_marker_path() -> Path:
"""Return the path to the --replace takeover marker file."""
home = get_hermes_home()
home = _get_process_hermes_home()
return home / _TAKEOVER_MARKER_FILENAME
def _get_planned_stop_marker_path() -> Path:
"""Return the path to the intentional gateway stop marker file."""
home = get_hermes_home()
home = _get_process_hermes_home()
return home / _PLANNED_STOP_MARKER_FILENAME
@ -1197,7 +1213,7 @@ def _consume_pid_marker_for_self(
# unaffected. Leave a mismatched marker in place so the correct
# profile can still consume it.
replacer_home = record.get("replacer_hermes_home")
if replacer_home is not None and replacer_home != str(get_hermes_home()):
if replacer_home is not None and replacer_home != str(_get_process_hermes_home()):
return False
our_pid = os.getpid()
@ -1246,7 +1262,7 @@ def write_takeover_marker(target_pid: int) -> bool:
"target_pid": target_pid,
"target_start_time": target_start_time,
"replacer_pid": os.getpid(),
"replacer_hermes_home": str(get_hermes_home()),
"replacer_hermes_home": str(_get_process_hermes_home()),
"written_at": _utc_now_iso(),
}
_write_json_file(_get_takeover_marker_path(), record)

View file

@ -53,6 +53,7 @@ AUTHOR_MAP = {
"allenliang2022@users.noreply.github.com": "allenliang2022", # PR #56932 test coverage folded into #56909 salvage (408 → retryable timeout)
"m888.braun@hotmail.com": "ManniBr", # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)
"austin@openvm067.space": "austinlaw076", # PR #57563 partial salvage (auth: lazy per-profile Anthropic OAuth file; gateway: whatsapp_cloud/line added to port-binding platform set)
"sunsky.lau@gmail.com": "liuhao1024", # PR #56993 salvage (gateway: process-level HERMES_HOME for pid/lock/status identity files; #56986)
"blueirobin02@gmail.com": "irresi", # PR #59048 salvage (gateway: scope reset banners' session info to the serving profile; #59003)
"jashlee+microsoft@microsoft.com": "s905060", # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169)
"lohinth25@proton.me": "l0h1nth", # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184)

View file

@ -293,6 +293,41 @@ class TestGatewayPidState:
finally:
status.release_gateway_runtime_lock()
def test_gateway_identity_files_use_process_home_not_context_override(
self, tmp_path, monkeypatch
):
"""Regression: pid/lock/state files must use process-level HERMES_HOME.
When a profile context override is active (e.g., during session dispatch
for a named profile), gateway identity files should still be written to
the process-level HERMES_HOME, not the profile's directory. See #56986.
"""
from hermes_constants import set_hermes_home_override, reset_hermes_home_override
process_home = tmp_path / "default"
process_home.mkdir()
profile_home = tmp_path / "profiles" / "cfo"
profile_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(process_home))
# Simulate a profile context override being active during write.
token = set_hermes_home_override(str(profile_home))
try:
status.write_pid_file()
finally:
reset_hermes_home_override(token)
# PID file must land in the process-level home, not the profile home.
assert (process_home / "gateway.pid").exists()
assert not (profile_home / "gateway.pid").exists()
payload = json.loads((process_home / "gateway.pid").read_text())
assert payload["pid"] == os.getpid()
# Cleanup for atexit hooks.
monkeypatch.setenv("HERMES_HOME", str(process_home))
(process_home / "gateway.pid").unlink(missing_ok=True)
class TestGatewayRuntimeStatus:
def test_write_json_file_uses_atomic_json_write(self, tmp_path, monkeypatch):