Repository navigation
[BUG] edit_note joins content with a single newline — appended --- silently turns the preceding paragraph into a setext H2 #1585
Description
Activity
The same single-newline join exists in the
prependpath, and it fails on a different set of inputs thanappenddoes.Baseline:
main@3bf2d523.apply_edit_operation,src/basic_memory/services/note_preparation.py:655-660— append branch._prepend_after_frontmatter, same file,635-643— both branches (line 639 for notes with frontmatter, line 643 without) use the identical("\n" if content and not content.endswith("\n") else "")guard.
I copied both join functions verbatim into a standalone script (no basic-memory install needed) and rendered the output with
markdown-it-py2.0.1 in CommonMark mode. "Broken" below means the rendered HTML differs from the same edit performed with a blank-line join.Append — base
"Some paragraph text", no trailing newline:appended joined renders as broken ---'…text\n---'<h2>Some paragraph text</h2>yes ==='…text\n==='<h1>Some paragraph text</h1>yes normal text'…text\nnormal text'<p>Some paragraph text normal text</p>yes ## Heading'…text\n## Heading'<p>…</p><h2>Heading</h2>no - list item'…text\n- list item'<p>…</p><ul><li>…</li></ul>no > quote'…text\n> quote'<p>…</p><blockquote>…</blockquote>no ATX headings, bullets and blockquotes survive because CommonMark lets those interrupt a paragraph. Setext underlines cannot be interrupted — they consume the line above. Control group: joining with
\n\ninstead makes all six render as two separate blocks, including---→<hr />.Prepend — this is the part I think is missing from the report. Prepending
"Some paragraph text"to a body that starts with certain constructs:body starts with result broken ---(thematic break)<h2>Some paragraph text</h2>— break is eatenyes Heading+---(setext)<h2>Some paragraph text Heading</h2>— mergedyes 2. second item<p>Some paragraph text 2. second item</p>— list destroyedyes # Heading,> quote,- itemunchanged no The
2.case is lazy continuation, not setext: only1.may interrupt a paragraph, so any note whose body opens with a renumbered ordered list loses the list entirely. I did not find a test covering newline joining for either operation.On the fix: an unconditional
\n\nis one line and closes every case above, but it does change output for the plainnormal textappend, where merging into the preceding paragraph may be what some callers rely on. Context-aware joining keeps that behavior but means parsing the boundary on both sides. Which of those you want probably depends on whetherappendis documented as "add a block" or "add text" — that is your call, not mine.Reacted by Mikhail Mikhailov- added a commit that references this issue
on Oct 9, 2026 - added a commit that references this issue
on Oct 10, 2026


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.