Skip to content

Commit b25ace8

Browse files
committed
Unify OpenCode agent memory containers
Use the shared repository container with sm_scope metadata and retain legacy Claude, Codex, and OpenCode reads.
1 parent dd7cf62 commit b25ace8

9 files changed

Lines changed: 1005 additions & 194 deletions

File tree

README.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Run `/supermemory-init` to have the agent explore and memorize the codebase.
125125

126126
On first message, the agent receives (invisible to user):
127127

128-
- User profile (cross-project preferences)
128+
- Personal profile for the current project
129129
- Project memories (all project knowledge)
130130
- Relevant user memories (semantic search)
131131

@@ -193,19 +193,28 @@ The `supermemory` tool is available to the agent:
193193
| `list` | `scope?`, `limit?` | List memories |
194194
| `forget` | `memoryId`, `scope?` | Delete memory |
195195

196-
**Scopes:** `user` (cross-project), `project` (default)
196+
**Scopes:** `user` (personal memories for the current project), `project` (default)
197197

198198
**Types:** `project-config`, `architecture`, `error-solution`, `preference`, `learned-pattern`, `conversation`
199199

200-
OpenCode sends entity context when saving memories so Supermemory can extract
201-
different facts for user profile memories versus project/codebase knowledge.
200+
OpenCode sends the same shared coding-agent entity context as Claude Code and
201+
Codex. Personal and project memories are distinguished with `sm_scope`
202+
metadata inside the shared repository container.
202203

203204
## Memory Scoping
204205

205-
| Scope | Tag | Persists |
206-
| ------- | -------------------------------------- | ------------ |
207-
| User | `opencode_user_{sha256(git email)}` | All projects |
208-
| Project | `opencode_project_{sha256(directory)}` | This project |
206+
| Scope | Tag | Metadata |
207+
| ------- | ------------------------------------------- | ----------------------- |
208+
| User | `repo_{project-name}__{repository-hash}` | `sm_scope: "personal"` |
209+
| Project | `repo_{project-name}__{repository-hash}` | `sm_scope: "project"` |
210+
211+
The repository hash comes from the normalized Git `origin` remote, so Claude
212+
Code, Codex, and OpenCode use the same container for the same repository.
213+
Repositories with the same name but different remotes remain isolated. Without
214+
an origin remote, OpenCode falls back to the repository's real filesystem path.
215+
OpenCode also reads previous `user_project_*`, `repo_<project-name>`,
216+
`claudecode_project_*`, `codex_user_*`, `codex_project_*`, `opencode_user_*`,
217+
and `opencode_project_*` containers, so upgrading does not require a migration.
209218

210219
## Configuration
211220

@@ -234,10 +243,10 @@ Create `~/.config/opencode/supermemory.jsonc`:
234243
// Include user profile in context
235244
"injectProfile": true,
236245

237-
// Prefix for container tags (used when userContainerTag/projectContainerTag not set)
246+
// Legacy prefix retained when reading containers made by older versions
238247
"containerTagPrefix": "opencode",
239248

240-
// Optional: Set exact user container tag (overrides auto-generated tag)
249+
// Optional legacy personal container to keep reading
241250
"userContainerTag": "my-custom-user-tag",
242251

243252
// Optional: Set exact project container tag (overrides auto-generated tag)
@@ -255,26 +264,28 @@ All fields optional. Env var `SUPERMEMORY_API_KEY` takes precedence over config
255264

256265
### Container Tag Selection
257266

258-
By default, container tags are auto-generated using `containerTagPrefix` plus a hash:
267+
By default, new writes use:
259268

260-
- User tag: `{prefix}_user_{hash(git_email)}`
261-
- Project tag: `{prefix}_project_{hash(directory)}`
269+
- Repository tag: `repo_{project-name}__{hash(normalized-origin-remote)}`
270+
- No origin remote: `repo_{project-name}__{hash(real-repository-path)}`
262271

263-
You can override this by specifying exact container tags:
272+
Older `{prefix}_user_*` and `{prefix}_project_*` containers remain readable.
273+
`userContainerTag` is treated as a legacy personal read. You can still override
274+
the unified write container with `projectContainerTag`:
264275

265276
```jsonc
266277
{
267-
// Use a specific container tag for user memories
278+
// Continue reading a personal container made by an older version
268279
"userContainerTag": "my-team-workspace",
269280

270-
// Use a specific container tag for project memories
281+
// Override the unified container used for new writes
271282
"projectContainerTag": "my-awesome-project",
272283
}
273284
```
274285

275286
This is useful when you want to:
276287

277-
- Share memories across team members (same `userContainerTag`)
288+
- Preserve a legacy personal memory container
278289
- Sync memories between different machines for the same project
279290
- Organize memories using your own naming scheme
280291
- Integrate with existing Supermemory container tags from other tools

