From 7e9100901819ee44c16b4ddcb79a6bcb7909f591 Mon Sep 17 00:00:00 2001 From: Teknium Date: Wed, 1 Apr 2026 11:29:20 -0700 Subject: [PATCH] fix: lazy-init SessionDB on adapter instance instead of per-request Reuse a single SessionDB across requests by caching on self._session_db with lazy initialization. Avoids creating a new SQLite connection per request when X-Hermes-Session-Id is used. Updated tests to set adapter._session_db directly instead of patching the constructor. --- gateway/platforms/api_server.py | 10 ++++++---- tests/gateway/test_api_server.py | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index e1b000851..2059a1aa6 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -2,7 +2,7 @@ OpenAI-compatible API server platform adapter. Exposes an HTTP server with endpoints: -- POST /v1/chat/completions — OpenAI Chat Completions format (stateless) +- POST /v1/chat/completions — OpenAI Chat Completions format (stateless; opt-in session continuity via X-Hermes-Session-Id header) - POST /v1/responses — OpenAI Responses API format (stateful via previous_response_id) - GET /v1/responses/{response_id} — Retrieve a stored response - DELETE /v1/responses/{response_id} — Delete a stored response @@ -300,6 +300,7 @@ class APIServerAdapter(BasePlatformAdapter): self._runner: Optional["web.AppRunner"] = None self._site: Optional["web.TCPSite"] = None self._response_store = ResponseStore() + self._session_db: Optional[Any] = None # Lazy-init SessionDB for session continuity @staticmethod def _parse_cors_origins(value: Any) -> tuple[str, ...]: @@ -502,9 +503,10 @@ class APIServerAdapter(BasePlatformAdapter): if provided_session_id: session_id = provided_session_id try: - from hermes_state import SessionDB - _db = SessionDB() - history = _db.get_messages_as_conversation(session_id) + if self._session_db is None: + from hermes_state import SessionDB + self._session_db = SessionDB() + history = self._session_db.get_messages_as_conversation(session_id) except Exception as e: logger.warning("Failed to load session history for %s: %s", session_id, e) history = [] diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 8969b8417..5bde076a6 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -1603,16 +1603,15 @@ class TestSessionIdHeader: async def test_provided_session_id_is_used_and_echoed(self, adapter): """When X-Hermes-Session-Id is provided, it's passed to the agent and echoed in the response.""" mock_result = {"final_response": "Continuing!", "messages": [], "api_calls": 1} + mock_db = MagicMock() + mock_db.get_messages_as_conversation.return_value = [ + {"role": "user", "content": "previous message"}, + {"role": "assistant", "content": "previous reply"}, + ] + adapter._session_db = mock_db app = _create_app(adapter) async with TestClient(TestServer(app)) as cli: - with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run, \ - patch("hermes_state.SessionDB") as mock_db_cls: - mock_db = MagicMock() - mock_db.get_messages_as_conversation.return_value = [ - {"role": "user", "content": "previous message"}, - {"role": "assistant", "content": "previous reply"}, - ] - mock_db_cls.return_value = mock_db + with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run: mock_run.return_value = (mock_result, {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}) resp = await cli.post( @@ -1634,13 +1633,12 @@ class TestSessionIdHeader: {"role": "user", "content": "stored message 1"}, {"role": "assistant", "content": "stored reply 1"}, ] + mock_db = MagicMock() + mock_db.get_messages_as_conversation.return_value = db_history + adapter._session_db = mock_db app = _create_app(adapter) async with TestClient(TestServer(app)) as cli: - with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run, \ - patch("hermes_state.SessionDB") as mock_db_cls: - mock_db = MagicMock() - mock_db.get_messages_as_conversation.return_value = db_history - mock_db_cls.return_value = mock_db + with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run: mock_run.return_value = (mock_result, {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}) resp = await cli.post( @@ -1667,6 +1665,8 @@ class TestSessionIdHeader: async def test_db_failure_falls_back_to_empty_history(self, adapter): """If SessionDB raises, history falls back to empty and request still succeeds.""" mock_result = {"final_response": "OK", "messages": [], "api_calls": 1} + # Simulate DB failure: _session_db is None and SessionDB() constructor raises + adapter._session_db = None app = _create_app(adapter) async with TestClient(TestServer(app)) as cli: with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run, \