From 6ea9102914129eda2075f005905cf3f74889b185 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 5 Jun 2026 16:29:51 +0530 Subject: [PATCH] Add Codex entity context --- README.md | 11 +++++++++++ src/services/capture.ts | 10 ++++++++-- src/services/client.ts | 34 +++++++++++++++++++++++++++++++++- src/skills/save-memory.ts | 30 +++++++++++++++++++++++++----- test/unit.mjs | 21 +++++++++++++++++++++ 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7ab7b0d..3613b6d 100644 --- a/README.md +++ b/README.md @@ -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 `...` is redacted before being sent to Supermemory. - โšก **Zero-config install** โ€” one command sets up `~/.codex/config.toml` and @@ -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 diff --git a/src/services/capture.ts b/src/services/capture.ts index 1d26d45..a97cb9d 100644 --- a/src/services/capture.ts +++ b/src/services/capture.ts @@ -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, @@ -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); diff --git a/src/services/client.ts b/src/services/client.ts index c8f4eff..cae51f7 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -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; @@ -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 @@ -213,6 +241,7 @@ export class SupermemoryClient { containerTag: string; metadata?: Record; customId?: string; + entityContext?: string; } = { content, containerTag, @@ -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 diff --git a/src/skills/save-memory.ts b/src/skills/save-memory.ts index a6c5294..35ca32d 100644 --- a/src/skills/save-memory.ts +++ b/src/skills/save-memory.ts @@ -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 } { @@ -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 { if (!isConfigured()) { console.error( @@ -46,7 +65,6 @@ async function main(): Promise { const projectName = getProjectName(process.cwd()); const effectiveTag = containerTag || projectTag; - try { const metadata = { type: "project-knowledge" as const, @@ -55,12 +73,14 @@ async function main(): Promise { 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 { diff --git a/test/unit.mjs b/test/unit.mjs index c80fcf7..4b8b8b9 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -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";