Skip to content

Commit a88b0bd

Browse files
jim80netclaude
andcommitted
fix: query legacy directory-hash tag to preserve existing memories on upgrade
Existing users have project memories stored under sha256(directory). After switching to sha256(normalizeGitUrl(remoteUrl)), those memories become silently inaccessible. Fix by querying both the new canonical tag and the legacy directory-based tag for all read operations (search, list, compaction context), deduplicating by memory ID. Writes go only to the new canonical tag so memories gradually migrate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c42b07e commit a88b0bd

3 files changed

Lines changed: 81 additions & 21 deletions

File tree

src/index.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,22 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
127127
if (isFirstMessage) {
128128
injectedSessions.add(input.sessionID);
129129

130-
const [profileResult, userMemoriesResult, projectMemoriesListResult] = await Promise.all([
130+
const [profileResult, userMemoriesResult, projectMemoriesListResult, legacyProjectResult] = await Promise.all([
131131
supermemoryClient.getProfile(tags.user, userMessage),
132132
supermemoryClient.searchMemories(userMessage, tags.user),
133133
supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories),
134+
tags.legacyProject
135+
? supermemoryClient.listMemories(tags.legacyProject, CONFIG.maxProjectMemories)
136+
: Promise.resolve({ success: true, memories: [] } as const),
134137
]);
135138

136139
const profile = profileResult.success ? profileResult : null;
137140
const userMemories = userMemoriesResult.success ? userMemoriesResult : { results: [] };
138-
const projectMemoriesList = projectMemoriesListResult.success ? projectMemoriesListResult : { memories: [] };
141+
const currentMemories = projectMemoriesListResult.success ? (projectMemoriesListResult.memories || []) : [];
142+
const legacyMemories = legacyProjectResult.success ? (legacyProjectResult.memories || []) : [];
143+
const seenIds = new Set(currentMemories.map((m: any) => m.id));
144+
const mergedMemories = [...currentMemories, ...legacyMemories.filter((m: any) => !seenIds.has(m.id))];
145+
const projectMemoriesList = { success: true, memories: mergedMemories };
139146

140147
const projectMemories = {
141148
results: (projectMemoriesList.memories || []).map((m: any) => ({
@@ -338,22 +345,28 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
338345
}
339346

340347
if (scope === "project") {
341-
const result = await supermemoryClient.searchMemories(
342-
args.query,
343-
tags.project
344-
);
348+
const [result, legacyResult] = await Promise.all([
349+
supermemoryClient.searchMemories(args.query, tags.project),
350+
tags.legacyProject
351+
? supermemoryClient.searchMemories(args.query, tags.legacyProject)
352+
: Promise.resolve({ success: true, results: [] } as const),
353+
]);
345354
if (!result.success) {
346355
return JSON.stringify({
347356
success: false,
348357
error: result.error || "Failed to search memories",
349358
});
350359
}
351-
return formatSearchResults(args.query, scope, result, args.limit);
360+
const merged = mergeSearchResults(result, legacyResult);
361+
return formatSearchResults(args.query, scope, merged, args.limit);
352362
}
353363

354-
const [userResult, projectResult] = await Promise.all([
364+
const [userResult, projectResult, legacyProjectResult] = await Promise.all([
355365
supermemoryClient.searchMemories(args.query, tags.user),
356366
supermemoryClient.searchMemories(args.query, tags.project),
367+
tags.legacyProject
368+
? supermemoryClient.searchMemories(args.query, tags.legacyProject)
369+
: Promise.resolve({ success: true, results: [] } as const),
357370
]);
358371

359372
if (!userResult.success || !projectResult.success) {
@@ -363,12 +376,14 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
363376
});
364377
}
365378

379+
const mergedProject = mergeSearchResults(projectResult, legacyProjectResult);
380+
366381
const combined = [
367382
...(userResult.results || []).map((r) => ({
368383
...r,
369384
scope: "user" as const,
370385
})),
371-
...(projectResult.results || []).map((r) => ({
386+
...(mergedProject.results || []).map((r) => ({
372387
...r,
373388
scope: "project" as const,
374389
})),
@@ -414,11 +429,15 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
414429
const limit = args.limit || 20;
415430
const containerTag =
416431
scope === "user" ? tags.user : tags.project;
417-
418-
const result = await supermemoryClient.listMemories(
419-
containerTag,
420-
limit
421-
);
432+
const legacyTag =
433+
scope === "project" ? tags.legacyProject : undefined;
434+
435+
const [result, legacyListResult] = await Promise.all([
436+
supermemoryClient.listMemories(containerTag, limit),
437+
legacyTag
438+
? supermemoryClient.listMemories(legacyTag, limit)
439+
: Promise.resolve({ success: true, memories: [] } as const),
440+
]);
422441

423442
if (!result.success) {
424443
return JSON.stringify({
@@ -427,7 +446,10 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
427446
});
428447
}
429448

430-
const memories = result.memories || [];
449+
const currentMems = result.memories || [];
450+
const legacyMems = legacyListResult.success ? (legacyListResult.memories || []) : [];
451+
const listSeenIds = new Set(currentMems.map((m: any) => m.id));
452+
const memories = [...currentMems, ...legacyMems.filter((m: any) => !listSeenIds.has(m.id))];
431453
return JSON.stringify({
432454
success: true,
433455
scope,
@@ -492,10 +514,23 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
492514
};
493515
};
494516

517+
type SearchResult = { id: string; memory?: string; chunk?: string; similarity?: number };
518+
type SearchResponse = { success?: boolean; results?: SearchResult[] };
519+
520+
function mergeSearchResults(primary: SearchResponse, legacy: SearchResponse): SearchResponse {
521+
const primaryResults = primary.results || [];
522+
const legacyResults = legacy.success ? (legacy.results || []) : [];
523+
const seenIds = new Set(primaryResults.map((r) => r.id));
524+
return {
525+
...primary,
526+
results: [...primaryResults, ...legacyResults.filter((r) => !seenIds.has(r.id))],
527+
};
528+
}
529+
495530
function formatSearchResults(
496531
query: string,
497532
scope: string | undefined,
498-
results: { results?: Array<{ id: string; memory?: string; chunk?: string; similarity?: number }> },
533+
results: SearchResponse,
499534
limit?: number
500535
): string {
501536
const memoryResults = results.results || [];

src/services/compaction.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export interface CompactionContext {
248248

249249
export function createCompactionHook(
250250
ctx: CompactionContext,
251-
tags: { user: string; project: string },
251+
tags: { user: string; project: string; legacyProject?: string },
252252
options?: CompactionOptions
253253
) {
254254
const state: CompactionState = {
@@ -262,9 +262,17 @@ export function createCompactionHook(
262262

263263
async function fetchProjectMemoriesForCompaction(): Promise<string[]> {
264264
try {
265-
const result = await supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories);
266-
const memories = result.memories || [];
267-
return memories.map((m: any) => m.summary || m.content || "").filter(Boolean);
265+
const [result, legacyResult] = await Promise.all([
266+
supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories),
267+
tags.legacyProject
268+
? supermemoryClient.listMemories(tags.legacyProject, CONFIG.maxProjectMemories)
269+
: Promise.resolve({ success: true, memories: [] } as const),
270+
]);
271+
const currentMems = result.memories || [];
272+
const legacyMems = legacyResult.success ? (legacyResult.memories || []) : [];
273+
const seenIds = new Set(currentMems.map((m: any) => m.id));
274+
const allMemories = [...currentMems, ...legacyMems.filter((m: any) => !seenIds.has(m.id))];
275+
return allMemories.map((m: any) => m.summary || m.content || "").filter(Boolean);
268276
} catch (err) {
269277
log("[compaction] failed to fetch project memories", { error: String(err) });
270278
return [];

src/services/tags.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,26 @@ export function getProjectTag(directory: string): string {
8484
return `${CONFIG.containerTagPrefix}_project_${sha256(directory)}`;
8585
}
8686

87-
export function getTags(directory: string): { user: string; project: string } {
87+
/**
88+
* Returns the legacy directory-hash project tag if it differs from the
89+
* current (remote-based) tag. Used to query old memories created before
90+
* the git-remote-based tagging was introduced.
91+
*/
92+
export function getLegacyProjectTag(directory: string): string | undefined {
93+
if (CONFIG.projectContainerTag) return undefined;
94+
95+
const remoteUrl = getGitRemoteUrl(directory);
96+
if (!remoteUrl) return undefined;
97+
98+
// A remote exists, so the canonical tag is remote-based.
99+
// Return the old directory-based tag for migration reads.
100+
return `${CONFIG.containerTagPrefix}_project_${sha256(directory)}`;
101+
}
102+
103+
export function getTags(directory: string): { user: string; project: string; legacyProject?: string } {
88104
return {
89105
user: getUserTag(),
90106
project: getProjectTag(directory),
107+
legacyProject: getLegacyProjectTag(directory),
91108
};
92109
}

0 commit comments

Comments
 (0)