Compare commits

...

39 Commits

Author SHA1 Message Date
Alex Newman 5de728612e chore: bump version to 10.0.6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:02:37 -05:00
Alex Newman 64019ee28d Merge pull request #1084 from thedotmack/fix/openclaw-plugin-project-query-and-feed-bottoken
fix(openclaw): fix MEMORY.md project query mismatch and add feed botToken support
2026-02-12 23:51:22 -05:00
Alex Newman 6cad77328b fix(openclaw): fix MEMORY.md project query mismatch and add feed botToken support
Three fixes for the OpenClaw plugin:

1. Fix MEMORY.md sync returning empty content
   - syncMemoryToWorkspace() was querying basename(workspaceDir) as the
     project name (e.g. 'workspace'), but observations are stored under
     agent-scoped names like 'openclaw-main'
   - Now queries both the base project and agent-scoped project name
   - Passes EventContext through so the correct project can be derived

2. Add dedicated botToken support for observation feed
   - New optional 'botToken' field in observationFeed config
   - When set, sends observations directly via Telegram Bot API instead
     of routing through the gateway's channel plugin
   - Allows using a separate bot for the observation stream

3. Fix plugin kind for memory slot compatibility
   - Changed plugin kind from 'integration' to 'memory' so OpenClaw
     recognizes it as a valid memory slot plugin
   - Fixes 'memory slot plugin not found' warning when
     plugins.slots.memory = 'claude-mem' is configured
2026-02-12 23:48:44 -05:00
Alex Newman 26ac35ad40 docs: update CHANGELOG.md for v10.0.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:27:19 -05:00
Alex Newman 31514d1943 chore: bump version to 10.0.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:26:36 -05:00
Alex Newman 52ea452010 Merge pull request #1076 from thedotmack/openclaw-installer
Add OpenClaw one-liner installer script with comprehensive test suite
2026-02-12 22:22:25 -05:00
Alex Newman c469e0acc3 fix: remove opinionated filters from OpenClaw plugin
Remove arbitrary TOOL_RESULT_MAX_LENGTH truncation, capture all content
blocks instead of only the first, observe all tool uses including
memory_ tools, and filter context injection by workspace directory
instead of hardcoded project name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:14:16 -05:00
Alex Newman f05f9ca735 Merge remote-tracking branch 'origin/main' into openclaw-installer
# Conflicts:
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/worker-service.cjs
2026-02-12 22:04:03 -05:00
Alex Newman 1d76f93304 feat: harden installer with rich health check diagnostics
Two-stage health verification (health + readiness), 30s timeout,
parse_health_json() helper with jq/python3/node fallbacks, smart
port-conflict handling with version/provider mismatch detection,
and enhanced completion summary showing version, AI auth, and uptime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:58:15 -05:00
Alex Newman 05e904e613 feat: enhance /api/health with version, uptime, workerPath, and AI status
Replace hardcoded TEST-008 build ID with real package version. Add worker
filesystem path, uptime counter, and AI provider status (including last
interaction success/failure tracking) to the health endpoint response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:16:22 -05:00
Alex Newman 095f6fde47 fix: also remove stale memory slot reference during reinstall cleanup
The stale config cleanup removed plugins.entries['claude-mem'] but left
plugins.slots.memory pointing to it. OpenClaw's config validator rejects
ALL CLI commands when a slot references a non-existent plugin, blocking
`openclaw plugins install` from running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:55:59 -05:00
Alex Newman 1130bbc090 fix: handle stale plugin config that blocks OpenClaw CLI during reinstall
OpenClaw's config validator rejects unrecognized keys and references to
uninstalled plugins, blocking ALL CLI commands including `plugins install`.
The installer now temporarily removes the stale claude-mem entry before
installing, then restores the saved plugin config (workerPort, observationFeed,
etc.) after successful installation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:41:44 -05:00
Alex Newman 76f984ce7c fix: add --branch flag and handle existing plugin on reinstall
The installer always cloned from main branch, making it impossible to test
changes on feature branches. Added --branch flag (e.g. --branch=openclaw-installer)
to override the default.

