mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-01 02:01:40 +00:00
fix(tools): prevent customId collisions in claude memory tool (#1547)
This commit is contained in:
parent
c927c98f2c
commit
a5a5dc85c6
3 changed files with 60 additions and 6 deletions
|
|
@ -210,3 +210,42 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => {
|
|||
expect(stored).toContain(`price is ${dollarSequence} today`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ClaudeMemoryTool path normalization collision resistance", () => {
|
||||
it("produces distinct customIds for paths that previously collided", () => {
|
||||
const tool = new ClaudeMemoryTool("test-api-key")
|
||||
const paths = [
|
||||
"/memories/notes.txt",
|
||||
"/memories/notes_txt",
|
||||
"/memories/notes/txt",
|
||||
"/memories/project/a.md",
|
||||
"/memories/project_a.md",
|
||||
]
|
||||
|
||||
const ids = paths.map((path) => tool.normalizePathToCustomId(path))
|
||||
const uniqueIds = new Set(ids)
|
||||
|
||||
expect(uniqueIds.size).toBe(paths.length)
|
||||
})
|
||||
|
||||
it("resolves documents stored under legacy customId format", async () => {
|
||||
// Mock a document saved with legacy normalization (memories_notes_txt)
|
||||
mockDocuments([
|
||||
{
|
||||
id: "legacy-doc",
|
||||
customId: "memories_notes_txt",
|
||||
filePath: "/memories/notes.txt",
|
||||
content: "legacy content",
|
||||
},
|
||||
])
|
||||
|
||||
const tool = new ClaudeMemoryTool("test-api-key")
|
||||
const result = await tool.handleCommand({
|
||||
command: "view",
|
||||
path: "/memories/notes.txt",
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.content).toContain("legacy content")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -57,13 +57,22 @@ export class ClaudeMemoryTool {
|
|||
|
||||
/**
|
||||
* Normalize file path to be used as customId
|
||||
* Converts /memories/file.txt -> memories_file_txt
|
||||
* Reversibly encodes path components to prevent collisions between paths like
|
||||
* `/memories/notes.txt`, `/memories/notes_txt`, and `/memories/notes/txt`.
|
||||
*/
|
||||
private normalizePathToCustomId(path: string): string {
|
||||
normalizePathToCustomId(path: string): string {
|
||||
return path
|
||||
.replace(/^\//, "") // Remove leading slash
|
||||
.replace(/\//g, "_") // Replace / with _
|
||||
.replace(/\./g, "_") // Replace . with _
|
||||
.replace(/_/g, "__") // Escape underscores: _ -> __
|
||||
.replace(/\//g, "_s_") // Encode slashes: / -> _s_
|
||||
.replace(/\./g, "_d_") // Encode dots: . -> _d_
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy normalization used in older versions (/ and . both flattened to _)
|
||||
*/
|
||||
private legacyNormalizePathToCustomId(path: string): string {
|
||||
return path.replace(/^\//, "").replace(/\//g, "_").replace(/\./g, "_")
|
||||
}
|
||||
|
||||
constructor(apiKey: string, config?: ClaudeMemoryConfig) {
|
||||
|
|
@ -650,8 +659,12 @@ export class ClaudeMemoryTool {
|
|||
})
|
||||
|
||||
for (const document of response.memories) {
|
||||
const isMatchingCustomId =
|
||||
document.customId === normalizedId ||
|
||||
document.customId === this.legacyNormalizePathToCustomId(filePath)
|
||||
|
||||
if (
|
||||
document.customId === normalizedId &&
|
||||
isMatchingCustomId &&
|
||||
this.getDocumentFilePath(document) === filePath &&
|
||||
this.isDocumentInConfiguredScope(document)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -344,7 +344,9 @@ describe("ClaudeMemoryTool", () => {
|
|||
|
||||
expect(result.success).toBe(true)
|
||||
expect(clientAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customId: "memories_renamed_txt" }),
|
||||
expect.objectContaining({
|
||||
customId: tool.normalizePathToCustomId("/memories/renamed.txt"),
|
||||
}),
|
||||
)
|
||||
expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] })
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue