diff --git a/tests/gateway/test_matrix_voice.py b/tests/gateway/test_matrix_voice.py index dab113c5d..3b3e08d14 100644 --- a/tests/gateway/test_matrix_voice.py +++ b/tests/gateway/test_matrix_voice.py @@ -184,8 +184,14 @@ class TestMatrixVoiceMessageDetection: f"Expected MessageType.AUDIO for non-voice, got {captured_event.message_type}" @pytest.mark.asyncio - async def test_regular_audio_has_http_url(self): - """Regular audio uploads should keep HTTP URL (not cached locally).""" + async def test_regular_audio_is_cached_locally(self): + """Regular audio uploads are cached locally for downstream tool access. + + Since PR #bec02f37 (encrypted-media caching refactor), all media + types — photo, audio, video, document — are cached locally when + received so tools can read them as real files. This applies equally + to voice messages and regular audio. + """ event = _make_audio_event(is_voice=False) captured_event = None @@ -200,10 +206,10 @@ class TestMatrixVoiceMessageDetection: assert captured_event is not None assert captured_event.media_urls is not None - # Should be HTTP URL, not local path - assert captured_event.media_urls[0].startswith("http"), \ - f"Non-voice audio should have HTTP URL, got {captured_event.media_urls[0]}" - self.adapter._client.download_media.assert_not_awaited() + # Should be a local path, not an HTTP URL. + assert not captured_event.media_urls[0].startswith("http"), \ + f"Regular audio should be cached locally, got {captured_event.media_urls[0]}" + self.adapter._client.download_media.assert_awaited_once() assert captured_event.media_types == ["audio/ogg"] diff --git a/tests/hermes_cli/test_setup_prompt_menus.py b/tests/hermes_cli/test_setup_prompt_menus.py index 5a7225d09..fd017d87d 100644 --- a/tests/hermes_cli/test_setup_prompt_menus.py +++ b/tests/hermes_cli/test_setup_prompt_menus.py @@ -2,7 +2,7 @@ from hermes_cli import setup as setup_mod def test_prompt_choice_uses_curses_helper(monkeypatch): - monkeypatch.setattr(setup_mod, "_curses_prompt_choice", lambda question, choices, default=0: 1) + monkeypatch.setattr(setup_mod, "_curses_prompt_choice", lambda question, choices, default=0, description=None: 1) idx = setup_mod.prompt_choice("Pick one", ["a", "b", "c"], default=0) @@ -10,7 +10,7 @@ def test_prompt_choice_uses_curses_helper(monkeypatch): def test_prompt_choice_falls_back_to_numbered_input(monkeypatch): - monkeypatch.setattr(setup_mod, "_curses_prompt_choice", lambda question, choices, default=0: -1) + monkeypatch.setattr(setup_mod, "_curses_prompt_choice", lambda question, choices, default=0, description=None: -1) monkeypatch.setattr("builtins.input", lambda _prompt="": "2") idx = setup_mod.prompt_choice("Pick one", ["a", "b", "c"], default=0) diff --git a/tests/run_agent/test_413_compression.py b/tests/run_agent/test_413_compression.py index b30f9f6bb..1d6f6cebb 100644 --- a/tests/run_agent/test_413_compression.py +++ b/tests/run_agent/test_413_compression.py @@ -430,8 +430,15 @@ class TestPreflightCompression: ) result = agent.run_conversation("hello", conversation_history=big_history) - # Preflight compression should have been called BEFORE the API call - mock_compress.assert_called_once() + # Preflight compression is a multi-pass loop (up to 3 passes for very + # large sessions, breaking when no further reduction is possible). + # First pass must have received the full oversized history. + assert mock_compress.call_count >= 1, "Preflight compression never ran" + first_call_messages = mock_compress.call_args_list[0].args[0] + assert len(first_call_messages) >= 40, ( + f"First preflight pass should see the full history, got " + f"{len(first_call_messages)} messages" + ) assert result["completed"] is True assert result["final_response"] == "After preflight" diff --git a/website/docs/developer-guide/agent-loop.md b/website/docs/developer-guide/agent-loop.md index 2d0df3278..1ec647010 100644 --- a/website/docs/developer-guide/agent-loop.md +++ b/website/docs/developer-guide/agent-loop.md @@ -108,13 +108,14 @@ Providers validate these sequences and will reject malformed histories. API requests are wrapped in `_api_call_with_interrupt()` which runs the actual HTTP call in a background thread while monitoring an interrupt event: ```text -┌──────────────────────┐ ┌──────────────┐ -│ Main thread │ │ API thread │ -│ wait on: │────▶│ HTTP POST │ -│ - response ready │ │ to provider │ -│ - interrupt event │ └──────────────┘ -│ - timeout │ -└──────────────────────┘ +┌────────────────────────────────────────────────────┐ +│ Main thread API thread │ +│ │ +│ wait on: HTTP POST │ +│ - response ready ───▶ to provider │ +│ - interrupt event │ +│ - timeout │ +└────────────────────────────────────────────────────┘ ``` When interrupted (user sends new message, `/stop` command, or signal): diff --git a/website/docs/developer-guide/architecture.md b/website/docs/developer-guide/architecture.md index 5b881c7e2..88ad96269 100644 --- a/website/docs/developer-guide/architecture.md +++ b/website/docs/developer-guide/architecture.md @@ -20,21 +20,21 @@ This page is the top-level map of Hermes Agent internals. Use it to orient yours │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────┐ -│ AIAgent (run_agent.py) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Prompt │ │ Provider │ │ Tool │ │ -│ │ Builder │ │ Resolution │ │ Dispatch │ │ -│ │ (prompt_ │ │ (runtime_ │ │ (model_ │ │ -│ │ builder.py) │ │ provider.py)│ │ tools.py) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │ -│ │ Compression │ │ 3 API Modes │ │ Tool Registry│ │ -│ │ & Caching │ │ chat_compl. │ │ (registry.py)│ │ -│ │ │ │ codex_resp. │ │ 47 tools │ │ -│ │ │ │ anthropic │ │ 19 toolsets │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ AIAgent (run_agent.py) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Prompt │ │ Provider │ │ Tool │ │ +│ │ Builder │ │ Resolution │ │ Dispatch │ │ +│ │ (prompt_ │ │ (runtime_ │ │ (model_ │ │ +│ │ builder.py) │ │ provider.py)│ │ tools.py) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │ +│ │ Compression │ │ 3 API Modes │ │ Tool Registry│ │ +│ │ & Caching │ │ chat_compl. │ │ (registry.py)│ │ +│ │ │ │ codex_resp. │ │ 47 tools │ │ +│ │ │ │ anthropic │ │ 19 toolsets │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ diff --git a/website/docs/developer-guide/gateway-internals.md b/website/docs/developer-guide/gateway-internals.md index f3a9942c8..3f9a46bec 100644 --- a/website/docs/developer-guide/gateway-internals.md +++ b/website/docs/developer-guide/gateway-internals.md @@ -27,25 +27,25 @@ The messaging gateway is the long-running process that connects Hermes to 14+ ex ```text ┌─────────────────────────────────────────────────┐ -│ GatewayRunner │ -│ │ +│ GatewayRunner │ +│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Telegram │ │ Discord │ │ Slack │ ... │ -│ │ Adapter │ │ Adapter │ │ Adapter │ │ -│ └─────┬─────┘ └─────┬────┘ └─────┬────┘ │ -│ │ │ │ │ -│ └──────────────┼──────────────┘ │ -│ ▼ │ -│ _handle_message() │ -│ │ │ -│ ┌────────────┼────────────┐ │ -│ ▼ ▼ ▼ │ -│ Slash command AIAgent Queue/BG │ -│ dispatch creation sessions │ -│ │ │ -│ ▼ │ -│ SessionStore │ -│ (SQLite persistence) │ +│ │ Telegram │ │ Discord │ │ Slack │ │ +│ │ Adapter │ │ Adapter │ │ Adapter │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ +│ └─────────────┼─────────────┘ │ +│ ▼ │ +│ _handle_message() │ +│ │ │ +│ ┌───────────┼───────────┐ │ +│ ▼ ▼ ▼ │ +│ Slash command AIAgent Queue/BG │ +│ dispatch creation sessions │ +│ │ │ +│ ▼ │ +│ SessionStore │ +│ (SQLite persistence) │ └─────────────────────────────────────────────────┘ ```