diff --git a/cron/scheduler.py b/cron/scheduler.py index 55a038785..5784aec3d 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -26,6 +26,7 @@ except ImportError: msvcrt = None from pathlib import Path from hermes_constants import get_hermes_home +from hermes_cli.config import load_config from typing import Optional from hermes_time import now as _hermes_now @@ -164,18 +165,29 @@ def _deliver_result(job: dict, content: str) -> None: logger.warning("Job '%s': platform '%s' not configured/enabled", job["id"], platform_name) return - # Wrap the content so the user knows this is a cron delivery and that - # the interactive agent has no visibility into it. - task_name = job.get("name", job["id"]) - wrapped = ( - f"Cronjob Response: {task_name}\n" - f"-------------\n\n" - f"{content}\n\n" - f"Note: The agent cannot see this message, and therefore cannot respond to it." - ) + # Optionally wrap the content with a header/footer so the user knows this + # is a cron delivery. Wrapping is on by default; set cron.wrap_response: false + # in config.yaml for clean output. + wrap_response = True + try: + user_cfg = load_config() + wrap_response = user_cfg.get("cron", {}).get("wrap_response", True) + except Exception: + pass + + if wrap_response: + task_name = job.get("name", job["id"]) + delivery_content = ( + f"Cronjob Response: {task_name}\n" + f"-------------\n\n" + f"{content}\n\n" + f"Note: The agent cannot see this message, and therefore cannot respond to it." + ) + else: + delivery_content = content # Run the async send in a fresh event loop (safe from any thread) - coro = _send_to_platform(platform, pconfig, chat_id, wrapped, thread_id=thread_id) + coro = _send_to_platform(platform, pconfig, chat_id, delivery_content, thread_id=thread_id) try: result = asyncio.run(coro) except RuntimeError: @@ -186,7 +198,7 @@ def _deliver_result(job: dict, content: str) -> None: coro.close() import concurrent.futures with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, wrapped, thread_id=thread_id)) + future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, delivery_content, thread_id=thread_id)) result = future.result(timeout=30) except Exception as e: logger.error("Job '%s': delivery to %s:%s failed: %s", job["id"], platform_name, chat_id, e) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3304a187e..8a65d0e2c 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -429,6 +429,12 @@ DEFAULT_CONFIG = { }, }, + "cron": { + # Wrap delivered cron responses with a header (task name) and footer + # ("The agent cannot see this message"). Set to false for clean output. + "wrap_response": True, + }, + # Config schema version - bump this when adding new required fields "_config_version": 10, } diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 25bc202cf..12bd80436 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -167,6 +167,32 @@ class TestDeliverResultWrapping: sent_content = send_mock.call_args.kwargs.get("content") or send_mock.call_args[0][-1] assert "Cronjob Response: abc-123" in sent_content + def test_delivery_skips_wrapping_when_config_disabled(self): + """When cron.wrap_response is false, deliver raw content without header/footer.""" + from gateway.config import Platform + + pconfig = MagicMock() + pconfig.enabled = True + mock_cfg = MagicMock() + mock_cfg.platforms = {Platform.TELEGRAM: pconfig} + + with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ + patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})) as send_mock, \ + patch("cron.scheduler.load_config", return_value={"cron": {"wrap_response": False}}): + job = { + "id": "test-job", + "name": "daily-report", + "deliver": "origin", + "origin": {"platform": "telegram", "chat_id": "123"}, + } + _deliver_result(job, "Clean output only.") + + send_mock.assert_called_once() + sent_content = send_mock.call_args.kwargs.get("content") or send_mock.call_args[0][-1] + assert sent_content == "Clean output only." + assert "Cronjob Response" not in sent_content + assert "The agent cannot see" not in sent_content + def test_no_mirror_to_session_call(self): """Cron deliveries should NOT mirror into the gateway session.""" from gateway.config import Platform