Also fixes "plugin already exists" error by removing the existing plugin
directory (~/.openclaw/extensions/claude-mem) before running plugins install,
allowing clean reinstallation without manual cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:10:59 -05:00
Alex Newman 6535ad597f fix: use event.prompt instead of ctx.sessionKey for prompt storage in OpenClaw plugin
The before_agent_start handler was passing ctx.sessionKey (e.g. "agent:main:main")
as the prompt to the worker API, causing the viewer to display the session key
instead of actual user prompt text. Now correctly reads event.prompt from
OpenClaw's BeforeAgentStartEvent.

Also adds message_received hook to capture inbound user prompts from messaging
channels (Telegram, Discord, etc.) and stores them via the worker API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:17:32 -05:00
Alex Newman 40f81b4d2b fix: detect both openclaw and openclaw.mjs binary names in gateway discovery
find_openclaw() only searched for openclaw.mjs, missing systems where the
binary is named just "openclaw" (e.g., npm install -g openclaw). Now checks
both names in PATH and in hardcoded paths. Added run_openclaw() helper that
invokes via node for .mjs files and directly for standalone binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:12:29 -05:00
Alex Newman 54ca601e8f fix: pass file paths via env vars instead of bash interpolation in node -e calls
Addresses PR review feedback: bash variable interpolation into JavaScript
string literals could allow injection if paths contain special characters.
All 4 node -e calls now receive paths via process.env instead of ${var}
interpolation: package.json writer, config creator, config updater, and
PID file writer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:04:13 -05:00
Alex Newman de549cac05 Update openclaw/install.sh
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-12 12:37:50 -05:00
Alex Newman 83d474b13d Update openclaw/install.sh
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-12 12:37:42 -05:00
Alex Newman 9480ef06ab Update openclaw/install.sh
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-12 12:36:28 -05:00
Alex Newman c099e8eb27 Update openclaw/install.sh
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-12 12:36:14 -05:00
Alex Newman 1325f05432 MAESTRO: finalize distribution with one-liner installer in SKILL.md and distribution readiness tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:38:32 -05:00
Alex Newman cfd19ae232 MAESTRO: fix check_uv test to handle system-path uv installations
The check_uv not-found test was failing because find_uv_path checks
hardcoded system paths (/opt/homebrew/bin/uv, /usr/local/bin/uv) that
can't be overridden via HOME or PATH. Added graceful skip when uv is
installed at non-overridable system paths.

