Skip to content

fix(mcp): resolve memory:// in move_note and stop false cross-project rejections#914

Merged
phernandez merged 2 commits into
mainfrom
fix/move-note-edge-cases
Jun 8, 2026
Merged

fix(mcp): resolve memory:// in move_note and stop false cross-project rejections#914
phernandez merged 2 commits into
mainfrom
fix/move-note-edge-cases

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

Fixes two confirmed move_note bugs found by the integration bug hunt, both in src/basic_memory/mcp/tools/move_note.py.

Bugs fixed

  • feat: return semantic info in markdown after write_note #11move_note did not resolve memory:// URL identifiers. Its docstring advertises memory:// URLs and read_note/edit_note/delete_note all accept them, but move_note passed the raw memory://... string straight to resolve_entity, which 404s. It now calls resolve_project_and_path() first (the same pattern the sibling tools use) to strip the scheme prefix and normalize the path before resolving the entity.
  • feat: configure logfire telemetry #12 (regression from fix(mcp): stop move_note reporting false success across boundaries #904) — false cross-project rejections on nested projects folders. Detection 2 (if len(path_parts) >= 3 and path_parts[1] == "projects") wrongly rejected legitimate same-project nested moves like notes/projects/2025/file.md, work/projects/q1/report.md, etc. The structural heuristic is fundamentally ambiguous (indistinguishable from a normal nested folder), so it has been removed entirely. Cross-project detection now relies only on Detection 1 (leading segment matches a known project name) plus the existing MOVE_OUTCOME_MISMATCH post-move backstop. A bare other-workspace/projects/x that matches no known project name is now correctly allowed as a normal nested folder.

Testing

Commands and results (all green):

  • uv run pytest test-int/bughunt_fixes/test_move_note_edge_cases.py test-int/mcp/test_move_note_integration.py -q --no-cov → 25 passed
  • uv run pytest tests/mcp/test_tool_move_note.py tests/mcp/test_tool_contracts.py tests/mcp/test_tool_telemetry.py -q --no-cov → 55 passed
  • uv run pytest tests/mcp/test_tool_move_note.py test-int/mcp/test_move_note_integration.py test-int/bughunt_fixes/test_move_note_edge_cases.py -q --no-cov → 74 passed
  • BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest test-int/bughunt_fixes/test_move_note_edge_cases.py test-int/mcp/test_move_note_integration.py -q --no-cov → 25 passed
  • uv run ruff check . --fix / uv run ruff format . (reverted incidental edits to unrelated files) — clean on touched files
  • uv run ty check src tests test-int → All checks passed

New regression tests live in test-int/bughunt_fixes/test_move_note_edge_cases.py (from the integration bug hunt). The now-incorrect assertions in test-int/mcp/test_move_note_integration.py (the two workspace-shaped-path rejection tests) and the unit test test_detect_cross_project_only_flags_workspace_shape in tests/mcp/test_tool_move_note.py were updated to match the corrected behavior — a bare other-workspace/projects/x is now an allowed nested move.

Risk

Low. No tool signature changes. The behavior change is narrow: paths whose leading segment is not a known project name but happen to contain a projects segment are now allowed as nested moves (previously falsely rejected). True cross-project intent is still caught by the known-project-name check and the post-move outcome backstop.

🤖 Generated with Claude Code

… rejections

Two confirmed bugs from the integration bug hunt in move_note:

- #11: move_note did not resolve memory:// URL identifiers even though its
  docstring advertises them and read_note/edit_note/delete_note all accept
  them. It passed the raw 'memory://...' string to resolve_entity, which 404s.
  Now it calls resolve_project_and_path() first (same as the sibling tools) to
  strip the scheme prefix and normalize the path before resolving the entity.

- #12 (regression from #904): the structural Detection 2 heuristic
  ('<seg>/projects/<seg>/file.md') wrongly rejected legitimate same-project
  nested moves like 'notes/projects/2025/file.md' as cross-project moves. The
  heuristic is fundamentally ambiguous, so it has been removed entirely.
  Cross-project detection now relies only on Detection 1 (leading segment ==
  a known project name) plus the existing MOVE_OUTCOME_MISMATCH backstop. A
  bare 'other-workspace/projects/x' that matches no known project name is now
  allowed as a normal nested folder.

Tests came from the integration bug hunt: added regression tests under
test-int/bughunt_fixes/test_move_note_edge_cases.py and updated the now-incorrect
assertions in test-int/mcp/test_move_note_integration.py and the unit test in
tests/mcp/test_tool_move_note.py to match the corrected behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@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: 4017d9aed3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/mcp/tools/move_note.py Outdated
Comment on lines +663 to +665
_, resolved_identifier, _ = await resolve_project_and_path(
client, identifier, active_project.name, context
)

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 Honor memory:// project prefixes before moving

When identifier is a project-prefixed URL like memory://other-project/docs/note and the caller omits project, this new resolution can return other-project, but the returned ProjectItem is discarded after get_project_client() has already selected the default/current project and after KnowledgeClient was bound to active_project.external_id. In that scenario resolve_entity() runs against the wrong project, so the new memory:// support still fails or targets the wrong route for the same URLs that read/edit/delete pre-detect before client selection.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @phernandez's task in 1m 46s —— View job


