diff --git a/gateway/session_context.py b/gateway/session_context.py index 9dc051e3a2c..b64f31de081 100644 --- a/gateway/session_context.py +++ b/gateway/session_context.py @@ -55,6 +55,7 @@ _SESSION_THREAD_ID: ContextVar = ContextVar("HERMES_SESSION_THREAD_ID", default= _SESSION_USER_ID: ContextVar = ContextVar("HERMES_SESSION_USER_ID", default=_UNSET) _SESSION_USER_NAME: ContextVar = ContextVar("HERMES_SESSION_USER_NAME", default=_UNSET) _SESSION_KEY: ContextVar = ContextVar("HERMES_SESSION_KEY", default=_UNSET) +_SESSION_ID: ContextVar = ContextVar("HERMES_SESSION_ID", default=_UNSET) # Cron auto-delivery vars — set per-job in run_job() so concurrent jobs # don't clobber each other's delivery targets. @@ -70,6 +71,7 @@ _VAR_MAP = { "HERMES_SESSION_USER_ID": _SESSION_USER_ID, "HERMES_SESSION_USER_NAME": _SESSION_USER_NAME, "HERMES_SESSION_KEY": _SESSION_KEY, + "HERMES_SESSION_ID": _SESSION_ID, "HERMES_CRON_AUTO_DELIVER_PLATFORM": _CRON_AUTO_DELIVER_PLATFORM, "HERMES_CRON_AUTO_DELIVER_CHAT_ID": _CRON_AUTO_DELIVER_CHAT_ID, "HERMES_CRON_AUTO_DELIVER_THREAD_ID": _CRON_AUTO_DELIVER_THREAD_ID, diff --git a/run_agent.py b/run_agent.py index 0aeacec7a32..aa01c8ecdf5 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1837,7 +1837,20 @@ class AIAgent: timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S") short_uuid = uuid.uuid4().hex[:6] self.session_id = f"{timestamp_str}_{short_uuid}" - + + # Expose session ID to tools (terminal, execute_code) so agents can + # reference their own session for --resume commands, cross-session + # coordination, and logging. Uses the ContextVar system from + # session_context.py for concurrency safety (gateway runs multiple + # sessions in one process). Also writes os.environ as fallback for + # CLI mode where ContextVars aren't used. + os.environ["HERMES_SESSION_ID"] = self.session_id + try: + from gateway.session_context import _SESSION_ID + _SESSION_ID.set(self.session_id) + except Exception: + pass # CLI/test mode — ContextVar not needed + # Session logs go into ~/.hermes/sessions/ alongside gateway sessions hermes_home = get_hermes_home() self.logs_dir = hermes_home / "sessions" @@ -10233,6 +10246,12 @@ class AIAgent: self._session_db.end_session(self.session_id, "compression") old_session_id = self.session_id self.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}" + os.environ["HERMES_SESSION_ID"] = self.session_id + try: + from gateway.session_context import _SESSION_ID + _SESSION_ID.set(self.session_id) + except Exception: + pass # Update session_log_file to point to the new session's JSON file self.session_log_file = self.logs_dir / f"session_{self.session_id}.json" self._session_db_created = False diff --git a/tests/run_agent/test_session_id_env.py b/tests/run_agent/test_session_id_env.py new file mode 100644 index 00000000000..73fd11890cc --- /dev/null +++ b/tests/run_agent/test_session_id_env.py @@ -0,0 +1,61 @@ +"""Test that HERMES_SESSION_ID is exposed as an env var and ContextVar.""" + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../..")) + +from run_agent import AIAgent + + +@pytest.fixture(autouse=True) +def _cleanup_env(): + """Remove HERMES_SESSION_ID before/after each test.""" + os.environ.pop("HERMES_SESSION_ID", None) + yield + os.environ.pop("HERMES_SESSION_ID", None) + + +def test_session_id_env_set_on_init(): + """AIAgent.__init__ sets HERMES_SESSION_ID in the environment.""" + agent = AIAgent( + api_key="test-key", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + assert os.environ.get("HERMES_SESSION_ID") == agent.session_id + assert len(agent.session_id) > 0 + + +def test_session_id_env_uses_provided_id(): + """When session_id is passed explicitly, HERMES_SESSION_ID reflects it.""" + custom_id = "20260511_120000_abc12345" + agent = AIAgent( + api_key="test-key", + base_url="https://openrouter.ai/api/v1", + session_id=custom_id, + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + assert os.environ["HERMES_SESSION_ID"] == custom_id + assert agent.session_id == custom_id + + +def test_session_id_contextvar_set(): + """AIAgent.__init__ also sets the ContextVar for concurrency safety.""" + custom_id = "20260511_130000_def67890" + AIAgent( + api_key="test-key", + base_url="https://openrouter.ai/api/v1", + session_id=custom_id, + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + from gateway.session_context import get_session_env + assert get_session_env("HERMES_SESSION_ID") == custom_id diff --git a/tools/environments/local.py b/tools/environments/local.py index 985bf4bdce8..7aa75a62d0c 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -274,6 +274,17 @@ def _make_run_env(env: dict) -> dict: if _profile_home: run_env["HOME"] = _profile_home + # Inject ContextVar-based session vars into subprocess env. + # ContextVars don't propagate to child processes, so we bridge them here. + try: + from gateway.session_context import get_session_env, _UNSET, _VAR_MAP + for var_name, var in _VAR_MAP.items(): + value = var.get() + if value is not _UNSET and value: + run_env[var_name] = value + except Exception: + pass + return run_env