From 4e1b3daa86f2002e0059f12bf6cfeb7f6e8ce036 Mon Sep 17 00:00:00 2001 From: easyvibecoding <266905211+easyvibecoding@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:16:11 -0700 Subject: [PATCH] fix(memory): warn when MEMORY.md / USER.md exceed their char limit on load The cap only fires on add/replace, so an externally written over-budget file rode silently in the system prompt while every later add was refused with no visible cause. Warn at load; entries stay loaded (never truncate a user's memories). Salvage of #10886 (original hunk targeted memory_tool.py before the store split); authored by @easyvibecoding. Refs #10877 --- tests/tools/test_memory_tool.py | 20 ++++++++++++++++++++ tools/memory_tool_store.py | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/tests/tools/test_memory_tool.py b/tests/tools/test_memory_tool.py index 11466ba217..6ea61b9fe6 100644 --- a/tests/tools/test_memory_tool.py +++ b/tests/tools/test_memory_tool.py @@ -270,6 +270,26 @@ class TestMemoryStorePersistence: assert len(store.memory_entries) == 2 +class TestMemoryStoreCharLimitOnLoad: + @pytest.mark.parametrize("filename, target", [("MEMORY.md", "memory"), ("USER.md", "user")]) + def test_over_limit_file_loads_but_warns(self, tmp_path, monkeypatch, caplog, filename, target): + """An externally written over-budget file is kept (no silent data loss) and named in a + warning; an in-budget file loads quietly (#10877).""" + import logging + monkeypatch.setattr("tools.memory_tool.get_memory_dir", lambda: tmp_path) + (tmp_path / filename).write_text("x" * 600, encoding="utf-8") + with caplog.at_level(logging.WARNING): + store = MemoryStore(memory_char_limit=500, user_char_limit=300) + store.load_from_disk() + assert filename in caplog.text and "exceeds" in caplog.text + assert len(store._entries_for(target)) == 1 + caplog.clear() + (tmp_path / filename).write_text("short", encoding="utf-8") + with caplog.at_level(logging.WARNING): + MemoryStore(memory_char_limit=500, user_char_limit=300).load_from_disk() + assert "exceeds" not in caplog.text + + class TestMemoryStoreSnapshot: def test_snapshot_frozen_at_load(self, store): assert store.format_for_system_prompt("memory") is None # empty store diff --git a/tools/memory_tool_store.py b/tools/memory_tool_store.py index 4575944443..ccc3d691e8 100644 --- a/tools/memory_tool_store.py +++ b/tools/memory_tool_store.py @@ -130,6 +130,13 @@ class MemoryStore: # Deduplicate (order-preserving, first occurrence wins). entries = list(dict.fromkeys(self._read_file(path))) self._set_entries(target, entries) + # External writers (MCP bridges, hand edits) can exceed the cap; the limit only fires on + # add/replace, so the oversized block would silently ride in the prompt while every later + # add is refused with no visible cause (#10877). Warn; never truncate a user's memories. + if (count := self._char_count(target)) > (limit := self._char_limit(target)): + logger.warning("%s exceeds its char limit on load: %d/%d chars. Entries stay loaded; " + "further additions are blocked until it is back under the limit.", + path.name, count, limit) self._system_prompt_snapshot[target] = self._render_block(target, [_sanitize(e, path.name) for e in entries]) @staticmethod