mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
fix(cli): synchronize HERMES_SESSION_ID across environment and contextvar during session switches
This commit is contained in:
parent
f63ef74eaf
commit
86871ee25a
7 changed files with 109 additions and 14 deletions
|
|
@ -976,16 +976,14 @@ def init_agent(
|
|||
|
||||
# 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"] = agent.session_id
|
||||
# coordination, and logging. Keep the ContextVar and os.environ
|
||||
# fallback synchronized because different tool paths still read both.
|
||||
try:
|
||||
from gateway.session_context import _SESSION_ID
|
||||
_SESSION_ID.set(agent.session_id)
|
||||
from gateway.session_context import set_current_session_id
|
||||
|
||||
set_current_session_id(agent.session_id)
|
||||
except Exception:
|
||||
pass # CLI/test mode — ContextVar not needed
|
||||
os.environ["HERMES_SESSION_ID"] = agent.session_id
|
||||
|
||||
# Session logs go into ~/.hermes/sessions/ alongside gateway sessions
|
||||
hermes_home = get_hermes_home()
|
||||
|
|
|
|||
|
|
@ -381,12 +381,12 @@ def compress_context(
|
|||
agent._session_db.end_session(agent.session_id, "compression")
|
||||
old_session_id = agent.session_id
|
||||
agent.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
||||
os.environ["HERMES_SESSION_ID"] = agent.session_id
|
||||
try:
|
||||
from gateway.session_context import _SESSION_ID
|
||||
_SESSION_ID.set(agent.session_id)
|
||||
from gateway.session_context import set_current_session_id
|
||||
|
||||
set_current_session_id(agent.session_id)
|
||||
except Exception:
|
||||
pass
|
||||
os.environ["HERMES_SESSION_ID"] = agent.session_id
|
||||
agent._session_db_created = False
|
||||
agent._session_db.create_session(
|
||||
session_id=agent.session_id,
|
||||
|
|
|
|||
14
cli.py
14
cli.py
|
|
@ -775,8 +775,6 @@ from rich.markup import escape as _escape
|
|||
from rich.panel import Panel
|
||||
from rich.text import Text as _RichText
|
||||
|
||||
import fire
|
||||
|
||||
# Import agent and tool systems lazily. Bare interactive startup only needs the
|
||||
# prompt; the full agent/tool registry is initialized on first use.
|
||||
def AIAgent(*args, **kwargs):
|
||||
|
|
@ -818,6 +816,13 @@ def validate_toolset(*args, **kwargs):
|
|||
|
||||
return _validate_toolset(*args, **kwargs)
|
||||
|
||||
|
||||
def _sync_process_session_id(session_id: str) -> None:
|
||||
"""Keep process-local session-id consumers aligned after CLI switches."""
|
||||
from gateway.session_context import set_current_session_id
|
||||
|
||||
set_current_session_id(session_id)
|
||||
|
||||
# Cron job system for scheduled tasks (execution is handled by the gateway)
|
||||
def get_job(*args, **kwargs):
|
||||
from cron import get_job as _get_job
|
||||
|
|
@ -6281,6 +6286,7 @@ class HermesCLI:
|
|||
self.conversation_history = []
|
||||
self._pending_title = None
|
||||
self._resumed = False
|
||||
_sync_process_session_id(self.session_id)
|
||||
|
||||
if self.agent:
|
||||
self.agent.session_id = self.session_id
|
||||
|
|
@ -6567,6 +6573,7 @@ class HermesCLI:
|
|||
self.session_id = target_id
|
||||
self._resumed = True
|
||||
self._pending_title = None
|
||||
_sync_process_session_id(target_id)
|
||||
|
||||
# Load conversation history (strip transcript-only metadata entries)
|
||||
restored = self._session_db.get_messages_as_conversation(target_id)
|
||||
|
|
@ -6740,6 +6747,7 @@ class HermesCLI:
|
|||
self.session_start = now
|
||||
self._pending_title = None
|
||||
self._resumed = True # Prevents auto-title generation
|
||||
_sync_process_session_id(new_session_id)
|
||||
|
||||
# Sync the agent
|
||||
if self.agent:
|
||||
|
|
@ -14777,4 +14785,6 @@ def main(
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import fire
|
||||
|
||||
fire.Fire(main)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,21 @@ _VAR_MAP = {
|
|||
}
|
||||
|
||||
|
||||
def set_current_session_id(session_id: str) -> None:
|
||||
"""Synchronize ``HERMES_SESSION_ID`` across ContextVar and ``os.environ``.
|
||||
|
||||
Long-lived single-process entrypoints like the CLI can rotate sessions via
|
||||
``/new``, ``/resume``, ``/branch``, or compression splits without
|
||||
reconstructing the entire agent. Tools still consult
|
||||
``get_session_env("HERMES_SESSION_ID")`` with an ``os.environ`` fallback,
|
||||
so both storage paths must move together when the active session changes.
|
||||
"""
|
||||
import os
|
||||
|
||||
os.environ["HERMES_SESSION_ID"] = session_id
|
||||
_SESSION_ID.set(session_id)
|
||||
|
||||
|
||||
def set_session_vars(
|
||||
platform: str = "",
|
||||
chat_id: str = "",
|
||||
|
|
|
|||
|
|
@ -168,6 +168,25 @@ class TestBranchCommandCLI:
|
|||
|
||||
assert cli_instance._resumed is True
|
||||
|
||||
def test_branch_rotates_hermes_session_id_env_and_context(self, cli_instance, session_db):
|
||||
"""Branching must update process-local session-id readers too."""
|
||||
from cli import HermesCLI
|
||||
from gateway.session_context import _UNSET, _VAR_MAP, get_session_env
|
||||
|
||||
old_session_id = cli_instance.session_id
|
||||
os.environ["HERMES_SESSION_ID"] = old_session_id
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set(old_session_id)
|
||||
|
||||
try:
|
||||
HermesCLI._handle_branch_command(cli_instance, "/branch")
|
||||
|
||||
assert cli_instance.session_id != old_session_id
|
||||
assert os.environ["HERMES_SESSION_ID"] == cli_instance.session_id
|
||||
assert get_session_env("HERMES_SESSION_ID") == cli_instance.session_id
|
||||
finally:
|
||||
os.environ.pop("HERMES_SESSION_ID", None)
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set(_UNSET)
|
||||
|
||||
def test_branch_fires_on_session_switch_hook(self, cli_instance, session_db):
|
||||
"""The /branch command must notify memory providers of the rotation.
|
||||
|
||||
|
|
|
|||
|
|
@ -333,6 +333,33 @@ class TestHistoryDisplay:
|
|||
assert "Checking Running Hermes Agent" in output
|
||||
assert "Use /resume <session id or title> to continue" in output
|
||||
|
||||
def test_resume_updates_hermes_session_id_env_and_context(self, tmp_path):
|
||||
from gateway.session_context import _UNSET, _VAR_MAP, get_session_env
|
||||
from hermes_state import SessionDB
|
||||
|
||||
cli = _make_cli()
|
||||
cli.session_id = "current_session"
|
||||
cli.conversation_history = []
|
||||
cli.agent = None
|
||||
cli._session_db = SessionDB(db_path=tmp_path / "state.db")
|
||||
cli._session_db.create_session("current_session", "cli")
|
||||
cli._session_db.create_session("target_session", "cli")
|
||||
cli._session_db.append_message("target_session", "user", "hello from resumed session")
|
||||
|
||||
os.environ["HERMES_SESSION_ID"] = "current_session"
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set("current_session")
|
||||
|
||||
try:
|
||||
cli._handle_resume_command("/resume target_session")
|
||||
|
||||
assert cli.session_id == "target_session"
|
||||
assert os.environ["HERMES_SESSION_ID"] == "target_session"
|
||||
assert get_session_env("HERMES_SESSION_ID") == "target_session"
|
||||
finally:
|
||||
cli._session_db.close()
|
||||
os.environ.pop("HERMES_SESSION_ID", None)
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set(_UNSET)
|
||||
|
||||
def test_sessions_command_no_args_lists_recent_sessions(self, capsys):
|
||||
"""/sessions with no args prints the recent-sessions table (TUI parity).
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import sys
|
|||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_state import SessionDB
|
||||
from tools.todo_tool import TodoStore
|
||||
|
||||
|
|
@ -138,6 +140,15 @@ def _prepare_cli_with_active_session(tmp_path):
|
|||
return cli
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_session_id_context():
|
||||
from gateway.session_context import _UNSET, _VAR_MAP
|
||||
|
||||
yield
|
||||
os.environ.pop("HERMES_SESSION_ID", None)
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set(_UNSET)
|
||||
|
||||
|
||||
def test_new_command_creates_real_fresh_session_and_resets_agent_state(tmp_path):
|
||||
cli = _prepare_cli_with_active_session(tmp_path)
|
||||
old_session_id = cli.session_id
|
||||
|
|
@ -164,6 +175,21 @@ def test_new_command_creates_real_fresh_session_and_resets_agent_state(tmp_path)
|
|||
cli.agent._invalidate_system_prompt.assert_called_once()
|
||||
|
||||
|
||||
def test_new_command_rotates_hermes_session_id_env_and_context(tmp_path):
|
||||
from gateway.session_context import _VAR_MAP, get_session_env
|
||||
|
||||
cli = _prepare_cli_with_active_session(tmp_path)
|
||||
old_session_id = cli.session_id
|
||||
os.environ["HERMES_SESSION_ID"] = old_session_id
|
||||
_VAR_MAP["HERMES_SESSION_ID"].set(old_session_id)
|
||||
|
||||
cli.process_command("/new")
|
||||
|
||||
assert cli.session_id != old_session_id
|
||||
assert os.environ["HERMES_SESSION_ID"] == cli.session_id
|
||||
assert get_session_env("HERMES_SESSION_ID") == cli.session_id
|
||||
|
||||
|
||||
def test_reset_command_is_alias_for_new_session(tmp_path):
|
||||
cli = _prepare_cli_with_active_session(tmp_path)
|
||||
old_session_id = cli.session_id
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue