diff --git a/cmd/claw/hermes_base_image_test.go b/cmd/claw/hermes_base_image_test.go index 3ed300c..e8fcc74 100644 --- a/cmd/claw/hermes_base_image_test.go +++ b/cmd/claw/hermes_base_image_test.go @@ -70,6 +70,9 @@ func TestHermesBaseImageSourceContract(t *testing.T) { `Matching tolerates case, whitespace, and punctuation`, `os.chmod(real_path, 0o666)`, `HERMES_CRON_DELIVER_TRANSIENT_FAILURES`, + `cron scheduler protected toolsets documentation`, + `cron scheduler memory tool suppression`, + `["cronjob", "messaging", "clarify", "memory"]`, `_claw_should_deliver_cron_failure`, `upstream request failed`, `suppressing transient cron failure delivery`, diff --git a/cmd/claw/hermes_base_spike_test.go b/cmd/claw/hermes_base_spike_test.go index 84ac073..1007974 100644 --- a/cmd/claw/hermes_base_spike_test.go +++ b/cmd/claw/hermes_base_spike_test.go @@ -49,6 +49,22 @@ assert importlib.util.find_spec("minisweagent_path") is not None import tools.terminal_tool from cron import scheduler as cron_scheduler +from model_tools import get_tool_definitions +cron_disabled = cron_scheduler._resolve_cron_disabled_toolsets({}) +assert "memory" in cron_disabled +cron_tools = { + tool["function"]["name"] + for tool in get_tool_definitions(disabled_toolsets=cron_disabled, quiet_mode=True) +} +assert "memory" not in cron_tools +interactive_tools = { + tool["function"]["name"] + for tool in get_tool_definitions( + disabled_toolsets=["cronjob", "messaging", "clarify"], + quiet_mode=True, + ) +} +assert "memory" in interactive_tools assert not cron_scheduler._claw_should_deliver_cron_failure("upstream request failed") assert not cron_scheduler._claw_should_deliver_cron_failure("Internal Server Error") assert cron_scheduler._claw_should_deliver_cron_failure("prompt injection scanner blocked the job") @@ -61,6 +77,7 @@ assert str(_get_lock_dir()) == "/tmp/hermes-gateway-locks" from toolsets import _HERMES_CORE_TOOLS, TOOLSETS assert "text_to_speech" not in _HERMES_CORE_TOOLS +assert "memory" in _HERMES_CORE_TOOLS for _name, _toolset in TOOLSETS.items(): if isinstance(_toolset, dict) and isinstance(_toolset.get("tools"), list): assert "text_to_speech" not in _toolset["tools"], _name diff --git a/dockerfiles/hermes-base/patch-hermes-runtime.py b/dockerfiles/hermes-base/patch-hermes-runtime.py index d453b87..fbbb732 100644 --- a/dockerfiles/hermes-base/patch-hermes-runtime.py +++ b/dockerfiles/hermes-base/patch-hermes-runtime.py @@ -844,6 +844,30 @@ def _atomic_write_text(file_path: Path, content: str, encoding: str = "utf-8") - cron_scheduler = purelib / "cron" / "scheduler.py" text = cron_scheduler.read_text() +# Cron jobs initialize Hermes with skip_memory=True so their system prompts do +# not mutate user representations. Keep the advertised tools consistent with +# that runtime capability while leaving native memory available to interactive +# agents. +text = replace_once( + text, + " Three protected toolsets are always disabled in cron context:\n" + " - ``cronjob`` — would let a cron-spawned agent schedule more cron jobs\n" + " - ``messaging`` — interactive, needs a live gateway session\n" + " - ``clarify`` — interactive, blocks waiting for user input\n", + " Four protected toolsets are always disabled in cron context:\n" + " - ``cronjob`` — would let a cron-spawned agent schedule more cron jobs\n" + " - ``messaging`` — interactive, needs a live gateway session\n" + " - ``clarify`` — interactive, blocks waiting for user input\n" + " - ``memory`` — unavailable because cron agents use ``skip_memory=True``\n", + "cron scheduler protected toolsets documentation", +) +text = replace_once( + text, + ' disabled = ["cronjob", "messaging", "clarify"]\n', + ' disabled = ["cronjob", "messaging", "clarify", "memory"]\n', + "cron scheduler memory tool suppression", +) + # Cron transient-failure delivery: provider/cllama outages are already logged # and recorded on the job run. They should not be posted into user channels as # actionable cron responses, while real operator-actionable cron failures still