From 0179ccbfe3b9aebf3c11dbd029978ac96e24b00d Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:23:37 +0530 Subject: [PATCH] Python: drop the memory placeholder only when it is the sole entry A genuine memory whose exact text equals the empty-list placeholder used to be dropped on read, so memories=[none yet] collapsed to an empty list. The write side now marks a genuine placeholder-valued memory with a distinguishing suffix, and the reader decodes the mark before applying the sole-placeholder drop, so a marked singleton round trips exactly while a bare placeholder written for an empty list still reads back empty and unmarked files keep their previous meaning. Tests cover the singleton round trip, the written marker form, and legacy unmarked behavior. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> --- .../core/agent_framework/_harness/_memory.py | 32 +++++++- .../core/tests/core/test_harness_memory.py | 75 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_harness/_memory.py b/python/packages/core/agent_framework/_harness/_memory.py index c3f02f883b5..f10c8036a98 100644 --- a/python/packages/core/agent_framework/_harness/_memory.py +++ b/python/packages/core/agent_framework/_harness/_memory.py @@ -45,6 +45,11 @@ DEFAULT_MEMORY_CONSOLIDATION_INTERVAL: Final[timedelta] = timedelta(hours=24) DEFAULT_MEMORY_INDEX_HEADER = "# MEMORY" DEFAULT_MEMORY_NO_TOPICS_TEXT = "- none yet" + +# Suffix marking a genuine memory whose exact text equals the empty-list +# placeholder, so a singleton real memory is distinguishable from a written +# placeholder line on read. Unmarked files keep their previous meaning. +_MEMORY_GENUINE_PLACEHOLDER_SUFFIX = " " DEFAULT_MEMORY_EXTRACTION_PROMPT = """You extract durable memory candidates from an agent transcript delta. Return only JSON with this exact shape: @@ -446,7 +451,7 @@ def to_markdown(self) -> str: # content (or LLM-supplied consolidation output) for a section delimiter. escaped_summary_lines = [_escape_markdown_line(line) for line in self.summary.splitlines()] escaped_summary = "\n".join(escaped_summary_lines) - memory_lines = [f"- {_escape_markdown_line(memory)}" for memory in self.memories] or [ + memory_lines = [f"- {_escape_markdown_line(self._encode_memory_text(memory))}" for memory in self.memories] or [ f"- {DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]}" ] return "\n".join([ @@ -462,6 +467,18 @@ def to_markdown(self) -> str: *memory_lines, ]).rstrip() + @staticmethod + def _encode_memory_text(memory: str) -> str: + """Mark a genuine memory equal to the placeholder so a reader keeps it. + + The write side emits the bare placeholder line only for an empty memory + list; a real memory whose exact text equals it gets a distinguishing + suffix so a singleton round trip cannot collapse to empty. + """ + if memory == DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]: + return memory + _MEMORY_GENUINE_PLACEHOLDER_SUFFIX + return memory + @classmethod def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> MemoryTopicRecord: """Parse one topic record from the canonical markdown format. @@ -515,9 +532,20 @@ def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> M continue if current_section == "memories" and stripped.startswith("- "): memory_text = _unescape_markdown_line(stripped[2:].strip()) - if memory_text and memory_text != DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]: + if memory_text: memories.append(memory_text) + # Decode the genuine-marker suffix first so a marked memory can never be + # mistaken for the placeholder, then drop a bare placeholder only when it + # is the sole entry: that is the only shape the write side emits it in, + # and unmarked files keep their previous meaning. + placeholder = DEFAULT_MEMORY_NO_TOPICS_TEXT[2:] + marked = placeholder + _MEMORY_GENUINE_PLACEHOLDER_SUFFIX + was_marked = [memory == marked for memory in memories] + memories = [placeholder if is_marked else memory for memory, is_marked in zip(memories, was_marked)] + if memories == [placeholder] and not was_marked[0]: + memories = [] + if topic is None: raise ValueError("Memory topic markdown is missing a '# ' heading.") return cls( diff --git a/python/packages/core/tests/core/test_harness_memory.py b/python/packages/core/tests/core/test_harness_memory.py index 464255d0b43..42f571a06ff 100644 --- a/python/packages/core/tests/core/test_harness_memory.py +++ b/python/packages/core/tests/core/test_harness_memory.py @@ -1046,6 +1046,81 @@ def test_memory_file_store_encodes_non_ascii_owner_ids(tmp_path) -> None: assert roots[0] != roots[1] +def test_memory_topic_record_keeps_a_memory_that_matches_the_placeholder(tmp_path) -> None: + """A real memory whose text equals the empty-list placeholder must survive a + round trip; only the placeholder written for an empty memory list is dropped.""" + record = MemoryTopicRecord( + topic="status", + summary="s", + memories=["none yet", "real fact"], + updated_at="2026-10-26T00:00:00+00:00", + ) + + parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="status") + + assert parsed.memories == ["none yet", "real fact"] + + +def test_memory_topic_record_round_trips_a_genuine_singleton_placeholder_memory() -> None: + """A real memories=[none yet] singleton must not collapse to an empty list. + + The write side marks a genuine memory whose exact text equals the + placeholder with a distinguishing suffix, and the reader decodes the mark + before applying the sole-placeholder drop, so only a bare placeholder line + written for an empty list reads back empty. + """ + record = MemoryTopicRecord( + topic="notes", + summary="s", + memories=["none yet"], + updated_at="2026-10-26T00:00:00+00:00", + ) + parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="notes") + assert parsed.memories == ["none yet"] + + +def test_memory_topic_record_marks_genuine_placeholder_on_write() -> None: + """The written form of a genuine placeholder-valued memory carries the + distinguishing suffix, so it cannot be confused with a written placeholder.""" + record = MemoryTopicRecord( + topic="notes", + summary="s", + memories=["none yet", "real fact"], + updated_at="2026-10-26T00:00:00+00:00", + ) + markdown = record.to_markdown() + assert "- none yet " in markdown + assert "- real fact" in markdown + + +def test_memory_topic_record_keeps_unmarked_placeholder_behavior_for_legacy_files() -> None: + """Files written before the marker keep their previous meaning: a sole bare + placeholder line still reads back as an empty memory list.""" + legacy = "\n".join([ + "# notes", + "", + "Updated: 2026-10-26T00:00:00+00:00", + "Sessions: -", + "", + "## Summary", + "s", + "", + "## Memories", + "- none yet", + ]) + parsed = MemoryTopicRecord.from_markdown(legacy, fallback_topic="notes") + assert parsed.memories == [] + + +def test_memory_topic_record_still_drops_the_placeholder_for_an_empty_list() -> None: + """An empty memory list writes the placeholder line and reads back empty.""" + record = MemoryTopicRecord(topic="empty", summary="s", memories=[], updated_at="2026-10-26T00:00:00+00:00") + + parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="empty") + + assert parsed.memories == [] + + def test_memory_file_store_uses_literal_folders_for_safe_identifiers(tmp_path) -> None: """Safe identifiers are readable on disk rather than opaque encoded segments.""" store = MemoryFileStore(tmp_path, owner_prefix="user_", owner_state_key="owner_id")