fix(memory): apply /memory approve against a fresh store when no live agent

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) <noreply@anthropic.com>
This commit is contained in:
Max Hsu 2026-06-16 07:58:56 +08:00 committed by kshitij
parent 100e7be20e
commit 3147cbb136
2 changed files with 40 additions and 0 deletions

View file

@ -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,

View file

@ -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
# ---------------------------------------------------------------------------