feat(plugins): share native session handoff and soften memory search
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Persistent memory for [OpenCode](https://opencode.ai). Your agent remembers decisions, preferences, and learnings across sessions automatically.
|
||||
|
||||
Current package version: `0.3.0`. This native TypeScript integration keeps its own tools and scopes while sharing redaction and lifecycle utilities with [agent-plugin-core](../agent-plugin-core/README.md).
|
||||
Current package version: `0.4.0`. This native TypeScript integration keeps its own tools and scopes while sharing redaction and lifecycle utilities with [agent-plugin-core](../agent-plugin-core/README.md).
|
||||
|
||||
## Install
|
||||
|
||||
@@ -31,12 +31,21 @@ Restart OpenCode.
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| **10 Native Memory Tools** | `add_memory`, `search_memories`, `get_memories`, `update_memory`, `delete_memory`, and more — registered as OpenCode tools, backed by the `mem0ai` SDK (no MCP server required) |
|
||||
| **Session handoff** | `/mem0-handoff codex` continues the current session in Codex |
|
||||
| **Lifecycle Hooks** | Auto-search on session start and every prompt, error memory lookup, compaction context, secret redaction |
|
||||
| **7 Skills** | `/mem0-remember`, `/mem0-tour`, `/mem0-search`, `/mem0-status`, `/mem0-scope`, `/mem0-forget`, `/mem0-context-loader` — discovered in place from the plugin via OpenCode's `skills.paths` |
|
||||
|
||||
## Session handoff
|
||||
|
||||
Run `/mem0-handoff codex` to continue the current OpenCode session in Codex. The plugin reads native session context, including readable compaction summaries and completed tool outcomes.
|
||||
|
||||
Requires **Python 3.11+** available as `python3` and an installed, signed-in Codex CLI with native session import support. Handoff works independently of Mem0 credentials and never sends the transcript through the Mem0 API. Unsupported content, missing results, and unrelated unfinished calls fail explicitly.
|
||||
|
||||
The shared engine is packaged in `dist/` by [agent-plugin-core](../agent-plugin-core/README.md); it is not maintained separately in this plugin. Failed imports save a private recovery bundle under `~/.mem0/handoffs/`. See the [session handoff guide](../../docs/integrations/session-handoff.mdx) for supported formats, privacy, and recovery.
|
||||
|
||||
## Hooks
|
||||
|
||||
Pure TypeScript — no Python, no shell scripts. Memory operations are native OpenCode tools backed by the [mem0ai](https://www.npmjs.com/package/mem0ai) SDK directly.
|
||||
Memory hooks use TypeScript. Session handoff uses the bundled Python importer. Memory operations are native OpenCode tools backed by the [mem0ai](https://www.npmjs.com/package/mem0ai) SDK directly.
|
||||
|
||||
| Hook | Event | What it does |
|
||||
|------|-------|-------------|
|
||||
|
||||
@@ -129,7 +129,8 @@ describe("resolveApiKey", () => {
|
||||
message: "MEM0_API_KEY environment variable not set. Get one at https://app.mem0.ai/dashboard/api-keys",
|
||||
},
|
||||
});
|
||||
expect(plugin).toEqual({});
|
||||
expect(Object.keys(plugin.tool ?? {})).toEqual(["mem0_handoff"]);
|
||||
expect(plugin["chat.message"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("recovers the issue's shell-profile startup path", async () => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {afterEach, describe, expect, mock, test} from "bun:test";
|
||||
const shared = await import("../agent-plugin-core/typescript/src/handoff.ts");
|
||||
const run = mock(async (_script: URL, _bundle: unknown) => "Created Codex task");
|
||||
mock.module("../agent-plugin-core/typescript/src/handoff.ts", () => ({...shared, runHandoff: run}));
|
||||
const {createHandoffTool, activeMessages, registerHandoffCommand} = await import("./handoff");
|
||||
afterEach(() => {run.mockReset(); run.mockResolvedValue("Created Codex task");});
|
||||
const user = (id: string, text: string) => ({info: {role: "user", id}, parts: [{type: "text", text}]});
|
||||
const toolMessage = (id: string, tool = "mem0_handoff") => ({info: {role: "assistant", id, time: {completed: 1}}, parts: [{type: "tool", callID: id, tool, state: {status: "running", input: {}}}]});
|
||||
function tool(messages: any[]) {
|
||||
return createHandoffTool({session: {
|
||||
get: async () => ({data: {id: "native", title: "Native task", directory: "/tmp"}}),
|
||||
messages: async () => ({data: messages}),
|
||||
}} as any);
|
||||
}
|
||||
const context = {sessionID: "native", messageID: "handoff"} as any;
|
||||
describe("native OpenCode handoff", () => {
|
||||
test("exports current full context and skips only its own invocation", async () => {
|
||||
const config: any = {};
|
||||
registerHandoffCommand(config);
|
||||
expect(config.command["mem0-handoff"].template).toContain("$ARGUMENTS");
|
||||
expect(await tool([user("u", "x".repeat(20000)), toolMessage("handoff")]).execute({}, context)).toBe("Created Codex task");
|
||||
const bundle = run.mock.calls[0][1] as any;
|
||||
expect(bundle.source.session_id).toBe("native");
|
||||
expect(bundle.items[0].content[0].text.length).toBe(20000);
|
||||
expect(bundle.items).toHaveLength(1);
|
||||
});
|
||||
test("preserves completed tools and refuses any other unfinished tool", async () => {
|
||||
const completed = {info: {role: "assistant", id: "done", time: {completed: 1}}, parts: [{type: "tool", callID: "done", tool: "read", state: {status: "completed", input: {path: "a"}, output: "full result", time: {}}}]};
|
||||
await tool([user("u", "task"), completed, toolMessage("handoff")]).execute({}, context);
|
||||
expect(JSON.stringify(run.mock.calls[0][1])).toContain("full result");
|
||||
await expect(tool([user("u", "task"), toolMessage("other", "read"), toolMessage("handoff")]).execute({}, context)).rejects.toThrow("unfinished");
|
||||
});
|
||||
test("refuses a different unfinished assistant stream", async () => {
|
||||
await expect(tool([user("u", "task"), {info: {role: "assistant", id: "stream", time: {}}, parts: [{type: "text", text: "partial"}]}]).execute({}, context)).rejects.toThrow("unfinished assistant");
|
||||
});
|
||||
test("rejects a failed assistant response even with a completed timestamp", async () => {
|
||||
await expect(tool([user("u", "task"), {
|
||||
info: {role: "assistant", id: "failed", time: {completed: 1}, error: {name: "MessageAbortedError", data: {message: "aborted"}}},
|
||||
parts: [{type: "text", text: "partial answer"}],
|
||||
}]).execute({}, context)).rejects.toThrow("interrupted or failed");
|
||||
});
|
||||
test("native compaction retains its summary and retained tail, excluding older history", () => {
|
||||
const old = user("old", "old context");
|
||||
const tail = user("tail", "retained");
|
||||
const boundary = {info: {role: "user", id: "c"}, parts: [{type: "compaction", tail_start_id: "tail"}]};
|
||||
const summary = {info: {role: "assistant", id: "s", parentID: "c", summary: true, finish: "stop"}, parts: [{type: "text", text: "Summary"}]};
|
||||
expect(activeMessages([old, tail, boundary, summary, user("new", "continue")] as any).map(m => m.info.id)).toEqual(["c", "s", "tail", "new"]);
|
||||
});
|
||||
test("fails on unsupported remote media and propagates importer errors", async () => {
|
||||
const image = {info: {role: "user", id: "u"}, parts: [{type: "file", mime: "image/png", url: "https://private/image.png"}]};
|
||||
await expect(tool([image]).execute({}, context)).rejects.toThrow("unavailable locally");
|
||||
run.mockRejectedValue(new Error("saved at /tmp/retry.json"));
|
||||
await expect(tool([user("u", "hi")]).execute({}, context)).rejects.toThrow("saved at /tmp/retry.json");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import {tool, type PluginInput} from "@opencode-ai/plugin";
|
||||
import type {SessionMessagesResponse} from "@opencode-ai/sdk";
|
||||
import {readFile} from "node:fs/promises";
|
||||
import {buildHandoffBundle, runHandoff} from "../agent-plugin-core/typescript/src/handoff.ts";
|
||||
|
||||
type NativeMessage = SessionMessagesResponse[number];
|
||||
/** OpenCode's native filterCompacted order: latest summary, retained tail, later turns. */
|
||||
export function activeMessages(messages: NativeMessage[]): NativeMessage[] {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const info = messages[i].info;
|
||||
if (info.role !== "assistant" || !info.summary || !info.finish || info.error) continue;
|
||||
const boundary = messages.findIndex(m => m.info.id === info.parentID && m.parts.some(p => p.type === "compaction"));
|
||||
if (boundary < 0) throw new Error("OpenCode compaction boundary is unavailable.");
|
||||
const part = messages[boundary].parts.find(p => p.type === "compaction") as {tail_start_id?: string};
|
||||
if (!part.tail_start_id) return messages.slice(boundary);
|
||||
const tail = messages.findIndex(m => m.info.id === part.tail_start_id);
|
||||
if (tail < 0 || tail >= boundary) throw new Error("OpenCode retained compaction history is unavailable.");
|
||||
return [...messages.slice(boundary, i + 1), ...messages.slice(tail, boundary), ...messages.slice(i + 1)];
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function createHandoffTool(client: PluginInput["client"]) {
|
||||
return tool({
|
||||
description: "Only on explicit user request, continue this OpenCode session in a new Codex task with its full active conversation and completed tool results. Requires Python 3.11+ and an installed, signed-in Codex CLI.",
|
||||
args: {},
|
||||
async execute(_args, context) {
|
||||
const [session, history] = await Promise.all([
|
||||
client.session.get({path: {id: context.sessionID}, throwOnError: true}),
|
||||
client.session.messages({path: {id: context.sessionID}, throwOnError: true}),
|
||||
]);
|
||||
if (!session.data || !history.data) throw new Error("The active OpenCode session is unavailable.");
|
||||
const messages: {role: string; content: unknown}[] = [];
|
||||
let omittedInvocation = false;
|
||||
let pruned = false;
|
||||
for (const message of activeMessages(history.data)) {
|
||||
if (message.info.role === "assistant" && message.info.error) {
|
||||
throw new Error("OpenCode assistant response was interrupted or failed; complete it before handoff.");
|
||||
}
|
||||
if (message.info.role === "assistant" && message.info.id !== context.messageID && !message.info.time.completed) {
|
||||
throw new Error("OpenCode has another unfinished assistant response; finish it before handoff.");
|
||||
}
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "text") {
|
||||
if (!part.ignored) messages.push({role: message.info.role, content: part.text});
|
||||
} else if (part.type === "reasoning") {
|
||||
messages.push({role: message.info.role, content: [{type: "reasoning", text: part.text}]});
|
||||
} else if (part.type === "tool") {
|
||||
if (message.info.id === context.messageID && part.tool === "mem0_handoff" && part.state.status === "running") {
|
||||
if (omittedInvocation) throw new Error("Multiple active handoff calls; invoke one handoff at a time.");
|
||||
omittedInvocation = true;
|
||||
continue;
|
||||
}
|
||||
if (part.state.status !== "completed" && part.state.status !== "error") throw new Error(`OpenCode tool ${part.tool} is unfinished; finish it before handoff.`);
|
||||
messages.push({role: "assistant", content: [{type: "toolCall", id: part.callID, name: part.tool, arguments: part.state.input}]});
|
||||
const compacted = part.state.status === "completed" && Boolean(part.state.time.compacted);
|
||||
pruned ||= compacted;
|
||||
const interruptedOutput = part.state.status === "error" && part.state.metadata?.interrupted === true && typeof part.state.metadata.output === "string" ? part.state.metadata.output : undefined;
|
||||
const content: unknown[] = [{type: "text", text: part.state.status === "error" ? interruptedOutput ?? part.state.error : compacted ? "[Old tool result content cleared]" : part.state.output}];
|
||||
if (part.state.status === "completed" && !compacted) for (const attachment of part.state.attachments ?? []) content.push(await fileContent(attachment));
|
||||
messages.push({role: "toolResult", content, ...{toolCallId: part.callID, isError: part.state.status === "error" && interruptedOutput === undefined}});
|
||||
} else if (part.type === "file") {
|
||||
// OpenCode expands these descriptors into adjacent text parts before saving.
|
||||
if (message.info.role !== "user" || !["text/plain", "application/x-directory"].includes(part.mime)) {
|
||||
messages.push({role: message.info.role, content: [await fileContent(part)]});
|
||||
}
|
||||
} else if (part.type === "compaction") {
|
||||
messages.push({role: "user", content: "What did we do so far?"});
|
||||
} else if (part.type === "subtask") {
|
||||
messages.push({role: "user", content: "The following tool was executed by the user"});
|
||||
} else if (!["step-start", "step-finish", "snapshot", "patch", "agent", "retry"].includes(part.type)) {
|
||||
throw new Error(`Unsupported OpenCode session part: ${part.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const bundle = await buildHandoffBundle({host: "opencode", session_id: context.sessionID, title: session.data.title, cwd: session.data.directory}, messages);
|
||||
if (pruned) bundle.warnings.push("OpenCode already cleared older tool outputs; its active-context placeholders were preserved.");
|
||||
return runHandoff(new URL("./session_handoff.py", import.meta.url), bundle);
|
||||
},
|
||||
});
|
||||
}
|
||||
async function fileContent(file: {mime: string; url: string}) {
|
||||
if (!file.mime.startsWith("image/")) throw new Error(`Unsupported OpenCode attachment type: ${file.mime}`);
|
||||
const url = file.url.startsWith("data:") ? file.url : file.url.startsWith("file:") ? `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}` : undefined;
|
||||
if (!url) throw new Error("OpenCode attachment bytes are unavailable locally.");
|
||||
return {type: "image", image_url: url};
|
||||
}
|
||||
export function registerHandoffCommand(config: {command?: Record<string, {template: string; description?: string}>}) {
|
||||
config.command ??= {};
|
||||
config.command["mem0-handoff"] = {
|
||||
description: "Continue this OpenCode session in Codex",
|
||||
template: `Transfer this OpenCode session to Codex using mem0_handoff. Usage: /mem0-handoff codex\nArguments: $ARGUMENTS\nRequire the single target codex. If invalid, show usage and do not invoke the tool. Do not summarize or read other sessions. Call mem0_handoff directly, alone, and report its result.`,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import {SEARCH_GUIDANCE} from "../agent-plugin-core/typescript/src/search_guidance.ts";
|
||||
import { resolveToolScope } from "../agent-plugin-core/typescript/src/scoping.ts";
|
||||
// Mem0 memory plugin for OpenCode: captures and recalls memories across sessions
|
||||
// (add / search / manage) via the Mem0 platform, wired through OpenCode plugin hooks.
|
||||
// Memory operations are exposed as native OpenCode tools backed by the mem0ai SDK
|
||||
// (no MCP server required).
|
||||
import type {Plugin} from "@opencode-ai/plugin";
|
||||
import type {Hooks, Plugin} from "@opencode-ai/plugin";
|
||||
import {tool} from "@opencode-ai/plugin";
|
||||
import {MemoryClient} from "mem0ai";
|
||||
import {userInfo} from "os";
|
||||
@@ -17,6 +18,7 @@ import {captureEvent} from "./telemetry";
|
||||
import {asScope, scopeSearchFilters, scopeWriteParams, resolveDefaultScope, SCOPE_GUIDANCE, type Scope} from "./scope";
|
||||
import {parseProjectFromRemote} from "./project";
|
||||
import {resolveApiKey} from "./api-key";
|
||||
import {createHandoffTool, registerHandoffCommand} from "./handoff";
|
||||
import {createMemoryLifecycle} from "../agent-plugin-core/typescript/src/lifecycle.ts";
|
||||
|
||||
async function getUserId(): Promise<string> {
|
||||
@@ -231,8 +233,9 @@ function extractUserText(input: any, output: any): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
const Mem0Plugin: Plugin = async (ctx) => {
|
||||
const Mem0Plugin: Plugin = async (ctx): Promise<Hooks> => {
|
||||
const {$, client} = ctx;
|
||||
const handoffTool = createHandoffTool(client);
|
||||
|
||||
const apiKey = resolveApiKey(process.env, process.env.HOME || process.env.USERPROFILE || homedir());
|
||||
|
||||
@@ -248,7 +251,7 @@ const Mem0Plugin: Plugin = async (ctx) => {
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
return {};
|
||||
return {tool: {mem0_handoff: handoffTool}, config: async (config) => registerHandoffCommand(config)};
|
||||
}
|
||||
|
||||
const mem0 = new MemoryClient({apiKey});
|
||||
@@ -356,6 +359,7 @@ Identity context (resolved at plugin startup):
|
||||
},
|
||||
|
||||
config: async (opencodeConfig: any) => {
|
||||
registerHandoffCommand(opencodeConfig);
|
||||
// Point OpenCode at the plugin's OWN skills directory via `skills.paths`
|
||||
const here = import.meta.filename;
|
||||
const skillsDir = [
|
||||
@@ -376,6 +380,7 @@ Identity context (resolved at plugin startup):
|
||||
},
|
||||
|
||||
tool: {
|
||||
mem0_handoff: handoffTool,
|
||||
add_memory: tool({
|
||||
description: "Add a new memory. This method is called everytime the user informs anything about themselves, their preferences, or anything that has any relevant information which can be useful in the future conversation. This can also be called when the user asks you to remember something. Set infer to false to store the memory verbatim without LLM fact extraction.",
|
||||
args: {
|
||||
@@ -424,7 +429,7 @@ Identity context (resolved at plugin startup):
|
||||
}),
|
||||
|
||||
search_memories: tool({
|
||||
description: "Search stored memories by semantic meaning. Use this proactively before answering when the request may depend on the user's past work, preferences, decisions, or environment -- relevant memories are not always auto-injected. For multi-part or comparative questions, run several searches with different phrasings and combine the results rather than stopping after one (multi-hop).",
|
||||
description: SEARCH_GUIDANCE,
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search query"),
|
||||
user_id: tool.schema.string().optional().describe("User ID"),
|
||||
@@ -636,9 +641,6 @@ Identity context (resolved at plugin startup):
|
||||
}
|
||||
|
||||
if (memoryCount > 0) {
|
||||
systemContext.push(
|
||||
"Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type.",
|
||||
);
|
||||
try {
|
||||
const res = await mem0.search(
|
||||
"recent session state decisions and learnings",
|
||||
@@ -659,9 +661,7 @@ Identity context (resolved at plugin startup):
|
||||
}
|
||||
}
|
||||
|
||||
systemContext.push(
|
||||
"Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id.",
|
||||
);
|
||||
systemContext.push(SEARCH_GUIDANCE);
|
||||
systemContext.push(SCOPE_GUIDANCE);
|
||||
const activeScope = loadDefaultScope();
|
||||
if (activeScope !== "project") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: mem0-context-loader
|
||||
description: Searches and injects relevant memories into context before starting work on a task. Use when beginning a new task, switching context, or when project history, past decisions, or coding conventions need to be loaded.
|
||||
description: Search earlier context when project history, decisions, or preferences could help with a specific question.
|
||||
---
|
||||
|
||||
# Context Loader
|
||||
@@ -9,23 +9,14 @@ Pre-fetches relevant memories to prime context before working on a task.
|
||||
|
||||
## When to use
|
||||
|
||||
- Session start (invoke manually or auto-triggered by skill description matching)
|
||||
- User starts work on a specific feature or file set
|
||||
- Complex multi-step task begins
|
||||
- User says "what do we know about X" or "context for X"
|
||||
Use when earlier work could explain a decision, fix, command, or preference you
|
||||
need. Skip this skill when the context already answers the question.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract topics** from current message/task. Identify: file paths, module names, feature areas, error patterns.
|
||||
|
||||
2. **Run 2-4 parallel `search_memories` calls** with different angles:
|
||||
|
||||
| Query angle | Filter | Purpose |
|
||||
|---|---|---|
|
||||
| Feature/module name | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}` | Architecture decisions |
|
||||
| File paths mentioned | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "convention"}}]}` | Coding patterns |
|
||||
| Error keywords (if any) | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "anti_pattern"}}]}` | Known pitfalls |
|
||||
| Broad project context | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}` | Catch-all |
|
||||
2. **Search one focused question** using `search_memories`. Keep the default project scope. Search again only if a specific gap remains.
|
||||
|
||||
3. **Deduplicate** results by memory ID across all search responses.
|
||||
|
||||
|
||||
@@ -27,10 +27,9 @@ When an ID is detected:
|
||||
|
||||
### Step 2: Search
|
||||
|
||||
Run 2 parallel `search_memories` calls:
|
||||
|
||||
1. Broad: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
|
||||
Call `search_memories` with `query=<user's query>`,
|
||||
`filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`.
|
||||
Search again only if a specific gap remains.
|
||||
|
||||
### Step 3: Display
|
||||
|
||||
|
||||
@@ -36,9 +36,7 @@ If `--all-projects` is NOT present, use the standard single-project flow below.
|
||||
When `/mem0-tour` receives a search query argument (e.g., `/mem0-tour auth middleware`)
|
||||
WITHOUT `--all-projects`, run in **peek mode** — compact one-liner results:
|
||||
|
||||
1. Run 2 parallel `search_memories` calls:
|
||||
- Broad: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
|
||||
1. Call `search_memories` with `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`. Search again only if a specific gap remains.
|
||||
2. Deduplicate by ID, display compact results:
|
||||
```
|
||||
## mem0 search: "<query>" (<N> results)
|
||||
@@ -60,15 +58,10 @@ Call `get_memories` to fetch all memories for this project:
|
||||
|
||||
`filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=100`
|
||||
|
||||
### Step 2: Run supplementary semantic searches
|
||||
### Step 2: Check coverage
|
||||
|
||||
In parallel, run these `search_memories` calls to get relevance-ranked results for key topics:
|
||||
|
||||
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
- `query="bugs errors failures anti-patterns"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
- `query="project setup tooling conventions preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
|
||||
**Do NOT filter by `metadata.type` in these calls.** The platform auto-assigns `categories` — filtering on `metadata.type` misses memories that were auto-categorized but don't have an explicit `metadata.type`.
|
||||
Use the fetched memories for the tour. If the user asks about a specific topic
|
||||
that the results do not cover, search that question with the same project filters.
|
||||
|
||||
### Step 3: Merge and group
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/opencode-plugin",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
|
||||
"main": "dist/index.js",
|
||||
@@ -35,8 +35,8 @@
|
||||
"opencode-skills"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext]",
|
||||
"dev": "bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext] --watch",
|
||||
"build": "bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext] && node ../agent-plugin-core/build/package_handoff.mjs",
|
||||
"dev": "mkdir -p dist && node ../agent-plugin-core/build/package_handoff.mjs && bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext] --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"prepack": "bun run build",
|
||||
"postpack": ""
|
||||
|
||||
Reference in New Issue
Block a user