diff --git a/headroom/memory/inline_extractor.py b/headroom/memory/inline_extractor.py index bcbf6aabf..f560ae3ed 100644 --- a/headroom/memory/inline_extractor.py +++ b/headroom/memory/inline_extractor.py @@ -126,9 +126,21 @@ def parse_response_with_memory(response_text: str) -> ParsedResponse: # Parse the JSON try: data = json.loads(memory_json) - memories = data.get("memories", []) except json.JSONDecodeError as e: logger.warning(f"Failed to parse memory JSON: {e}") + else: + # The model controls the block content. A valid-JSON non-object + # (e.g. a bare array) would make `.get` raise AttributeError, which + # the JSONDecodeError guard above does not catch; likewise a + # non-list `memories` would break downstream iteration. + if isinstance(data, dict): + extracted = data.get("memories", []) + if isinstance(extracted, list): + memories = extracted + else: + logger.warning("Memory JSON 'memories' field is not a list; ignoring") + else: + logger.warning("Memory JSON is not an object; ignoring") return ParsedResponse( content=content, diff --git a/tests/test_memory_wrapper.py b/tests/test_memory_wrapper.py index c09a4c5e3..0bd009713 100644 --- a/tests/test_memory_wrapper.py +++ b/tests/test_memory_wrapper.py @@ -205,3 +205,22 @@ def test_memory_api_methods_delegate_to_underlying_memory() -> None: assert api.clear() == 2 assert api.stats() == {"total": 2} assert fake_memory.last_clear == {"user_id": "alice"} + + +def test_parse_response_with_memory_tolerates_non_object_memory_block() -> None: + from headroom.memory.inline_extractor import parse_response_with_memory + + # The model controls the block. A valid-JSON non-object (a bare + # array) or a non-list `memories` field must not crash the parse: the + # `json.loads` succeeds, so the JSONDecodeError guard does not apply, and + # `.get`/iteration on the wrong type would otherwise raise. + assert parse_response_with_memory('hi ["x"] bye').memories == [] + assert parse_response_with_memory('{"memories": "nope"}').memories == [] + + # Malformed JSON is still handled, and a well-formed block still parses. + assert parse_response_with_memory("{not json").memories == [] + parsed = parse_response_with_memory( + 'text {"memories": [{"content": "User likes Python"}]}' + ) + assert parsed.memories == [{"content": "User likes Python"}] + assert parsed.content == "text"