Skip to content

fix(tools): prevent customId collisions in claude memory tool (#1547) - #1676

Open
Adityakk9031 wants to merge 3 commits into
supermemoryai:mainfrom
Adityakk9031:fix/claude-memory-path-collision-1547
Open

Adityakk9031 wants to merge 3 commits into
supermemoryai:mainfrom
Adityakk9031:fix/claude-memory-path-collision-1547

Conversation

@Adityakk9031

Copy link
Copy Markdown
Contributor

Resolves #1547

Problem

In packages/tools/src/claude-memory.ts, normalizePathToCustomId flattened both / and . directly into _:

private normalizePathToCustomId(path: string): string {
    return path
        .replace(/^\//, "")
        .replace(/\//g, "_")
        .replace(/\./g, "_")
}

This mapping was not injective and caused document collisions:

  • /memories/notes.txt, /memories/notes_txt, and /memories/notes/txt all mapped to memories_notes_txt.
  • /memories/project/a.md collided with /memories/project_a.md.
    Because customId is the unique document identity when saving with client.add(), creating or editing one path silently overwrote or destroyed documents at colliding paths.

Solution

  1. Collision-Free Reversible Normalization:
    • Escapes literal _ -> __
    • Encodes path slashes / -> _s_
    • Encodes dots . -> _d_
    • Ensures distinct paths produce distinct customIds (e.g. memories_s_notes_d_txt vs memories_s_notes__txt vs memories_s_notes_s_txt).
  2. Backward Compatibility:
    • Maintained fallback support in getFileDocument candidate resolution for documents previously created under legacy normalization (memories_notes_txt).
  3. Tests:
    • Added unit tests in claude-memory.test.ts asserting uniqueness across previously-colliding paths and verifying legacy document resolution.

Copy link
Copy Markdown

One migration edge looks worth covering before merge: the legacy read fallback is path-safe, but the write paths can strand both identities for the same logical file.

getFileDocument() accepts either the new customId or the legacy customId, while also requiring the stored file_path to match. That avoids reintroducing the original cross-path collision. But create, str_replace, and insert all write unconditionally under the new normalized customId. Unlike rename/delete, they do not remove the resolved legacy document.

So after an upgrade, starting with one legacy document for /memories/notes.txt:

  1. create (overwrite), str_replace, or insert succeeds and writes memories_s_notes_d_txt.
  2. The old memories_notes_txt document is still present with the same file_path.
  3. The next view/edit/delete sees two exact matches, and getFileDocument() correctly fails them as ambiguous.

That means the first successful edit of a legacy file can make the file unusable on the next operation—the exact migration path the fallback is intended to preserve.

A useful regression would seed one legacy-ID document, perform each mutating write path, then assert there is still exactly one resolvable logical file with the requested new content. The implementation could migrate/update the old identity or delete it after the new write succeeds; the important invariants are one logical file → one resolvable document, and failure ordering that does not delete the only good copy before the replacement write is durable.

@phant0um

Copy link
Copy Markdown

I checked this PR and #1586, which both address #1547.

The new encoding looks injective. I tested it by brute force over every path of up to 6 characters from a _ / . s d and found no collisions. Merged onto main (cfa6c7c), src/claude-memory.test.ts passes 30 of 30, and the new test fails on the old code.

The risk is in existing data. The encoding changes the customId of every path, including /memories/notes.txt, which becomes memories_s_notes_d_txt. Reads fall back to the legacy id, but writes do not. str_replace and insert read a legacy document and then call client.add with the new id (claude-memory.ts lines 457 and 515 on this branch). The legacy document is not removed, so after one edit the same file_path has two documents. view then returns whichever one matches first.

A possible fix: when the document that was read has the legacy id, write back to that id, or delete it after the new write.

This branch has not been deployed

No deployments
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.

Claude memory tool: path → customId normalization collides, silently overwriting unrelated memory files

3 participants