Skip to content

fix(core): let edit_note metadata set a note's type - #1417

Open
sammywachtel wants to merge 1 commit into
basicmachines-co:mainfrom
sammywachtel:fix/allow-metadata-to-set-note-type
Open

fix(core): let edit_note metadata set a note's type#1417
sammywachtel wants to merge 1 commit into
basicmachines-co:mainfrom
sammywachtel:fix/allow-metadata-to-set-note-type

Conversation

@sammywachtel

Copy link
Copy Markdown
Contributor

What this changes

_METADATA_IDENTITY_FIELDS in src/basic_memory/services/note_preparation.py drops
title, type and permalink from an edit_note metadata merge. This removes type
from that set and leaves the other two alone.

Why

The comment on the constant explains the exclusion:

# title/type/permalink already have dedicated resolution paths in
# prepare_edit_entity_content (H1 title reconciliation, permalink resolver). Letting a
# metadata merge touch them would race with those paths and could be silently reverted.

That is exactly right for two of the three, and I want to be clear that I am not
proposing to weaken it:

  • title is read out of frontmatter and then handed to
    reconcile_prepared_edit_title_from_h1(...), which can replace it with the note's H1.
    A merged title really can be overwritten.
  • permalink is read and then passed through resolve_permalink(...), which
    suffixes on collision. A merged permalink really can come back as something else.

type does not share that property. Its entire handling in
prepare_edit_entity_content is:

if "type" in content_frontmatter:
    note_type = _coerce_to_string(content_frontmatter["type"])

A plain read, with nothing downstream that can disagree. The note_type that reaches
the entity row comes from _build_entity_fields, which re-parses the merged markdown
(entity_markdown.frontmatter.type) — so writing type into the frontmatter is how
you feed that path, not a race against it. The one other place the local note_type
travels is _build_frontmatter_markdown(...) on the permalink branch, where
resolve_permalink only ever looks at frontmatter.permalink and ignores the type
entirely.

So the guard is correct for title and permalink and over-broad by one member.

The behavior before this change

edit_note(..., metadata={"type": "decision"}) returned a successful edit and left the
note's type as it was. Nothing in the response distinguished that from a type change
that worked, so the only way to notice is to read the note back afterwards and compare.
Setting the type through metadata is the natural thing to reach for, since every other
frontmatter field works that way.

Tests

  • test_merge_metadata_into_markdown_writes_type — the merge writes type while still
    dropping title and permalink from the same payload.
  • test_prepare_edit_entity_content_metadata_sets_note_type — the prepared write carries
    the new type in both the markdown frontmatter and entity_fields.note_type, with title
    and permalink unmoved.
  • test_edit_note_metadata_sets_note_type (integration) — end to end through the MCP
    tool: the type lands in the file, and search_notes(note_types=["decision"]) finds the
    note, so the index agrees with the file.
  • The existing ..._metadata_ignores_identity_fields tests, unit and integration, keep
    their title and permalink hijack attempts and keep asserting those are refused. I
    dropped only the type key from their payloads and added an assertion that an
    unsupplied type is still left alone.

All three new tests fail against the current main behavior and pass with the change.

Docs updated in the same commit: the edit_note tool docstring, the EditEntityRequest
comment in schemas/request.py, and the metadata bullet in man3/edit-note(3).md.

How I tested

  • just test-unit-sqlite — 5735 passed, 43 skipped.
  • just test-int-sqlite — 501 passed, 16 skipped.
  • Postgres: tests/services/test_entity_service_prepare.py and
    test-int/mcp/test_edit_note_integration.py with BASIC_MEMORY_TEST_POSTGRES=1, all
    passing. This change touches only frontmatter merging, so I ran the affected suites
    against Postgres rather than the whole matrix.
  • just fix, just format, just typecheck — clean, apart from four pre-existing
    unresolved-import diagnostics for pymilvus, which lives in the optional milvus
    extra that a plain uv sync does not install. They are present on an unmodified
    checkout too.

Three failures in tests/cli/test_cloud_promo.py on my machine are environmental: those
tests construct a real ConfigManager() with no isolation, so they read
~/.basic-memory/config.json, and mine already records the promo as shown. They fail
identically on a clean checkout of main here. Nothing to do with this change, and I
left them alone.

_METADATA_IDENTITY_FIELDS drops title, type and permalink from a metadata
merge, on the grounds that all three have dedicated resolution paths in
prepare_edit_entity_content that a merge would race.

That holds for two of the three. A merged title can be overwritten by
reconcile_prepared_edit_title_from_h1, and a merged permalink can come back
suffixed from resolve_permalink, so in both cases the caller's value can
vanish without a word. type has no such second opinion. Its whole "resolution
path" is one line reading the frontmatter back out, and the note_type that
reaches the database comes from re-parsing the merged markdown - so writing
type into frontmatter feeds that path instead of fighting it.

Net effect before this change: edit_note(..., metadata={"type": "decision"})
returned success and changed nothing, with no way for the caller to tell.

Drop type from the exclusion set and leave title and permalink where they
are. Tests cover both halves: type now lands in the file's frontmatter and in
the indexed entity, and title and permalink are still refused.

Signed-off-by: samwachtel <sammy@wachtel.us>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e301d130ad

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# hand back a value the caller did not ask for, so a metadata merge that set them would
# be silently reverted. `type` has no such second opinion: prepare_edit_entity_content
# just reads it back out of the frontmatter, so writing it there is how you set it.
_METADATA_IDENTITY_FIELDS = frozenset({"title", "permalink"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route type metadata through the auto-create path

When append or prepend targets a missing note, edit_note bypasses this merge and constructs an Entity with entity_metadata=metadata but the default note_type="note" (edit_note.py:718-724). schema_to_markdown() then explicitly removes type from entity metadata and writes schema.note_type (markdown/utils.py:108-117), so metadata={"type": "decision"} still silently creates a note even though the same request changes the type when the target already exists. Pass the metadata type as the auto-created entity's note_type, or apply the standard edit after creation.

Useful? React with 👍 / 👎.

Comment on lines +703 to +704
Identity fields (title/permalink) are dropped from the merge; every other key,
``type`` included, overwrites the existing frontmatter value or is added new. The

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the newly writable type field

When an existing note receives metadata={"type": ""} or a whitespace-only value, this newly allowed key is written without passing through the NoteType boundary validation, so the accepted Markdown and Entity.note_type contain an invalid empty classification. This is not merely cosmetic: WikiSourceNote.__post_init__ rejects blank note types (wiki_projector.py:97-98), allowing one edit request to break projection snapshot construction. Validate and normalize this special metadata key before merging it, just as the regular note_type request field is validated.

AGENTS.md reference: AGENTS.md:L128-L129

Useful? React with 👍 / 👎.

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

Labels

None yet

1 participant