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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,12 @@ Drop this file in to override defaults:
| `apiKey` | `string` | — | API key (env var takes precedence, browser auth is preferred). |
| `baseUrl` | `string` | `https://api.supermemory.ai` | Supermemory API base URL (`SUPERMEMORY_API_URL`/`SUPERMEMORY_BASE_URL` env vars take precedence). |
| `similarityThreshold` | `number` | `0.6` | Minimum similarity score for retrieved memories. |
| `maxMemories` | `number` | `5` | Max memories injected per prompt. |
| `maxProfileItems` | `number` | `5` | Max profile items considered from each persistent/recent section. |
| `maxMemories` | `number` | `5` | Global max memories injected per prompt across all searched containers. |
| `maxProfileItems` | `number` | `5` | Max profile items from each persistent and recent section. |
| `maxRecallTokens` | `number` | `2500` | Approximate whole-context token budget for session-start profile recall. |
| `maxPromptRecallTokens` | `number` | `maxRecallTokens` | Approximate whole-context token budget for prompt recall. |
| `autoRecallContainers` | `boolean` | `false` | Search every configured custom container on each substantive prompt. |
| `customContainers` | `{ tag: string, description: string }[]` | `[]` | Custom containers available for automatic recall. |
| `injectProfile` | `boolean` | `true` | Whether to fetch and inject the user profile. |
| `containerTagPrefix` | `string` | `"codex"` | Legacy prefix retained when reading containers created by older versions. |
| `userContainerTag` | `string` | auto | Legacy personal container retained for backward-compatible reads. |
Expand Down
19 changes: 18 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ interface HookEntry {
command: string;
timeout?: number;
statusMessage?: string;
additionalContextLimit?: number;
async?: boolean;
}

