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

0open issues
0open PRs
6items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #51056 fix(memory): apply /memory approve against a fresh on-disk store when no live agent

    merged to main

    files: gateway/slash_commands.py, hermes_cli/cli_commands_mixin.py, tests/tools/test_write_approval.py, tools/memory_tool.py

    Show 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

  • #46926 fix(memory): apply /memory approve against a fresh on-disk store when no live agent · done elsewhere → 3147cbb, #46783
  • #47488 fix(cli): fall back to on-disk MemoryStore when agent store unavailable · done elsewhere → 3147cbb, 0e69cd4
Closed issues · 3
  • Closed#46783/memory approve all can fail with "memory store unavailable" in Desktop/CLI approval path
  • Closed#47363[BUG] /memory approve fails with "memory store unavailable" in TUI/dashboard chat
  • Closed#47941write_approval: /memory approve and /skills approve fail with 'memory store unavailable'
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.00 means 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 Lab

At a glance

Subsystem
agent
Merged PRs
#51056
Closed issues
3
Attempts
2 PRs closed without merging
Fix link checked
22 Jun 2026

For your agent

The same facts, typed.