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 1/2] 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") From 4d0416da330793825e89148dc3728cda619babfc Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:06:38 +0530 Subject: [PATCH 2/2] Python: keep literal memory marker text intact across round trips Marker-shaped memories gain one more marker on write and lose exactly one on read, and files only decode markers when their header records them, so legacy literal marker text keeps its exact bytes. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> --- .../core/agent_framework/_harness/_memory.py | 59 +++++++++++++++---- .../core/tests/core/test_harness_memory.py | 49 +++++++++++++++ 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/python/packages/core/agent_framework/_harness/_memory.py b/python/packages/core/agent_framework/_harness/_memory.py index f10c8036a98..60238a5cf67 100644 --- a/python/packages/core/agent_framework/_harness/_memory.py +++ b/python/packages/core/agent_framework/_harness/_memory.py @@ -50,6 +50,16 @@ # 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 = " " +# Header line recording that the memories in this file carry genuine markers. +# Only files containing this line decode markers, so literal marker-shaped +# text written by earlier versions keeps its exact bytes. +_MEMORY_GENUINE_MARKER_LINE = "Encoding: genuine memory markers v1" +# A placeholder followed by one or more genuine markers: the written shape of +# a genuine placeholder-valued memory (one marker) and of literal marker-shaped +# text (every marker beyond the first survives the round trip). +_MEMORY_GENUINE_MARKED_SHAPE = re.compile( + re.escape(DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]) + "(?:" + re.escape(_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: @@ -129,6 +139,13 @@ def _unescape_markdown_line(line: str) -> str: return line +def _decode_genuine_memory(memory: str) -> str: + """Strip exactly one genuine marker from a marked memory line.""" + if _MEMORY_GENUINE_MARKED_SHAPE.fullmatch(memory): + return memory[: -len(_MEMORY_GENUINE_PLACEHOLDER_SUFFIX)] + return memory + + def _slugify_topic(topic: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", _normalize_topic(topic).lower()).strip("-") return slug or "memory-topic" @@ -451,14 +468,20 @@ 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(self._encode_memory_text(memory))}" for memory in self.memories] or [ + encoded_memories = [self._encode_memory_text(memory) for memory in self.memories] + memory_lines = [f"- {_escape_markdown_line(memory)}" for memory in encoded_memories] or [ f"- {DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]}" ] - return "\n".join([ + header_lines = [ f"# {self.topic}", "", f"Updated: {self.updated_at}", f"Sessions: {session_line}", + ] + if any(encoded != memory for encoded, memory in zip(encoded_memories, self.memories)): + header_lines.append(_MEMORY_GENUINE_MARKER_LINE) + return "\n".join([ + *header_lines, "", "## Summary", escaped_summary, @@ -469,13 +492,14 @@ def to_markdown(self) -> str: @staticmethod def _encode_memory_text(memory: str) -> str: - """Mark a genuine memory equal to the placeholder so a reader keeps it. + """Mark placeholder-shaped memories so the reader can tell them apart. 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. + list. A real memory whose text equals the placeholder, or already looks + like a marked one, gets one more marker, so the reader can strip a + single marker and still recover the exact original text. """ - if memory == DEFAULT_MEMORY_NO_TOPICS_TEXT[2:]: + if memory == DEFAULT_MEMORY_NO_TOPICS_TEXT[2:] or _MEMORY_GENUINE_MARKED_SHAPE.fullmatch(memory): return memory + _MEMORY_GENUINE_PLACEHOLDER_SUFFIX return memory @@ -501,6 +525,7 @@ def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> M summary_lines: list[str] = [] memories: list[str] = [] current_section: str | None = None + genuine_markers = False for raw_line in markdown.splitlines(): line = raw_line.rstrip() @@ -509,6 +534,9 @@ def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> M topic = stripped[2:].strip() current_section = None continue + if current_section is None and stripped == _MEMORY_GENUINE_MARKER_LINE: + genuine_markers = True + continue if current_section is None and stripped.startswith("Updated: "): updated_at = stripped.removeprefix("Updated: ").strip() continue @@ -535,14 +563,19 @@ def from_markdown(cls, markdown: str, *, fallback_topic: str | None = None) -> M 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. + # Markers decode only in files whose header records them, so literal + # marker-shaped text written by earlier versions keeps its exact bytes. + # Each marked line loses a single marker: a genuine placeholder-valued + # memory carries one, and literal marker-shaped text keeps every marker + # beyond the first. The sole-placeholder drop then applies only to a + # bare placeholder, the one shape the write side emits for an empty + # memory list. + if genuine_markers: + was_marked = [_MEMORY_GENUINE_MARKED_SHAPE.fullmatch(memory) is not None for memory in memories] + memories = [_decode_genuine_memory(memory) for memory in memories] + else: + was_marked = [False] * len(memories) 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 = [] diff --git a/python/packages/core/tests/core/test_harness_memory.py b/python/packages/core/tests/core/test_harness_memory.py index 42f571a06ff..d20477bd0c0 100644 --- a/python/packages/core/tests/core/test_harness_memory.py +++ b/python/packages/core/tests/core/test_harness_memory.py @@ -1121,6 +1121,55 @@ def test_memory_topic_record_still_drops_the_placeholder_for_an_empty_list() -> assert parsed.memories == [] +def test_memory_topic_record_round_trips_literal_marker_shaped_text() -> None: + """A memory whose literal text equals the genuine marker must survive a + round trip instead of collapsing onto the placeholder value.""" + 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_keeps_the_placeholder_value_and_the_literal_marker_apart() -> None: + """A genuine placeholder-valued memory and the literal marker text must + round trip as two distinct memories rather than merging into one.""" + record = MemoryTopicRecord( + topic="notes", + summary="s", + memories=["none yet", "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", "none yet "] + + +def test_memory_topic_record_preserves_literal_marker_text_in_legacy_files() -> None: + """Files written before the marker keep literal marker-shaped text verbatim, + exactly as the base reader reads them.""" + 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 == ["none yet "] + + 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")