diff --git a/hermes_cli/config.py b/hermes_cli/config.py index f374055eace..f2ee3ea48af 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1428,6 +1428,12 @@ DEFAULT_CONFIG = { "tui_agents_nudge": True, "bell_on_complete": False, "show_reasoning": False, + # Background self-improvement review notifications surfaced in chat. + # "off" — no chat notification (the review still runs and writes) + # "on" — generic "💾 Memory updated" line (default) + # "verbose" — include a compact content preview of what changed + # Per-platform overrides via display.platforms..memory_notifications. + "memory_notifications": "on", "streaming": False, "timestamps": False, # Show [HH:MM] on user and assistant labels "final_response_markdown": "strip", # render | strip | raw diff --git a/tests/run_agent/test_background_review.py b/tests/run_agent/test_background_review.py index b512497c1cf..8bce7e1507b 100644 --- a/tests/run_agent/test_background_review.py +++ b/tests/run_agent/test_background_review.py @@ -315,3 +315,117 @@ def test_background_review_fork_skips_external_memory_plugins(monkeypatch): "the fork leaks harness prompts into the user's real memory " "namespace via on_turn_start / prefetch_all / sync_all." ) + + +# --------------------------------------------------------------------------- +# memory_notifications mode: off | on | verbose +# --------------------------------------------------------------------------- + +import json as _json + +from agent.background_review import summarize_background_review_actions + + +def _memory_add_review(): + """A minimal review transcript: one memory add (assistant call + tool result).""" + return [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_mem1", + "function": { + "name": "memory", + "arguments": _json.dumps( + { + "action": "add", + "target": "memory", + "content": "User prefers terse replies", + } + ), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_mem1", + "content": _json.dumps( + {"success": True, "message": "Entry added.", "target": "memory"} + ), + }, + ] + + +def _skill_patch_review(): + return [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_skill1", + "function": { + "name": "skill_manage", + "arguments": _json.dumps( + {"action": "patch", "name": "demo", "old_string": "a", "new_string": "b"} + ), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_skill1", + "content": _json.dumps( + { + "success": True, + "message": "Patched SKILL.md in skill 'demo' (1 replacement).", + "_change": {"old": "a", "new": "b"}, + } + ), + }, + ] + + +def test_memory_notifications_off_returns_nothing(): + actions = summarize_background_review_actions( + _memory_add_review(), [], notification_mode="off" + ) + assert actions == [] + + +def test_memory_notifications_on_returns_generic_line(): + actions = summarize_background_review_actions( + _memory_add_review(), [], notification_mode="on" + ) + assert actions == ["Memory updated"] + + +def test_memory_notifications_verbose_includes_content_preview(): + actions = summarize_background_review_actions( + _memory_add_review(), [], notification_mode="verbose" + ) + assert len(actions) == 1 + # Verbose surfaces the actual content that was saved. + assert "User prefers terse replies" in actions[0] + assert actions[0] != "Memory updated" + + +def test_memory_notifications_default_is_on(): + """No mode passed → behaves like 'on' (generic line, not empty/verbose).""" + actions = summarize_background_review_actions(_memory_add_review(), []) + assert actions == ["Memory updated"] + + +def test_skill_patch_off_silent_verbose_shows_diff(): + assert ( + summarize_background_review_actions( + _skill_patch_review(), [], notification_mode="off" + ) + == [] + ) + verbose = summarize_background_review_actions( + _skill_patch_review(), [], notification_mode="verbose" + ) + assert len(verbose) == 1 + assert "demo" in verbose[0] and "→" in verbose[0] diff --git a/website/docs/user-guide/features/memory.md b/website/docs/user-guide/features/memory.md index 1f0ee16942f..91874c73e01 100644 --- a/website/docs/user-guide/features/memory.md +++ b/website/docs/user-guide/features/memory.md @@ -245,6 +245,27 @@ This is the answer to "the agent saved a wrong assumption about me": set `write_approval: true`, and every save — especially the unprompted background ones — waits for your yes/no before it ever enters your profile. +## Background review notifications (`display.memory_notifications`) + +After a turn, the background self-improvement review may quietly save a memory +or update a skill. By default it surfaces a short `💾 Memory updated` line in +chat so you know it happened. Control how chatty that is: + +```yaml +display: + memory_notifications: on # off | on (default) | verbose +``` + +| Value | Behaviour | +|-------|-----------| +| `off` | No chat notification. The review still runs and still writes — you just don't see a line for it. | +| `on` (default) | Generic line, e.g. `💾 Memory updated`, `💾 Skill 'foo' patched`. | +| `verbose` | Includes a compact preview of what changed, e.g. `💾 Memory ➕ User prefers terse replies` or a `"old" → "new"` skill diff snippet. | + +> This only governs the **gateway** chat notification. The review itself, and +> writes to your memory/skill stores, are unaffected by this setting. Set it +> per-platform via `display.platforms..memory_notifications`. + ## Controlling skill writes (`skills.write_approval`) Skills use the same on/off gate, but the review UX differs because a