PR Review: fix(mcp): resolve memory:// in move_note and stop false cross-project rejections

  • Read changed files and gather context
  • Analyze bug fixes and implementation
  • Review test coverage
  • Check against team checklist
  • Post comprehensive review

Summary

This is a clean, targeted fix for two confirmed move_note bugs. The implementation is sound and consistent with established sibling-tool patterns. No architectural concerns; see one note below on scope.


Bug Fix Analysis

Bug #11memory:// URL resolution (move_note.py:663-665)

The fix is correct and matches the exact pattern used by read_note and edit_note:

# move_note (new)
_, resolved_identifier, _ = await resolve_project_and_path(
    client, identifier, active_project.name, context
)

# read_note (existing, for comparison)
_, entity_path, _ = await resolve_project_and_path(
    client, identifier, active_project.name, context
)

The discarded first tuple element (the returned ProjectItem) is intentional and consistent with sibling tools — the client and active_project are already bound by the outer get_project_client(). This is not a regression.

Regarding the Codex P2 comment ("Honor memory:// project prefixes before moving"): The reviewer's concern about a cross-project URL like memory://other-project/docs/note is technically accurate but is a pre-existing architectural limitation that equally applies to read_note, edit_note, and delete_note. None of those tools re-route based on the project prefix in the URL when a project has already been bound via get_project_client(). This is out of scope for this PR and does not represent a regression.

Bug #12 — Removal of the projects-segment heuristic (move_note.py:59-68)

The removal is correct. The comment block explaining the decision is excellent — it uses the CLAUDE.md literate-programming style (trigger / why / outcome) and documents an absence, which is exactly the right thing to do when removing non-obvious logic. No concerns here.

The remaining backstops are:

  1. Detection 1: leading segment matches a known project name → rejected
  2. MOVE_OUTCOME_MISMATCH: post-move path validation (existing)

These are sufficient. The false-rejection rate for legitimate nested projects/ folders drops to zero without meaningfully weakening cross-project detection.


Team Checklist