Expand Down Expand Up @@ -279,6 +280,7 @@ function ensureHookRegistered(
statusMessage: string,
background = false,
matcher?: string,
additionalContextLimit?: number,
): void {
const exists = groups.some((g) => g.hooks.some((h) => h.command === command));
if (exists) {
Expand All @@ -287,6 +289,9 @@ function ensureHookRegistered(
if (hook.command === command) {
hook.timeout = timeout;
hook.statusMessage = statusMessage;
if (additionalContextLimit !== undefined) {
hook.additionalContextLimit = additionalContextLimit;
}
if (background) hook.async = true;
else delete hook.async;
}
Expand All @@ -301,6 +306,7 @@ function ensureHookRegistered(
command,
timeout,
statusMessage,
...(additionalContextLimit !== undefined ? { additionalContextLimit } : {}),
...(background ? { async: true } : {}),
};
if (matchingGroup) {
Expand Down Expand Up @@ -346,11 +352,22 @@ function mergeHooksJson(add: boolean) {
sessionStartCmd,
SESSION_START_TIMEOUT_SECONDS,
"Loading memory profile...",
false,
undefined,
0,
);

// Recall must stay synchronous because its output is injected.
if (!hooks.UserPromptSubmit) hooks.UserPromptSubmit = [];
ensureHookRegistered(hooks.UserPromptSubmit, recallCmd, RECALL_TIMEOUT_SECONDS, "Searching memories...");
ensureHookRegistered(
hooks.UserPromptSubmit,
recallCmd,
RECALL_TIMEOUT_SECONDS,
"Searching memories...",
false,
undefined,
0,
);

// Remove the old per-prompt capture hook. Stop now owns automatic capture.
hooks.UserPromptSubmit = removeHookCommands(
Expand Down
29 changes: 29 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export const CONFIG_FILE = join(homedir(), ".codex", "supermemory.json");
export const DEFAULT_BASE_URL = "https://api.supermemory.ai";

export type RecallMode = "direct" | "off" | "advisory";
export interface CustomContainer {
tag: string;
description: string;
}
export const DEFAULT_RECALL_DIRECTIVE =
"Relevant prior context may exist in Supermemory. Search memory before answering when the request depends on previous decisions, preferences, or project history.";

Expand All @@ -18,6 +22,10 @@ interface CodexSupermemoryConfig {
similarityThreshold?: number;
maxMemories?: number;
maxProfileItems?: number;
maxRecallTokens?: number;
maxPromptRecallTokens?: number;
autoRecallContainers?: boolean;
customContainers?: CustomContainer[];
injectProfile?: boolean;
containerTagPrefix?: string;
userContainerTag?: string;
Expand Down Expand Up @@ -49,6 +57,8 @@ const DEFAULTS = {
similarityThreshold: 0.6,
maxMemories: 5,
maxProfileItems: 5,
maxRecallTokens: 2500,
autoRecallContainers: false,
injectProfile: true,
containerTagPrefix: "codex",
filterPrompt:
Expand Down Expand Up @@ -133,11 +143,30 @@ export function reloadApiKey(): void {
}

const recallMode = resolveRecallMode(fileConfig);
const maxRecallTokens = fileConfig.maxRecallTokens ?? DEFAULTS.maxRecallTokens;
const customContainers = Array.isArray(fileConfig.customContainers)
? fileConfig.customContainers
.filter(
(container): container is CustomContainer =>
!!container &&
typeof container.tag === "string" &&
container.tag.trim().length > 0 &&
typeof container.description === "string",
)
.map((container) => ({
tag: container.tag.trim(),
description: container.description.trim(),
}))
: [];

export const CONFIG = {
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories,
maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems,
maxRecallTokens,
maxPromptRecallTokens: fileConfig.maxPromptRecallTokens ?? maxRecallTokens,
autoRecallContainers: fileConfig.autoRecallContainers ?? DEFAULTS.autoRecallContainers,
customContainers,
injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile,
containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix,
userContainerTag: fileConfig.userContainerTag,
Expand Down
54 changes: 18 additions & 36 deletions src/hooks/recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { getSeenFacts, addSeenFacts, factKey } from "../services/factCache.js";
import { getSessionId } from "../services/session.js";
import { getHookProfileWithSearchMany } from "../services/hookRecallClient.js";
import { prepareRecallQuery, shouldRecallPrompt } from "../services/recallPolicy.js";

const MAX_RESULT_CHARS = 300;
import { formatRecallContext } from "../services/context.js";

interface CodexHookPayload {
session_id?: string;
Expand Down Expand Up @@ -37,29 +36,6 @@ function exitWithContext(additionalContext: string, systemMessage?: string): nev
process.exit(0);
}

interface RecallItem {
memory: string;
title?: string;
filepath?: string;
}

function formatRecall(items: RecallItem[], containerTag: string): string {
const lines = items.map((item) => {
const text = item.memory.replace(/\s+/g, " ").slice(0, MAX_RESULT_CHARS);
const title = item.title?.trim();
const prefix = title && !text.startsWith(title) ? `${title} — ` : "";
const filepath = item.filepath ? ` (${item.filepath})` : "";
return `- ◪ ${prefix}${text}${filepath}`;
});

return `<supermemory-recall>
◪ Recalled from supermemory for this prompt (relevance-ranked):
${lines.join("\n")}

When one of these shapes your answer, credit it naturally with the ◪ prefix (e.g. "◪ earlier you decided X"); if you name the source, say "from supermemory" — never "from memory". For deeper history, call the supermemory search_memory tool (containerTag: "${containerTag}").
</supermemory-recall>`;
}

async function main() {
let rawInput = "";
try {
Expand Down Expand Up @@ -122,24 +98,27 @@ async function main() {

const seen = getSeenFacts(sessionId);
const matches = profileResult.searchResults?.results ?? [];
const fresh = matches
.filter((item) => !seen.has(factKey(item.memory)))
.slice(0, Math.min(CONFIG.maxMemories, 5));
const repeats = matches.length - fresh.length;
const repeats = matches.filter((item) => seen.has(factKey(item.memory))).length;
const { text: additionalContext, newFacts } = formatRecallContext(matches, {
containerTag: tags.canonical,
maxMemories: CONFIG.maxMemories,
maxTokens: CONFIG.maxPromptRecallTokens,
seenFacts: seen,
customContainers: CONFIG.autoRecallContainers ? CONFIG.customContainers : [],
});

log("recall: done", {
matchCount: matches.length,
freshCount: fresh.length,
freshCount: newFacts.length,
seenCount: seen.size,
});

if (fresh.length > 0) {
addSeenFacts(sessionId, fresh.map((item) => item.memory));
const additionalContext = formatRecall(fresh, tags.canonical);
if (newFacts.length > 0) {
addSeenFacts(sessionId, newFacts);
const tokens = Math.round(additionalContext.length / 4);
const label = repeats > 0
? `recalled ${fresh.length} new (${tokens} tok) · ${repeats} already in context`
: `recalled ${fresh.length} ${fresh.length === 1 ? "memory" : "memories"} (${tokens} tok)`;
? `recalled ${newFacts.length} new (${tokens} tok) · ${repeats} already in context`
: `recalled ${newFacts.length} ${newFacts.length === 1 ? "memory" : "memories"} (${tokens} tok)`;
log("recall: emit context", {
additionalContextLength: additionalContext.length,
});
Expand All @@ -149,7 +128,10 @@ async function main() {
exitWithContext("");
} catch (error) {
log("recall: error", { error: String(error) });
exitWithContext("", "◪ supermemory · recall unavailable; continuing without recalled context");
const message = error instanceof RangeError
? `◪ supermemory · invalid recall configuration: ${error.message}`
: "◪ supermemory · recall unavailable; continuing without recalled context";
exitWithContext("", message);
}
}

Expand Down
27 changes: 14 additions & 13 deletions src/hooks/session-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { homedir } from "node:os";
import { isConfigured, CONFIG, PLUGIN_VERSION, reloadApiKey } from "../config.js";
import { HOOK_API_TIMEOUT_MS, SupermemoryClient } from "../services/client.js";
import { getTags } from "../services/tags.js";
import { formatCombinedContext } from "../services/context.js";
import { formatSessionContext } from "../services/context.js";
import { log } from "../services/logger.js";
import { startAuthFlow, AUTH_BASE_URL } from "../services/auth.js";
import { getSeenFacts, addSeenFacts } from "../services/factCache.js";
Expand Down Expand Up @@ -108,15 +108,19 @@ async function main() {
{ timeoutMs: HOOK_API_TIMEOUT_MS },
);
const seen = getSeenFacts(sessionId);
const { text, newFacts } = formatCombinedContext(
const { text, newFacts } = formatSessionContext(
{
success: profileResult.success,
profile: profileResult.profile,
searchResults: undefined,
},
0,
CONFIG.maxProfileItems,
seen,
{
maxProfileItems: CONFIG.maxProfileItems,
maxTokens: CONFIG.maxRecallTokens,
seenFacts: seen,
projectName: tags.projectName,
containerTag: tags.canonical,
},
);

if (!profileResult.success) {
Expand All @@ -133,13 +137,7 @@ async function main() {
if (newFacts.length > 0) {
addSeenFacts(sessionId, newFacts);
const updateNotice = await updateCheck;
const context = `<supermemory-context>
Recalled memory for this project (${tags.projectName}). Every line marked ◪ comes from supermemory — when citing one, keep the mark and phrase it naturally. If you name the source, say "from supermemory" — never "from memory".
This project's memory container: ${tags.canonical}

${text}
</supermemory-context>`;
exitWithContext(context, combineContextParts([
exitWithContext(text, combineContextParts([
updateNotice,
`◪ supermemory · active · ${newFacts.length} ${newFacts.length === 1 ? "memory" : "memories"} loaded for ${tags.projectName}`,
markTip(),
Expand All @@ -158,11 +156,14 @@ ${text}
]));
} catch (error) {
log("session-start: error", { error: String(error) });
const message = error instanceof RangeError
? `◪ supermemory · invalid recall configuration: ${error.message}`
: "◪ supermemory · profile unavailable; continuing without recalled context";
exitWithContext(
"",
combineContextParts([
await updateCheck,
"◪ supermemory · profile unavailable; continuing without recalled context",
message,
]),
);
}
Expand Down
Loading