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.
This commit is contained in:
kshitijk4poor 2026-06-23 03:05:31 +05:30 committed by kshitij
parent 3147cbb136
commit 0e69cd4b37
4 changed files with 66 additions and 6 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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
# ---------------------------------------------------------------------------

View file

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