From 0e69cd4b37aa3f218ada018d5f0456660e0b726b Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:05:31 +0530 Subject: [PATCH] fix(memory): honor configured char limits in the no-agent on-disk store Follow-up to the /memory approve fresh-store fix. Both the CLI fallback and the messaging-gateway handler built a bare MemoryStore() with the hardcoded default char limits (2200/1375), ignoring the user's configured memory.memory_char_limit / user_char_limit. A live agent honors those overrides (agent/agent_init.py), so an approval applied without a live agent could accept a write the user's lower cap would reject, or vice versa. Extract a shared tools.memory_tool.load_on_disk_store() factory that reads the configured limits (falling back to defaults if config can't load) and wire both the CLI and gateway handlers to it, closing the gap on both surfaces and de-duplicating the construction block. --- gateway/slash_commands.py | 6 +++--- hermes_cli/cli_commands_mixin.py | 7 ++++--- tests/tools/test_write_approval.py | 27 +++++++++++++++++++++++++ tools/memory_tool.py | 32 ++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) 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