Skip to content

fix(api): resolve note directories case-insensitively - #1329

Merged
phernandez merged 2 commits into
mainfrom
fix/1326-case-insensitive-directory
Aug 25, 2026
Merged

fix(api): resolve note directories case-insensitively#1329
phernandez merged 2 commits into
mainfrom
fix/1326-case-insensitive-directory

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

MCP write_note with directory: "schemas" beside an existing Schemas/ folder silently created a case-duplicate sibling folder instead of resolving to the existing one, and move_note's destination parent had the same failure mode. The failure is an LLM guessing plausible-but-wrong casing; it shows up on Basic Memory Cloud where the storage backend is case-sensitive (local macOS masks it).

The requested directory now resolves against the project's known folders derived from the database (EntityRepository.get_distinct_directories, the same source list_directory uses) — never by probing the filesystem — so local and cloud runtimes resolve identically.

Resolution rules

Applied per path segment, each segment resolving against children of the already-resolved parent (so schemas/drafts matches an existing Schemas/Drafts):

  1. Exact match always wins — no behavior change.
  2. Unique case-insensitive match — the note lands in the existing folder's casing.
  3. Zero matches — the folder is created as given (today's behavior).
  4. Multiple case-variant folders already exist (e.g. both Schemas/ and schemas/) — ambiguous; today's exact behavior is kept.

Where it lives

  • resolve_directory_casing() in src/basic_memory/utils.py — the pure segment-wise resolver.
  • resolve_accepted_note_directory() / resolve_accepted_note_schema_directory() in src/basic_memory/indexing/accepted_note_mutation_runner.py — wired into the accepted-note create (POST), replace (PUT), and move runners, the server-side layer both the local and cloud runtimes share. The PUT path matters because write_note falls back to update on conflict; without it a case-variant directory would rename the existing note into the wrong-cased folder.
  • AcceptedNoteMutationEntityRepository protocol gains get_distinct_directories (the concrete EntityRepository already implements it).
  • move_note MCP tool: the outcome-validation backstop (move_note falsely reports success for unsupported cross-workspace/cross-project moves #881) now treats a case-only difference between requested and actual landing path as success — that difference is this resolution at work, not a cross-boundary degradation; the success message already reports the actual path.

Out of scope by design: directory moves / case-only folder renames and index dedup of existing duplicates (#1281).

Tests

  • tests/utils/test_resolve_directory_casing.py — all four rules, nested segments, ambiguous-parent non-splicing, root/empty inputs (10 tests).
  • tests/indexing/test_accepted_note_mutation_runner.py — create resolves (and leaves the route-owned schema untouched), create keeps ambiguous casing, update replaces in place instead of renaming, move resolves the destination parent, move onto a case-variant of the note's own path rejects as same-path (5 tests).
  • tests/api/v2/test_knowledge_router.py — real-stack create resolution, nested resolution, ambiguous-siblings passthrough, move resolution (4 tests).
  • tests/mcp/test_tool_write_note.py / test_tool_move_note.py — end-to-end write_note and move_note flows land in the existing folder's casing (2 tests).

Verification

uv run pytest tests/utils/test_resolve_directory_casing.py tests/indexing/test_accepted_note_mutation_runner.py   # 46 passed
uv run pytest tests/api/v2/ tests/indexing/ tests/utils/ tests/cloud/test_cloud_services.py tests/index/test_local_accepted_note_repositories.py   # 952 passed, 1 skipped
uv run pytest tests/mcp/test_tool_write_note.py tests/mcp/test_tool_move_note.py   # 103 passed
uv run pytest test-int/mcp/test_write_note_integration.py test-int/mcp/test_move_note_integration.py   # 56 passed
uv run ruff check --fix && uv run ruff format .
uv run ty check src tests test-int   # only pre-existing pymilvus env diagnostics, also present on main

Fixes #1326

🤖 Generated with Claude Code

https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9

MCP write_note with directory "schemas" beside an existing "Schemas/"
silently created a case-duplicate sibling folder on case-sensitive
storage (Basic Memory Cloud); move_note's destination parent had the
same failure mode. The requested directory now resolves against the
project's known folders, derived from indexed entity file paths in the
database (the same source list_directory uses), so local and cloud
runtimes behave identically and storage is never probed for casing.

Resolution rules, applied per path segment against children of the
already-resolved parent:

- Exact match wins (no behavior change).
- Unique case-insensitive match adopts the existing folder's casing.
- Zero matches create the folder as given (today's behavior).
- Multiple existing case-variant folders are ambiguous; the requested
  casing is kept (today's exact behavior).

The resolution lives in the accepted-note mutation runner shared by the
create (POST), replace (PUT), and move flows in both runtimes. The
move_note MCP tool's outcome backstop now accepts a case-only
difference between the requested and actual landing path, since that is
this resolution at work rather than a cross-boundary degradation.

Fixes #1326

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9
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: 3f1b8db40b

ℹ️ 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/indexing/accepted_note_mutation_runner.py
Comment thread src/basic_memory/mcp/tools/move_note.py Outdated
Address two Codex review findings on PR #1329:

- Skip directory-casing resolution on PUT updates whose requested
  directory exactly matches the addressed entity's current directory.
  Content-only saves (e.g. repeated collaboration-relay writes) are the
  hot path and must not pay the O(project entities) distinct file_path
  scan; an exact match would resolve to itself anyway because exact
  match always wins. Case-variant and relocating PUTs still resolve.

- The move_note outcome backstop now forgives a case-only difference
  only in the parent directories, comparing the basename exactly. The
  server-side resolution preserves the requested filename verbatim, so
  a basename divergence (even case-only) still reports the honest
  failure instead of a fake success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9
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: d71ce6ddee

ℹ️ 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/indexing/accepted_note_mutation_runner.py
@phernandez
phernandez merged commit f7e18a5 into main Aug 25, 2026
33 checks passed
@phernandez
phernandez deleted the fix/1326-case-insensitive-directory branch August 25, 2026 19:07
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.

write_note creates case-duplicate folders (schemas/ beside existing Schemas/)

1 participant