Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/basic_memory/services/file_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,15 +481,21 @@ async def update_frontmatter_with_result(
if file_utils.has_frontmatter(content):
try:
current_fm = file_utils.parse_frontmatter(content)
content = file_utils.remove_frontmatter(content)
except (ParseError, yaml.YAMLError) as e: # pragma: no cover
# Log warning and treat as plain markdown without frontmatter
logger.warning( # pragma: no cover
except (ParseError, yaml.YAMLError) as e:
# Malformed YAML in the existing block: fall through and replace
# it below rather than merging into it.
logger.warning(
f"Failed to parse YAML frontmatter in {full_path}: {e}. "
"Treating file as plain markdown without frontmatter."
"Replacing malformed frontmatter instead of merging into it."
)
# Keep full content, treat as having no frontmatter
current_fm = {} # pragma: no cover
current_fm = {}
# Trigger: an existing frontmatter block was detected, whether or not
# it parsed as valid YAML.
# Why: the writer must never emit a second `---` block ahead of the
# existing one — that silently shadows every field in it (issue #1171).
# Outcome: always strip the detected block from the body so the new
# block replaces it in place instead of stacking on top of it.
content = file_utils.remove_frontmatter(content)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve malformed metadata instead of deleting it

When a fenced block contains any YAML parse error, current_fm is reset to {} and this line removes the entire original block; the replacement therefore contains only updates. Callers such as batch permalink normalization and note moves commonly update only permalink, so merely syncing or moving such a note silently erases its title, tags, timestamps, description, and other user metadata. Fail the rewrite or preserve/repair the malformed block rather than writing a partial replacement.

AGENTS.md reference: AGENTS.md:L132-L133

Useful? React with 👍 / 👎.


# Update frontmatter
new_fm = {**current_fm, **updates}
Expand Down
24 changes: 24 additions & 0 deletions tests/services/test_file_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,30 @@ async def fake_write_file_atomic(path: Path, content: str) -> None:
setattr(result, "checksum", "changed")


@pytest.mark.asyncio
async def test_update_frontmatter_replaces_malformed_block_instead_of_stacking(
tmp_path: Path, file_service: FileService
):
"""A frontmatter block that fails to parse as YAML must be replaced, never
left in place with a second block prepended ahead of it (issue #1171)."""
test_path = tmp_path / "note.md"
# Colon inside an unquoted scalar makes this block detectable via fences
# (has_frontmatter -> True) but invalid YAML (parse_frontmatter -> raises).
test_path.write_text(
"---\ndescription: bad: value: shape\n---\n\n# Note\nBody\n",
encoding="utf-8",
)

result = await file_service.update_frontmatter_with_result(
test_path,
{"permalink": "test/note"},
)

assert result.content.count("---\n") == 2
assert "permalink: test/note" in result.content
assert "# Note\nBody" in result.content


@pytest.mark.asyncio
async def test_read_file_content(tmp_path: Path, file_service: FileService):
"""Test read_file_content returns just the content without checksum."""
Expand Down
Loading