Skip to content
Merged
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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

<img width="2048" height="512" alt="Untitled_Artwork 3" src="https://github.com/user-attachments/assets/e68fe07d-bc1f-49a1-a40c-3560f1a079b2" />


Long-term memory for OpenClaw. Automatically remembers conversations, recalls relevant context, and builds a persistent user profile — all powered by [Supermemory](https://supermemory.ai) cloud. No local infrastructure required.

> **Requires [Supermemory Pro or above](https://console.supermemory.ai/billing)** - Unlock the state of the art memory for your OpenClaw bot.
> **Requires [Supermemory Pro or above](https://app.supermemory.ai/?view=integrations)** - Unlock the state of the art memory for your OpenClaw bot.

## Install

Expand All @@ -21,7 +20,7 @@ Restart OpenClaw after installing.
openclaw supermemory setup
```

Enter your API key from [console.supermemory.ai](https://console.supermemory.ai). That's it.
Enter your API key from [app.supermemory.ai](https://app.supermemory.ai/?view=integrations). That's it.

### Advanced Setup

Expand Down
43 changes: 37 additions & 6 deletions commands/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
const configPath = path.join(configDir, "openclaw.json")

console.log("\n🧠 Supermemory Setup\n")
console.log("Get your API key from: https://console.supermemory.ai\n")
console.log("Get your API key from: https://app.supermemory.ai\n")

const rl = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -87,7 +87,7 @@ export function registerCliSetup(api: OpenClawPluginApi): void {

console.log("\n🧠 Supermemory Advanced Setup\n")
console.log("Press Enter to use default values shown in [brackets]\n")
console.log("Get your API key from: https://console.supermemory.ai\n")
console.log("Get your API key from: https://app.supermemory.ai\n")

const rl = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -190,6 +190,17 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
console.log(" Invalid value, using default: all")
}

console.log("\nEntity context:")
console.log(
" Instructions that guide what memories are extracted from conversations.",
)
console.log(
" Leave blank to use the built-in default (recommended for most users).",
)
const entityContextInput = await ask(
"Entity context (optional, press Enter for default): ",
)

console.log("\n--- Custom Container Tags (Advanced) ---")
console.log("Define custom containers for AI-driven memory routing.")
const enableCustomContainerTagsInput = await ask(
Expand Down Expand Up @@ -264,6 +275,8 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
if (profileFrequency !== 50)
pluginConfig.profileFrequency = profileFrequency
if (captureMode !== "all") pluginConfig.captureMode = captureMode
if (entityContextInput.trim())
pluginConfig.entityContext = entityContextInput.trim()
if (enableCustomContainerTags)
pluginConfig.enableCustomContainerTags = true
if (customContainerInstructions.trim()) {
Expand Down Expand Up @@ -298,10 +311,20 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
console.log(` Max results: ${maxRecallResults}`)
console.log(` Profile freq: ${profileFrequency}`)
console.log(` Capture mode: ${captureMode}`)
const entityPreview = entityContextInput.trim()
if (entityPreview) {
const truncated =
entityPreview.length > 50
? `${entityPreview.slice(0, 50)}...`
: entityPreview
console.log(` Entity context: "${truncated}"`)
} else {
console.log(" Entity context: (default)")
}
console.log(
` Custom containers: ${enableCustomContainerTags ? "enabled" : "disabled"}`,
` Container tags: ${enableCustomContainerTags ? "enabled" : "disabled"}`,
)
console.log(` Custom containers: ${customContainers.length}`)
console.log(` Container count: ${customContainers.length}`)
if (customContainerInstructions.trim()) {
console.log(
` Routing instructions: "${customContainerInstructions.trim().slice(0, 50)}${customContainerInstructions.length > 50 ? "..." : ""}"`,
Expand Down Expand Up @@ -381,10 +404,18 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
console.log(
` Capture mode: ${pluginConfig.captureMode ?? "all"}`,
)
const entityCtx = pluginConfig.entityContext as string | undefined
if (entityCtx) {
const truncated =
entityCtx.length > 50 ? `${entityCtx.slice(0, 50)}...` : entityCtx
console.log(` Entity context: "${truncated}"`)
} else {
console.log(" Entity context: (default)")
}
console.log(
` Custom containers: ${pluginConfig.enableCustomContainerTags ? "enabled" : "disabled"}`,
` Container tags: ${pluginConfig.enableCustomContainerTags ? "enabled" : "disabled"}`,
)
console.log(` Custom containers: ${customContainers.length}`)
console.log(` Container count: ${customContainers.length}`)
console.log("")
})
},
Expand Down
6 changes: 4 additions & 2 deletions commands/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function registerStubCommands(api: OpenClawPluginApi): void {
export function registerCommands(
api: OpenClawPluginApi,
client: SupermemoryClient,
_cfg: SupermemoryConfig,
cfg: SupermemoryConfig,
getSessionKey: () => string | undefined,
): void {
api.registerCommand({
Expand All @@ -56,6 +56,8 @@ export function registerCommands(
text,
{ type: category, source: "openclaw_command" },
sk ? buildDocumentId(sk) : undefined,
undefined,
cfg.entityContext,
)

const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text
Expand All @@ -81,7 +83,7 @@ export function registerCommands(
log.debug(`/recall command: "${query}"`)

try {
const results = await client.search(query, _cfg.maxRecallResults)
const results = await client.search(query, cfg.maxRecallResults)

if (results.length === 0) {
return { text: `No memories found for: "${query}"` }
Expand Down
8 changes: 8 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { hostname } from "node:os"
import { DEFAULT_ENTITY_CONTEXT } from "./memory.ts"

export type CaptureMode = "everything" | "all"

Expand All @@ -15,6 +16,7 @@ export type SupermemoryConfig = {
maxRecallResults: number
profileFrequency: number
captureMode: CaptureMode
entityContext: string
debug: boolean
enableCustomContainerTags: boolean
customContainers: CustomContainer[]
Expand All @@ -29,6 +31,7 @@ const ALLOWED_KEYS = [
"maxRecallResults",
"profileFrequency",
"captureMode",
"entityContext",
"debug",
"enableCustomContainerTags",
"customContainers",
Expand Down Expand Up @@ -117,6 +120,10 @@ export function parseConfig(raw: unknown): SupermemoryConfig {
cfg.captureMode === "everything"
? ("everything" as const)
: ("all" as const),
entityContext:
typeof cfg.entityContext === "string" && cfg.entityContext.trim()
? cfg.entityContext.trim()
: DEFAULT_ENTITY_CONTEXT,
debug: (cfg.debug as boolean) ?? false,
enableCustomContainerTags:
(cfg.enableCustomContainerTags as boolean) ?? false,
Expand All @@ -140,6 +147,7 @@ export const supermemoryConfigSchema = {
maxRecallResults: { type: "number" },
profileFrequency: { type: "number" },
captureMode: { type: "string", enum: ["all", "everything"] },
entityContext: { type: "string" },
debug: { type: "boolean" },
enableCustomContainerTags: { type: "boolean" },
customContainers: {
Expand Down
10 changes: 6 additions & 4 deletions hooks/capture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { SupermemoryClient } from "../client.ts"
import type { SupermemoryConfig } from "../config.ts"
import { log } from "../logger.ts"
import { buildDocumentId, ENTITY_CONTEXT } from "../memory.ts"
import { buildDocumentId } from "../memory.ts"

const SKIPPED_PROVIDERS = ["exec-event", "cron-event", "heartbeat"]

function getLastTurn(messages: unknown[]): unknown[] {
let lastUserIdx = -1
Expand Down Expand Up @@ -31,8 +33,8 @@ export function buildCaptureHandler(
log.info(
`agent_end fired: provider="${ctx.messageProvider}" success=${event.success}`,
)
const provider = ctx.messageProvider
if (provider === "exec-event" || provider === "cron-event") {
const provider = ctx.messageProvider as string
if (SKIPPED_PROVIDERS.includes(provider)) {
return
}

Expand Down Expand Up @@ -107,7 +109,7 @@ export function buildCaptureHandler(
{ source: "openclaw", timestamp: new Date().toISOString() },
customId,
undefined,
ENTITY_CONTEXT,
cfg.entityContext,
)
} catch (err) {
log.error("capture failed", err)
Expand Down
32 changes: 30 additions & 2 deletions memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,36 @@ export function detectCategory(text: string): MemoryCategory {
return "other"
}

export const ENTITY_CONTEXT =
"Messages are tagged with [role: user] and [role: assistant]. Only create memories from what the user actually said — their preferences, decisions, and important personal details. Agent (assistant) responses are just context, not facts to remember. Only remember things that will be useful later. Ignore noise like greetings or status updates."
export const DEFAULT_ENTITY_CONTEXT = `Conversation between a user and an AI assistant. Format: [role: user] ... [user:end] and [role: assistant] ... [assistant:end].

You do NOT need to generate memories for every message. Most messages are not worth remembering. Only extract things that will be useful in FUTURE conversations.

REMEMBER (lasting personal facts):
- "doesn't eat pork or beef" ← dietary restriction, useful forever
- "prefers TypeScript over JavaScript" ← preference
- "works at Acme Corp as a backend engineer" ← personal detail
- "lives in San Francisco" ← personal detail
- "uses Neovim, prefers dark themes" ← preference
- "building a recipe app in Next.js" ← ongoing project
- "weekly standup on Mondays at 10am EST" ← routine
- "remember my server IP is 192.168.1.100" ← user explicitly asked to remember

DO NOT REMEMBER (session-specific, ephemeral, or assistant-generated):
- "looking for food recommendations" ← temporary intent, not a lasting fact
- "wants a list of YC companies" ← one-time task, not a preference
- "found 193 YC companies from the directory" ← the ASSISTANT did this, not the user
- "saved a JSON file at /path/to/file" ← the ASSISTANT did this
- "is using Algolia API to search" ← implementation detail of current task
- "wants chicken pho, ramen, udon..." ← assistant's suggestions, not user's preference
- Any action the assistant performed (searching, writing files, generating code)
- Any recommendation or list the assistant provided
- Any in-progress task status or intermediate step

KEY RULES:
- The assistant's output is CONTEXT ONLY — never attribute assistant actions to the user
- If the user asks "find X" or "do Y", that is a one-time request, NOT a memory
- Only store preferences if the user explicitly states them ("I like...", "I prefer...", "I always...")
- When in doubt, do NOT create a memory. Less is more.`

export function buildDocumentId(sessionKey: string): string {
const sanitized = sessionKey
Expand Down
9 changes: 8 additions & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"label": "Supermemory API Key",
"sensitive": true,
"placeholder": "sm_...",
"help": "Your API key from console.supermemory.ai (or use ${SUPERMEMORY_OPENCLAW_API_KEY})"
"help": "Your API key from app.supermemory.ai (or use ${SUPERMEMORY_OPENCLAW_API_KEY})"
},
"containerTag": {
"label": "Container Tag",
Expand Down Expand Up @@ -39,6 +39,12 @@
"help": "'all' (default) = filter out short texts and injected context, 'everything' = capture all messages",
"advanced": true
},
"entityContext": {
"label": "Entity Context",
"placeholder": "Custom instructions for memory extraction...",
"help": "Instructions that guide what memories are extracted from conversations. Leave blank to use the built-in default.",
"advanced": true
},
"debug": {
"label": "Debug Logging",
"help": "Enable verbose debug logs for API calls and responses",
Expand Down Expand Up @@ -71,6 +77,7 @@
"maxRecallResults": { "type": "number", "minimum": 1, "maximum": 20 },
"profileFrequency": { "type": "number", "minimum": 1, "maximum": 500 },
"captureMode": { "type": "string", "enum": ["everything", "all"] },
"entityContext": { "type": "string" },
"debug": { "type": "boolean" },
"enableCustomContainerTags": { "type": "boolean" },
"customContainers": {
Expand Down
3 changes: 2 additions & 1 deletion tools/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
export function registerStoreTool(
api: OpenClawPluginApi,
client: SupermemoryClient,
_cfg: SupermemoryConfig,
cfg: SupermemoryConfig,
getSessionKey: () => string | undefined,
): void {
api.registerTool(
Expand Down Expand Up @@ -48,6 +48,7 @@ export function registerStoreTool(
{ type: category, source: "openclaw_tool" },
customId,
params.containerTag,
cfg.entityContext,
)

const preview =
Expand Down