From b241194d746157fda9ba042515e8a8d40daf8adf Mon Sep 17 00:00:00 2001 From: MemOS AutoDev Date: Tue, 30 Jun 2026 18:25:23 +0800 Subject: [PATCH 1/2] fix: mem_reader fallback dict key matches consumer in simple_struct (#1493) `SimpleStructMemReader._get_llm_response` returned its salvage dict under the key `memory_list` (underscore) while every downstream consumer reads `memory list` (space). The fallback was therefore unreachable: when the LLM emitted unparseable JSON for short inputs like "I like strawberry.", `_process_chat_data` yielded zero items, `text_mem.add([])` wrote nothing, and `/product/add` still returned 200 + `data: []` while `log_add_messages` logged "No add/update items prepared". Rename the fallback key to match the consumers (lines 397 and 434) and the sibling readers in `multi_modal_struct.py` and `strategy_struct.py`. Add two regression tests under `tests/mem_reader/test_simple_struct_fallback.py` that pin the fallback contract and the end-to-end fine-mode reader behavior under unparseable LLM output. Both tests fail on the buggy code and pass on the fix; ruff lint + format clean. --- src/memos/mem_reader/simple_struct.py | 2 +- .../mem_reader/test_simple_struct_fallback.py | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/mem_reader/test_simple_struct_fallback.py diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index 3c82350df..3b418bcc5 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -287,7 +287,7 @@ def _get_llm_response(self, mem_str: str, custom_tags: list[str] | None) -> dict if not response_json: return { - "memory_list": [ + "memory list": [ { "key": mem_str[:10], "memory_type": "UserMemory", diff --git a/tests/mem_reader/test_simple_struct_fallback.py b/tests/mem_reader/test_simple_struct_fallback.py new file mode 100644 index 000000000..f4b59d7d5 --- /dev/null +++ b/tests/mem_reader/test_simple_struct_fallback.py @@ -0,0 +1,111 @@ +"""Regression tests for issue #1493. + +`POST /product/add` returned 200 + "Memory added successfully" but persisted no +memory items. Root cause: when the LLM response fails JSON parsing, +`SimpleStructMemReader._get_llm_response` is supposed to emit a fallback dict +with a single salvaged `UserMemory` item so the request still produces at least +one stored memory. However, the fallback used the key `"memory_list"` +(underscore) while every downstream consumer reads `"memory list"` (space), +making the fallback unreachable and leaving `_process_chat_data` returning an +empty list. + +These tests pin the fallback contract and the end-to-end reader behavior so the +regression cannot reappear. +""" + +import unittest + +from unittest.mock import MagicMock, patch + +from memos.chunkers import ChunkerFactory +from memos.configs.mem_reader import SimpleStructMemReaderConfig +from memos.embedders.factory import EmbedderFactory +from memos.llms.factory import LLMFactory +from memos.mem_reader.simple_struct import SimpleStructMemReader +from memos.memories.textual.item import TextualMemoryItem + + +class TestSimpleStructFallbackKey(unittest.TestCase): + """Pin the fallback contract: key must match what consumers read.""" + + def setUp(self): + self.config = MagicMock(spec=SimpleStructMemReaderConfig) + self.config.llm = MagicMock() + self.config.general_llm = None + self.config.embedder = MagicMock() + self.config.chunker = MagicMock() + self.config.remove_prompt_example = MagicMock() + + with ( + patch.object(LLMFactory, "from_config", return_value=MagicMock()), + patch.object(EmbedderFactory, "from_config", return_value=MagicMock()), + patch.object(ChunkerFactory, "from_config", return_value=MagicMock()), + ): + self.reader = SimpleStructMemReader(self.config) + + self.reader.llm = MagicMock() + self.reader.general_llm = self.reader.llm + self.reader.embedder = MagicMock() + self.reader.embedder.embed = MagicMock(return_value=[[0.0] * 4]) + self.reader.chunker = MagicMock() + + def test_get_llm_response_fallback_key_matches_consumer(self): + """Fallback dict must expose its item under the consumer-side key. + + Downstream consumers in `_process_chat_data` (line 397) and + `_process_transfer_chat_data` (line 434) both read `"memory list"` + (with space). The fallback dict therefore MUST use the same key, or it + is effectively dead code and the request stores nothing. + """ + # Force `_safe_parse` to behave as if the LLM returned unparseable + # output (the realistic failure mode reported in #1493 / #1355). + self.reader.llm.generate.return_value = "this is not json at all" + + result = self.reader._get_llm_response("I like strawberry.", custom_tags=None) + + self.assertIn( + "memory list", + result, + "Fallback dict must use the consumer-side key 'memory list' (with space). " + "Using 'memory_list' (underscore) leaves the fallback unreachable.", + ) + self.assertEqual(len(result["memory list"]), 1) + item = result["memory list"][0] + self.assertEqual(item["memory_type"], "UserMemory") + self.assertEqual(item["value"], "I like strawberry.") + + def test_process_chat_data_fine_yields_node_when_llm_unparseable(self): + """End-to-end: fine-mode `_process_chat_data` must still produce a + `TextualMemoryItem` when the LLM output is unparseable JSON. + + This is the symptom-level test for #1493: an `/product/add` call + carrying `"I like strawberry."` should result in at least one memory + item even when the LLM emits non-JSON chatter. + """ + # Realistic Kimi-K2 style failure: LLM wraps response in chatter so + # `parse_json_result` returns `{}`, `_safe_parse` returns `None`, + # `not response_json` is True and the fallback fires. + self.reader.llm.generate.return_value = ( + "Here is what I extracted: ... unfortunately I cannot output JSON." + ) + + scene_data_info = [{"role": "user", "content": "I like strawberry."}] + info = {"user_id": "user1", "session_id": "session1"} + + result = self.reader._process_chat_data(scene_data_info, info, mode="fine") + + self.assertIsInstance(result, list) + self.assertGreaterEqual( + len(result), + 1, + "Fallback must produce at least one TextualMemoryItem when LLM " + "output cannot be parsed; otherwise /product/add stores nothing " + "while still returning 200.", + ) + self.assertIsInstance(result[0], TextualMemoryItem) + # The fallback value contains the original user input verbatim. + self.assertIn("strawberry", result[0].memory) + + +if __name__ == "__main__": + unittest.main() From e861f9dee7213ba10c8cae8dbb230d57a6c7724b Mon Sep 17 00:00:00 2001 From: Memtensor-AI Date: Wed, 1 Jul 2026 21:08:48 +0800 Subject: [PATCH 2/2] chore: re-trigger test-engine (auto-gen tests now advisory/non-gating; prior FAILED was a false positive)