Skip to content

[BUG] edit_note joins content with a single newline — appended --- silently turns the preceding paragraph into a setext H2 #1585

Description

@Hetox

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:

// 1. create a note whose last line is a paragraph
write_note(project="main", title="Setext Repro", folder="probe", content=
  "# Setext Repro\n\n## Section One\nNote: live Kraken prices checked 2026-09-20.")

// 2. append a separator + a new section
edit_note(project="main", identifier="main/probe/setext-repro", operation="append",
  content="---\n\n## Section Two\nsecond body")

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions