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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ and the lessons learned across every project — automatically.
when saving, searching, or forgetting memories.
- 🏷️ **Project + user scoping** — automatic session memories are stored per-user,
while explicit project knowledge is tagged per-repo so context never leaks across repos.
- **Entity-aware extraction** - user and project containers get purpose-specific
extraction context so Supermemory stores durable preferences separately from
project/codebase facts.
- 🔒 **Privacy-aware** — anything wrapped in `<private>...</private>` is redacted
before being sent to Supermemory.
- ⚡ **Zero-config install** — one command sets up `~/.codex/config.toml` and
Expand Down Expand Up @@ -108,6 +111,14 @@ derived from the Git common directory when available, so linked worktrees and
Conductor workspaces for the same repository share one project container by default.
Set `SUPERMEMORY_ISOLATE_WORKTREES=true` to keep each worktree isolated.

### Entity context

Codex sends an `entityContext` whenever it saves memories. The user container is
guided toward durable user preferences and workflows; the project container is
guided toward repo architecture, conventions, setup, decisions, and implementation
lessons. Supermemory stores this context on the container tag and uses it to guide
memory extraction.

### Signal extraction (optional)

When `signalExtraction` is enabled, only conversation turns containing signal keywords
Expand Down
10 changes: 8 additions & 2 deletions src/services/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* and saves to the user container.
*/
import { existsSync } from "node:fs";
import { SupermemoryClient } from "./client.js";
import {
SupermemoryClient,
USER_ENTITY_CONTEXT,
} from "./client.js";
import { log } from "./logger.js";
import {
parseTranscript,
Expand Down Expand Up @@ -141,7 +144,10 @@ export async function captureEntries(
// knowledge is still saved via the supermemory-save skill.
// Use customId so all session turns go into the same document.
try {
await client.addMemory(content, tags.user, metadata, { customId: sessionId });
await client.addMemory(content, tags.user, metadata, {
customId: sessionId,
entityContext: USER_ENTITY_CONTEXT,
});

const lastEntry = newEntries[newEntries.length - 1];
setLastCapturedIndex(sessionId, lastEntry.index);
Expand Down
34 changes: 33 additions & 1 deletion src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ export interface ProfileWithSearchResult {
error?: string;
}

export const USER_ENTITY_CONTEXT = `Developer coding session transcript for a persistent user profile.

EXTRACT:
- User preferences: preferred languages, frameworks, libraries, editors, workflows, and communication style
- Stable habits: testing style, code review expectations, formatting preferences, privacy preferences
- Repeated personal decisions: tools the user consistently chooses or avoids
- Long-lived learnings: concepts the user learned or wants remembered across projects

SKIP:
- Project-specific architecture unless it reflects a durable user preference
- One-off assistant suggestions the user did not accept
- Low-level implementation details that only matter inside the current repository`;

export const PROJECT_ENTITY_CONTEXT = `Project/codebase knowledge from Codex coding sessions.

EXTRACT:
- Architecture: repo structure, services, modules, data flow, and integration boundaries
- Conventions: naming, component patterns, API patterns, testing practices, and style rules
- Decisions: chosen approaches, tradeoffs, migrations, and rejected alternatives
- Setup: commands, environment requirements, deployment notes, and debugging workflows
- Implementation lessons: bugs fixed, root causes, and reusable project-specific context

SKIP:
- Generic user preferences that are not specific to this project
- Verbatim assistant explanations unless they became an accepted project decision
- Transient command output with no lasting project value`;

export class SupermemoryClient {
private client: Supermemory | null = null;

Expand Down Expand Up @@ -190,12 +217,13 @@ export class SupermemoryClient {
content: string,
containerTag: string,
metadata?: { type?: MemoryType; tool?: string; [key: string]: unknown },
options?: { customId?: string }
options?: { customId?: string; entityContext?: string }
) {
log("addMemory: start", {
containerTag,
contentLength: content.length,
customId: options?.customId,
hasEntityContext: !!options?.entityContext,
});
try {
// Always stamp `sm_source` so mono's `document.source` column attributes
Expand All @@ -213,6 +241,7 @@ export class SupermemoryClient {
containerTag: string;
metadata?: Record<string, string | number | boolean | string[]>;
customId?: string;
entityContext?: string;
} = {
content,
containerTag,
Expand All @@ -221,6 +250,9 @@ export class SupermemoryClient {
if (options?.customId) {
payload.customId = options.customId;
}
if (options?.entityContext) {
payload.entityContext = options.entityContext;
}
const result = await withTimeout(
this.getClient().memories.add(payload),
TIMEOUT_MS
Expand Down
30 changes: 25 additions & 5 deletions src/skills/save-memory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isConfigured, validateContainerTag } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { CONFIG, isConfigured, validateContainerTag } from "../config.js";
import { PROJECT_ENTITY_CONTEXT, SupermemoryClient } from "../services/client.js";
import { getProjectName, getProjectTag } from "../services/tags.js";

function parseArgs(args: string[]): { content: string; containerTag?: string } {
Expand All @@ -17,6 +17,25 @@ function parseArgs(args: string[]): { content: string; containerTag?: string } {
return { content: contentParts.join(" "), containerTag };
}

function getEntityContext(containerTag: string | undefined): string {
if (!containerTag) return PROJECT_ENTITY_CONTEXT;

const customContainer = CONFIG.customContainers.find((c) => c.tag === containerTag);
if (!customContainer) return PROJECT_ENTITY_CONTEXT;

return `Custom Codex memory container.

Purpose: ${customContainer.description}

EXTRACT:
- Memories that match this container's purpose
- Stable facts, preferences, decisions, workflows, and implementation lessons relevant to this container

SKIP:
- Unrelated project or user context that belongs in another container
- One-off assistant suggestions the user did not accept`;
}

async function main(): Promise<void> {
if (!isConfigured()) {
console.error(
Expand Down Expand Up @@ -46,7 +65,6 @@ async function main(): Promise<void> {
const projectName = getProjectName(process.cwd());
const effectiveTag = containerTag || projectTag;


try {
const metadata = {
type: "project-knowledge" as const,
Expand All @@ -55,12 +73,14 @@ async function main(): Promise<void> {
timestamp: new Date().toISOString(),
};

const result = await client.addMemory(content, effectiveTag, metadata);
const result = await client.addMemory(content, effectiveTag, metadata, {
entityContext: getEntityContext(containerTag),
});

if (result.success) {
if (!containerTag) {
await client.updateContainerTagName(projectTag, `Codex · ${projectName}`);
}
}
const tagLabel = containerTag ? `container '${containerTag}'` : `project '${effectiveTag}'`;
console.log(`Memory saved (id: ${result.id}) to ${tagLabel}`);
} else {
Expand Down
21 changes: 21 additions & 0 deletions test/unit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,27 @@ describe("stripPrivateContent", () => {

// ─── hooks.json format ──────────────────────────────────────────────────────

describe("entity context wiring", () => {
test("client addMemory forwards entityContext into the API payload", () => {
const content = readFileSync(new URL("../src/services/client.ts", import.meta.url), "utf-8");
assert.ok(content.includes("USER_ENTITY_CONTEXT"));
assert.ok(content.includes("PROJECT_ENTITY_CONTEXT"));
assert.ok(content.includes("entityContext?: string"));
assert.ok(content.includes("payload.entityContext = options.entityContext"));
});

test("automatic capture writes user entity context", () => {
const content = readFileSync(new URL("../src/services/capture.ts", import.meta.url), "utf-8");
assert.ok(content.includes("entityContext: USER_ENTITY_CONTEXT"));
});

test("manual save writes project entity context", () => {
const content = readFileSync(new URL("../src/skills/save-memory.ts", import.meta.url), "utf-8");
assert.ok(content.includes("PROJECT_ENTITY_CONTEXT"));
assert.ok(content.includes("entityContext: getEntityContext(containerTag)"));
});
});

describe("hooks.json format", () => {
test("wrapped hooks.json shape is valid JSON", () => {
const recallScript = "/home/user/.codex/supermemory/recall.js";
Expand Down