memory approve command fails when store unavailable
NousResearch/hermes-agent · cluster 46783 · agent
Data as of
Nothing open on GitHub. Fix merged in #51056. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #51056. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #51056. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple reports and fixes around /memory approve and /skills approve failing with 'memory store unavailable' errors across CLI, Desktop, and TUI environments. Work includes fallback behavior using on-disk MemoryStore when no live agent is present.
Open issues0
Open PRs0
Fixes on record
-
#51056 fix(memory): apply /memory approve against a fresh on-disk store when no live agent
files:
gateway/slash_commands.py,hermes_cli/cli_commands_mixin.py,tests/tools/test_write_approval.py,tools/memory_tool.pyShow the diff
-
gateway/slash_commands.py@@ -2343,7 +2343,7 @@ async def _handle_memory_command(self, event: MessageEvent) -> str: from gateway.run import _hermes_home from hermes_cli.write_approval_commands import handle_pending_subcommand from tools import write_approval as wa - from tools.memory_tool import MemoryStore + from tools.memory_tool import load_on_disk_store raw_args = event.get_command_args().strip() args = raw_args.split() if raw_args else [] @@ -2363,8 +2363,8 @@ def _set_approval(enabled: bool): # Apply approved writes against a fresh on-disk store (the gateway has # no long-lived agent; the store persists to the same MEMORY/USER.md). - store = MemoryStore() - store.load_from_disk() + # load_on_disk_store() honors the user's configured char limits. + store = load_on_disk_store() out = handle_pending_subcommand( wa.MEMORY, args, memory_store=store, set_mode_fn=_set_approval, -
hermes_cli/cli_commands_mixin.py@@ -1361,6 +1361,17 @@ def _handle_memory_command(self, cmd: str): 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. + # load_on_disk_store() honors the user's configured char limits, so + # an approval here enforces the same caps as the live agent would. + from tools.memory_tool import load_on_disk_store + store = load_on_disk_store() out = handle_pending_subcommand( wa.MEMORY, args, memory_store=store, -
tests/tools/test_write_approval.py@@ -107,6 +107,63 @@ 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) + + +def test_load_on_disk_store_honors_configured_char_limits(hermes_home, monkeypatch): + """load_on_disk_store() must read memory.memory_char_limit / + user_char_limit from config so approvals applied without a live agent + enforce the SAME caps as the live agent (agent_init.py). Falls back to + defaults when config can't be loaded. + """ + from tools.memory_tool import load_on_disk_store + + # Config override path: helper picks up the configured limits. + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"memory": {"memory_char_limit": 999, "user_char_limit": 444}}, + ) + store = load_on_disk_store() + assert store.memory_char_limit == 999 + assert store.user_char_limit == 444 + + # Failure path: config raises → defaults, never blows up. + def _boom(): + raise RuntimeError("no config") + + monkeypatch.setattr("hermes_cli.config.load_config", _boom) + fallback = load_on_disk_store() + assert fallback.memory_char_limit == 2200 + assert fallback.user_char_limit == 1375 + + # --------------------------------------------------------------------------- # Skill gate # --------------------------------------------------------------------------- -
tools/memory_tool.py@@ -731,6 +731,38 @@ def _write_file(path: Path, entries: List[str]): raise RuntimeError(f"Failed to write memory file {path}: {e}") +def load_on_disk_store() -> "MemoryStore": + """Build a fresh on-disk :class:`MemoryStore`, honoring configured char limits. + + Use this from any context that has no live agent (the messaging gateway, the + Desktop GUI, the bare CLI ``/memory`` handler) but still needs to read or + apply approved memory writes. Mirrors how the live agent constructs its store + in ``agent/agent_init.py`` — including the user's ``memory.memory_char_limit`` + / ``memory.user_char_limit`` overrides — so an approval applied without a live + agent enforces the SAME caps as one applied with one. + + Falls back to the built-in defaults if config can't be loaded, so this can + never raise on a missing/unreadable config. + """ + memory_char_limit = 2200 + user_char_limit = 1375 + try: + from hermes_cli.config import load_config + + mem_cfg = (load_config() or {}).get("memory", {}) or {} + memory_char_limit = int(mem_cfg.get("memory_char_limit", memory_char_limit)) + user_char_limit = int(mem_cfg.get("user_char_limit", user_char_limit)) + except Exception: + pass # config optional — fall back to defaults rather than break /memory + + store = MemoryStore( + memory_char_limit=memory_char_limit, + user_char_limit=user_char_limit, + ) + store.load_from_disk() + return store + + def _apply_write_gate(action: str, target: str, content: Optional[str], old_text: Optional[str]) -> Optional[str]: """Evaluate the memory write gate. Returns a JSON tool-result string when
-
2 PRs closed without merging
From the close comments: done elsewhere 2
Closed issues · 3
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #51056
- Closed issues
- 3
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 22 Jun 2026