src/cli.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ How the codebase works and why:
5757
- Known issues and their solutions
5858
5959
**User-scoped** (\`scope: "user"\`):
60-
- Personal coding preferences across all projects
60+
- Personal coding preferences relevant to this project
6161
- Communication style preferences
6262
- General workflow habits
6363
@@ -598,11 +598,12 @@ async function status(): Promise<number> {
598598
lines.push(`Connected: ${isConfigured() ? "checking..." : "no"}`);
599599
lines.push(`API key: ${maskKey(SUPERMEMORY_API_KEY)} (${getKeySource()})`);
600600
lines.push(`API URL: ${apiUrl}`);
601-
lines.push("Memory scope: current project + user profile");
601+
lines.push("Memory scope: unified project container with personal/project metadata");
602602
lines.push(`Recall mode: ${CONFIG.autoRecallEveryPrompt ? "auto-recall on every prompt" : "session/event based"}`);
603603
lines.push(`Capture cadence: ${CONFIG.captureEveryNTurns > 0 ? `every ${CONFIG.captureEveryNTurns} turn${CONFIG.captureEveryNTurns === 1 ? "" : "s"} + session end` : "session end only"}`);
604-
lines.push(`Project tag: ${tags.project}`);
605-
lines.push(`User tag: ${tags.user}`);
604+
lines.push(`Project container: ${tags.canonical}`);
605+
lines.push(`Personal reads: ${tags.personalReads.join(", ")}`);
606+
lines.push(`Project reads: ${tags.projectReads.join(", ")}`);
606607

607608
if (!isConfigured()) {
608609
lines.push("");
@@ -613,7 +614,11 @@ async function status(): Promise<number> {
613614

614615
const client = new SupermemoryClient();
615616
const [profileResult, accountInfo] = await Promise.all([
616-
client.getProfile(tags.user),
617+
client.getProfileScoped(
618+
tags.canonical,
619+
tags.personalReads,
620+
"personal",
621+
),
617622
getAccountInfo(apiUrl),
618623
]);
619624

src/index.ts

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import type { Plugin, PluginInput } from "@opencode-ai/plugin";
22
import type { Part } from "@opencode-ai/sdk";
33
import { tool } from "@opencode-ai/plugin";
44

5-
import {
6-
PROJECT_ENTITY_CONTEXT,
7-
USER_ENTITY_CONTEXT,
8-
} from "./services/entity-context.js";
5+
import { AGENT_ENTITY_CONTEXT } from "./services/entity-context.js";
96
import { supermemoryClient } from "./services/client.js";
107
import { formatContextForPrompt } from "./services/context.js";
118
import { getTags } from "./services/tags.js";
@@ -27,7 +24,7 @@ The user wants you to remember something. You MUST use the \`supermemory\` tool
2724
2825
Extract the key information the user wants remembered and save it as a concise, searchable memory.
2926
- Use \`scope: "project"\` for project-specific preferences (e.g., "run lint with tests")
30-
- Use \`scope: "user"\` for cross-project preferences (e.g., "prefers concise responses")
27+
- Use \`scope: "user"\` for personal preferences in this project (e.g., "prefers concise responses")
3128
- Choose an appropriate \`type\`: "preference", "project-config", "learned-pattern", etc.
3229
3330
DO NOT skip this step. The user explicitly asked you to remember.`;
@@ -146,9 +143,24 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
146143

147144
if (CONFIG.autoRecallEveryPrompt) {
148145
const [profileResult, userMemoriesResult, projectMemoriesListResult] = await Promise.all([
149-
supermemoryClient.getProfile(tags.user, userMessage),
150-
supermemoryClient.searchMemories(userMessage, tags.user),
151-
supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories),
146+
supermemoryClient.getProfileScoped(
147+
tags.canonical,
148+
tags.personalReads,
149+
"personal",
150+
userMessage,
151+
),
152+
supermemoryClient.searchMemoriesScoped(
153+
userMessage,
154+
tags.canonical,
155+
tags.personalReads,
156+
"personal",
157+
),
158+
supermemoryClient.listMemoriesScoped(
159+
tags.canonical,
160+
tags.projectReads,
161+
"project",
162+
CONFIG.maxProjectMemories,
163+
),
152164
]);
153165

154166
const profile = profileResult.success ? profileResult : null;
@@ -173,7 +185,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
173185
projectMemories
174186
);
175187
} else {
176-
const profileResult = await supermemoryClient.getProfile(tags.user);
188+
const profileResult = await supermemoryClient.getProfileScoped(
189+
tags.canonical,
190+
tags.personalReads,
191+
"personal",
192+
);
177193
const profile = profileResult.success ? profileResult : null;
178194
memoryContext = formatContextForPrompt(profile, { results: [] }, { results: [] });
179195
}
@@ -283,7 +299,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
283299
},
284300
],
285301
scopes: {
286-
user: "Cross-project preferences and knowledge",
302+
user: "Personal preferences and knowledge for this project",
287303
project: "Project-specific knowledge (default)",
288304
},
289305
types: [
@@ -314,16 +330,20 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
314330
}
315331

