extensions: add memory-powermem plugin for long-term memory

This commit is contained in:
Teingi 2026-01-29 20:31:52 +08:00
parent 6e09492a37
commit fd7999cefa
3 changed files with 64 additions and 46 deletions

View File

@ -21,25 +21,33 @@ Moltbot long-term memory plugin backed by [PowerMem](https://github.com/oceanbas
## Moltbot configuration
1. Set the memory slot to this plugin:
1. Enable the plugin and set the memory slot to this plugin. Edit your Moltbot config (e.g. `~/.clawdbot/config.json` or `moltbot.json` in your state dir):
```yaml
plugins:
slots:
memory: memory-powermem
config:
memory-powermem:
baseUrl: "http://localhost:8000"
# apiKey: "optional-if-auth-enabled"
autoCapture: true
autoRecall: true
inferOnAdd: true
# userId: "optional-override"
# agentId: "optional-override"
```json
{
"plugins": {
"slots": { "memory": "memory-powermem" },
"entries": {
"memory-powermem": {
"enabled": true,
"config": {
"baseUrl": "http://localhost:8000",
"autoCapture": true,
"autoRecall": true,
"inferOnAdd": true
}
}
}
}
}
```
Optional: `apiKey` (if PowerMem server has auth), `userId`, `agentId` — see Options below.
2. Ensure PowerMem server is running before starting the gateway.
**Auto-capture:** When a conversation ends, the full user/assistant text is sent to PowerMem with `infer: true`; PowerMem extracts and stores memories (no trigger phrases) (e.g. “remember …”, “I like …”, “important”, or Chinese “记得/记住/我喜欢/重要/偏好”). At most 3 chunks per session (each up to 6000 chars). To test: run `moltbot ltm health`, then `moltbot ltm add "User prefers dark mode"` and `moltbot ltm search "dark mode"`. In chat, ask the agent to “remember that I prefer tea” or say “我喜欢用 Python” so a message is auto-captured.
## Options
| Option | Required | Description |
@ -62,6 +70,7 @@ Moltbot long-term memory plugin backed by [PowerMem](https://github.com/oceanbas
- `moltbot ltm search <query> [--limit n]` — Search memories.
- `moltbot ltm health` — Check PowerMem server health.
- `moltbot ltm add "<text>"` — Manually store one memory (for testing or one-off storage).
## Running tests

View File

@ -17,32 +17,6 @@ import {
} from "./config.js";
import { PowerMemClient } from "./client.js";
// ============================================================================
// Rule-based capture filter (same idea as memory-lancedb)
// ============================================================================
const MEMORY_TRIGGERS = [
/zapamatuj si|pamatuj|remember/i,
/preferuji|radši|nechci|prefer/i,
/rozhodli jsme|budeme používat/i,
/\+\d{10,}/,
/[\w.-]+@[\w.-]+\.\w+/,
/můj\s+\w+\s+je|je\s+můj/i,
/my\s+\w+\s+is|is\s+my/i,
/i (like|prefer|hate|love|want|need)/i,
/always|never|important/i,
];
function shouldCapture(text: string): boolean {
if (text.length < 10 || text.length > 500) return false;
if (text.includes("<relevant-memories>")) return false;
if (text.startsWith("<") && text.includes("</")) return false;
if (text.includes("**") && text.includes("\n-")) return false;
const emojiCount = (text.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length;
if (emojiCount > 3) return false;
return MEMORY_TRIGGERS.some((r) => r.test(text));
}
// ============================================================================
// Plugin Definition
// ============================================================================
@ -309,6 +283,24 @@ const memoryPlugin = {
process.exitCode = 1;
}
});
ltm
.command("add")
.description("Manually add a memory (for testing or one-off storage)")
.argument("<text>", "Content to store")
.action(async (text: string) => {
try {
const created = await client.add(text.trim(), { infer: cfg.inferOnAdd });
if (created.length === 0) {
console.log("Stored (no inferred items).");
} else {
console.log(`Stored ${created.length} item(s):`, created.map((c) => c.memory_id));
}
} catch (err) {
console.error("PowerMem add failed:", err);
process.exitCode = 1;
}
});
},
{ commands: ["ltm"] },
);
@ -372,16 +364,34 @@ const memoryPlugin = {
}
}
const toCapture = texts.filter((t) => t && shouldCapture(t));
if (toCapture.length === 0) return;
// Use all conversation content; PowerMem infers memories (no trigger-phrase filter)
const MIN_LEN = 10;
const MAX_CHUNK_LEN = 6000;
const MAX_CHUNKS_PER_SESSION = 3;
const sanitized = texts
.filter((t): t is string => typeof t === "string" && t.trim().length >= MIN_LEN)
.map((t) => t.trim())
.filter(
(t) =>
!t.includes("<relevant-memories>") &&
!(t.startsWith("<") && t.includes("</")),
);
if (sanitized.length === 0) return;
const combined = sanitized.join("\n\n");
const chunks: string[] = [];
for (let i = 0; i < combined.length; i += MAX_CHUNK_LEN) {
if (chunks.length >= MAX_CHUNKS_PER_SESSION) break;
chunks.push(combined.slice(i, i + MAX_CHUNK_LEN));
}
let stored = 0;
for (const text of toCapture.slice(0, 3)) {
const created = await client.add(text, { infer: cfg.inferOnAdd });
for (const chunk of chunks) {
const created = await client.add(chunk, { infer: cfg.inferOnAdd });
stored += created.length;
}
if (stored > 0) {
api.logger.info?.(`memory-powermem: auto-captured ${stored} memories`);
api.logger.info?.(`memory-powermem: auto-captured ${stored} memories from conversation`);
}
} catch (err) {
api.logger.warn?.(`memory-powermem: capture failed: ${String(err)}`);

View File

@ -1 +0,0 @@
../../../../node_modules/.pnpm/@sinclair+typebox@0.34.47/node_modules/@sinclair/typebox