# NousResearch/hermes-agent · memory approve command fails when store unavailable

## Open now

- 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.

## TL;DR

- 6 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #51056. Verify it addresses your case.
- **Recommended:** read PR #51056 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-51056`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #46783: /memory approve all can fail with "memory store unavailable" in Desktop/CLI approval path
- #47363: \[BUG\] /memory approve fails with "memory store unavailable" in TUI/dashboard chat
- #47941: write_approval: /memory approve and /skills approve fail with 'memory store unavailable'

## Prior & competing PRs

- **#46926: fix(memory): apply /memory approve against a fresh on-disk store when no live agent**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3147cbb13`, `#46783`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#47488: fix(cli): fall back to on-disk MemoryStore when agent store unavailable**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3147cbb1363554a404e6941f1862981326348d1b`, `0e69cd4b37aa3f218ada018d5f0456660e0b726b`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#51056: fix(memory): apply /memory approve against a fresh on-disk store when no live agent**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/slash_commands.py`, `hermes_cli/cli_commands_mixin.py`, `tests/tools/test_write_approval.py`, `tools/memory_tool.py`
  - fix hunks:
  - `gateway/slash_commands.py`

    ```diff
    @@ -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 []
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

## Fix status

- Merged to main in PR #51056 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-22`.
- Subsystem: `agent`
