Skip to content

fix(core): move a directory's notes as accepted note moves - #1670

Merged
phernandez merged 4 commits into
mainfrom
accepted-directory-moves
Oct 7, 2026
Merged

phernandez merged 4 commits into
mainfrom
accepted-directory-moves

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

Closes #1667.

The move-directory endpoint moved every file through the legacy storage-first EntityService.move_entity. For a Markdown note that moved the stored object and the entity row but never note_content, which kept the old path and revision. In Basic Memory Cloud a later materialization could then write the note back at its old path, and a directory move published no per-note note.moved, vacate marker or embeddings refresh.

  • New services/directory_moves.py: each Markdown note moves through NoteContentMutationService.move_note plus the runtime's materialize_write_change, exactly like the single-move endpoint. Regular files (no accepted content) still move their stored bytes via move_regular_file. Per-file partial failure is reported as before.
  • The route calls it; EntityService.move_directory is removed.

Tests

  • test-int/test_directory_moves.py (HTTP, real DB): a note and a regular file move together; note_content follows the note to a new revision at the new path, bytes land at the new paths and nothing remains at the old ones. Fails on the old path with ('drafts/Plan.md', 1) == ('archive/drafts/Plan.md', 2). A refused note (destination conflict) is reported while the rest move.
  • Read-cache tests: the cancellation test now pins move_regular_file; the directory-move invalidation test observes accepted state (reads serve accepted content) and asserts each note's invalidations land before the next note is accepted.
  • Full suite (excluding semantic/benchmarks) passed locally apart from the read-cache test updated here.

Follow-up in Basic Memory Cloud: drop the request-time vector scheduler (basic-memory-cloud#2371) now that every scheduling route materializes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T8wjd6HrtSA2LN9ssC4NzF

The directory-move endpoint moved every file through the legacy storage-first
EntityService.move_entity. For a Markdown note that moved the stored object
and the entity row but never note_content, which kept the old path and
revision; a later materialization could write the note back at the old path,
and the move published no per-note move event, vacate marker or embeddings.

Each Markdown note now moves through NoteContentMutationService.move_note and
the runtime's materialization hand-off, exactly like the single-move endpoint.
Regular files have no accepted content, so their stored bytes still move
directly. Per-file partial failure is reported as before. The service-level
EntityService.move_directory is gone.

Closes #1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8wjd6HrtSA2LN9ssC4NzF
Signed-off-by: phernandez <paul@basicmachines.co>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T19:56:03.794623Z 929f43b New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

ℹ️ 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 on lines +101 to +102
schedule_followups(entity.id)
moved_files.append(destination_path)

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 Return the accepted path for moved notes

When the requested destination differs only in case from an existing directory (for example, moving to archive when Archive/ exists), move_note canonicalizes the destination to the existing casing, but this code still appends the pre-normalized destination_path. The API and MCP tool therefore report archive/Plan.md even though the accepted note and, on case-sensitive storage, the file are at Archive/Plan.md, causing follow-up operations using the reported path to miss the note. Use the file path from the returned accepted/materialized payload for Markdown entries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 04e3a34: Markdown entries report the file path from the accepted payload (a test moves into archive while Archive/ exists and expects Archive/Plan.md).

Comment on lines +69 to +70
for entity in entities:
destination_path = entity.file_path.replace(f"{source}/", f"{destination}/", 1)

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 Preserve case-variant source prefix handling

On the default SQLite backend, find_by_directory_prefix uses case-insensitive LIKE, so requesting source drafts can return an entity at Drafts/Plan.md. This case-sensitive replacement then leaves the path unchanged, and the accepted-note runner rejects it as a same-path move; the previous implementation handled a nonmatching prefix by constructing a destination under the requested directory. Canonicalize the source casing or explicitly derive the suffix from the matched entity so case-variant source requests do not turn every note into a failed move.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 04e3a34: the destination keeps the entity's own path below the matched prefix, and the project root keeps the whole path. A SQLite test moves drafts when the folder is Drafts/sub and expects archive/sub/Plan.md.

phernandez and others added 2 commits October 7, 2026 13:56
…ectory moves

A note's accepted move can adopt an existing folder's casing, so the result
now reports the path from the accepted payload. SQLite matches the source
folder in any casing, so the destination keeps the entity's own path below
the matched prefix instead of a case-sensitive replace that left it
unchanged; the project root keeps the whole path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8wjd6HrtSA2LN9ssC4NzF
Signed-off-by: phernandez <paul@basicmachines.co>
No route moves a Markdown note through EntityService.move_entity anymore:
single moves and directory moves both use the accepted note move. Its
permalink and frontmatter branches were dead, and a direct call would have
left note_content behind. It now refuses Markdown notes, takes only the
identifier and destination, and moves a regular file's bytes and index row.

The twelve service tests that drove Markdown notes through it are gone; the
behavior that still applies (missing source, taken destination, unsafe path,
new destination folder, rollback after a failed row update) is covered for
regular files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8wjd6HrtSA2LN9ssC4NzF
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: efba9efd08

ℹ️ 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".

moved_path = (
await move_regular_file(
file_path=entity.file_path,
destination_path=destination_path,

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 Canonicalize destination casing for regular files

When a directory contains both Markdown notes and regular files and the requested destination differs only by case from an existing directory (for example, archive while Archive/ exists), accepted note moves canonicalize to Archive/..., but this branch passes the raw archive/... path to the regular-file mover. On case-sensitive local storage this creates a second directory and splits a single directory move across Archive and archive; it can also bypass conflicts with regular files already under the canonical directory. Resolve the destination directory casing once for the entire batch and use it for both branches.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 929f43b: the destination's casing is resolved once per directory move with the same rule accepted note moves use, and both branches build paths from it. A test moves a note and a PNG into archive beside an existing Archive/ and expects both under Archive/; it fails without the fix.

…ry file

Accepted note moves adopt an existing folder's casing (#1326), but regular
files took the requested casing, so moving into 'archive' beside an existing
'Archive/' split one directory move across two folders on case-sensitive
storage. The batch now resolves the destination by the same rule and uses it
for notes and regular files alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8wjd6HrtSA2LN9ssC4NzF
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez merged commit dd5e277 into main Oct 7, 2026
36 checks passed
@phernandez
phernandez deleted the accepted-directory-moves branch October 7, 2026 21:05
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.

Route directory moves through the accepted-write path so Cloud can drop the request-time vector scheduler

1 participant