diff --git a/python/packages/core/agent_framework/_harness/_memory.py b/python/packages/core/agent_framework/_harness/_memory.py index c3f02f883b..731af85092 100644 --- a/python/packages/core/agent_framework/_harness/_memory.py +++ b/python/packages/core/agent_framework/_harness/_memory.py @@ -108,19 +108,44 @@ def _normalize_memory_text(memory: str) -> str: return normalized +_LEADING_BACKSLASHES_BEFORE_HASH: re.Pattern[str] = re.compile(r"^(\\+)#") + + def _escape_markdown_line(line: str) -> str: - """Escape a markdown line so it cannot be re-interpreted as a heading on a later read.""" - if line.lstrip().startswith(("#", "\\#")): - return f"\\{line}" + """Escape a markdown line so it cannot be re-interpreted as a heading on a later read. + + A line starting with ``#`` gets one backslash. A line that already starts + with backslashes before ``#`` gets one more than it had, so a reader can + always recover the exact original run: prepending a single backslash to an + escaped hash produced a two backslash prefix the old reader never stripped, + corrupting the round trip. + """ + leading_whitespace_length = len(line) - len(line.lstrip()) + body = line[leading_whitespace_length:] + match = _LEADING_BACKSLASHES_BEFORE_HASH.match(body) + if match: + run_length = len(match.group(1)) + escaped_run = "\\" * (2 * run_length + 1) + return line[:leading_whitespace_length] + escaped_run + body[run_length:] + if body.startswith("#"): + return "\\" + line return line def _unescape_markdown_line(line: str) -> str: - """Reverse :func:`_escape_markdown_line` when reading persisted memory content.""" + """Reverse :func:`_escape_markdown_line` when reading persisted memory content. + + An odd backslash run before ``#`` is one this module wrote (``n`` originals + become ``n + 1``), so the reader keeps ``(run - 1) // 2`` originals. An even + run was not produced here and is left untouched. + """ leading_whitespace_length = len(line) - len(line.lstrip()) body = line[leading_whitespace_length:] - if body.startswith("\\#"): - return f"{line[:leading_whitespace_length]}{body[1:]}" + match = _LEADING_BACKSLASHES_BEFORE_HASH.match(body) + if match and len(match.group(1)) % 2 == 1: + original_length = (len(match.group(1)) - 1) // 2 + original_run = "\\" * original_length + return line[:leading_whitespace_length] + original_run + body[len(match.group(1)) :] return line diff --git a/python/packages/core/tests/core/test_harness_memory.py b/python/packages/core/tests/core/test_harness_memory.py index 464255d0b4..df33cb2d5f 100644 --- a/python/packages/core/tests/core/test_harness_memory.py +++ b/python/packages/core/tests/core/test_harness_memory.py @@ -1046,6 +1046,24 @@ def test_memory_file_store_encodes_non_ascii_owner_ids(tmp_path) -> None: assert roots[0] != roots[1] +def test_memory_topic_record_round_trips_backslash_hash_lines() -> None: + """Lines starting with backslash hash must survive a round trip unchanged. + + Escaping such a line used to add a second backslash the reader never + stripped, so every reload corrupted the memory a little more. + """ + record = MemoryTopicRecord( + topic="notes", + summary="\\# summary line", + memories=["\\# literal fact", "# heading shaped memory", "\\\\# two backslashes"], + updated_at="2026-10-26T00:00:00+00:00", + ) + + parsed = MemoryTopicRecord.from_markdown(record.to_markdown(), fallback_topic="notes") + + assert parsed.memories == ["\\# literal fact", "# heading shaped memory", "\\\\# two backslashes"] + + 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")