Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9e3b659d3 | |||
| af9584a174 | |||
| 63827c9dcb | |||
| 809175612c | |||
| 06d9ef24f1 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.1",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+84
-8
@@ -2,6 +2,90 @@
|
||||
|
||||
All notable changes to claude-mem.
|
||||
|
||||
## [v10.0.0] - 2026-02-11
|
||||
|
||||
## OpenClaw Plugin — Persistent Memory for OpenClaw Agents
|
||||
|
||||
Claude-mem now has an official [OpenClaw](https://openclaw.ai) plugin, bringing persistent memory to agents running on the OpenClaw gateway. This is a major milestone — claude-mem's memory system is no longer limited to Claude Code sessions.
|
||||
|
||||
### What It Does
|
||||
|
||||
The plugin bridges claude-mem's observation pipeline with OpenClaw's embedded runner (`pi-embedded`), which calls the Anthropic API directly without spawning a `claude` process. Three core capabilities:
|
||||
|
||||
1. **Observation Recording** — Captures every tool call from OpenClaw agents and sends it to the claude-mem worker for AI-powered compression and storage
|
||||
2. **MEMORY.md Live Sync** — Writes a continuously-updated memory timeline to each agent's workspace, so agents start every session with full context from previous work
|
||||
3. **Observation Feed** — Streams new observations to messaging channels (Telegram, Discord, Slack, Signal, WhatsApp, LINE) in real-time via SSE
|
||||
|
||||
### Quick Start
|
||||
|
||||
Add claude-mem to your OpenClaw gateway config:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"claude-mem": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"project": "my-project",
|
||||
"syncMemoryFile": true,
|
||||
"observationFeed": {
|
||||
"enabled": true,
|
||||
"channel": "telegram",
|
||||
"to": "your-chat-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The claude-mem worker service must be running on the same machine (`localhost:37777`).
|
||||
|
||||
### Commands
|
||||
|
||||
- `/claude-mem-status` — Worker health check, active sessions, feed connection state
|
||||
- `/claude-mem-feed` — Show/toggle observation feed status
|
||||
- `/claude-mem-feed on|off` — Enable/disable feed
|
||||
|
||||
### How the Event Lifecycle Works
|
||||
|
||||
```
|
||||
OpenClaw Gateway
|
||||
├── session_start ──────────→ Init claude-mem session
|
||||
├── before_agent_start ─────→ Sync MEMORY.md + track workspace
|
||||
├── tool_result_persist ────→ Record observation + re-sync MEMORY.md
|
||||
├── agent_end ──────────────→ Summarize + complete session
|
||||
├── session_end ────────────→ Clean up session tracking
|
||||
└── gateway_start ──────────→ Reset all tracking
|
||||
```
|
||||
|
||||
All observation recording and MEMORY.md syncs are fire-and-forget — they never block the agent.
|
||||
|
||||
📖 Full documentation: [OpenClaw Integration Guide](https://docs.claude-mem.ai/docs/openclaw-integration)
|
||||
|
||||
---
|
||||
|
||||
## Windows Platform Improvements
|
||||
|
||||
- **ProcessManager**: Migrated daemon spawning from deprecated WMIC to PowerShell `Start-Process` with `-WindowStyle Hidden`
|
||||
- **ChromaSync**: Re-enabled vector search on Windows (was previously disabled entirely)
|
||||
- **Worker Service**: Added unified DB-ready gate middleware — all DB-dependent endpoints now wait for initialization instead of returning "Database not initialized" errors
|
||||
- **EnvManager**: Switched from fragile allowlist to simple blocklist for subprocess env vars (only strips `ANTHROPIC_API_KEY` per Issue #733)
|
||||
|
||||
## Session Management Fixes
|
||||
|
||||
- Fixed unbounded session tracking map growth — maps are now cleaned up on `session_end`
|
||||
- Session init moved to `session_start` and `after_compaction` hooks for correct lifecycle handling
|
||||
|
||||
## SSE Fixes
|
||||
|
||||
- Fixed stream URL consistency across the codebase
|
||||
- Fixed multi-line SSE data frame parsing (concatenates `data:` lines per SSE spec)
|
||||
|
||||
## Issue Triage
|
||||
|
||||
Closed 37+ duplicate/stale/invalid issues across multiple triage phases, significantly cleaning up the issue tracker.
|
||||
|
||||
## [v9.1.1] - 2026-02-07
|
||||
|
||||
## Critical Bug Fix: Worker Initialization Failure
|
||||
@@ -1455,11 +1539,3 @@ Set in ~/.claude-mem/settings.json:
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.4.5...v8.0.0
|
||||
**View PR**: https://github.com/thedotmack/claude-mem/pull/412
|
||||
|
||||
## [v7.4.5] - 2025-12-21
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fix missing `formatDateTime` import in SearchManager that broke `get_context_timeline` mem-search function
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "claude-mem",
|
||||
"name": "Claude-Mem (Persistent Memory)",
|
||||
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
||||
"kind": "memory",
|
||||
"kind": "integration",
|
||||
"version": "1.0.0",
|
||||
"author": "thedotmack",
|
||||
"homepage": "https://claude-mem.com",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@claude-mem/openclaw-plugin",
|
||||
"name": "@openclaw/claude-mem",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -11,5 +11,10 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.2.1",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./dist/index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ describe("Observation I/O event handlers", () => {
|
||||
assert.equal(initRequests.length, 1, "should re-init after compaction");
|
||||
});
|
||||
|
||||
it("before_agent_start does not call init", async () => {
|
||||
it("before_agent_start calls init for session privacy check", async () => {
|
||||
const { api, fireEvent } = createMockApi({ workerPort });
|
||||
claudeMemPlugin(api);
|
||||
|
||||
@@ -354,7 +354,7 @@ describe("Observation I/O event handlers", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
||||
assert.equal(initRequests.length, 0, "before_agent_start should not init");
|
||||
assert.equal(initRequests.length, 1, "before_agent_start should init session");
|
||||
});
|
||||
|
||||
it("tool_result_persist sends observation to worker", async () => {
|
||||
|
||||
+67
-8
@@ -124,7 +124,7 @@ interface ObservationSSEPayload {
|
||||
concepts: string | null;
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
project: string;
|
||||
project: string | null;
|
||||
prompt_number: number;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
@@ -160,6 +160,44 @@ const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
||||
const DEFAULT_WORKER_PORT = 37777;
|
||||
const TOOL_RESULT_MAX_LENGTH = 1000;
|
||||
|
||||
// Agent emoji map for observation feed messages.
|
||||
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
||||
const AGENT_EMOJI_MAP: Record<string, string> = {
|
||||
"main": "🦞",
|
||||
"openclaw": "🦞",
|
||||
"devops": "🔧",
|
||||
"architect": "📐",
|
||||
"researcher": "🔍",
|
||||
"code-reviewer": "🔎",
|
||||
"coder": "💻",
|
||||
"tester": "🧪",
|
||||
"debugger": "🐛",
|
||||
"opsec": "🛡️",
|
||||
"cloudfarm": "☁️",
|
||||
"extractor": "📦",
|
||||
};
|
||||
|
||||
// Project prefixes that indicate Claude Code sessions (not OpenClaw agents)
|
||||
const CLAUDE_CODE_EMOJI = "⌨️";
|
||||
const OPENCLAW_DEFAULT_EMOJI = "🦀";
|
||||
|
||||
function getSourceLabel(project: string | null | undefined): string {
|
||||
if (!project) return OPENCLAW_DEFAULT_EMOJI;
|
||||
// OpenClaw agent projects are formatted as "openclaw-<agentId>"
|
||||
if (project.startsWith("openclaw-")) {
|
||||
const agentId = project.slice("openclaw-".length);
|
||||
const emoji = AGENT_EMOJI_MAP[agentId] || OPENCLAW_DEFAULT_EMOJI;
|
||||
return `${emoji} ${agentId}`;
|
||||
}
|
||||
// OpenClaw project without agent suffix
|
||||
if (project === "openclaw") {
|
||||
return `🦞 openclaw`;
|
||||
}
|
||||
// Everything else is from Claude Code (project = working directory name)
|
||||
const emoji = CLAUDE_CODE_EMOJI;
|
||||
return `${emoji} ${project}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker HTTP Client
|
||||
// ============================================================================
|
||||
@@ -233,7 +271,8 @@ async function workerGetText(
|
||||
|
||||
function formatObservationMessage(observation: ObservationSSEPayload): string {
|
||||
const title = observation.title || "Untitled";
|
||||
let message = `🧠 Claude-Mem Observation\n**${title}**`;
|
||||
const source = getSourceLabel(observation.project);
|
||||
let message = `${source}\n**${title}**`;
|
||||
if (observation.subtitle) {
|
||||
message += `\n${observation.subtitle}`;
|
||||
}
|
||||
@@ -387,7 +426,14 @@ async function connectToSSEStream(
|
||||
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
||||
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
||||
const projectName = userConfig.project || "openclaw";
|
||||
const baseProjectName = userConfig.project || "openclaw";
|
||||
|
||||
function getProjectName(ctx: EventContext): string {
|
||||
if (ctx.agentId) {
|
||||
return `openclaw-${ctx.agentId}`;
|
||||
}
|
||||
return baseProjectName;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Session tracking for observation I/O
|
||||
@@ -407,7 +453,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
||||
const contextText = await workerGetText(
|
||||
workerPort,
|
||||
`/api/context/inject?projects=${encodeURIComponent(projectName)}`,
|
||||
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
||||
api.logger
|
||||
);
|
||||
if (contextText && contextText.trim().length > 0) {
|
||||
@@ -429,7 +475,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: projectName,
|
||||
project: getProjectName(ctx),
|
||||
prompt: "",
|
||||
}, api.logger);
|
||||
|
||||
@@ -444,7 +490,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: projectName,
|
||||
project: getProjectName(ctx),
|
||||
prompt: "",
|
||||
}, api.logger);
|
||||
|
||||
@@ -452,7 +498,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: before_agent_start — sync MEMORY.md + track workspace
|
||||
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_agent_start", async (_event, ctx) => {
|
||||
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||
@@ -460,6 +506,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||
}
|
||||
|
||||
// Initialize session in the worker so observations are not skipped
|
||||
// (the privacy check requires a stored user prompt to exist)
|
||||
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: getProjectName(ctx),
|
||||
prompt: ctx.sessionKey || "agent run",
|
||||
}, api.logger);
|
||||
|
||||
// Sync MEMORY.md before agent runs (provides context to agent)
|
||||
if (syncMemoryFile && ctx.workspaceDir) {
|
||||
await syncMemoryToWorkspace(ctx.workspaceDir);
|
||||
@@ -470,6 +525,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
// Event: tool_result_persist — record tool observations + sync MEMORY.md
|
||||
// ------------------------------------------------------------------
|
||||
api.on("tool_result_persist", (event, ctx) => {
|
||||
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
||||
const toolName = event.toolName;
|
||||
if (!toolName || toolName.startsWith("memory_")) return;
|
||||
|
||||
@@ -527,7 +583,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
}
|
||||
|
||||
workerPostFireAndForget(workerPort, "/api/sessions/summarize", {
|
||||
// Await summarize so the worker receives it before complete.
|
||||
// This also gives in-flight tool_result_persist observations time to arrive
|
||||
// (they use fire-and-forget and may still be in transit).
|
||||
await workerPost(workerPort, "/api/sessions/summarize", {
|
||||
contentSessionId,
|
||||
last_assistant_message: lastAssistantMessage,
|
||||
}, api.logger);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.1",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.1",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.1",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -217,6 +217,14 @@ export function buildIsolatedEnv(includeCredentials: boolean = true): Record<str
|
||||
if (credentials.OPENROUTER_API_KEY) {
|
||||
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
|
||||
}
|
||||
|
||||
// 4. Pass through Claude CLI's OAuth token if available (fallback for CLI subscription billing)
|
||||
// When no ANTHROPIC_API_KEY is configured, the spawned CLI uses subscription billing
|
||||
// which requires either ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN.
|
||||
// The worker inherits this token from the Claude Code session that started it.
|
||||
if (!isolatedEnv.ANTHROPIC_API_KEY && process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||
isolatedEnv.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
}
|
||||
}
|
||||
|
||||
return isolatedEnv;
|
||||
@@ -257,5 +265,8 @@ export function getAuthMethodDescription(): string {
|
||||
if (hasAnthropicApiKey()) {
|
||||
return 'API key (from ~/.claude-mem/.env)';
|
||||
}
|
||||
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||
return 'Claude Code OAuth token (from parent process)';
|
||||
}
|
||||
return 'Claude Code CLI (subscription billing)';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user