fix(api): resolve note directories case-insensitively - #1329
Conversation
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>
There was a problem hiding this comment.
💡 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".
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>
There was a problem hiding this comment.
💡 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".
Summary
MCP
write_notewithdirectory: "schemas"beside an existingSchemas/folder silently created a case-duplicate sibling folder instead of resolving to the existing one, andmove_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 sourcelist_directoryuses) — 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/draftsmatches an existingSchemas/Drafts):Schemas/andschemas/) — ambiguous; today's exact behavior is kept.Where it lives
resolve_directory_casing()insrc/basic_memory/utils.py— the pure segment-wise resolver.resolve_accepted_note_directory()/resolve_accepted_note_schema_directory()insrc/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 becausewrite_notefalls back to update on conflict; without it a case-variant directory would rename the existing note into the wrong-cased folder.AcceptedNoteMutationEntityRepositoryprotocol gainsget_distinct_directories(the concreteEntityRepositoryalready implements it).move_noteMCP tool: the outcome-validation backstop (move_notefalsely 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-endwrite_noteandmove_noteflows land in the existing folder's casing (2 tests).Verification
Fixes #1326
🤖 Generated with Claude Code
https://claude.ai/code/session_01G4rbaeHJN3L7CREp5v38J9