Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ Create `~/.config/opencode/supermemory.jsonc`:
"baseUrl": "https://api.supermemory.ai",

// Min similarity for memory retrieval (0-1)
"similarityThreshold": 0.6,
"similarityThreshold": 0.55,

// Max memories injected per request
"maxMemories": 5,
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const DEFAULT_KEYWORD_PATTERNS = [
];

const DEFAULTS: Required<Omit<SupermemoryConfig, "apiKey" | "baseUrl" | "userContainerTag" | "projectContainerTag" | "recallDirective">> = {
similarityThreshold: 0.6,
similarityThreshold: 0.55,
maxMemories: 5,
maxProjectMemories: 10,
maxProfileItems: 5,
Expand Down
34 changes: 29 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { supermemoryClient } from "./services/client.js";
import { formatContextForPrompt } from "./services/context.js";
import { createCaptureHook } from "./services/capture.js";
import { buildRecallDirective } from "./services/recall.js";
import {
formatRecallHit,
normalizeRecallResult,
} from "./services/recall-results.js";
import { getTags } from "./services/tags.js";
import { stripPrivateContent, isFullyPrivate } from "./services/privacy.js";
import { createCompactionHook, type CompactionContext } from "./services/compaction.js";
Expand Down Expand Up @@ -591,19 +595,39 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
function formatSearchResults(
query: string,
scope: string | undefined,
results: { results?: Array<{ id?: string; memory?: string; chunk?: string; similarity?: number }> },
results: {
results?: Array<{
id?: string;
memory?: string;
chunk?: string;
content?: string;
text?: string;
context?: unknown;
similarity?: number;
score?: number;
title?: string;
filepath?: string;
metadata?: Record<string, unknown> | null;
}>;
},
limit?: number
): string {
const memoryResults = results.results || [];
const memoryResults = (results.results || [])
.map((result) => normalizeRecallResult(result))
.filter((hit): hit is NonNullable<typeof hit> => hit !== null)
.slice(0, limit ?? 10);
return JSON.stringify({
success: true,
query,
scope,
count: memoryResults.length,
results: memoryResults.slice(0, limit || 10).map((r) => {
results: memoryResults.map((hit) => {
const r = hit.result;
const result = {
content: r.memory ?? r.chunk,
similarity: Math.round((r.similarity ?? 0) * 100),
content: formatRecallHit(hit),
similarity: Math.round(hit.similarity * 100),
...(hit.title ? { title: hit.title } : {}),
...(hit.filepath ? { filepath: hit.filepath } : {}),
};

return r.memory === undefined
Expand Down
6 changes: 2 additions & 4 deletions src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ export interface SearchResultItem {
memory?: string;
content?: string;
chunk?: string;
text?: string;
context?: unknown;
score?: number;
similarity?: number;
title?: string;
filepath?: string;
updatedAt?: string;
metadata?: Record<string, unknown> | null;
containerTag?: string;
Expand Down Expand Up @@ -269,10 +271,6 @@ export class SupermemoryClient {
result.searchResults.results as SearchResultItem[]
).map((item) => ({
...item,
memory:
item.memory ??
item.content ??
String(item.context ?? ""),
containerTag,
})),
total: result.searchResults.total,
Expand Down
32 changes: 14 additions & 18 deletions src/services/context.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import type { ProfileResponse } from "./client.js";
import type { ProfileResponse, SearchResultItem } from "./client.js";
import { CONFIG } from "../config.js";

interface MemoryResultMinimal {
similarity?: number;
memory?: string;
chunk?: string;
}
import {
formatRecallHit,
normalizeRecallResults,
} from "./recall-results.js";

interface MemoriesResponseMinimal {
results?: MemoryResultMinimal[];
results?: SearchResultItem[];
}

function extractFactText(fact: unknown): string {
Expand Down Expand Up @@ -48,23 +46,21 @@ export function formatContextForPrompt(
}
}

const projectResults = projectMemories.results || [];
const projectResults = normalizeRecallResults(projectMemories.results || []);
if (projectResults.length > 0) {
parts.push("\nProject Knowledge:");
projectResults.forEach((mem) => {
const similarity = Math.round((mem.similarity ?? 0) * 100);
const content = mem.memory || mem.chunk || "";
parts.push(`- [${similarity}%] ${content}`);
projectResults.forEach((hit) => {
const similarity = Math.round(hit.similarity * 100);
parts.push(`- [${similarity}%] ${formatRecallHit(hit)}`);
});
}

const userResults = userMemories.results || [];
const userResults = normalizeRecallResults(userMemories.results || []);
if (userResults.length > 0) {
parts.push("\nRelevant Memories:");
userResults.forEach((mem) => {
const similarity = Math.round((mem.similarity ?? 0) * 100);
const content = mem.memory || mem.chunk || "";
parts.push(`- [${similarity}%] ${content}`);
userResults.forEach((hit) => {
const similarity = Math.round(hit.similarity * 100);
parts.push(`- [${similarity}%] ${formatRecallHit(hit)}`);
});
}

Expand Down
36 changes: 36 additions & 0 deletions src/services/recall-results.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test";

import {
formatRecallHit,
normalizeRecallResults,
} from "./recall-results.js";

describe("recall result normalization", () => {
test("keeps the strongest supported result shapes with provenance", () => {
const hits = normalizeRecallResults([
{ memory: "memory result", similarity: 0.99 },
{ chunk: "chunk result", similarity: 0.9 },
{ content: "content result", similarity: 0.8 },
{ text: "text result", similarity: 0.7 },
{
context: "context result",
similarity: 0.6,
title: "Decision",
filepath: "src/index.ts",
},
{ memory: "below threshold", similarity: 0.54 },
{ memory: "sixth result", similarity: 0.56 },
]);

expect(hits.map((hit) => hit.text)).toEqual([
"memory result",
"chunk result",
"content result",
"text result",
"context result",
]);
expect(formatRecallHit(hits[4]!)).toBe(
"Decision: context result (src/index.ts)",
);
});
});
104 changes: 104 additions & 0 deletions src/services/recall-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { SearchResultItem } from "./client.js";

export const MIN_RECALL_SIMILARITY = 0.55;
export const MAX_RECALL_RESULTS = 5;
export const MAX_RECALL_HIT_CHARS = 300;

export interface RecallHit {
result: SearchResultItem;
text: string;
similarity: number;
title?: string;
filepath?: string;
}

function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: undefined;
}

export function getRecallResultText(result: SearchResultItem): string {
return (
nonEmptyString(result.memory) ??
nonEmptyString(result.chunk) ??
nonEmptyString(result.content) ??
nonEmptyString(result.text) ??
nonEmptyString(result.context) ??
""
);
}

function getRecallResultTitle(result: SearchResultItem): string | undefined {
return nonEmptyString(result.title) ?? nonEmptyString(result.metadata?.title);
}

function getRecallResultFilepath(result: SearchResultItem): string | undefined {
return (
nonEmptyString(result.filepath) ??
nonEmptyString(result.metadata?.filepath) ??
nonEmptyString(result.metadata?.filePath) ??
nonEmptyString(result.metadata?.path)
);
}

function truncateHit(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
if (maxChars <= 3) return text.slice(0, maxChars);
return `${text.slice(0, maxChars - 3).trimEnd()}...`;
}

export function normalizeRecallResult(
result: SearchResultItem,
maxHitChars?: number,
): RecallHit | null {
const text = getRecallResultText(result);
if (!text) return null;

return {
result,
text:
maxHitChars === undefined
? text
: truncateHit(text, Math.max(1, maxHitChars)),
similarity: result.similarity ?? result.score ?? 0,
title: getRecallResultTitle(result),
filepath: getRecallResultFilepath(result),
};
}

export function normalizeRecallResults(
results: SearchResultItem[],
options?: {
limit?: number;
minSimilarity?: number;
maxHitChars?: number;
},
): RecallHit[] {
const limit = Math.max(0, options?.limit ?? MAX_RECALL_RESULTS);
const minSimilarity = Math.max(
MIN_RECALL_SIMILARITY,
options?.minSimilarity ?? MIN_RECALL_SIMILARITY,
);
const maxHitChars = Math.max(1, options?.maxHitChars ?? MAX_RECALL_HIT_CHARS);
const seen = new Set<string>();

return results
.map((result) => normalizeRecallResult(result, maxHitChars))
.filter((hit): hit is RecallHit => hit !== null)
.filter((hit) => hit.similarity >= minSimilarity)
.sort((a, b) => b.similarity - a.similarity)
.filter((hit) => {
const key = hit.text.toLowerCase().replace(/\s+/g, " ").trim();
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.slice(0, limit);
}

export function formatRecallHit(hit: RecallHit): string {
const title = hit.title ? `${hit.title}: ` : "";
const filepath = hit.filepath ? ` (${hit.filepath})` : "";
return `${title}${hit.text}${filepath}`;
}
12 changes: 2 additions & 10 deletions src/services/result-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
SearchResponse,
SearchResultItem,
} from "./client.js";
import { getRecallResultText } from "./recall-results.js";

function normalize(value: unknown): string {
return String(value ?? "").toLowerCase().trim();
Expand All @@ -20,17 +21,8 @@ function dedupe<T>(items: T[], getKey: (item: T) => string): T[] {
});
}

function memoryText(result: SearchResultItem): string {
return (
result.memory ??
result.chunk ??
result.content ??
String(result.context ?? "")
);
}

function searchKey(result: SearchResultItem): string {
const content = normalize(memoryText(result));
const content = normalize(getRecallResultText(result));
if (content) return `content:${content}`;
return result.id ? `id:${result.id}` : "";
}
Expand Down
Loading