Bug Description
edit_note joins appended/replacement content to the surrounding text with a single \n rather than a blank line. Because Markdown block constructs need a blank line to be separated, this silently corrupts document structure. The worst case is CommonMark's setext heading rule: appending a --- thematic break after a paragraph turns that paragraph into an <h2>.
The corruption is invisible in the tool response (which reports success) and only shows up later — in rendered output, and in Basic Memory's own section parsing, since the phantom heading becomes a real section boundary.
Steps To Reproduce
Against a basic-memory mcp --transport streamable-http server, v0.23.2:
Resulting file on disk:
## Section One
Note: live Kraken prices checked 2026-09-20.
---
## Section Two
second body
Parsed with the same markdown-it-py Basic Memory bundles:
heading_open h2 markup='##' 'Section One'
heading_open h2 markup='-' 'Note: live Kraken prices checked 2026-09-20.' <-- setext H2
heading_open h2 markup='##' 'Section Two'
The --- never becomes a thematic break, and the note now has a heading nobody wrote.
Expected Behavior
append should separate appended block content from existing content with a blank line, so the --- parses as <hr> and the paragraph stays a paragraph.
Actual Behavior
The paragraph is promoted to an <h2> whose text is the paragraph body. edit_note reports operation: Added 4 lines to end of note with no warning.
Same class, other operations
replace_section also drops the blank line before the following heading:
## Observations
- [test] replaced observation #probe
Note: live prices checked 2026-09-21.
## Next Section <-- blank line before this heading removed
Here ATX syntax saves it, but any ---, setext underline, indented block or list that lands adjacent is subject to the same silent reinterpretation. In practice this bites agents that maintain long notes: a section rewrite eats the --- separators, the agent appends them back, and each re-added --- creates a new phantom <h2>.
Files are also written with no trailing newline, which contributes (\ No newline at end of file on every commit of a Basic Memory-managed note).
Environment
- OS: Ubuntu 24.04
- Python: 3.13 (container)
- Basic Memory: 0.23.2 (
ghcr.io/basicmachines-co/basic-memory:latest)
- Installation: Docker, streamable-http transport
- Client: agent over MCP (TypingMind / Open WebUI-style host), also reproduced with a raw JSON-RPC client
Possible Solution
src/basic_memory/services/note_preparation.py, apply_edit_operation:
if operation == "append":
return (
current_content
+ ("\n" if current_content and not current_content.endswith("\n") else "")
+ content
)
This guarantees at most one newline. Appending block-level Markdown needs two. Something like:
if operation == "append":
if not current_content:
return content
sep = "\n" * max(0, 2 - (len(current_content) - len(current_content.rstrip("\n"))))
return current_content + sep + content
with the equivalent blank-line guarantee applied in replace_section_content and insert_relative_to_section (both around the boundary they splice at), plus a trailing newline when writing the file.
A cheap defence-in-depth check would be to re-parse the edited content and reject/repair an edit that creates a heading the caller did not write — that is exactly the signature of this bug.
Happy to open a PR if the approach looks right.
Bug Description
edit_notejoins appended/replacement content to the surrounding text with a single\nrather than a blank line. Because Markdown block constructs need a blank line to be separated, this silently corrupts document structure. The worst case is CommonMark's setext heading rule: appending a---thematic break after a paragraph turns that paragraph into an<h2>.The corruption is invisible in the tool response (which reports success) and only shows up later — in rendered output, and in Basic Memory's own section parsing, since the phantom heading becomes a real section boundary.
Steps To Reproduce
Against a
basic-memory mcp --transport streamable-httpserver, v0.23.2:Resulting file on disk:
Parsed with the same
markdown-it-pyBasic Memory bundles:The
---never becomes a thematic break, and the note now has a heading nobody wrote.Expected Behavior
appendshould separate appended block content from existing content with a blank line, so the---parses as<hr>and the paragraph stays a paragraph.Actual Behavior
The paragraph is promoted to an
<h2>whose text is the paragraph body.edit_notereportsoperation: Added 4 lines to end of notewith no warning.Same class, other operations
replace_sectionalso drops the blank line before the following heading:Here ATX syntax saves it, but any
---, setext underline, indented block or list that lands adjacent is subject to the same silent reinterpretation. In practice this bites agents that maintain long notes: a section rewrite eats the---separators, the agent appends them back, and each re-added---creates a new phantom<h2>.Files are also written with no trailing newline, which contributes (
\ No newline at end of fileon every commit of a Basic Memory-managed note).Environment
ghcr.io/basicmachines-co/basic-memory:latest)Possible Solution
src/basic_memory/services/note_preparation.py,apply_edit_operation:This guarantees at most one newline. Appending block-level Markdown needs two. Something like:
with the equivalent blank-line guarantee applied in
replace_section_contentandinsert_relative_to_section(both around the boundary they splice at), plus a trailing newline when writing the file.A cheap defence-in-depth check would be to re-parse the edited content and reject/repair an edit that creates a heading the caller did not write — that is exactly the signature of this bug.
Happy to open a PR if the approach looks right.