316332
const scope = args.scope || "project";
317-
const containerTag =
318-
scope === "user" ? tags.user : tags.project;
319-
const entityContext =
320-
scope === "user" ? USER_ENTITY_CONTEXT : PROJECT_ENTITY_CONTEXT;
333+
const internalScope =
334+
scope === "user" ? "personal" : "project";
321335

322336
const result = await supermemoryClient.addMemory(
323337
sanitizedContent,
324-
containerTag,
325-
{ type: args.type },
326-
{ entityContext }
338+
tags.canonical,
339+
{
340+
type: args.type,
341+
project: tags.projectName,
342+
sm_project_id: tags.projectId,
343+
sm_scope: internalScope,
344+
sm_capture_mode: "tool",
345+
},
346+
{ entityContext: AGENT_ENTITY_CONTEXT }
327347
);
328348

329349
if (!result.success) {
@@ -353,9 +373,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
353373
const scope = args.scope;
354374

355375
if (scope === "user") {
356-
const result = await supermemoryClient.searchMemories(
376+
const result = await supermemoryClient.searchMemoriesScoped(
357377
args.query,
358-
tags.user
378+
tags.canonical,
379+
tags.personalReads,
380+
"personal",
359381
);
360382
if (!result.success) {
361383
return JSON.stringify({
@@ -367,9 +389,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
367389
}
368390

369391
if (scope === "project") {
370-
const result = await supermemoryClient.searchMemories(
392+
const result = await supermemoryClient.searchMemoriesScoped(
371393
args.query,
372-
tags.project
394+
tags.canonical,
395+
tags.projectReads,
396+
"project",
373397
);
374398
if (!result.success) {
375399
return JSON.stringify({
@@ -380,46 +404,30 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
380404
return formatSearchResults(args.query, scope, result, args.limit);
381405
}
382406

383-
const [userResult, projectResult] = await Promise.all([
384-
supermemoryClient.searchMemories(args.query, tags.user),
385-
supermemoryClient.searchMemories(args.query, tags.project),
386-
]);
387-
388-
if (!userResult.success || !projectResult.success) {
407+
const result = await supermemoryClient.searchMemoriesMany(
408+
args.query,
409+
tags.allReads,
410+
);
411+
if (!result.success) {
389412
return JSON.stringify({
390413
success: false,
391-
error: userResult.error || projectResult.error || "Failed to search memories",
414+
error: result.error || "Failed to search memories",
392415
});
393416
}
394-
395-
const combined = [
396-
...(userResult.results || []).map((r) => ({
397-
...r,
398-
scope: "user" as const,
399-
})),
400-
...(projectResult.results || []).map((r) => ({
401-
...r,
402-
scope: "project" as const,
403-
})),
404-
].sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
405-
406-
return JSON.stringify({
407-
success: true,
408-
query: args.query,
409-
count: combined.length,
410-
results: combined.slice(0, args.limit || 10).map((r) => ({
411-
id: r.id,
412-
content: r.memory || r.chunk,
413-
similarity: Math.round((r.similarity ?? 0) * 100),
414-
scope: r.scope,
415-
})),
416-
});
417+
return formatSearchResults(
418+
args.query,
419+
undefined,
420+
result,
421+
args.limit,
422+
);
417423
}
418424

419425
case "profile": {
420-
const result = await supermemoryClient.getProfile(
421-
tags.user,
422-
args.query
426+
const result = await supermemoryClient.getProfileScoped(
427+
tags.canonical,
428+
tags.personalReads,
429+
"personal",
430+
args.query,
423431
);
424432

425433
if (!result.success) {
@@ -441,12 +449,16 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
441449
case "list": {
442450
const scope = args.scope || "project";
443451
const limit = args.limit || 20;
444-
const containerTag =
445-
scope === "user" ? tags.user : tags.project;
446-
447-
const result = await supermemoryClient.listMemories(
448-
containerTag,
449-
limit
452+
const internalScope =
453+
scope === "user" ? "personal" : "project";
454+
const readTags =
455+
scope === "user" ? tags.personalReads : tags.projectReads;
456+
457+
const result = await supermemoryClient.listMemoriesScoped(
458+
tags.canonical,
459+
readTags,
460+
internalScope,
461+
limit,
450462
);
451463

452464
if (!result.success) {
@@ -524,7 +536,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
524536
function formatSearchResults(
525537
query: string,
526538
scope: string | undefined,
527-
results: { results?: Array<{ id: string; memory?: string; chunk?: string; similarity?: number }> },
539+
results: { results?: Array<{ id?: string; memory?: string; chunk?: string; similarity?: number }> },
528540
limit?: number
529541
): string {
530542
const memoryResults = results.results || [];

0 commit comments

Comments
 (0)