Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de728612e | |||
| 64019ee28d | |||
| 6cad77328b | |||
| 26ac35ad40 | |||
| 31514d1943 | |||
| 52ea452010 | |||
| c469e0acc3 | |||
| f05f9ca735 | |||
| 1d76f93304 | |||
| 05e904e613 | |||
| 095f6fde47 | |||
| 1130bbc090 | |||
| 76f984ce7c | |||
| 6535ad597f | |||
| 40f81b4d2b | |||
| 54ca601e8f | |||
| de549cac05 | |||
| 83d474b13d | |||
| 9480ef06ab | |||
| c099e8eb27 | |||
| 1325f05432 | |||
| cfd19ae232 | |||
| cb6ff8738b | |||
| 81ebb8b6c0 | |||
| 9bdd00ea5a | |||
| 6f35e543ca | |||
| ee61270e1b | |||
| e902b74267 | |||
| 3eb6d9ea8e | |||
| 98d87d7573 | |||
| f7fea1f779 | |||
| 1f834863a7 | |||
| e4846a2046 | |||
| 0dda593c45 | |||
| 1bfb473c19 | |||
| 3f01baebfe | |||
| 46b61857ab | |||
| 2c5c99c0c7 | |||
| a3f9e7f638 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.2",
|
||||
"version": "10.0.6",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+108
-49
@@ -2,6 +2,114 @@
|
||||
|
||||
All notable changes to claude-mem.
|
||||
|
||||
## [v10.0.5] - 2026-02-13
|
||||
|
||||
## OpenClaw Installer & Distribution
|
||||
|
||||
This release introduces the OpenClaw one-liner installer and fixes several OpenClaw plugin issues.
|
||||
|
||||
### New Features
|
||||
|
||||
- **OpenClaw Installer** (`openclaw/install.sh`): Full cross-platform installer script with `curl | bash` support
|
||||
- Platform detection (macOS, Linux, WSL)
|
||||
- Automatic dependency management (Bun, uv, Node.js)
|
||||
- Interactive AI provider setup with settings writer
|
||||
- OpenClaw gateway detection, plugin install, and memory slot configuration
|
||||
- Worker startup and health verification with rich diagnostics
|
||||
- TTY detection, `--provider`/`--api-key` CLI flags
|
||||
- Error recovery and upgrade handling for existing installations
|
||||
- jq/python3/node fallback chain for JSON config writing
|
||||
- **Distribution readiness tests** (`openclaw/test-install.sh`): Comprehensive test suite for the installer
|
||||
- **Enhanced `/api/health` endpoint**: Now returns version, uptime, workerPath, and AI status
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: use `event.prompt` instead of `ctx.sessionKey` for prompt storage in OpenClaw plugin
|
||||
- Fix: detect both `openclaw` and `openclaw.mjs` binary names in gateway discovery
|
||||
- Fix: pass file paths via env vars instead of bash interpolation in `node -e` calls
|
||||
- Fix: handle stale plugin config that blocks OpenClaw CLI during reinstall
|
||||
- Fix: remove stale memory slot reference during reinstall cleanup
|
||||
- Fix: remove opinionated filters from OpenClaw plugin
|
||||
|
||||
## [v10.0.4] - 2026-02-12
|
||||
|
||||
## Revert: v10.0.3 chroma-mcp spawn storm fix
|
||||
|
||||
v10.0.3 introduced regressions. This release reverts the codebase to the stable v10.0.2 state.
|
||||
|
||||
### What was reverted
|
||||
|
||||
- Connection mutex via promise memoization
|
||||
- Pre-spawn process count guard
|
||||
- Hardened `close()` with try-finally + Unix `pkill -P` fallback
|
||||
- Count-based orphan reaper in `ProcessManager`
|
||||
- Circuit breaker (3 failures → 60s cooldown)
|
||||
- `etime`-based sorting for process guards
|
||||
|
||||
### Files restored to v10.0.2
|
||||
|
||||
- `src/services/sync/ChromaSync.ts`
|
||||
- `src/services/infrastructure/GracefulShutdown.ts`
|
||||
- `src/services/infrastructure/ProcessManager.ts`
|
||||
- `src/services/worker-service.ts`
|
||||
- `src/services/worker/ProcessRegistry.ts`
|
||||
- `tests/infrastructure/process-manager.test.ts`
|
||||
- `tests/integration/chroma-vector-sync.test.ts`
|
||||
|
||||
## [v10.0.3] - 2026-02-11
|
||||
|
||||
## Fix: Prevent chroma-mcp spawn storm (PR #1065)
|
||||
|
||||
Fixes a critical bug where killing the worker daemon during active sessions caused **641 chroma-mcp Python processes** to spawn in ~5 minutes, consuming 75%+ CPU and ~64GB virtual memory.
|
||||
|
||||
### Root Cause
|
||||
|
||||
`ChromaSync.ensureConnection()` had no connection mutex. Concurrent fire-and-forget `syncObservation()` calls from multiple sessions raced through the check-then-act guard, each spawning a chroma-mcp subprocess via `StdioClientTransport`. Error-driven reconnection created a positive feedback loop.
|
||||
|
||||
### 5-Layer Defense
|
||||
|
||||
| Layer | Mechanism | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| **0** | Connection mutex via promise memoization | Coalesces concurrent callers onto a single spawn attempt |
|
||||
| **1** | Pre-spawn process count guard (`execFileSync('ps')`) | Kills excess chroma-mcp processes before spawning new ones |
|
||||
| **2** | Hardened `close()` with try-finally + Unix `pkill -P` fallback | Guarantees state reset even on error, kills orphaned children |
|
||||
| **3** | Count-based orphan reaper in `ProcessManager` | Kills by count (not age), catches spawn storms where all processes are young |
|
||||
| **4** | Circuit breaker (3 failures → 60s cooldown) | Stops error-driven reconnection positive feedback loop |
|
||||
|
||||
### Additional Fix
|
||||
|
||||
- Process guards now use `etime`-based sorting instead of PID ordering for reliable age determination (PIDs wrap and don't guarantee ordering)
|
||||
|
||||
### Testing
|
||||
|
||||
- 16 new tests for mutex, circuit breaker, close() hardening, and count guard
|
||||
- All tests pass (947 pass, 3 skip)
|
||||
|
||||
Closes #1063, closes #695. Relates to #1010, #707.
|
||||
|
||||
**Contributors:** @rodboev
|
||||
|
||||
## [v10.0.2] - 2026-02-11
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Prevent daemon silent death from SIGHUP + unhandled errors** — Worker process could silently die when receiving SIGHUP signals or encountering unhandled errors, leaving hooks without a backend. Now properly handles these signals and prevents silent crashes.
|
||||
- **Hook resilience and worker lifecycle improvements** — Comprehensive fixes for hook command error classification, addressing issues #957, #923, #984, #987, and #1042. Hooks now correctly distinguish between worker unavailability errors and other failures.
|
||||
- **Clarify TypeError order dependency in error classifier** — Fixed error classification logic to properly handle TypeError ordering edge cases.
|
||||
|
||||
## New Features
|
||||
|
||||
- **Project-scoped statusline counter utility** — Added `statusline-counts.js` for tracking observation counts per project in the Claude Code status line.
|
||||
|
||||
## Internal
|
||||
|
||||
- Added test coverage for hook command error classification and process manager
|
||||
- Worker service and MCP server lifecycle improvements
|
||||
- Process manager enhancements for better cross-platform stability
|
||||
|
||||
### Contributors
|
||||
- @rodboev — Hook resilience and worker lifecycle fixes (PR #1056)
|
||||
|
||||
## [v10.0.1] - 2026-02-11
|
||||
|
||||
## What's Changed
|
||||
@@ -1457,52 +1565,3 @@ This patch release improves stability by adding proper error handling to Chroma
|
||||
|
||||
Refactored context loading logic to differentiate between code and non-code modes, resolving issues where mode-specific observations were filtered by stale settings.
|
||||
|
||||
## [v8.0.4] - 2025-12-23
|
||||
|
||||
## Changes
|
||||
|
||||
- Changed worker start script
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
## [v8.0.3] - 2025-12-23
|
||||
|
||||
Fix critical worker crashes on startup (v8.0.2 regression)
|
||||
|
||||
## [v8.0.2] - 2025-12-23
|
||||
|
||||
New "chill" remix of code mode for users who want fewer, more selective observations.
|
||||
|
||||
## Features
|
||||
|
||||
- **code--chill mode**: A behavioral variant that produces fewer observations
|
||||
- Only records things "painful to rediscover" - shipped features, architectural decisions, non-obvious gotchas
|
||||
- Skips routine work, straightforward implementations, and obvious changes
|
||||
- Philosophy: "When in doubt, skip it"
|
||||
|
||||
## Documentation
|
||||
|
||||
- Updated modes.mdx with all 28 language modes (was 10)
|
||||
- Added Code Mode Variants section documenting chill mode
|
||||
|
||||
## Usage
|
||||
|
||||
Set in ~/.claude-mem/settings.json:
|
||||
```json
|
||||
{
|
||||
"CLAUDE_MEM_MODE": "code--chill"
|
||||
}
|
||||
```
|
||||
|
||||
## [v8.0.1] - 2025-12-23
|
||||
|
||||
## 🎨 UI Improvements
|
||||
|
||||
- **Header Redesign**: Moved documentation and X (Twitter) links from settings modal to main header for better accessibility
|
||||
- **Removed Product Hunt Badge**: Cleaned up header layout by removing the Product Hunt badge
|
||||
- **Icon Reorganization**: Reordered header icons for improved UX flow (Docs → X → Discord → GitHub)
|
||||
|
||||
---
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
|
||||
+48
-8
@@ -1,8 +1,46 @@
|
||||
# Claude-Mem OpenClaw Plugin — Setup Guide
|
||||
|
||||
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway from scratch. Follow every step in order. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
||||
This guide walks through setting up the claude-mem plugin on an OpenClaw gateway. By the end, your agents will have persistent memory across sessions, a live-updating MEMORY.md in their workspace, and optionally a real-time observation feed streaming to a messaging channel.
|
||||
|
||||
## Step 1: Clone the Claude-Mem Repo
|
||||
## Quick Install (Recommended)
|
||||
|
||||
Run this one-liner to install everything automatically:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash
|
||||
```
|
||||
|
||||
The installer handles dependency checks (Bun, uv), plugin installation, memory slot configuration, AI provider setup, worker startup, and optional observation feed configuration — all interactively.
|
||||
|
||||
### Install with options
|
||||
|
||||
Pre-select your AI provider and API key to skip interactive prompts:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY
|
||||
```
|
||||
|
||||
For fully unattended installation (defaults to Claude Max Plan, skips observation feed):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --non-interactive
|
||||
```
|
||||
|
||||
To upgrade an existing installation (preserves settings, updates plugin):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --upgrade
|
||||
```
|
||||
|
||||
After installation, skip to [Step 4: Restart the Gateway and Verify](#step-4-restart-the-gateway-and-verify) to confirm everything is working.
|
||||
|
||||
---
|
||||
|
||||
## Manual Setup
|
||||
|
||||
The steps below are for manual installation if you prefer not to use the automated installer, or need to troubleshoot individual steps.
|
||||
|
||||
### Step 1: Clone the Claude-Mem Repo
|
||||
|
||||
First, clone the claude-mem repository to a location accessible by your OpenClaw gateway. This gives you the worker service source and the plugin code.
|
||||
|
||||
@@ -20,11 +58,11 @@ You'll need **bun** installed for the worker service. If you don't have it:
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
## Step 2: Get the Worker Running
|
||||
### Step 2: Get the Worker Running
|
||||
|
||||
The claude-mem worker is an HTTP service on port 37777. It stores observations, generates summaries, and serves the context timeline. The plugin talks to it over HTTP — it doesn't matter where the worker is running, just that it's reachable on localhost:37777.
|
||||
|
||||
### Check if it's already running
|
||||
#### Check if it's already running
|
||||
|
||||
If this machine also runs Claude Code with claude-mem installed, the worker may already be running:
|
||||
|
||||
@@ -36,7 +74,7 @@ curl http://localhost:37777/api/health
|
||||
|
||||
**Got connection refused or no response?** The worker isn't running. Continue below.
|
||||
|
||||
### If Claude Code has claude-mem installed
|
||||
#### If Claude Code has claude-mem installed
|
||||
|
||||
If claude-mem is installed as a Claude Code plugin (at `~/.claude/plugins/marketplaces/thedotmack/`), start the worker from that installation:
|
||||
|
||||
@@ -54,7 +92,7 @@ curl http://localhost:37777/api/health
|
||||
|
||||
**Still not working?** Check `npm run worker:status` for error details, or check that bun is installed and on your PATH.
|
||||
|
||||
### If there's no Claude Code installation
|
||||
#### If there's no Claude Code installation
|
||||
|
||||
Run the worker from the cloned repo:
|
||||
|
||||
@@ -77,7 +115,7 @@ curl http://localhost:37777/api/health
|
||||
- Check logs: `npm run worker:logs` (if available)
|
||||
- Try running it directly to see errors: `bun plugin/scripts/worker-service.cjs start`
|
||||
|
||||
## Step 3: Add the Plugin to Your Gateway
|
||||
### Step 3: Add the Plugin to Your Gateway
|
||||
|
||||
Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
||||
|
||||
@@ -96,7 +134,7 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
||||
}
|
||||
```
|
||||
|
||||
### Config fields explained
|
||||
#### Config fields explained
|
||||
|
||||
- **`project`** (string, default: `"openclaw"`) — The project name that scopes all observations in the memory database. Use a unique name per gateway/use-case so observations don't mix. For example, if this gateway runs a coding bot, use `"coding-bot"`.
|
||||
|
||||
@@ -104,6 +142,8 @@ Add the `claude-mem` plugin to your OpenClaw gateway configuration:
|
||||
|
||||
- **`workerPort`** (number, default: `37777`) — The port where the claude-mem worker service is listening. Only change this if you configured the worker to use a different port.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Restart the Gateway and Verify
|
||||
|
||||
Restart your OpenClaw gateway so it picks up the new plugin configuration. After restart, check the gateway logs for:
|
||||
|
||||
Executable
+1801
File diff suppressed because it is too large
Load Diff
@@ -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": "integration",
|
||||
"kind": "memory",
|
||||
"version": "1.0.0",
|
||||
"author": "thedotmack",
|
||||
"homepage": "https://claude-mem.com",
|
||||
@@ -41,6 +41,10 @@
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "Target chat/user ID to send observations to"
|
||||
},
|
||||
"botToken": {
|
||||
"type": "string",
|
||||
"description": "Optional dedicated Telegram bot token for the feed (bypasses gateway channel)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+86
-19
@@ -67,13 +67,27 @@ interface SessionEndEvent {
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
interface MessageReceivedEvent {
|
||||
from: string;
|
||||
content: string;
|
||||
timestamp?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EventContext {
|
||||
sessionKey?: string;
|
||||
workspaceDir?: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
interface MessageContext {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
}
|
||||
|
||||
type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
|
||||
type MessageEventCallback<T> = (event: T, ctx: MessageContext) => void | Promise<void>;
|
||||
|
||||
interface OpenClawPluginApi {
|
||||
id: string;
|
||||
@@ -100,6 +114,7 @@ interface OpenClawPluginApi {
|
||||
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
||||
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
||||
((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
|
||||
((event: "message_received", callback: MessageEventCallback<MessageReceivedEvent>) => void) &
|
||||
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
||||
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
||||
runtime: {
|
||||
@@ -149,6 +164,7 @@ interface ClaudeMemPluginConfig {
|
||||
enabled?: boolean;
|
||||
channel?: string;
|
||||
to?: string;
|
||||
botToken?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +174,6 @@ interface ClaudeMemPluginConfig {
|
||||
|
||||
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.
|
||||
@@ -291,12 +306,44 @@ const CHANNEL_SEND_MAP: Record<string, { namespace: string; functionName: string
|
||||
line: { namespace: "line", functionName: "sendMessageLine" },
|
||||
};
|
||||
|
||||
async function sendDirectTelegram(
|
||||
botToken: string,
|
||||
chatId: string,
|
||||
text: string,
|
||||
logger: PluginLogger
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: "Markdown",
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
logger.warn(`[claude-mem] Direct Telegram send failed (${response.status}): ${body}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logger.warn(`[claude-mem] Direct Telegram send error: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function sendToChannel(
|
||||
api: OpenClawPluginApi,
|
||||
channel: string,
|
||||
to: string,
|
||||
text: string
|
||||
text: string,
|
||||
botToken?: string
|
||||
): Promise<void> {
|
||||
// If a dedicated bot token is provided for Telegram, send directly
|
||||
if (botToken && channel === "telegram") {
|
||||
return sendDirectTelegram(botToken, to, text, api.logger);
|
||||
}
|
||||
|
||||
const mapping = CHANNEL_SEND_MAP[channel];
|
||||
if (!mapping) {
|
||||
api.logger.warn(`[claude-mem] Unsupported channel type: ${channel}`);
|
||||
@@ -332,7 +379,8 @@ async function connectToSSEStream(
|
||||
channel: string,
|
||||
to: string,
|
||||
abortController: AbortController,
|
||||
setConnectionState: (state: ConnectionState) => void
|
||||
setConnectionState: (state: ConnectionState) => void,
|
||||
botToken?: string
|
||||
): Promise<void> {
|
||||
let backoffMs = 1000;
|
||||
const maxBackoffMs = 30000;
|
||||
@@ -393,7 +441,7 @@ async function connectToSSEStream(
|
||||
if (parsed.type === "new_observation" && parsed.observation) {
|
||||
const event = parsed as SSENewObservationEvent;
|
||||
const message = formatObservationMessage(event.observation);
|
||||
await sendToChannel(api, channel, to, message);
|
||||
await sendToChannel(api, channel, to, message, botToken);
|
||||
}
|
||||
} catch (parseError: unknown) {
|
||||
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
||||
@@ -450,10 +498,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
return sessionIds.get(key)!;
|
||||
}
|
||||
|
||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
||||
async function syncMemoryToWorkspace(workspaceDir: string, ctx?: EventContext): Promise<void> {
|
||||
// Include both the base project and agent-scoped project (e.g. "openclaw" + "openclaw-main")
|
||||
const projects = [baseProjectName];
|
||||
const agentProject = ctx ? getProjectName(ctx) : null;
|
||||
if (agentProject && agentProject !== baseProjectName) {
|
||||
projects.push(agentProject);
|
||||
}
|
||||
const contextText = await workerGetText(
|
||||
workerPort,
|
||||
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
||||
`/api/context/inject?projects=${encodeURIComponent(projects.join(","))}`,
|
||||
api.logger
|
||||
);
|
||||
if (contextText && contextText.trim().length > 0) {
|
||||
@@ -482,6 +536,20 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: message_received — capture inbound user prompts from channels
|
||||
// ------------------------------------------------------------------
|
||||
api.on("message_received", async (event, ctx) => {
|
||||
const sessionKey = ctx.conversationId || ctx.channelId || "default";
|
||||
const contentSessionId = getContentSessionId(sessionKey);
|
||||
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: baseProjectName,
|
||||
prompt: event.content || "[media prompt]",
|
||||
}, api.logger);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Event: after_compaction — re-init session after context compaction
|
||||
// ------------------------------------------------------------------
|
||||
@@ -500,7 +568,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
// ------------------------------------------------------------------
|
||||
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
||||
// ------------------------------------------------------------------
|
||||
api.on("before_agent_start", async (_event, ctx) => {
|
||||
api.on("before_agent_start", async (event, ctx) => {
|
||||
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||
if (ctx.workspaceDir) {
|
||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||
@@ -512,12 +580,12 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
await workerPost(workerPort, "/api/sessions/init", {
|
||||
contentSessionId,
|
||||
project: getProjectName(ctx),
|
||||
prompt: ctx.sessionKey || "agent run",
|
||||
prompt: event.prompt || "agent run",
|
||||
}, api.logger);
|
||||
|
||||
// Sync MEMORY.md before agent runs (provides context to agent)
|
||||
if (syncMemoryFile && ctx.workspaceDir) {
|
||||
await syncMemoryToWorkspace(ctx.workspaceDir);
|
||||
await syncMemoryToWorkspace(ctx.workspaceDir, ctx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -527,20 +595,18 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
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;
|
||||
if (!toolName) return;
|
||||
|
||||
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||
|
||||
// Extract result text from message content
|
||||
// Extract result text from all content blocks
|
||||
let toolResponseText = "";
|
||||
const content = event.message?.content;
|
||||
if (Array.isArray(content)) {
|
||||
const textBlock = content.find(
|
||||
(block) => block.type === "tool_result" || block.type === "text"
|
||||
);
|
||||
if (textBlock && "text" in textBlock) {
|
||||
toolResponseText = String(textBlock.text).slice(0, TOOL_RESULT_MAX_LENGTH);
|
||||
}
|
||||
toolResponseText = content
|
||||
.filter((block) => (block.type === "tool_result" || block.type === "text") && "text" in block)
|
||||
.map((block) => String(block.text))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Fire-and-forget: send observation + sync MEMORY.md in parallel
|
||||
@@ -554,7 +620,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
|
||||
const workspaceDir = ctx.workspaceDir || workspaceDirsBySessionKey.get(ctx.sessionKey || "default");
|
||||
if (syncMemoryFile && workspaceDir) {
|
||||
syncMemoryToWorkspace(workspaceDir);
|
||||
syncMemoryToWorkspace(workspaceDir, ctx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -653,7 +719,8 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||
feedConfig.channel,
|
||||
feedConfig.to,
|
||||
sseAbortController,
|
||||
(state) => { connectionState = state; }
|
||||
(state) => { connectionState = state; },
|
||||
feedConfig.botToken
|
||||
);
|
||||
},
|
||||
stop: async (_ctx) => {
|
||||
|
||||
Executable
+2339
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.2",
|
||||
"version": "10.0.6",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "10.0.2",
|
||||
"version": "10.0.6",
|
||||
"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.2",
|
||||
"version": "10.0.6",
|
||||
"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
@@ -30,6 +30,19 @@ export interface RouteHandler {
|
||||
setupRoutes(app: Application): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI provider status for health endpoint
|
||||
*/
|
||||
export interface AiStatus {
|
||||
provider: string;
|
||||
authMethod: string;
|
||||
lastInteraction: {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for initializing the server
|
||||
*/
|
||||
@@ -42,6 +55,10 @@ export interface ServerOptions {
|
||||
onShutdown: () => Promise<void>;
|
||||
/** Restart function for admin endpoints */
|
||||
onRestart: () => Promise<void>;
|
||||
/** Filesystem path to the worker entry point */
|
||||
workerPath: string;
|
||||
/** Callback to get current AI provider status */
|
||||
getAiStatus: () => AiStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,20 +157,20 @@ export class Server {
|
||||
* Setup core system routes (health, readiness, version, admin)
|
||||
*/
|
||||
private setupCoreRoutes(): void {
|
||||
// Test build ID for debugging which build is running
|
||||
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
|
||||
|
||||
// Health check endpoint - always responds, even during initialization
|
||||
this.app.get('/api/health', (_req: Request, res: Response) => {
|
||||
res.status(200).json({
|
||||
status: 'ok',
|
||||
build: TEST_BUILD_ID,
|
||||
version: BUILT_IN_VERSION,
|
||||
workerPath: this.options.workerPath,
|
||||
uptime: Date.now() - this.startTime,
|
||||
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
||||
hasIpc: typeof process.send === 'function',
|
||||
platform: process.platform,
|
||||
pid: process.pid,
|
||||
initialized: this.options.getInitializationComplete(),
|
||||
mcpReady: this.options.getMcpReady(),
|
||||
ai: this.options.getAiStatus(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
||||
import { getAuthMethodDescription } from '../shared/EnvManager.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
||||
@@ -170,6 +171,14 @@ export class WorkerService {
|
||||
// Orphan reaper cleanup function (Issue #737)
|
||||
private stopOrphanReaper: (() => void) | null = null;
|
||||
|
||||
// AI interaction tracking for health endpoint
|
||||
private lastAiInteraction: {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
provider: string;
|
||||
error?: string;
|
||||
} | null = null;
|
||||
|
||||
constructor() {
|
||||
// Initialize the promise that will resolve when background initialization completes
|
||||
this.initializationComplete = new Promise((resolve) => {
|
||||
@@ -206,7 +215,24 @@ export class WorkerService {
|
||||
getInitializationComplete: () => this.initializationCompleteFlag,
|
||||
getMcpReady: () => this.mcpReady,
|
||||
onShutdown: () => this.shutdown(),
|
||||
onRestart: () => this.shutdown()
|
||||
onRestart: () => this.shutdown(),
|
||||
workerPath: __filename,
|
||||
getAiStatus: () => {
|
||||
let provider = 'claude';
|
||||
if (isOpenRouterSelected() && isOpenRouterAvailable()) provider = 'openrouter';
|
||||
else if (isGeminiSelected() && isGeminiAvailable()) provider = 'gemini';
|
||||
return {
|
||||
provider,
|
||||
authMethod: getAuthMethodDescription(),
|
||||
lastInteraction: this.lastAiInteraction
|
||||
? {
|
||||
timestamp: this.lastAiInteraction.timestamp,
|
||||
success: this.lastAiInteraction.success,
|
||||
...(this.lastAiInteraction.error && { error: this.lastAiInteraction.error }),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Register route handlers
|
||||
@@ -459,6 +485,7 @@ export class WorkerService {
|
||||
|
||||
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
||||
let hadUnrecoverableError = false;
|
||||
let sessionFailed = false;
|
||||
|
||||
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
||||
|
||||
@@ -473,9 +500,16 @@ export class WorkerService {
|
||||
'CLAUDE_CODE_PATH',
|
||||
'ENOENT',
|
||||
'spawn',
|
||||
'Invalid API key',
|
||||
];
|
||||
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
||||
hadUnrecoverableError = true;
|
||||
this.lastAiInteraction = {
|
||||
timestamp: Date.now(),
|
||||
success: false,
|
||||
provider: providerName,
|
||||
error: errorMessage,
|
||||
};
|
||||
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
||||
sessionId: session.sessionDbId,
|
||||
project: session.project,
|
||||
@@ -512,11 +546,27 @@ export class WorkerService {
|
||||
project: session.project,
|
||||
provider: providerName
|
||||
}, error as Error);
|
||||
sessionFailed = true;
|
||||
this.lastAiInteraction = {
|
||||
timestamp: Date.now(),
|
||||
success: false,
|
||||
provider: providerName,
|
||||
error: errorMessage,
|
||||
};
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
session.generatorPromise = null;
|
||||
|
||||
// Record successful AI interaction if no error occurred
|
||||
if (!sessionFailed && !hadUnrecoverableError) {
|
||||
this.lastAiInteraction = {
|
||||
timestamp: Date.now(),
|
||||
success: true,
|
||||
provider: providerName,
|
||||
};
|
||||
}
|
||||
|
||||
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
||||
if (hadUnrecoverableError) {
|
||||
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
||||
|
||||
@@ -246,6 +246,12 @@ export class SDKAgent {
|
||||
throw new Error('Claude session context overflow: prompt is too long');
|
||||
}
|
||||
|
||||
// Detect invalid API key — SDK returns this as response text, not an error.
|
||||
// Throw so it surfaces in health endpoint and prevents silent failures.
|
||||
if (typeof textContent === 'string' && textContent.includes('Invalid API key')) {
|
||||
throw new Error('Invalid API key: check your API key configuration in ~/.claude-mem/settings.json or ~/.claude-mem/.env');
|
||||
}
|
||||
|
||||
// Parse and process response using shared ResponseProcessor
|
||||
await processAgentResponse(
|
||||
textContent,
|
||||
|
||||
Reference in New Issue
Block a user