All 171/171 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:34:27 -05:00
Alex Newman cb6ff8738b MAESTRO: add error recovery, upgrade handling, and edge case hardening to install.sh
- Add --upgrade flag that detects existing installations and skips clone/build/register
- Add global trap-based cleanup (register_cleanup_dir + cleanup_on_exit) for temp dirs
- Add check_git() with platform-specific install suggestions (xcode-select on macOS, apt on Linux)
- Add check_port_37777() to detect worker already running before starting a new one
- Add is_claude_mem_installed() for upgrade detection via plugin directory check
- Add ensure_jq_or_fallback() utility for JSON operations with jq/node fallback
- All 160 tests pass (23 new tests for error handling functions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:10:39 -05:00
Alex Newman 81ebb8b6c0 MAESTRO: add TTY detection, --provider/--api-key flags, and curl | bash support to install.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:06:20 -05:00
Alex Newman 9bdd00ea5a MAESTRO: add jq/python3/node fallback chain for observation feed config writing
write_observation_feed_config() now uses jq as the primary JSON
manipulation tool, falls back to python3, then to node. This gives
users the most reliable path regardless of their system tooling.
Added 15 new tests covering all three fallback paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:59:19 -05:00
Alex Newman 6f35e543ca MAESTRO: add observation feed interactive setup, config writer, and updated completion summary to install.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:55:24 -05:00
Alex Newman ee61270e1b MAESTRO: validate install.sh — fix IS_WSL bug and API key injection in write_settings
- Fixed IS_WSL=false (non-empty string) causing "(WSL)" to always display
  on Linux platforms; now uses empty string initialization
- Refactored write_settings() to pass API key via environment variables
  instead of interpolating into JavaScript string literals, preventing
  potential injection or breakage with special characters
- Passes bash -n and shellcheck with zero warnings
- All 74/74 existing tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:51:09 -05:00
Alex Newman e902b74267 MAESTRO: add worker startup, health verification, and completion summary to install.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:39:54 -05:00
Alex Newman 3eb6d9ea8e docs: update CHANGELOG.md for v10.0.4 2026-02-11 21:37:12 -05:00
Alex Newman 98d87d7573 chore: bump version to 10.0.4
Reverts v10.0.3 chroma-mcp spawn storm fix (broken release).
Restores codebase to v10.0.2 state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:36:34 -05:00
Alex Newman f7fea1f779 MAESTRO: add interactive AI provider setup and settings writer to install.sh
Add setup_ai_provider() with 3-option menu (Claude Max/Gemini/OpenRouter),
mask_api_key() for secure terminal display, and write_settings() that generates
~/.claude-mem/settings.json with all 35 defaults from SettingsDefaultsManager.ts
in flat JSON schema. Preserves existing user customizations on re-run.
23 new tests added (46/46 total pass). Passes bash -n and shellcheck clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:35:40 -05:00
Alex Newman 1f834863a7 MAESTRO: add OpenClaw gateway detection, plugin install, and memory slot config to install.sh
Add find_openclaw()/check_openclaw() for gateway detection across PATH,
~/.openclaw/, /usr/local/bin/, and node_modules paths. Add install_plugin()
that clones, builds, creates installable package, and runs plugins install/enable
following the Dockerfile.e2e flow. Add configure_memory_slot() that creates or
updates ~/.openclaw/openclaw.json with plugins.slots.memory="claude-mem" while
preserving existing config. Includes test-install.sh with 23 passing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:31:15 -05:00
Alex Newman e4846a2046 MAESTRO: add openclaw/install.sh script foundation with banner, platform detection, and dependency management
Creates the core installer script with:
- ASCII banner and ANSI color utility functions (info/success/warn/error/prompt_user)
- Automatic terminal color support detection
- Platform detection (macOS, Linux, WSL, Windows/MINGW)
- Bun detection, version checking (>=1.1.14), and auto-installation
- uv detection and auto-installation
- find_bun_path() helper returning full path to bun binary
- --non-interactive flag for curl|bash piping safety
- All dependency patterns translated from plugin/scripts/smart-install.js

Passes bash -n syntax check and shellcheck with zero warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:27:29 -05:00
Alex Newman 0dda593c45 docs: update CHANGELOG.md for v10.0.3 2026-02-11 15:45:25 -05:00
Alex Newman 1bfb473c19 chore: bump version to 10.0.3 2026-02-11 15:44:45 -05:00
Alex Newman 3f01baebfe Merge remote-tracking branch 'origin/main' into fix/chroma-mcp-spawn-storm
# Conflicts:
#	src/services/worker-service.ts
#	tests/infrastructure/process-manager.test.ts
2026-02-11 15:43:08 -05:00
Alex Newman 46b61857ab docs: update CHANGELOG.md for v10.0.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:26:31 -05:00
Rod Boev 2c5c99c0c7 fix: use etime-based sorting instead of PID ordering for process guards
Addresses Greptile review feedback:
- ChromaSync: replace PID-based sort with ps etime column + parseElapsedTime()
  for reliable age ordering (PIDs wrap and don't guarantee ordering)
- ProcessManager: filter out entries with unparseable etime (-1) before
  sorting to prevent sort corruption in cleanupExcessChromaProcesses()
2026-02-11 07:19:28 -05:00
Rod Boev a3f9e7f638 fix: prevent chroma-mcp spawn storm with 5-layer defense (641 processes → max 2)
During SIGHUP testing with 6+ active sessions, ChromaSync.ensureConnection()
had no mutex — concurrent fire-and-forget syncObservation() calls each spawned
a chroma-mcp subprocess via StdioClientTransport, creating 641 orphans in ~5min.
Error-driven reconnection formed a positive feedback loop amplifying the storm.

Defense layers:
- Layer 0: Connection mutex via promise memoization (prevents concurrent spawns)
- Layer 1: Pre-spawn process count guard using execFileSync('ps') (kills excess)
- Layer 2: Hardened close() with try-finally + Unix pkill in GracefulShutdown
- Layer 3: Count-based orphan reaper in ProcessManager (not age-based)
- Layer 4: Circuit breaker stops retries after 3 consecutive failures for 60s

Closes #1063, closes #695
Relates to #1010, #707
2026-02-11 07:19:28 -05:00
15 changed files with 4566 additions and 183 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+1801
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -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
View File
@@ -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) => {
+2339
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
+21 -4
View File
@@ -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(),
});
});
+51 -1
View File
@@ -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', {
+6
View File
@@ -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,