From dc0ca71c18100690b219836bc9b9e459a5e26945 Mon Sep 17 00:00:00 2001 From: phernandez Date: Fri, 27 Feb 2026 22:52:16 -0600 Subject: [PATCH] fix: avoid Post(**metadata) crash when frontmatter contains 'content' or 'handler' keys frontmatter.Post.__init__ takes `content` and `handler` as positional parameters. When user YAML frontmatter contains these as field names, unpacking metadata via **kwargs causes "got multiple values for argument 'content'". Replace frontmatter.loads() with frontmatter.parse() + Post() + update() in entity_parser, and replace the **metadata unpacking in entity_service.update_entity() with the same safe pattern. Fixes basic-memory-cloud#375 Co-Authored-By: Claude Opus 4.6 Signed-off-by: phernandez --- src/basic_memory/markdown/entity_parser.py | 10 ++++-- src/basic_memory/services/entity_service.py | 7 ++-- .../markdown/test_date_frontmatter_parsing.py | 32 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/basic_memory/markdown/entity_parser.py b/src/basic_memory/markdown/entity_parser.py index 8d11d89b..0259df92 100644 --- a/src/basic_memory/markdown/entity_parser.py +++ b/src/basic_memory/markdown/entity_parser.py @@ -249,9 +249,15 @@ class EntityParser: content = strip_bom(content) - # Parse frontmatter with proper error handling for malformed YAML + # Parse frontmatter with proper error handling for malformed YAML. + # We use frontmatter.parse() instead of frontmatter.loads() because + # loads() does Post(content, handler, **metadata), which crashes when + # the YAML contains reserved keys like 'content' or 'handler'. + # See basic-memory-cloud#375. try: - post = frontmatter.loads(content) + fm_metadata, fm_content = frontmatter.parse(content) + post = frontmatter.Post(fm_content) + post.metadata.update(fm_metadata) except yaml.YAMLError as e: logger.warning( f"Failed to parse YAML frontmatter in {file_path}: {e}. " diff --git a/src/basic_memory/services/entity_service.py b/src/basic_memory/services/entity_service.py index 1b9cbf40..4847367f 100644 --- a/src/basic_memory/services/entity_service.py +++ b/src/basic_memory/services/entity_service.py @@ -361,8 +361,11 @@ class EntityService(BaseService[EntityModel]): # in the existing file. Setting it unconditionally preserves the correct value. existing_markdown.frontmatter.metadata["permalink"] = new_permalink - # Create a new post with merged metadata - merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata) + # Create a new post with merged metadata. + # Avoid **metadata unpacking — user frontmatter may contain reserved keys + # like 'content' or 'handler' that conflict with Post.__init__ (cloud#375). + merged_post = frontmatter.Post(post.content) + merged_post.metadata.update(existing_markdown.frontmatter.metadata) # write file final_content = dump_frontmatter(merged_post) diff --git a/tests/markdown/test_date_frontmatter_parsing.py b/tests/markdown/test_date_frontmatter_parsing.py index 60741af8..be6664a3 100644 --- a/tests/markdown/test_date_frontmatter_parsing.py +++ b/tests/markdown/test_date_frontmatter_parsing.py @@ -311,3 +311,35 @@ async def test_parse_file_with_datetime_objects(tmp_path): assert "2025-10-24" in updated_at and "00:00:00" in updated_at, ( f"Datetime at midnight should be normalized to ISO format, got: {updated_at}" ) + + +@pytest.mark.asyncio +async def test_parse_file_with_reserved_frontmatter_field_content(tmp_path): + """Test that a 'content' field in frontmatter doesn't break parsing. + + Reproduces basic-memory-cloud#375 where frontmatter containing a field named + 'content' causes frontmatter.Post.__init__() to receive multiple values for + the 'content' positional argument. + """ + test_file = tmp_path / "topic-note-template.md" + test_file.write_text(dedent("""\ + --- + title: Topic Note Template + content: Template for topic notes + handler: some-handler-value + --- + + # Template Body + + Actual body content here. + """)) + + parser = EntityParser(tmp_path) + entity_markdown = await parser.parse_file(test_file) + + assert entity_markdown.frontmatter.title == "Topic Note Template" + # The 'content' and 'handler' fields should be preserved in metadata + assert entity_markdown.frontmatter.metadata.get("content") == "Template for topic notes" + assert entity_markdown.frontmatter.metadata.get("handler") == "some-handler-value" + # The actual body content should be parsed correctly + assert "Template Body" in entity_markdown.content