fix: inject SessionDB into AIAgent for WebUI sessions — enables session_search (#356)
* fix: inject SessionDB into AIAgent for WebUI sessions session_search tool requires a SessionDB instance passed via the session_db parameter. The CLI and gateway paths already do this, but the WebUI streaming path was missing it, causing every session_search call to return 'Session database not available'. Initialize SessionDB before creating the AIAgent and pass it through. Failure is non-fatal — a warning is printed and session_search gracefully degrades. * fix: inject SessionDB into AIAgent for WebUI sessions (enables session_search) (#356) - api/streaming.py: initialize SessionDB() before AIAgent construction and pass session_db= kwarg so session_search works in WebUI sessions - tests/test_sprint42.py: 7 new tests covering SessionDB injection, try/except guard, WARNING log, ordering, and AST lock-safety check - CHANGELOG.md: v0.50.13 entry; 822 tests total (up from 815) --------- Co-authored-by: 王昌旭 <wangchangxu@xiaohongshu.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
|
||||
---
|
||||
|
||||
## [v0.50.13] Fix session_search in WebUI sessions — inject SessionDB into AIAgent (PR #356)
|
||||
|
||||
- **`session_search` now works in WebUI sessions** (`api/streaming.py`): The agent's `session_search` tool returned "Session database not available" for all WebUI sessions. The CLI and gateway code paths both initialize a `SessionDB` instance and pass it via `session_db=` to `AIAgent.__init__()`, but the WebUI streaming path was missing this step. `_run_agent_streaming` now initializes `SessionDB()` before constructing the agent and passes it in. A `try/except` wrapper makes the init non-fatal — if `hermes_state` is unavailable (older installs, test environments), a `WARNING` is printed and `session_db=None` is passed instead, preserving the prior behavior gracefully.
|
||||
- 7 new tests in `tests/test_sprint42.py`; 822 tests total (up from 815)
|
||||
|
||||
## [v0.50.12] Profile .env isolation — prevent API key leakage on profile switch (fixes #351)
|
||||
|
||||
- **API keys no longer leak between profiles on switch** (`api/profiles.py`): `_reload_dotenv()` now tracks which env vars were loaded from the active profile's `.env` and clears them before loading the next profile. Previously, switching from a profile with `OPENAI_API_KEY=X` to a profile without that key left `X` in `os.environ` for the duration of the process — effectively leaking credentials across the profile boundary. A module-level `_loaded_profile_env_keys: set[str]` tracks loaded keys; it is cleared and repopulated on every `_reload_dotenv()` call.
|
||||
|
||||
@@ -186,6 +186,14 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
||||
_AIAgent = _get_ai_agent()
|
||||
if _AIAgent is None:
|
||||
raise ImportError("AIAgent not available -- check that hermes-agent is on sys.path")
|
||||
|
||||
# Initialize SessionDB so session_search works in WebUI sessions
|
||||
_session_db = None
|
||||
try:
|
||||
from hermes_state import SessionDB
|
||||
_session_db = SessionDB()
|
||||
except Exception as _db_err:
|
||||
print(f"[webui] WARNING: SessionDB init failed — session_search will be unavailable: {_db_err}", flush=True)
|
||||
resolved_model, resolved_provider, resolved_base_url = resolve_model_provider(model)
|
||||
|
||||
# Resolve API key via Hermes runtime provider (matches gateway behaviour).
|
||||
@@ -235,6 +243,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
||||
enabled_toolsets=_toolsets,
|
||||
fallback_model=_fallback_resolved,
|
||||
session_id=session_id,
|
||||
session_db=_session_db,
|
||||
stream_delta_callback=on_token,
|
||||
tool_progress_callback=on_tool,
|
||||
)
|
||||
|
||||
107
tests/test_sprint42.py
Normal file
107
tests/test_sprint42.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Sprint 42 Tests: SessionDB injection into AIAgent for WebUI sessions (PR #356).
|
||||
|
||||
Covers:
|
||||
- streaming.py: SessionDB is initialized inside _run_agent_streaming (import present)
|
||||
- streaming.py: try/except guards SessionDB init so failures are non-fatal
|
||||
- streaming.py: session_db= kwarg is passed to AIAgent constructor
|
||||
- streaming.py: SessionDB init failure prints a WARNING (not silently swallowed)
|
||||
- streaming.py: SessionDB init is placed before AIAgent construction
|
||||
"""
|
||||
import ast
|
||||
import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
|
||||
|
||||
|
||||
class TestSessionDBInjection(unittest.TestCase):
|
||||
"""Verify SessionDB is initialized and passed to AIAgent in streaming.py."""
|
||||
|
||||
def test_hermes_state_import_present(self):
|
||||
"""SessionDB must be imported from hermes_state inside _run_agent_streaming."""
|
||||
self.assertIn(
|
||||
"from hermes_state import SessionDB",
|
||||
STREAMING_PY,
|
||||
"SessionDB import missing from streaming.py (PR #356)",
|
||||
)
|
||||
|
||||
def test_session_db_kwarg_passed_to_agent(self):
|
||||
"""session_db= must be passed to the AIAgent constructor call."""
|
||||
self.assertIn(
|
||||
"session_db=_session_db",
|
||||
STREAMING_PY,
|
||||
"session_db kwarg not passed to AIAgent (PR #356)",
|
||||
)
|
||||
|
||||
def test_sessiondb_init_in_try_except(self):
|
||||
"""SessionDB() init must be wrapped in try/except for non-fatal failure handling."""
|
||||
# Check that the try/except pattern surrounding SessionDB() is present
|
||||
pattern = r"try:\s*\n\s*from hermes_state import SessionDB\s*\n\s*_session_db\s*=\s*SessionDB\(\)"
|
||||
self.assertRegex(
|
||||
STREAMING_PY,
|
||||
pattern,
|
||||
"SessionDB() init must be inside a try block for non-fatal error handling (PR #356)",
|
||||
)
|
||||
|
||||
def test_sessiondb_failure_logs_warning(self):
|
||||
"""A failure initializing SessionDB must print a WARNING (not silently drop the error)."""
|
||||
self.assertIn(
|
||||
"WARNING: SessionDB init failed",
|
||||
STREAMING_PY,
|
||||
"SessionDB init failure must log a WARNING message (PR #356)",
|
||||
)
|
||||
|
||||
def test_session_db_initialized_before_agent_construction(self):
|
||||
"""SessionDB initialization must appear before the AIAgent(...) constructor call."""
|
||||
db_pos = STREAMING_PY.find("from hermes_state import SessionDB")
|
||||
agent_pos = STREAMING_PY.find("session_db=_session_db")
|
||||
self.assertGreater(
|
||||
agent_pos,
|
||||
db_pos,
|
||||
"SessionDB init must appear before AIAgent construction (PR #356)",
|
||||
)
|
||||
|
||||
def test_session_db_default_is_none(self):
|
||||
"""_session_db must be initialized to None before the try block (safe default)."""
|
||||
# Pattern: _session_db = None followed (eventually) by the try/SessionDB block
|
||||
pattern = r"_session_db\s*=\s*None\s*\n\s*try:"
|
||||
self.assertRegex(
|
||||
STREAMING_PY,
|
||||
pattern,
|
||||
"_session_db must default to None before try/except block (PR #356)",
|
||||
)
|
||||
|
||||
|
||||
class TestSessionDBAST(unittest.TestCase):
|
||||
"""AST-level checks: verify the try/except is not inside _ENV_LOCK (deadlock guard)."""
|
||||
|
||||
def setUp(self):
|
||||
self.tree = ast.parse(STREAMING_PY)
|
||||
|
||||
def test_sessiondb_try_not_inside_env_lock(self):
|
||||
"""The try block that wraps SessionDB init must NOT be inside a 'with _ENV_LOCK:' block.
|
||||
|
||||
Putting a try/except inside _ENV_LOCK is the deadlock pattern caught by test_sprint34.
|
||||
The SessionDB try/except is outside the lock scope, which is correct.
|
||||
"""
|
||||
# Find all 'with _ENV_LOCK:' nodes; check none of their bodies contain
|
||||
# a Try node that also contains 'from hermes_state import SessionDB'
|
||||
for node in ast.walk(self.tree):
|
||||
if not isinstance(node, ast.With):
|
||||
continue
|
||||
names = [getattr(item.context_expr, "id", "") for item in node.items]
|
||||
if "_ENV_LOCK" not in names:
|
||||
continue
|
||||
# Walk the with-body for Try nodes
|
||||
for stmt in node.body:
|
||||
if isinstance(stmt, ast.Try):
|
||||
# Check if this try imports hermes_state
|
||||
src = ast.unparse(stmt)
|
||||
self.assertNotIn(
|
||||
"hermes_state",
|
||||
src,
|
||||
"SessionDB try/except must NOT be inside _ENV_LOCK body (deadlock risk)",
|
||||
)
|
||||
Reference in New Issue
Block a user