From 3147cbb1363554a404e6941f1862981326348d1b Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Tue, 16 Jun 2026 07:58:56 +0800 Subject: [PATCH] fix(memory): apply /memory approve against a fresh store when no live agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI /memory slash handler (cli_commands_mixin._handle_memory_command) passed self.agent._memory_store straight through, which is None when the command runs without a live agent — e.g. /memory approve from the Desktop GUI. The shared write-approval handler then returns "memory store unavailable" and applies nothing, even with built-in memory enabled and pending writes present. Fall back to a freshly loaded on-disk MemoryStore when no live store is available, mirroring the gateway path (gateway/slash_commands.py). It persists to the same MEMORY/USER.md and creates MEMORY.md on the first approved write. Fixes #46783 Co-Authored-By: Claude Opus 4.8 (1M context) --- hermes_cli/cli_commands_mixin.py | 10 ++++++++++ tests/tools/test_write_approval.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index d8df27a5df4..b645900d4f9 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -1361,6 +1361,16 @@ class CLICommandsMixin: parts = cmd.strip().split() args = parts[1:] if len(parts) > 1 else [] store = getattr(self.agent, "_memory_store", None) if getattr(self, "agent", None) else None + if store is None: + # No live agent store (e.g. /memory approve invoked from the Desktop + # GUI, or any context without an active agent). Apply against a freshly + # loaded on-disk store, mirroring the gateway path + # (gateway/slash_commands.py): it persists to the same MEMORY/USER.md + # and creates MEMORY.md on the first approved write. Without this the + # shared handler returns "memory store unavailable". See #46783. + from tools.memory_tool import MemoryStore + store = MemoryStore() + store.load_from_disk() out = handle_pending_subcommand( wa.MEMORY, args, memory_store=store, diff --git a/tests/tools/test_write_approval.py b/tests/tools/test_write_approval.py index fbfa804fbb9..7b65978f0ac 100644 --- a/tests/tools/test_write_approval.py +++ b/tests/tools/test_write_approval.py @@ -107,6 +107,36 @@ def test_memory_gate_on_then_apply(hermes_home): assert "approved entry" in store.user_entries[0] +def test_cli_memory_approve_without_live_agent_uses_fresh_store(hermes_home, capsys): + """#46783: ``/memory approve`` from a context with no live agent (e.g. the + Desktop GUI) passed ``memory_store=None`` into the shared handler, which + returned "memory store unavailable" and applied nothing. The CLI handler must + fall back to a freshly loaded on-disk store, like the gateway path does.""" + import json + from tools.memory_tool import memory_tool, MemoryStore + from tools import write_approval as wa + from hermes_cli.cli_commands_mixin import CLICommandsMixin + + _set_approval("memory", True) + staging = MemoryStore(); staging.load_from_disk() + r = json.loads(memory_tool("add", "memory", "remember the launch date", store=staging)) + assert r.get("pending_id"), r + assert wa.pending_count("memory") == 1 + + # Bare CLI handler with no live agent → store resolves to None pre-fix. + handler = CLICommandsMixin.__new__(CLICommandsMixin) + handler.agent = None + handler._handle_memory_command("/memory approve all") + + out = capsys.readouterr().out + assert "memory store unavailable" not in out, out + assert "Approved 1" in out, out + assert wa.pending_count("memory") == 0 + # The approved write landed in a freshly loaded on-disk store (MEMORY.md). + reloaded = MemoryStore(); reloaded.load_from_disk() + assert any("remember the launch date" in e for e in reloaded.memory_entries) + + # --------------------------------------------------------------------------- # Skill gate # ---------------------------------------------------------------------------