diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index f35682f8603..ab9ea9759bd 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -2343,7 +2343,7 @@ class GatewaySlashCommandsMixin: 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 @@ class GatewaySlashCommandsMixin: # 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, diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index b645900d4f9..95292314c5a 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -1368,9 +1368,10 @@ class CLICommandsMixin: # (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() + # 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, diff --git a/tests/tools/test_write_approval.py b/tests/tools/test_write_approval.py index 7b65978f0ac..73ea119e0e5 100644 --- a/tests/tools/test_write_approval.py +++ b/tests/tools/test_write_approval.py @@ -137,6 +137,33 @@ def test_cli_memory_approve_without_live_agent_uses_fresh_store(hermes_home, cap 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 # --------------------------------------------------------------------------- diff --git a/tools/memory_tool.py b/tools/memory_tool.py index 33d6ffff5e5..47d9d2c9922 100644 --- a/tools/memory_tool.py +++ b/tools/memory_tool.py @@ -731,6 +731,38 @@ class MemoryStore: 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