Item Status
Follows Basic Memory coding conventions (CLAUDE.md)
Python 3.12+ type annotations and async patterns
SQLAlchemy / FastAPI / Typer conventions ✅ (no ORM/router changes)
100-char line length
No commented-out code blocks ✅ (NOTE block is a decision-point comment, not dead code)
Unit tests for new functions tests/mcp/test_tool_move_note.py updated
Integration tests for new behaviour test-int/bughunt_fixes/test_move_note_edge_cases.py (5 new tests)
Edge-case coverage ✅ control tests for delete_note/edit_note memory:// acceptance included
100% coverage maintained ✅ (as confirmed by PR author's test runs)
No schema changes → no Alembic migration needed
Local-first architecture preserved
No hardcoded secrets or credentials
Input validation / path traversal protection unchanged
Windows path compatibility (Path / PureWindowsPath) ✅ (imports at top of file)
No emojis/unicode in log or console output
No tool signature changes
Semantic PR title fix(mcp): ...
Commits signed ✅ (-s)

Minor Observations (non-blocking)

  1. test_move_note_accepts_memory_url uses a same-project URL only (memory://src/memory-url-move). There is no test for a cross-project memory:// URL like memory://other-project/... — but since cross-project moves are explicitly unsupported and the sibling tools have the same gap, this is acceptable. A follow-up tracking issue might be worthwhile for completeness.

  2. test_detect_cross_project_only_flags_known_project_name (tests/mcp/test_tool_move_note.py:54) now registers other-project in the mock project list and explicitly asserts it is rejected. This is a net improvement over the previous test which only checked the structural heuristic.

  3. The NOTE comment block (move_note.py:59-68) is slightly long for a comment explaining removed code, but it captures exactly why the heuristic was removed and what the fallback is. This is defensible under the CLAUDE.md literate-programming guidelines.


Verdict

Approve. Both bugs are properly fixed, tests are comprehensive, and the implementation is consistent with sibling-tool patterns. Low regression risk. Ready to merge.

Codex review: the memory:// resolution captured the resolved path but discarded the resolved project, so a 'memory://other-project/...' source would be looked up against the active project's client (misroute). move_note is single-project; reject when the source URL resolves to a different project than the active one, consistent with the cross-project destination guard. Same-project memory:// URLs are unaffected (verified by the existing tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@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: f719ad7e39

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# the other project, looked up in the active one); move_note cannot move across
# projects.
# Outcome: reject up front with the cross-project guidance instead of misrouting.
if source_project.external_id != active_project.external_id:

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 project-prefixed memory URLs before rejecting

When project/project_id is omitted and the source is a project-prefixed URL like memory://other-project/src/note, get_project_client() has already selected the default/current project before this check runs, so this branch rejects a normal same-project move within other-project unless the caller redundantly supplies project="other-project". The fresh evidence is that the new code now explicitly returns CROSS_PROJECT_MOVE_NOT_SUPPORTED for the resolved source project instead of using that project to select the client first, unlike read_note/edit_note/delete_note which pre-detect the memory URL prefix before routing.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @phernandez's task in 2m 38s —— View job


PR Review: fix(mcp): resolve memory:// in move_note and stop false cross-project rejections

  • Read changed files and gather context
  • Analyze bug fixes and implementation
  • Review test coverage
  • Check against team checklist
  • Post comprehensive review

Summary

Both bugs are correctly fixed. The memory:// resolution matches the sibling-tool pattern exactly, and the structural heuristic removal is well-justified and documented. One coverage gap is worth flagging before merge.


Bug Fix Analysis

Bug #11memory:// URL Resolution (move_note.py:663–692)

The fix is correct. resolve_project_and_path() strips the memory:// prefix and normalizes the path identically to how read_note, edit_note, and delete_note handle it. The resolved_identifier is then passed to resolve_entity() instead of the raw memory://... string, which was 404ing before.

The new cross-project guard (lines 675–692) is a net improvement: instead of silently misrouting a memory://other-project/... URL against the active project's client, it rejects fast with clear guidance. The source_project.external_id != active_project.external_id comparison is the right check.

Coverage gap — new branch needs a test or # pragma: no cover:
The condition at line 675 (source_project.external_id != active_project.external_id) fires only for a cross-project memory:// URL like memory://other-project/.... None of the new tests exercise this branch — test_move_note_accepts_memory_url uses a same-project URL, so the condition is always False in tests. All test runs in the PR used --no-cov, so coverage was not actually measured. If this branch isn't marked # pragma: no cover, it will fail the 100% gate in CI.

Either add a two-project integration test for a cross-project memory:// rejection, or add # pragma: no cover to lines 676–692 if a multi-project test fixture is too heavy for this PR.

Bug #12 — Removal of the projects-segment heuristic (move_note.py:59–68)

Correct removal. The NOTE comment block explains the trigger, why, and outcome following the CLAUDE.md literate-programming style. Cross-project detection now relies on:

  1. Detection 1: leading path segment matches a known project name (retained)
  2. MOVE_OUTCOME_MISMATCH: post-move path validation backstop (retained)

These two backstops are sufficient. The false-rejection rate for legitimate nested projects/ folders drops to zero.

The Codex P2 comment from the second review about "rejecting a same-project move within other-project" is the same architectural limitation that read_note/edit_note/delete_note share (none re-route on a cross-project memory:// prefix once get_project_client() has bound the client). It's a pre-existing gap, not a regression introduced here.


Team Checklist

Item Status
Follows Basic Memory coding conventions (CLAUDE.md)
Python 3.12+ type annotations and async patterns
SQLAlchemy 2.0 best practices ✅ (no ORM changes)
FastAPI and Typer conventions followed
100-character line length limit maintained
No commented-out code blocks ✅ (NOTE block is a decision-point comment, not dead code)
Unit tests for new functions/methods
Integration tests for new MCP tools test-int/bughunt_fixes/test_move_note_edge_cases.py
Test coverage for edge cases ✅ (happy paths; see coverage note above)
100% test coverage maintained ⚠️ needs verification — tests ran with --no-cov; new branch at line 675 appears untested
Documentation updated (docstring, decision comments)
CLAUDE.md updated if conventions change N/A
MCP tools follow atomic, composable design
Database changes include Alembic migrations N/A
Preserves local-first architecture principles
Knowledge graph operations maintain consistency
Markdown file handling preserves integrity
No hardcoded secrets or credentials
Input validation for MCP tools
Proper error handling and logging
Performance considerations addressed
No sensitive data in logs or commits
Windows path compatibility (PureWindowsPath)
No emojis/unicode in log or console output ✅ (emojis in text response strings are pre-existing)
No tool signature changes
Semantic PR title fix(mcp): ...
Commits signed ✅ (-s)

Minor Observations (non-blocking)

  1. test_move_note_accepts_memory_url covers only same-project URLs. There is no test for memory://other-project/... being correctly rejected. The sibling-tool controls (test_delete_note_accepts_memory_url_control, test_edit_note_accepts_memory_url_control) are a good addition for the contract, but don't exercise the new rejection branch.

  2. _detect_cross_project_move_attempt still runs after the resolve_project_and_path check. For a memory:// URL that resolves to the active project, resolved_identifier is a bare path. The subsequent _detect_cross_project_move_attempt call (line 803) checks destination paths — that ordering is correct and unchanged.

  3. Updated assertions in test_move_note_integration.py are accurate. The two renamed tests (test_move_note_unknown_workspace_shaped_path_allowed*) now assert moved=True for other-workspace/projects/x/... destinations, which is the correct post-fix behavior. The assertions are clean and correctly invert the old expectations.


Verdict

Approve with one pre-merge action: verify the new cross-project memory:// rejection branch at move_note.py:675–692 is either covered by an integration test (preferred) or annotated with # pragma: no cover. Everything else — logic, structure, documentation, and test direction — is correct and ready to merge.

@phernandez phernandez merged commit 480a2d9 into main Jun 8, 2026
27 checks passed
@phernandez phernandez deleted the fix/move-note-edge-cases branch June 8, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant